Repository: QuantConnect/lean-cli
Pull request #500 (2024-09-18 20:44:43)

Feature: support new config type `account-id`
#### Related PR
- https://github.com/QuantConnect/Lean.Brokerages.TradeStation/pull/27

#### How Has This Been Tested?
##### Valid `account-id`
`lean live deploy mynewalgo --brokerage TradeStation --trade-station-account-id "SIMXXXXXXXM"`
![image](https://github.com/user-attachments/assets/ee512dcc-dcfe-4b0f-b998-52b628edd583)

##### Invalid `account-id`
`lean live deploy mynewalgo --brokerage tradestation --data-provider-live tradestation --trade-station-account-id SIMXXX`
![image](https://github.com/user-attachments/assets/8bf186e5-641d-4697-a0fa-b7901c926919)





Patch:
diff --git a/README.md b/README.md
index 5629fa1..86ecd8d 100644
--- a/README.md
+++ b/README.md
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
@@ -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
@@ -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
diff --git a/lean/models/api.py b/lean/models/api.py
index 14851de..99a6950 100644
--- a/lean/models/api.py
+++ b/lean/models/api.py
@@ -21,8 +21,34 @@ from lean.models.pydantic import WrappedBaseModel, validator
 # 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
diff --git a/lean/models/click_options.py b/lean/models/click_options.py
index 15d9534..6552633 100644
--- a/lean/models/click_options.py
+++ b/lean/models/click_options.py
@@ -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()
diff --git a/lean/models/configuration.py b/lean/models/configuration.py
index 867abb2..b3cd23d 100644
--- a/lean/models/configuration.py
+++ b/lean/models/configuration.py
@@ -96,10 +96,12 @@ class Configuration(ABC):
         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
@@ -137,6 +139,10 @@ class Filter:
         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.
diff --git a/lean/models/json_module.py b/lean/models/json_module.py
index 8b18f4e..c07d177 100644
--- a/lean/models/json_module.py
+++ b/lean/models/json_module.py
@@ -22,7 +22,7 @@ from lean.components.util.logger import Logger
 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
 
@@ -60,13 +60,17 @@ class JsonModule(ABC):
 
     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.
@@ -86,7 +90,11 @@ class JsonModule(ABC):
                     # 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
 
@@ -207,10 +215,27 @@ class JsonModule(ABC):
                     _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)


Test patch:
diff --git a/tests/components/api/test_auth0_client.py b/tests/components/api/test_auth0_client.py
new file mode 100644
index 0000000..5495747
--- /dev/null
+++ b/tests/components/api/test_auth0_client.py
@@ -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