Skip to content

Commit a031b02

Browse files
authored
Optional Downloader Data Provider (#424)
* feat: optional download data provider * feat: test to download data provider with different params * feat: installed data provider list * feat: general tests * refactor: separate data-provider from data-feed * fix: validation of amount of request on api * feat: remove default value for data-provider-historical * feat: test case with local historical provider * remove: repeat test case refactor: assertion in test with different version of py * fix: missed rename data_feed to data_provider * feat: additional test case * revert: data_provider -> data_feed commit: e52b3a4 * revert: repeat code of finding data-provider * rename: helper method more fit name * rename: missed PR: #416
1 parent fe5c546 commit a031b02

File tree

4 files changed

+234
-12
lines changed

4 files changed

+234
-12
lines changed

lean/commands/live/deploy.py

+17-8
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ def _install_modules(modules: List[LeanConfigConfigurer], user_kwargs: Dict[str,
7979
:param modules: the modules to check
8080
"""
8181
for module in modules:
82-
if not module._installs:
82+
if module is None or not module._installs:
8383
continue
8484
organization_id = container.organization_manager.try_get_working_organization_id()
8585
module.ensure_module_installed(organization_id)
@@ -192,6 +192,15 @@ def _configure_lean_config_interactively(lean_config: Dict[str, Any],
192192

193193
_cached_lean_config = None
194194

195+
def _try_get_data_historical_name(data_provider_historical_name: str, data_provider_live_name: str) -> str:
196+
""" Get name for historical data provider based on data provider live (if exist)
197+
198+
:param data_provider_historical_name: the current (default) data provider historical
199+
:param data_provider_live_name: the current data provider live name
200+
"""
201+
return next((live_data_historical.get_name() for live_data_historical in all_data_providers
202+
if live_data_historical.get_name() in data_provider_live_name), data_provider_historical_name)
203+
195204

196205
# being used by lean.models.click_options.get_the_correct_type_default_value()
197206
def _get_default_value(key: str) -> Optional[Any]:
@@ -235,7 +244,6 @@ def _get_default_value(key: str) -> Optional[Any]:
235244
help="The live data provider to use")
236245
@option("--data-provider-historical",
237246
type=Choice([dp.get_name() for dp in all_data_providers if dp._id != "TerminalLinkBrokerage"], case_sensitive=False),
238-
default="Local",
239247
help="Update the Lean configuration file to retrieve data from the given historical provider")
240248
@options_from_json(get_configs_for_options("live-local"))
241249
@option("--release",
@@ -387,7 +395,7 @@ def deploy(project: Path,
387395
[update_essential_properties_available(data_feed_configurers, kwargs)]
388396

389397
elif brokerage is not None or len(data_provider_live) > 0:
390-
ensure_options(["brokerage", "data_feed"])
398+
ensure_options(["brokerage", "data_provider_live"])
391399

392400
environment_name = "lean-cli"
393401
lean_config = lean_config_manager.get_complete_lean_config(environment_name, algorithm_file, None)
@@ -408,9 +416,10 @@ def deploy(project: Path,
408416
lean_config = lean_config_manager.get_complete_lean_config(environment_name, algorithm_file, None)
409417
_configure_lean_config_interactively(lean_config, environment_name, kwargs, show_secrets=show_secrets)
410418

411-
if data_provider_historical is not None:
412-
[data_provider_configurer] = [get_and_build_module(data_provider_historical, all_data_providers, kwargs, logger)]
413-
data_provider_configurer.configure(lean_config, environment_name)
419+
if data_provider_historical is None:
420+
data_provider_historical = _try_get_data_historical_name("Local", data_provider_live)
421+
[data_provider_configurer] = [get_and_build_module(data_provider_historical, all_data_providers, kwargs, logger)]
422+
data_provider_configurer.configure(lean_config, environment_name)
414423

415424
if "environments" not in lean_config or environment_name not in lean_config["environments"]:
416425
lean_config_path = lean_config_manager.get_lean_config_path()
@@ -422,7 +431,7 @@ def deploy(project: Path,
422431
"https://www.lean.io/docs/v2/lean-cli/live-trading/brokerages/quantconnect-paper-trading")
423432

424433
env_brokerage, env_data_queue_handlers = _get_configurable_modules_from_environment(lean_config, environment_name)
425-
_install_modules([env_brokerage] + env_data_queue_handlers, kwargs)
434+
_install_modules([env_brokerage] + env_data_queue_handlers + [data_provider_configurer], kwargs)
426435

427436
_raise_for_missing_properties(lean_config, environment_name, lean_config_manager.get_lean_config_path())
428437

@@ -497,4 +506,4 @@ def deploy(project: Path,
497506
raise RuntimeError(f"InteractiveBrokers is currently not supported for ARM hosts")
498507

499508
lean_runner = container.lean_runner
500-
lean_runner.run_lean(lean_config, environment_name, algorithm_file, output, engine_image, None, release, detach, loads(extra_docker_config))
509+
lean_runner.run_lean(lean_config, environment_name, algorithm_file, output, engine_image, None, release, detach, loads(extra_docker_config))

lean/models/data_providers/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,4 @@
2323

2424
# QuantConnect DataProvider
2525
[QuantConnectDataProvider] = [
26-
data_provider for data_provider in all_data_providers if data_provider._id == "QuantConnect"]
26+
data_provider for data_provider in all_data_providers if data_provider._id == "QuantConnect"]

tests/commands/test_live.py

+206-3
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@
2525
from lean.constants import DEFAULT_ENGINE_IMAGE
2626
from lean.container import container
2727
from lean.models.docker import DockerImage
28-
from tests.test_helpers import create_fake_lean_cli_directory
28+
from tests.test_helpers import create_fake_lean_cli_directory, reset_state_installed_modules
2929
from tests.conftest import initialize_container
30+
from click.testing import Result
3031

3132
ENGINE_IMAGE = DockerImage.parse(DEFAULT_ENGINE_IMAGE)
3233

@@ -413,7 +414,21 @@ def test_live_sets_dependent_configurations_from_modules_json_based_on_environme
413414
"Terminal Link": terminal_link_required_options,
414415
"Kraken": brokerage_required_options["Kraken"],
415416
"TDAmeritrade": brokerage_required_options["TDAmeritrade"],
416-
"Bybit": brokerage_required_options["Bybit"]
417+
"Bybit": brokerage_required_options["Bybit"],
418+
}
419+
420+
data_provider_required_options = {
421+
"IEX": {
422+
"iex-cloud-api-key": "123",
423+
"iex-price-plan": "Launch",
424+
},
425+
"Polygon": {
426+
"polygon-api-key": "123",
427+
},
428+
"AlphaVantage": {
429+
"alpha-vantage-api-key": "111",
430+
"alpha-vantage-price-plan": "Free"
431+
}
417432
}
418433

419434

@@ -530,7 +545,7 @@ def test_live_non_interactive_raise_error_when_missing_data_provider_live_option
530545

531546
error_msg = str(result.exc_info[1]).split()
532547

533-
assert "data-provider-live" in error_msg
548+
assert "--data-provider-live" in error_msg
534549
assert "data-queue-handler" not in error_msg
535550

536551
assert result.exit_code != 0
@@ -1026,3 +1041,191 @@ def test_live_passes_live_holdings_to_lean_runner_when_given_as_option(brokerage
10261041
return
10271042

10281043
assert args[0]["live-holdings"] == holding_list
1044+
1045+
def test_live_non_interactive_deploy_with_live_and_historical_provider_missed_historical_not_optional_config() -> None:
1046+
create_fake_lean_cli_directory()
1047+
create_fake_environment("live-paper", True)
1048+
1049+
container.initialize(docker_manager=mock.Mock(), lean_runner=mock.Mock(), api_client = mock.MagicMock())
1050+
1051+
provider_live_option = ["--data-provider-live", "IEX",
1052+
"--iex-cloud-api-key", "123",
1053+
"--iex-price-plan", "Launch"]
1054+
1055+
provider_history_option = ["--data-provider-historical", "Polygon"]
1056+
# "--polygon-api-key", "123"]
1057+
1058+
result = CliRunner().invoke(lean, ["live", "deploy" , "--brokerage", "Paper Trading",
1059+
*provider_live_option,
1060+
*provider_history_option,
1061+
"Python Project",
1062+
])
1063+
error_msg = str(result.exc_info[1]).split()
1064+
1065+
assert "--polygon-api-key" in error_msg
1066+
assert "--iex-cloud-api-key" not in error_msg
1067+
1068+
assert result.exit_code == 1
1069+
1070+
def test_live_non_interactive_deploy_with_live_and_historical_provider_missed_live_not_optional_config() -> None:
1071+
create_fake_lean_cli_directory()
1072+
create_fake_environment("live-paper", True)
1073+
1074+
container.initialize(docker_manager=mock.Mock(), lean_runner=mock.Mock(), api_client = mock.MagicMock())
1075+
1076+
provider_live_option = ["--data-provider-live", "IEX",
1077+
"--iex-cloud-api-key", "123"]
1078+
#"--iex-price-plan", "Launch"]
1079+
1080+
provider_history_option = ["--data-provider-historical", "Polygon", "--polygon-api-key", "123"]
1081+
1082+
result = CliRunner().invoke(lean, ["live", "deploy" , "--brokerage", "Paper Trading",
1083+
*provider_live_option,
1084+
*provider_history_option,
1085+
"Python Project",
1086+
])
1087+
1088+
error_msg = str(result.exc_info[1]).split()
1089+
1090+
assert "--iex-price-plan" in error_msg
1091+
assert "--polygon-api-key" not in error_msg
1092+
1093+
assert result.exit_code == 1
1094+
1095+
def test_live_non_interactive_deploy_with_real_brokerage_without_credentials() -> None:
1096+
create_fake_lean_cli_directory()
1097+
create_fake_environment("live-paper", True)
1098+
1099+
container.initialize(docker_manager=mock.Mock(), lean_runner=mock.Mock(), api_client = mock.MagicMock())
1100+
1101+
# create fake environment has IB configs already
1102+
brokerage = ["--brokerage", "OANDA"]
1103+
1104+
provider_live_option = ["--data-provider-live", "IEX",
1105+
"--iex-cloud-api-key", "123",
1106+
"--iex-price-plan", "Launch"]
1107+
1108+
result = CliRunner().invoke(lean, ["live", "deploy" ,
1109+
*brokerage,
1110+
*provider_live_option,
1111+
"Python Project",
1112+
])
1113+
assert result.exit_code == 1
1114+
1115+
error_msg = str(result.exc_info[1]).split()
1116+
1117+
assert "--oanda-account-id" in error_msg
1118+
assert "--oanda-access-token" in error_msg
1119+
assert "--oanda-environment" in error_msg
1120+
assert "--iex-price-plan" not in error_msg
1121+
1122+
def create_lean_option(brokerage_name: str, data_provider_live_name: str, data_provider_historical_name: str, api_client: any) -> Result:
1123+
reset_state_installed_modules()
1124+
create_fake_lean_cli_directory()
1125+
create_fake_environment("live-paper", True)
1126+
1127+
initialize_container(api_client_to_use=api_client)
1128+
1129+
option = ["--brokerage", brokerage_name]
1130+
for key, value in brokerage_required_options[brokerage_name].items():
1131+
option.extend([f"--{key}", value])
1132+
1133+
data_feed_required_options.update(data_provider_required_options)
1134+
1135+
option.extend(["--data-provider-live", data_provider_live_name])
1136+
for key, value in data_feed_required_options[data_provider_live_name].items():
1137+
if f"--{key}" not in option:
1138+
option.extend([f"--{key}", value])
1139+
1140+
if data_provider_historical_name is not None:
1141+
option.extend(["--data-provider-historical", data_provider_historical_name])
1142+
if data_provider_historical_name is not "Local":
1143+
for key, value in data_feed_required_options[data_provider_historical_name].items():
1144+
if f"--{key}" not in option:
1145+
option.extend([f"--{key}", value])
1146+
1147+
result = CliRunner().invoke(lean, ["live", "deploy",
1148+
*option,
1149+
"Python Project",
1150+
])
1151+
assert result.exit_code == 0
1152+
return result
1153+
1154+
@pytest.mark.parametrize("brokerage_name,data_provider_live_name,data_provider_historical_name,brokerage_product_id,data_provider_live_product_id,data_provider_historical_id",
1155+
[("Interactive Brokers", "IEX", "Polygon", "181", "333", "306"),
1156+
("Paper Trading", "IEX", "Polygon", None, "333", "306"),
1157+
("Tradier", "IEX", "AlphaVantage", "185", "333", "334"),
1158+
("Paper Trading", "IEX", "Local", None, "333", "222")])
1159+
def test_live_deploy_with_different_brokerage_and_different_live_data_provider_and_historical_data_provider(brokerage_name: str, data_provider_live_name: str, data_provider_historical_name: str, brokerage_product_id: str, data_provider_live_product_id: str, data_provider_historical_id: str) -> None:
1160+
api_client = mock.MagicMock()
1161+
create_lean_option(brokerage_name, data_provider_live_name, data_provider_historical_name, api_client)
1162+
1163+
is_exists = []
1164+
if brokerage_product_id is None and data_provider_historical_name != "Local":
1165+
assert len(api_client.method_calls) == 3
1166+
for m_c, id in zip(api_client.method_calls, [data_provider_live_product_id, data_provider_historical_id]):
1167+
if id in m_c[1]:
1168+
is_exists.append(True)
1169+
assert is_exists
1170+
assert len(is_exists) == 2
1171+
elif brokerage_product_id is None and data_provider_historical_name == "Local":
1172+
assert len(api_client.method_calls) == 2
1173+
if data_provider_live_product_id in api_client.method_calls[0][1]:
1174+
is_exists.append(True)
1175+
assert is_exists
1176+
assert len(is_exists) == 1
1177+
else:
1178+
assert len(api_client.method_calls) == 3
1179+
for m_c, id in zip(api_client.method_calls, [brokerage_product_id, data_provider_live_product_id, data_provider_historical_id]):
1180+
if id in f"{m_c[1]}":
1181+
is_exists.append(True)
1182+
assert is_exists
1183+
assert len(is_exists) == 3
1184+
1185+
@pytest.mark.parametrize("brokerage_name,data_provider_live_name,brokerage_product_id,data_provider_live_product_id",
1186+
[("Interactive Brokers", "IEX", "181", "333"),
1187+
("Tradier", "IEX", "185", "333")])
1188+
def test_live_non_interactive_deploy_with_different_brokerage_and_different_live_data_provider(brokerage_name: str, data_provider_live_name: str, brokerage_product_id: str, data_provider_live_product_id: str) -> None:
1189+
api_client = mock.MagicMock()
1190+
create_lean_option(brokerage_name, data_provider_live_name, None, api_client)
1191+
1192+
assert len(api_client.method_calls) == 2
1193+
is_exists = []
1194+
for m_c, id in zip(api_client.method_calls, [brokerage_product_id, data_provider_live_product_id]):
1195+
if id in m_c[1]:
1196+
is_exists.append(True)
1197+
1198+
assert is_exists
1199+
assert len(is_exists) == 2
1200+
1201+
@pytest.mark.parametrize("brokerage_name,data_provider_live_name,brokerage_product_id",
1202+
[("Bybit", "Bybit", "305"),
1203+
("Coinbase Advanced Trade", "Coinbase Advanced Trade", "183"),
1204+
("Interactive Brokers", "Interactive Brokers", "181"),
1205+
("Tradier", "Tradier", "185")])
1206+
def test_live_non_interactive_deploy_with_different_brokerage_with_the_same_live_data_provider(brokerage_name: str, data_provider_live_name: str, brokerage_product_id: str) -> None:
1207+
api_client = mock.MagicMock()
1208+
create_lean_option(brokerage_name, data_provider_live_name, None, api_client)
1209+
1210+
print(api_client.call_args_list)
1211+
print(api_client.call_args)
1212+
1213+
for m_c in api_client.method_calls:
1214+
if brokerage_product_id in m_c[1]:
1215+
is_exist = True
1216+
1217+
assert is_exist
1218+
1219+
@pytest.mark.parametrize("brokerage_name,data_provider_live_name,data_provider_live_product_id",
1220+
[("Paper Trading", "IEX", "333"),
1221+
("Paper Trading", "Polygon", "306")])
1222+
def test_live_non_interactive_deploy_paper_brokerage_different_live_data_provider(brokerage_name: str, data_provider_live_name: str, data_provider_live_product_id: str) -> None:
1223+
api_client = mock.MagicMock()
1224+
create_lean_option(brokerage_name, data_provider_live_name, None, api_client)
1225+
1226+
assert len(api_client.method_calls) == 2
1227+
for m_c in api_client.method_calls:
1228+
if data_provider_live_product_id in m_c[1]:
1229+
is_exist = True
1230+
1231+
assert is_exist

tests/test_helpers.py

+10
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
from datetime import datetime
1616
from pathlib import Path
1717
from typing import List
18+
from lean.models.data_providers import all_data_providers
19+
from lean.models.brokerages.local import all_local_brokerages, all_local_data_feeds
1820

1921
from lean.commands.create_project import (DEFAULT_CSHARP_MAIN, DEFAULT_CSHARP_NOTEBOOK, DEFAULT_PYTHON_MAIN,
2022
DEFAULT_PYTHON_NOTEBOOK, LIBRARY_PYTHON_MAIN, LIBRARY_CSHARP_MAIN)
@@ -224,3 +226,11 @@ def create_lean_environments() -> List[QCLeanEnvironment]:
224226
description="",
225227
public=True)
226228
]
229+
230+
def reset_state_installed_modules() -> None:
231+
for data_provider in all_data_providers:
232+
data_provider.__setattr__("_is_module_installed", False)
233+
for local_brokerage in all_local_brokerages:
234+
local_brokerage.__setattr__("_is_module_installed", False)
235+
for local_data_feed in all_local_data_feeds:
236+
local_data_feed.__setattr__("_is_module_installed", False)

0 commit comments

Comments
 (0)