Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Modules case sensitivity fix #430

Merged
merged 2 commits into from
Mar 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions lean/commands/cloud/live/deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,12 +277,12 @@ def deploy(project: str,
if cash_balance_option != LiveInitialStateInput.NotSupported:
live_cash_balance = configure_initial_cash_balance(logger, cash_balance_option, live_cash_balance, last_cash)
elif live_cash_balance is not None and live_cash_balance != "":
raise RuntimeError(f"Custom cash balance setting is not available for {brokerage_instance.get_name()}")
raise RuntimeError(f"Custom cash balance setting is not available for {brokerage_instance}")

if holdings_option != LiveInitialStateInput.NotSupported:
live_holdings = configure_initial_holdings(logger, holdings_option, live_holdings, last_holdings)
elif live_holdings is not None and live_holdings != "":
raise RuntimeError(f"Custom portfolio holdings setting is not available for {brokerage_instance.get_name()}")
raise RuntimeError(f"Custom portfolio holdings setting is not available for {brokerage_instance}")

else:
# let the user choose the brokerage
Expand Down
6 changes: 3 additions & 3 deletions lean/commands/live/deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ def deploy(project: Path,

if environment_name in lean_config["environments"]:
lean_environment = lean_config["environments"][environment_name]
for key in ["live-mode-brokerage", "data-queue-handler"]:
for key in ["live-mode-brokerage", "data-queue-handler", "history-provider"]:
if key not in lean_environment:
raise MoreInfoError(f"The '{environment_name}' environment does not specify a {rename_internal_config_to_user_friendly_format(key)}",
"https://www.lean.io/docs/v2/lean-cli/live-trading/algorithm-control")
Expand Down Expand Up @@ -319,12 +319,12 @@ def deploy(project: Path,
if cash_balance_option != LiveInitialStateInput.NotSupported:
live_cash_balance = configure_initial_cash_balance(logger, cash_balance_option, live_cash_balance, last_cash)
elif live_cash_balance is not None and live_cash_balance != "":
raise RuntimeError(f"Custom cash balance setting is not available for {brokerage}")
raise RuntimeError(f"Custom cash balance setting is not available for {brokerage_instance}")

if holdings_option != LiveInitialStateInput.NotSupported:
live_holdings = configure_initial_holdings(logger, holdings_option, live_holdings, last_holdings)
elif live_holdings is not None and live_holdings != "":
raise RuntimeError(f"Custom portfolio holdings setting is not available for {brokerage}")
raise RuntimeError(f"Custom portfolio holdings setting is not available for {brokerage_instance}")

if live_cash_balance:
lean_config["live-cash-balance"] = live_cash_balance
Expand Down
3 changes: 3 additions & 0 deletions lean/components/config/lean_config_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,11 +235,14 @@ def get_complete_lean_config(self,
config["debugging"] = False
config["debugging-method"] = "LocalCmdline"

from lean.components.util.organization_manager import get_organization

# The following key -> value pairs are added to the config unless they are already set by the user
config_defaults = {
"job-user-id": self._cli_config_manager.user_id.get_value(default="0"),
"api-access-token": self._cli_config_manager.api_token.get_value(default=""),
"job-project-id": self._project_config_manager.get_local_id(algorithm_file.parent),
"job-organization-id": get_organization(config),

"ib-host": "127.0.0.1",
"ib-port": "4002",
Expand Down
31 changes: 21 additions & 10 deletions lean/components/util/json_modules_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,30 +44,41 @@ def non_interactive_config_build_for_name(lean_config: Dict[str, Any], target_mo
environment_name=environment_name)


def config_build_for_name(lean_config: Dict[str, Any], target_module_name: str, module_list: List[JsonModule],
properties: Dict[str, Any], logger: Logger, interactive: bool,
environment_name: str = None) -> JsonModule:
def find_module(target_module_name: str, module_list: List[JsonModule], logger: Logger) -> JsonModule:
target_module: JsonModule = None
# because we compare str we normalize everything to lower case
target_module_name = target_module_name.lower()
module_class_name = target_module_name.rfind('.')
for module in module_list:
if module.get_id() == target_module_name or module.get_name() == target_module_name:
# we search in the modules name and id
module_id = module.get_id().lower()
module_name = module.get_name().lower()

if module_id == target_module_name or module_name == target_module_name:
target_module = module
break
else:
index = target_module_name.rfind('.')
if (index != -1 and module.get_id() == target_module_name[index + 1:]
or module.get_name() == target_module_name[index + 1:]):
if (module_class_name != -1 and module_id == target_module_name[module_class_name + 1:]
or module_name == target_module_name[module_class_name + 1:]):
target_module = module
break

if not target_module:
for module in module_list:
if module.get_config_value_from_value(target_module_name):
# we search in the modules configuration values, this is for when the user provides an environment
if (module.is_value_in_config(target_module_name)
or module_class_name != -1 and module.is_value_in_config(target_module_name[module_class_name + 1:])):
target_module = module
if not target_module:
raise RuntimeError(f"""Failed to resolve module for name: '{target_module_name}'""")
else:
logger.debug(f'Found module \'{target_module_name}\' from given name')
logger.debug(f'Found module \'{target_module_name}\' from given name')
return target_module


def config_build_for_name(lean_config: Dict[str, Any], target_module_name: str, module_list: List[JsonModule],
properties: Dict[str, Any], logger: Logger, interactive: bool,
environment_name: str = None) -> JsonModule:
target_module = find_module(target_module_name, module_list, logger)
target_module.config_build(lean_config, logger, interactive=interactive, properties=properties,
environment_name=environment_name)
_update_settings(logger, environment_name, target_module, lean_config)
Expand Down
17 changes: 14 additions & 3 deletions lean/components/util/organization_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,29 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from typing import Optional
from typing import Optional, Dict, Any

from lean.components.config.lean_config_manager import LeanConfigManager
from lean.components.util.logger import Logger


def get_organization(config: Dict[str, Any]):
organization_id = None
if "job-organization-id" in config:
organization_id = config["job-organization-id"]
elif "organization-id" in config:
# for backwards compatibility with local platform and old lean cli setups
organization_id = config["organization-id"]
return organization_id


class OrganizationManager:
"""The OrganizationManager class provides utilities to handle the working organization."""

def __init__(self, logger: Logger, lean_config_manager: LeanConfigManager) -> None:
"""Creates a new OrganizationManager instance.

:param logger: the logger to use to log messages with
:param api_client: the API client to use to fetch organizations info
:param lean_config_manager: the LeanConfigManager to use to manipulate the lean configuration file
"""
self._logger = logger
Expand All @@ -39,7 +48,7 @@ def get_working_organization_id(self) -> Optional[str]:
"""
if self._working_organization_id is None:
lean_config = self._lean_config_manager.get_lean_config()
self._working_organization_id = lean_config.get("organization-id")
self._working_organization_id = get_organization(lean_config)

return self._working_organization_id

Expand All @@ -64,5 +73,7 @@ def configure_working_organization_id(self, organization_id: str) -> None:

:param organization_id: the working organization di
"""
self._lean_config_manager.set_properties({"job-organization-id": organization_id})
# for backwards compatibility with local platform
self._lean_config_manager.set_properties({"organization-id": organization_id})
self._working_organization_id = organization_id
13 changes: 10 additions & 3 deletions lean/models/json_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,16 @@ def get_config_value_from_name(self, target_name: str) -> str:
if self._lean_configs[i]._id == target_name]
return self._lean_configs[idx]._value

def get_config_value_from_value(self, value: str) -> bool:
def is_value_in_config(self, searched_value: str) -> bool:
searched_value = searched_value.lower()
for i in range(len(self._lean_configs)):
if value in self._lean_configs[i]._value:
value = self._lean_configs[i]._value
if isinstance(value, str):
value = value.lower()
if isinstance(value, list):
value = [x.lower() for x in value]

if searched_value in value:
return True
return False

Expand Down Expand Up @@ -223,7 +230,7 @@ def config_build(self,

def ensure_module_installed(self, organization_id: str) -> None:
if not self._is_module_installed and self._installs:
container.logger.debug(f"JsonModule.ensure_module_installed(): installing module for module {self._id}: {self._product_id}")
container.logger.debug(f"JsonModule.ensure_module_installed(): installing module {self}: {self._product_id}")
container.module_manager.install_module(
self._product_id, organization_id)
self._is_module_installed = True
Expand Down
4 changes: 2 additions & 2 deletions tests/commands/cloud/live/test_cloud_live_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ def test_cloud_live_deploy_with_live_holdings(brokerage: str, holdings: str) ->
"--node", "live", "--auto-restart", "yes", "--notify-order-events", "no",
"--notify-insights", "no", *options, "--data-provider-live", "QuantConnect"])

if (brokerage != "Paper Trading" and holdings != "")\
if (brokerage != "Paper Trading" and brokerage != "Binance" and holdings != "")\
or brokerage == "Terminal Link": # non-cloud brokerage
assert result.exit_code != 0
api_client.live.start.assert_not_called()
Expand All @@ -348,7 +348,7 @@ def test_cloud_live_deploy_with_live_holdings(brokerage: str, holdings: str) ->
{"symbol": "AA", "symbolId": "AA 2T", "quantity": 2, "averagePrice": 20.35}]
elif len(holding) == 1:
holding_list = [{"symbol": "A", "symbolId": "A 2T", "quantity": 1, "averagePrice": 145.1}]
elif brokerage == "Paper Trading":
elif brokerage == "Paper Trading" or brokerage == "Binance":
holding_list = []

api_client.live.start.assert_called_once_with(mock.ANY,
Expand Down
1 change: 1 addition & 0 deletions tests/commands/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ def test_init_creates_clean_config_file_from_repo() -> None:
assert config_path.read_text(encoding="utf-8") == """
{{
"data-folder": "data",
"job-organization-id": "{0}",
"organization-id": "{0}"
}}
""".format(_get_test_organization().id).strip()
Expand Down
2 changes: 1 addition & 1 deletion tests/commands/test_live.py
Original file line number Diff line number Diff line change
Expand Up @@ -1004,7 +1004,7 @@ def test_live_passes_live_holdings_to_lean_runner_when_given_as_option(brokerage
result = CliRunner().invoke(lean, ["live", "Python Project", "--brokerage", brokerage, "--live-holdings", holdings,
"--data-provider-live", "Custom data only", *options])

if brokerage not in ["Paper Trading", "Terminal Link"] and holdings != "":
if brokerage not in ["Paper Trading", "Terminal Link", "Binance"] and holdings != "":
assert result.exit_code != 0
lean_runner.run_lean.start.assert_not_called()
return
Expand Down
49 changes: 49 additions & 0 deletions tests/components/util/test_json_modules_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# 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.
from unittest.mock import MagicMock

import pytest

from lean.components.util.json_modules_handler import find_module
from lean.constants import MODULE_CLI_PLATFORM, MODULE_BROKERAGE
from lean.models.json_module import JsonModule
from tests.test_helpers import create_fake_lean_cli_directory


@pytest.mark.parametrize("id,display,search_name", [("ads", "binAnce", "BiNAnce"),
("binAnce", "a", "BiNAnce"),
("ads", "binAnce", "QC.Brokerage.Binance.BiNAnce"),
("binAnce", "a", "QC.Brokerage.Binance.BiNAnce")])
def test_finds_module_case_insensitive_name(id: str, display: str, search_name: str) -> None:
create_fake_lean_cli_directory()

module = JsonModule({"id": id, "configurations": [], "display-id": display},
MODULE_BROKERAGE, MODULE_CLI_PLATFORM)
result = find_module(search_name, [module], MagicMock())
assert result == module


@pytest.mark.parametrize("searching,expected", [("ads", False),
("BinanceFuturesBrokerage", True)])
def test_is_value_in_config(searching: str, expected: bool) -> None:
module = JsonModule({"id": "asd", "configurations": [
{
"id": "live-mode-brokerage",
"type": "info",
"value": "BinanceFuturesBrokerage"
}
], "display-id": "OUS"}, MODULE_BROKERAGE, MODULE_CLI_PLATFORM)

result = module.is_value_in_config(searching)

assert expected == result
4 changes: 2 additions & 2 deletions tests/components/util/test_organization_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ def test_try_get_id_aborts_if_organization_id_is_not_in_the_lean_config() -> Non

def test_organization_manager_sets_working_organization_id_in_lean_config():
organization_id = "abc123"
lean_config_updates = {"organization-id": organization_id}

lean_config_manager = mock.Mock()
lean_config_manager.set_properties = mock.Mock()
Expand All @@ -57,4 +56,5 @@ def test_organization_manager_sets_working_organization_id_in_lean_config():

organization_manager.configure_working_organization_id(organization_id)

lean_config_manager.set_properties.assert_called_once_with(lean_config_updates)
lean_config_manager.set_properties.assert_has_calls(calls=[mock.call({"job-organization-id": organization_id}),
mock.call({"organization-id": organization_id})])
Loading