Skip to content

Commit

Permalink
Feature: support new config type account-id (#500)
Browse files Browse the repository at this point in the history
* feat: support `account-id` config

* refactor: reuse interactive logic of `input-account-id`

* refactor: update account_ids in AuthConfiguration
feat: skip account ids if it is not provided

* test:feat: Auth0Client

* feat: support Prompt in AccountIdsConfiguration

* feat: handle accountIds and provide different option to user

* refactor: handle of accountIds in json_module

* refactor: validate len api_accounts_ids
refactor: use elif instead of if

* feat: handle when api return None of api_account_ids

* feat: skip validation of choices (not provided)

* feat: filter dict config by regex condition

* refactor: update of api_account_ids

* remove: not used class AccountIdsConfiguration

* remove: missed import class

* remove: not used import

* test:refactor: test_auth0_client

* feat: new config of TredeStation brokerage in README

* refactor: change spacing

* feat: class Accounts in auth0_client

* feat: skip choice type without choices

* feat: new object account in Auth0 model
refactor: parsing of new object Auth0
test:refactor: use new mock object of Auth0

* refactor: QCAuth0Authorization model

* test:feat: add alpaca configuration test

* remove: trade-station-account-type parameter (deprecated)

* feat: add filter_dependency flag

* refactor: remove optional in trade-station-account-id
  • Loading branch information
Romazes authored Oct 1, 2024
1 parent 90984b5 commit a7236c1
Show file tree
Hide file tree
Showing 6 changed files with 166 additions and 30 deletions.
48 changes: 24 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,8 +198,6 @@ Options:
Your ThetaData subscription price plan
--terminal-link-connection-type [DAPI|SAPI]
Terminal Link Connection Type [DAPI, SAPI]
--terminal-link-server-auth-id TEXT
The Auth ID of the TerminalLink server
--terminal-link-environment [Production|Beta]
The environment to run in
--terminal-link-server-host TEXT
Expand All @@ -208,12 +206,14 @@ Options:
The port of the TerminalLink server
--terminal-link-openfigi-api-key TEXT
The Open FIGI API key to use for mapping options
--terminal-link-server-auth-id TEXT
The Auth ID of the TerminalLink server
--bybit-api-key TEXT Your Bybit API key
--bybit-api-secret TEXT Your Bybit API secret
--trade-station-environment [live|paper]
Whether Live or Paper environment should be used
--trade-station-account-type [Cash|Margin|Futures|DVP]
Specifies the type of account on TradeStation
--trade-station-account-id TEXT
The TradeStation account Id
--alpaca-environment [live|paper]
Whether Live or Paper environment should be used
--download-data Update the Lean configuration file to download data from the QuantConnect API, alias
Expand Down Expand Up @@ -435,8 +435,8 @@ Options:
Your Bybit VIP Level
--trade-station-environment [live|paper]
Whether Live or Paper environment should be used
--trade-station-account-type [Cash|Margin|Futures|DVP]
Specifies the type of account on TradeStation
--trade-station-account-id TEXT
The TradeStation account Id
--alpaca-environment [live|paper]
Whether Live or Paper environment should be used
--polygon-api-key TEXT Your Polygon.io API Key
Expand Down Expand Up @@ -900,8 +900,6 @@ Options:
Your ThetaData subscription price plan
--terminal-link-connection-type [DAPI|SAPI]
Terminal Link Connection Type [DAPI, SAPI]
--terminal-link-server-auth-id TEXT
The Auth ID of the TerminalLink server
--terminal-link-environment [Production|Beta]
The environment to run in
--terminal-link-server-host TEXT
Expand All @@ -910,12 +908,14 @@ Options:
The port of the TerminalLink server
--terminal-link-openfigi-api-key TEXT
The Open FIGI API key to use for mapping options
--terminal-link-server-auth-id TEXT
The Auth ID of the TerminalLink server
--bybit-api-key TEXT Your Bybit API key
--bybit-api-secret TEXT Your Bybit API secret
--trade-station-environment [live|paper]
Whether Live or Paper environment should be used
--trade-station-account-type [Cash|Margin|Futures|DVP]
Specifies the type of account on TradeStation
--trade-station-account-id TEXT
The TradeStation account Id
--alpaca-environment [live|paper]
Whether Live or Paper environment should be used
--dataset TEXT The name of the dataset to download non-interactively
Expand Down Expand Up @@ -1341,8 +1341,6 @@ Options:
commodities on MCX
--terminal-link-connection-type [DAPI|SAPI]
Terminal Link Connection Type [DAPI, SAPI]
--terminal-link-server-auth-id TEXT
The Auth ID of the TerminalLink server
--terminal-link-environment [Production|Beta]
The environment to run in
--terminal-link-server-host TEXT
Expand All @@ -1356,20 +1354,22 @@ Options:
--terminal-link-emsx-team TEXT The EMSX team to receive order events from (Optional).
--terminal-link-openfigi-api-key TEXT
The Open FIGI API key to use for mapping options
--terminal-link-server-auth-id TEXT
The Auth ID of the TerminalLink server
--tt-user-name TEXT Your Trading Technologies username
--tt-session-password TEXT Your Trading Technologies session password
--tt-account-name TEXT Your Trading Technologies account name
--tt-rest-app-key TEXT Your Trading Technologies REST app key
--tt-rest-app-secret TEXT Your Trading Technologies REST app secret
--tt-rest-environment TEXT The REST environment to run in
--tt-order-routing-sender-comp-id TEXT
The order routing sender comp id to use
--tt-market-data-sender-comp-id TEXT
The market data sender comp id to use
--tt-market-data-target-comp-id TEXT
The market data target comp id to use
--tt-market-data-host TEXT The host of the market data server
--tt-market-data-port TEXT The port of the market data server
--tt-order-routing-sender-comp-id TEXT
The order routing sender comp id to use
--tt-order-routing-target-comp-id TEXT
The order routing target comp id to use
--tt-order-routing-host TEXT The host of the order routing server
Expand All @@ -1392,8 +1392,8 @@ Options:
Whether the testnet should be used
--trade-station-environment [live|paper]
Whether Live or Paper environment should be used
--trade-station-account-type [Cash|Margin|Futures|DVP]
Specifies the type of account on TradeStation
--trade-station-account-id TEXT
The TradeStation account Id
--alpaca-environment [live|paper]
Whether Live or Paper environment should be used
--ib-enable-delayed-streaming-data BOOLEAN
Expand Down Expand Up @@ -1791,8 +1791,6 @@ Options:
Your ThetaData subscription price plan
--terminal-link-connection-type [DAPI|SAPI]
Terminal Link Connection Type [DAPI, SAPI]
--terminal-link-server-auth-id TEXT
The Auth ID of the TerminalLink server
--terminal-link-environment [Production|Beta]
The environment to run in
--terminal-link-server-host TEXT
Expand All @@ -1801,12 +1799,14 @@ Options:
The port of the TerminalLink server
--terminal-link-openfigi-api-key TEXT
The Open FIGI API key to use for mapping options
--terminal-link-server-auth-id TEXT
The Auth ID of the TerminalLink server
--bybit-api-key TEXT Your Bybit API key
--bybit-api-secret TEXT Your Bybit API secret
--trade-station-environment [live|paper]
Whether Live or Paper environment should be used
--trade-station-account-type [Cash|Margin|Futures|DVP]
Specifies the type of account on TradeStation
--trade-station-account-id TEXT
The TradeStation account Id
--alpaca-environment [live|paper]
Whether Live or Paper environment should be used
--lean-config FILE The Lean configuration file that should be used (defaults to the nearest lean.json)
Expand Down Expand Up @@ -1963,8 +1963,6 @@ Options:
Your ThetaData subscription price plan
--terminal-link-connection-type [DAPI|SAPI]
Terminal Link Connection Type [DAPI, SAPI]
--terminal-link-server-auth-id TEXT
The Auth ID of the TerminalLink server
--terminal-link-environment [Production|Beta]
The environment to run in
--terminal-link-server-host TEXT
Expand All @@ -1973,12 +1971,14 @@ Options:
The port of the TerminalLink server
--terminal-link-openfigi-api-key TEXT
The Open FIGI API key to use for mapping options
--terminal-link-server-auth-id TEXT
The Auth ID of the TerminalLink server
--bybit-api-key TEXT Your Bybit API key
--bybit-api-secret TEXT Your Bybit API secret
--trade-station-environment [live|paper]
Whether Live or Paper environment should be used
--trade-station-account-type [Cash|Margin|Futures|DVP]
Specifies the type of account on TradeStation
--trade-station-account-id TEXT
The TradeStation account Id
--alpaca-environment [live|paper]
Whether Live or Paper environment should be used
--download-data Update the Lean configuration file to download data from the QuantConnect API, alias
Expand Down
28 changes: 27 additions & 1 deletion lean/models/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,34 @@
# The models in this module are all parts of responses from the QuantConnect API
# The keys of properties are not changed, so they don't obey the rest of the project's naming conventions


class QCAuth0Authorization(WrappedBaseModel):
authorization: Optional[Dict[str, str]]
authorization: Optional[Dict[str, Any]]

def get_account_ids(self) -> List[str]:
"""
Retrieves a list of account IDs from the list of Account objects.
This method returns only the 'id' values from each account in the 'accounts' list.
If there are no accounts, it returns an empty list.
Returns:
List[str]: A list of account IDs.
"""
accounts = self.authorization.get('accounts', [])
return [account["id"] for account in accounts] if accounts else []

def get_authorization_config_without_account(self) -> Dict[str, str]:
"""
Returns the authorization data without the 'accounts' key.
Iterates through the 'authorization' dictionary and excludes the 'accounts' entry.
Returns:
Dict[str, str]: Authorization details excluding 'accounts'.
"""
return {key: value for key, value in self.authorization.items() if key != 'accounts'}


class ProjectEncryptionKey(WrappedBaseModel):
id: str
Expand Down
3 changes: 3 additions & 0 deletions lean/models/click_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ def get_click_option_type(configuration: Configuration):
if configuration._input_method == "confirm":
return bool
elif configuration._input_method == "choice":
# Skip validation if no predefined choices in config and user provided input manually
if not configuration._choices:
return str
return Choice(configuration._choices, case_sensitive=False)
elif configuration._input_method == "prompt":
return configuration.get_input_type()
Expand Down
6 changes: 6 additions & 0 deletions lean/models/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,12 @@ def __init__(self, config_json_object):
self._is_required_from_user = False
self._save_persistently_in_lean = False
self._log_message: str = ""
self.has_filter_dependency: bool = False
if "log-message" in config_json_object.keys():
self._log_message = config_json_object["log-message"]
if "filters" in config_json_object.keys():
self._filter = Filter(config_json_object["filters"])
self.has_filter_dependency = Filter.has_conditions
else:
self._filter = Filter([])
self._input_default = config_json_object["input-default"] if "input-default" in config_json_object else None
Expand Down Expand Up @@ -137,6 +139,10 @@ def __init__(self, filter_conditions):
self._conditions: List[BaseCondition] = [BaseCondition.factory(
condition["condition"]) for condition in filter_conditions]

@property
def has_conditions(self) -> bool:
"""Returns True if there are any conditions, False otherwise."""
return bool(self._conditions)

class InfoConfiguration(Configuration):
"""Configuration class used for informational configurations.
Expand Down
35 changes: 30 additions & 5 deletions lean/models/json_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from lean.constants import MODULE_TYPE, MODULE_PLATFORM, MODULE_CLI_PLATFORM
from lean.container import container
from lean.models.configuration import BrokerageEnvConfiguration, Configuration, InternalInputUserInput, \
PathParameterUserInput, AuthConfiguration
PathParameterUserInput, AuthConfiguration, ChoiceUserInput
from copy import copy
from abc import ABC

Expand Down Expand Up @@ -60,13 +60,17 @@ def get_id(self):

def sort_configs(self) -> List[Configuration]:
sorted_configs = []
filter_configs = []
brokerage_configs = []
for config in self._lean_configs:
if isinstance(config, BrokerageEnvConfiguration):
brokerage_configs.append(config)
else:
sorted_configs.append(config)
return brokerage_configs + sorted_configs
if config.has_filter_dependency:
filter_configs.append(config)
else:
sorted_configs.append(config)
return brokerage_configs + sorted_configs + filter_configs

def get_name(self) -> str:
"""Returns the user-friendly name which users can identify this object by.
Expand All @@ -86,7 +90,11 @@ def _check_if_config_passes_filters(self, config: Configuration, all_for_platfor
# skip, we want all configurations that match type and platform, for help
continue
target_value = self.get_config_value_from_name(condition._dependent_config_id)
if not target_value or not condition.check(target_value):
if not target_value:
return False
elif isinstance(target_value, dict):
return all(condition.check(value) for value in target_value.values())
elif not condition.check(target_value):
return False
return True

Expand Down Expand Up @@ -207,10 +215,27 @@ def config_build(self,
_logged_messages.add(log_message)
if type(configuration) is InternalInputUserInput:
continue
if isinstance(configuration, ChoiceUserInput) and len(configuration._choices) == 0:
logger.debug(f"skipping configuration '{configuration._id}': no choices available.")
continue
elif isinstance(configuration, AuthConfiguration):
auth_authorizations = get_authorization(container.api_client.auth0, self._display_name.lower(), logger)
logger.debug(f'auth: {auth_authorizations}')
configuration._value = auth_authorizations.authorization
configuration._value = auth_authorizations.get_authorization_config_without_account()
for inner_config in self._lean_configs:
if any(condition._dependent_config_id == configuration._id for condition in
inner_config._filter._conditions):
api_account_ids = auth_authorizations.get_account_ids()
config_dash = inner_config._id.replace('-', '_')
inner_config._choices = api_account_ids
if user_provided_options and config_dash in user_provided_options:
user_provide_account_id = user_provided_options[config_dash]
if (api_account_ids and len(api_account_ids) > 0 and
not any(account_id.lower() == user_provide_account_id.lower()
for account_id in api_account_ids)):
raise ValueError(f"The provided account id '{user_provide_account_id}' is not valid, "
f"available: {api_account_ids}")
break
continue

property_name = self.convert_lean_key_to_variable(configuration._id)
Expand Down
76 changes: 76 additions & 0 deletions tests/components/api/test_auth0_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
# Lean CLI v1.0. Copyright 2021 QuantConnect Corporation.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import responses
from unittest import mock
from lean.constants import API_BASE_URL
from lean.components.api.api_client import APIClient
from lean.components.util.http_client import HTTPClient


@responses.activate
def test_auth0client_trade_station() -> None:
api_clint = APIClient(mock.Mock(), HTTPClient(mock.Mock()), user_id="123", api_token="abc")

responses.add(
responses.POST,
f"{API_BASE_URL}live/auth0/read",
json={
"authorization": {
"trade-station-client-id": "123",
"trade-station-refresh-token": "456",
"accounts": [
{"id": "11223344", "name": "11223344 | Margin | USD"},
{"id": "55667788", "name": "55667788 | Futures | USD"}
]
},
"success": "true"},
status=200
)

brokerage_id = "TestBrokerage"

result = api_clint.auth0.read(brokerage_id)

assert result
assert result.authorization
assert len(result.authorization) > 0
assert len(result.get_authorization_config_without_account()) > 0
assert len(result.get_account_ids()) > 0


@responses.activate
def test_auth0client_alpaca() -> None:
api_clint = APIClient(mock.Mock(), HTTPClient(mock.Mock()), user_id="123", api_token="abc")

responses.add(
responses.POST,
f"{API_BASE_URL}live/auth0/read",
json={
"authorization": {
"alpaca-access-token": "XXXX-XXX-XXX-XXX-XXXXX-XX",
"accounts": [{"id": "XXXX-XXX-XXX-XXX-XXXXX-XX", "name": " |USD"}]
},
"success": "true"},
status=200
)

brokerage_id = "TestBrokerage"

result = api_clint.auth0.read(brokerage_id)

assert result
assert result.authorization
assert len(result.authorization) > 0
assert len(result.get_authorization_config_without_account()) > 0
assert len(result.get_account_ids()) > 0

0 comments on commit a7236c1

Please sign in to comment.