Repository: QuantConnect/lean-cli
Pull request #442 (2024-04-04 20:41:49)

Json modules path configs support
- Add support for Json modules to have configs that are paths and require mounting in the container
- Support for FactSet data provider
    - FactSet credentials are a Json file, so this module uses a config that needs to be mounted to the container so that the data source can read the file and use them.


Patch:
diff --git a/README.md b/README.md
index d72b795..3f9ed30 100644
--- a/README.md
+++ b/README.md
@@ -143,7 +143,7 @@ Options:
   -d, --detach                    Run the backtest in a detached Docker container and return immediately
   --debug [pycharm|ptvsd|vsdbg|rider|local-platform]
                                   Enable a certain debugging method (see --help for more information)
-  --data-provider-historical [Binance|Kraken|IQFeed|Polygon|IEX|AlphaVantage|CoinApi|QuantConnect|Local|Terminal Link]
+  --data-provider-historical [Binance|Kraken|IQFeed|Polygon|FactSet|IEX|AlphaVantage|CoinApi|QuantConnect|Local|Terminal Link]
                                   Update the Lean configuration file to retrieve data from the given historical provider
   --binance-exchange-name [Binance|BinanceUS|Binance-USDM-Futures|Binance-COIN-Futures]
                                   Binance exchange name [Binance, BinanceUS, Binance-USDM-Futures, Binance-COIN-Futures]
@@ -161,6 +161,8 @@ Options:
   --iqfeed-version TEXT           The product version of your IQFeed developer account
   --iqfeed-host TEXT              The IQFeed host address
   --polygon-api-key TEXT          Your Polygon.io API Key
+  --factset-auth-config-file FILE
+                                  The path to the FactSet authentication configuration file
   --iex-cloud-api-key TEXT        Your iexcloud.io API token publishable key
   --iex-price-plan [Launch|Grow|Enterprise]
                                   Your IEX Cloud Price plan
@@ -1081,7 +1083,7 @@ Options:
                                   The brokerage to use
   --data-provider-live [Interactive Brokers|Tradier|Oanda|Bitfinex|Coinbase Advanced Trade|Binance|Zerodha|Samco|Terminal Link|Trading Technologies|Kraken|TDAmeritrade|IQFeed|Polygon|IEX|CoinApi|Custom data only|Bybit]
                                   The live data provider to use
-  --data-provider-historical [Binance|Kraken|IQFeed|Polygon|IEX|AlphaVantage|CoinApi|QuantConnect|Local]
+  --data-provider-historical [Binance|Kraken|IQFeed|Polygon|FactSet|IEX|AlphaVantage|CoinApi|QuantConnect|Local]
                                   Update the Lean configuration file to retrieve data from the given historical provider
   --ib-user-name TEXT             Your Interactive Brokers username
   --ib-account TEXT               Your Interactive Brokers account id
@@ -1194,6 +1196,8 @@ Options:
   --coinapi-api-key TEXT          Your coinapi.io Api Key
   --coinapi-product [Free|Startup|Streamer|Professional|Enterprise]
                                   CoinApi pricing plan (https://www.coinapi.io/market-data-api/pricing)
+  --factset-auth-config-file FILE
+                                  The path to the FactSet authentication configuration file
   --alpha-vantage-api-key TEXT    Your Alpha Vantage Api Key
   --alpha-vantage-price-plan [Free|Plan30|Plan75|Plan150|Plan300|Plan600|Plan1200]
                                   Your Alpha Vantage Premium API Key plan
@@ -1486,7 +1490,7 @@ Options:
   --parameter <TEXT FLOAT FLOAT FLOAT>...
                                   The 'parameter min max step' pairs configuring the parameters to optimize
   --constraint TEXT               The 'statistic operator value' pairs configuring the constraints of the optimization
-  --data-provider-historical [Binance|Kraken|IQFeed|Polygon|IEX|AlphaVantage|CoinApi|QuantConnect|Local|Terminal Link]
+  --data-provider-historical [Binance|Kraken|IQFeed|Polygon|FactSet|IEX|AlphaVantage|CoinApi|QuantConnect|Local|Terminal Link]
                                   Update the Lean configuration file to retrieve data from the given historical provider
   --download-data                 Update the Lean configuration file to download data from the QuantConnect API, alias
                                   for --data-provider-historical QuantConnect
@@ -1517,6 +1521,8 @@ Options:
   --iqfeed-version TEXT           The product version of your IQFeed developer account
   --iqfeed-host TEXT              The IQFeed host address
   --polygon-api-key TEXT          Your Polygon.io API Key
+  --factset-auth-config-file FILE
+                                  The path to the FactSet authentication configuration file
   --iex-cloud-api-key TEXT        Your iexcloud.io API token publishable key
   --iex-price-plan [Launch|Grow|Enterprise]
                                   Your IEX Cloud Price plan
@@ -1641,7 +1647,7 @@ Usage: lean research [OPTIONS] PROJECT
 
 Options:
   --port INTEGER                  The port to run Jupyter Lab on (defaults to 8888)
-  --data-provider-historical [Binance|Kraken|IQFeed|Polygon|IEX|AlphaVantage|CoinApi|QuantConnect|Local|Terminal Link]
+  --data-provider-historical [Binance|Kraken|IQFeed|Polygon|FactSet|IEX|AlphaVantage|CoinApi|QuantConnect|Local|Terminal Link]
                                   Update the Lean configuration file to retrieve data from the given historical provider
   --binance-exchange-name [Binance|BinanceUS|Binance-USDM-Futures|Binance-COIN-Futures]
                                   Binance exchange name [Binance, BinanceUS, Binance-USDM-Futures, Binance-COIN-Futures]
@@ -1659,6 +1665,8 @@ Options:
   --iqfeed-version TEXT           The product version of your IQFeed developer account
   --iqfeed-host TEXT              The IQFeed host address
   --polygon-api-key TEXT          Your Polygon.io API Key
+  --factset-auth-config-file FILE
+                                  The path to the FactSet authentication configuration file
   --iex-cloud-api-key TEXT        Your iexcloud.io API token publishable key
   --iex-price-plan [Launch|Grow|Enterprise]
                                   Your IEX Cloud Price plan
diff --git a/lean/commands/live/deploy.py b/lean/commands/live/deploy.py
index ffc3347..25a28c2 100644
--- a/lean/commands/live/deploy.py
+++ b/lean/commands/live/deploy.py
@@ -282,9 +282,11 @@ def deploy(project: Path,
                                                                  environment_name=environment_name))
 
     organization_id = container.organization_manager.try_get_working_organization_id()
+    paths_to_mount = {}
     for module in (data_provider_live_instances + [data_downloader_instances, brokerage_instance]
                    + history_providers_instances):
         module.ensure_module_installed(organization_id)
+        paths_to_mount.update(module.get_paths_to_mount())
 
     if not lean_config["environments"][environment_name]["live-mode"]:
         raise MoreInfoError(f"The '{environment_name}' is not a live trading environment (live-mode is set to false)",
@@ -362,4 +364,13 @@ def deploy(project: Path,
             raise RuntimeError(f"InteractiveBrokers is currently not supported for ARM hosts")
 
     lean_runner = container.lean_runner
-    lean_runner.run_lean(lean_config, environment_name, algorithm_file, output, engine_image, None, release, detach, loads(extra_docker_config))
+    lean_runner.run_lean(lean_config,
+                         environment_name,
+                         algorithm_file,
+                         output,
+                         engine_image,
+                         None,
+                         release,
+                         detach,
+                         loads(extra_docker_config),
+                         paths_to_mount)
diff --git a/lean/commands/optimize.py b/lean/commands/optimize.py
index 92611b2..7df8065 100644
--- a/lean/commands/optimize.py
+++ b/lean/commands/optimize.py
@@ -305,11 +305,14 @@ def optimize(project: Path,
     if download_data:
         data_provider_historical = "QuantConnect"
 
+    paths_to_mount = None
+
     if data_provider_historical is not None:
         data_provider = non_interactive_config_build_for_name(lean_config, data_provider_historical,
                                                               cli_data_downloaders, kwargs, logger, environment_name)
         data_provider.ensure_module_installed(organization_id)
         container.lean_config_manager.set_properties(data_provider.get_settings())
+        paths_to_mount = data_provider.get_paths_to_mount()
 
     lean_config_manager.configure_data_purchase_limit(lean_config, data_purchase_limit)
 
@@ -339,7 +342,7 @@ def optimize(project: Path,
     container.update_manager.pull_docker_image_if_necessary(engine_image, update, no_update)
 
     run_options = lean_runner.get_basic_docker_config(lean_config, algorithm_file, output, None, release, should_detach,
-                                                      engine_image)
+                                                      engine_image, paths_to_mount)
 
     run_options["working_dir"] = "/Lean/Optimizer.Launcher/bin/Debug"
     run_options["commands"].append(f"dotnet QuantConnect.Optimizer.Launcher.dll{' --estimate' if estimate else ''}")
diff --git a/lean/commands/research.py b/lean/commands/research.py
index 4271558..6596e87 100644
--- a/lean/commands/research.py
+++ b/lean/commands/research.py
@@ -113,12 +113,15 @@ def research(project: Path,
     if download_data:
         data_provider_historical = "QuantConnect"
 
+    paths_to_mount = None
+
     if data_provider_historical is not None:
         organization_id = container.organization_manager.try_get_working_organization_id()
         data_provider = non_interactive_config_build_for_name(lean_config, data_provider_historical,
                                                               cli_data_downloaders, kwargs, logger, environment_name)
         data_provider.ensure_module_installed(organization_id)
         container.lean_config_manager.set_properties(data_provider.get_settings())
+        paths_to_mount = data_provider.get_paths_to_mount()
     lean_config_manager.configure_data_purchase_limit(lean_config, data_purchase_limit)
 
     lean_runner = container.lean_runner
@@ -145,7 +148,8 @@ def research(project: Path,
                                                       None,
                                                       False,
                                                       detach,
-                                                      research_image)
+                                                      research_image,
+                                                      paths_to_mount)
 
     # Mount the config in the notebooks directory as well
     local_config_path = next(m["Source"] for m in run_options["mounts"] if m["Target"].endswith("config.json"))
diff --git a/lean/components/docker/lean_runner.py b/lean/components/docker/lean_runner.py
index 9a1e050..4c24786 100644
--- a/lean/components/docker/lean_runner.py
+++ b/lean/components/docker/lean_runner.py
@@ -73,7 +73,8 @@ class LeanRunner:
                  debugging_method: Optional[DebuggingMethod],
                  release: bool,
                  detach: bool,
-                 extra_docker_config: Optional[Dict[str, Any]] = None) -> None:
+                 extra_docker_config: Optional[Dict[str, Any]] = None,
+                 paths_to_mount: Optional[Dict[str, str]] = None) -> None:
         """Runs the LEAN engine locally in Docker.
 
         Raises an error if something goes wrong.
@@ -87,6 +88,7 @@ class LeanRunner:
         :param release: whether C# projects should be compiled in release configuration instead of debug
         :param detach: whether LEAN should run in a detached container
         :param extra_docker_config: additional docker configurations
+        :param paths_to_mount: additional paths to mount to the container
         """
         self._logger.debug(f'LeanRunner().run_lean: lean_config: {lean_config}')
         project_dir = algorithm_file.parent
@@ -99,7 +101,8 @@ class LeanRunner:
                                                    debugging_method,
                                                    release,
                                                    detach,
-                                                   image)
+                                                   image,
+                                                   paths_to_mount)
 
         # Add known additional run options from the extra docker config
         self.parse_extra_docker_config(run_options, extra_docker_config)
@@ -178,7 +181,8 @@ class LeanRunner:
                                 debugging_method: Optional[DebuggingMethod],
                                 release: bool,
                                 detach: bool,
-                                image: DockerImage) -> Dict[str, Any]:
+                                image: DockerImage,
+                                paths_to_mount: Optional[Dict[str, str]] = None) -> Dict[str, Any]:
         """Creates a basic Docker config to run the engine with.
 
         This method constructs the parts of the Docker config that is the same for both the engine and the optimizer.
@@ -191,6 +195,7 @@ class LeanRunner:
         :param detach: whether LEAN should run in a detached container
         :param image: The docker image that will be used
         :return: the Docker configuration containing basic configuration to run Lean
+        :param paths_to_mount: additional paths to mount to the container
         """
         from docker.types import Mount
         from uuid import uuid4
@@ -240,6 +245,9 @@ class LeanRunner:
             "ports": docker_project_config.get("ports", {})
         }
 
+        # mount the paths passed in
+        self.mount_paths(paths_to_mount, lean_config, run_options)
+
         # mount the project and library directories
         self.mount_project_and_library_directories(project_dir, run_options)
 
@@ -782,6 +790,30 @@ for library_id, library_data in project_assets["targets"][project_target].items(
                 "mode": "rw"
             }
 
+    def mount_paths(self, paths_to_mount, lean_config, run_options):
+        if not paths_to_mount:
+            return
+
+        from docker.types import Mount
+
+        environment = {}
+        if "environment" in lean_config and "environments" in lean_config:
+            environment = lean_config["environments"][lean_config["environment"]]
+
+        mounts = run_options["mounts"]
+
+        for key, pathStr in paths_to_mount.items():
+            path = Path(pathStr).resolve()
+            target = f"/Files/{Path(path).name}"
+
+            self._logger.info(f"Mounting {path} to {target}")
+
+            mounts.append(Mount(target=target,
+                                source=str(path),
+                                type="bind",
+                                read_only=True))
+            environment[key] = target
+
     @staticmethod
     def parse_extra_docker_config(run_options: Dict[str, Any], extra_docker_config: Optional[Dict[str, Any]]) -> None:
         from docker.types import DeviceRequest
diff --git a/lean/models/json_module.py b/lean/models/json_module.py
index 071daac..dca7b55 100644
--- a/lean/models/json_module.py
+++ b/lean/models/json_module.py
@@ -20,7 +20,8 @@ from click.core import ParameterSource
 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
+from lean.models.configuration import BrokerageEnvConfiguration, Configuration, InternalInputUserInput, \
+    PathParameterUserInput
 from copy import copy
 from abc import ABC
 
@@ -228,6 +229,12 @@ class JsonModule(ABC):
                                .join(missing_options)}""".strip())
         return self
 
+    def get_paths_to_mount(self) -> Dict[str, str]:
+        return {config._id: config._value
+                for config in self._lean_configs
+                if (isinstance(config, PathParameterUserInput)
+                    and self._check_if_config_passes_filters(config, all_for_platform_type=False))}
+
     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 {self}: {self._product_id}")


Test patch:
diff --git a/lean/commands/backtest.py b/lean/commands/backtest.py
index b7ba0bc..832fe22 100644
--- a/lean/commands/backtest.py
+++ b/lean/commands/backtest.py
@@ -352,12 +352,14 @@ def backtest(project: Path,
         data_provider_historical = "QuantConnect"
 
     organization_id = container.organization_manager.try_get_working_organization_id()
+    paths_to_mount = None
 
     if data_provider_historical is not None:
         data_provider = non_interactive_config_build_for_name(lean_config, data_provider_historical,
                                                               cli_data_downloaders, kwargs, logger, environment_name)
         data_provider.ensure_module_installed(organization_id)
         container.lean_config_manager.set_properties(data_provider.get_settings())
+        paths_to_mount = data_provider.get_paths_to_mount()
 
     lean_config_manager.configure_data_purchase_limit(lean_config, data_purchase_limit)
 
@@ -406,4 +408,5 @@ def backtest(project: Path,
                          debugging_method,
                          release,
                          detach,
-                         loads(extra_docker_config))
+                         loads(extra_docker_config),
+                         paths_to_mount)
diff --git a/tests/commands/test_backtest.py b/tests/commands/test_backtest.py
index a59ea0f..9a3a373 100644
--- a/tests/commands/test_backtest.py
+++ b/tests/commands/test_backtest.py
@@ -24,6 +24,7 @@ from lean.components.util.xml_manager import XMLManager
 from lean.constants import DEFAULT_ENGINE_IMAGE
 from lean.container import container
 from lean.models.api import QCLanguage
+from lean.models.json_module import JsonModule
 from lean.models.utils import DebuggingMethod
 from lean.models.docker import DockerImage
 from tests.conftest import initialize_container
@@ -51,14 +52,15 @@ def test_backtest_calls_lean_runner_with_correct_algorithm_file() -> None:
     assert result.exit_code == 0
 
     container.lean_runner.run_lean.assert_called_once_with(mock.ANY,
-                                                 "backtesting",
-                                                 Path("Python Project/main.py").resolve(),
-                                                 mock.ANY,
-                                                 ENGINE_IMAGE,
-                                                 None,
-                                                 False,
-                                                 False,
-                                                 {})
+                                                           "backtesting",
+                                                           Path("Python Project/main.py").resolve(),
+                                                           mock.ANY,
+                                                           ENGINE_IMAGE,
+                                                           None,
+                                                           False,
+                                                           False,
+                                                           {},
+                                                           {})
 
 
 def test_backtest_calls_lean_runner_with_default_output_directory() -> None:
@@ -83,14 +85,15 @@ def test_backtest_calls_lean_runner_with_custom_output_directory() -> None:
     assert result.exit_code == 0
 
     container.lean_runner.run_lean.assert_called_once_with(mock.ANY,
-                                                 "backtesting",
-                                                 Path("Python Project/main.py").resolve(),
-                                                 Path.cwd() / "Python Project" / "custom",
-                                                 ENGINE_IMAGE,
-                                                 None,
-                                                 False,
-                                                 False,
-                                                 {})
+                                                           "backtesting",
+                                                           Path("Python Project/main.py").resolve(),
+                                                           Path.cwd() / "Python Project" / "custom",
+                                                           ENGINE_IMAGE,
+                                                           None,
+                                                           False,
+                                                           False,
+                                                           {},
+                                                           {})
 
 
 def test_backtest_calls_lean_runner_with_release_mode() -> None:
@@ -101,14 +104,15 @@ def test_backtest_calls_lean_runner_with_release_mode() -> None:
     assert result.exit_code == 0
 
     container.lean_runner.run_lean.assert_called_once_with(mock.ANY,
-                                                 "backtesting",
-                                                 Path("CSharp Project/Main.cs").resolve(),
-                                                 mock.ANY,
-                                                 ENGINE_IMAGE,
-                                                 None,
-                                                 True,
-                                                 False,
-                                                 {})
+                                                           "backtesting",
+                                                           Path("CSharp Project/Main.cs").resolve(),
+                                                           mock.ANY,
+                                                           ENGINE_IMAGE,
+                                                           None,
+                                                           True,
+                                                           False,
+                                                           {},
+                                                           {})
 
 
 def test_backtest_calls_lean_runner_with_detach() -> None:
@@ -119,14 +123,15 @@ def test_backtest_calls_lean_runner_with_detach() -> None:
     assert result.exit_code == 0
 
     container.lean_runner.run_lean.assert_called_once_with(mock.ANY,
-                                                 "backtesting",
-                                                 Path("Python Project/main.py").resolve(),
-                                                 mock.ANY,
-                                                 ENGINE_IMAGE,
-                                                 None,
-                                                 False,
-                                                 True,
-                                                 {})
+                                                           "backtesting",
+                                                           Path("Python Project/main.py").resolve(),
+                                                           mock.ANY,
+                                                           ENGINE_IMAGE,
+                                                           None,
+                                                           False,
+                                                           True,
+                                                           {},
+                                                           {})
 
 
 def test_backtest_aborts_when_project_does_not_exist() -> None:
@@ -161,14 +166,15 @@ def test_backtest_forces_update_when_update_option_given() -> None:
 
     docker_manager.pull_image.assert_called_once_with(ENGINE_IMAGE)
     container.lean_runner.run_lean.assert_called_once_with(mock.ANY,
-                                                 "backtesting",
-                                                 Path("Python Project/main.py").resolve(),
-                                                 mock.ANY,
-                                                 ENGINE_IMAGE,
-                                                 None,
-                                                 False,
-                                                 False,
-                                                 {})
+                                                           "backtesting",
+                                                           Path("Python Project/main.py").resolve(),
+                                                           mock.ANY,
+                                                           ENGINE_IMAGE,
+                                                           None,
+                                                           False,
+                                                           False,
+                                                           {},
+                                                           {})
 
 
 def test_backtest_passes_custom_image_to_lean_runner_when_set_in_config() -> None:
@@ -181,14 +187,15 @@ def test_backtest_passes_custom_image_to_lean_runner_when_set_in_config() -> Non
     assert result.exit_code == 0
 
     container.lean_runner.run_lean.assert_called_once_with(mock.ANY,
-                                                 "backtesting",
-                                                 Path("Python Project/main.py").resolve(),
-                                                 mock.ANY,
-                                                 DockerImage(name="custom/lean", tag="123"),
-                                                 None,
-                                                 False,
-                                                 False,
-                                                 {})
+                                                           "backtesting",
+                                                           Path("Python Project/main.py").resolve(),
+                                                           mock.ANY,
+                                                           DockerImage(name="custom/lean", tag="123"),
+                                                           None,
+                                                           False,
+                                                           False,
+                                                           {},
+                                                           {})
 
 
 def test_backtest_passes_custom_image_to_lean_runner_when_given_as_option() -> None:
@@ -201,19 +208,20 @@ def test_backtest_passes_custom_image_to_lean_runner_when_given_as_option() -> N
     assert result.exit_code == 0
 
     container.lean_runner.run_lean.assert_called_once_with(mock.ANY,
-                                                 "backtesting",
-                                                 Path("Python Project/main.py").resolve(),
-                                                 mock.ANY,
-                                                 DockerImage(name="custom/lean", tag="456"),
-                                                 None,
-                                                 False,
-                                                 False,
-                                                 {})
+                                                           "backtesting",
+                                                           Path("Python Project/main.py").resolve(),
+                                                           mock.ANY,
+                                                           DockerImage(name="custom/lean", tag="456"),
+                                                           None,
+                                                           False,
+                                                           False,
+                                                           {},
+                                                           {})
 
 
 @pytest.mark.parametrize("python_venv", ["Custom-venv",
-                                        "/Custom-venv",
-                                        None])
+                                         "/Custom-venv",
+                                         None])
 def test_backtest_passes_custom_python_venv_to_lean_runner_when_given_as_option(python_venv: str) -> None:
     create_fake_lean_cli_directory()
 
@@ -290,14 +298,15 @@ def test_backtest_passes_correct_debugging_method_to_lean_runner(value: str, deb
     assert result.exit_code == 0
 
     container.lean_runner.run_lean.assert_called_once_with(mock.ANY,
-                                                 "backtesting",
-                                                 Path("Python Project/main.py").resolve(),
-                                                 mock.ANY,
-                                                 ENGINE_IMAGE,
-                                                 debugging_method,
-                                                 False,
-                                                 False,
-                                                 {})
+                                                           "backtesting",
+                                                           Path("Python Project/main.py").resolve(),
+                                                           mock.ANY,
+                                                           ENGINE_IMAGE,
+                                                           debugging_method,
+                                                           False,
+                                                           False,
+                                                           {},
+                                                           {})
 
 
 def test_backtest_auto_updates_outdated_python_pycharm_debug_config() -> None:
@@ -612,7 +621,8 @@ def test_backtest_passes_data_purchase_limit_to_lean_runner() -> None:
 }
         """)
 
-    result = CliRunner().invoke(lean, ["backtest", "Python Project", "--data-provider-historical", "QuantConnect", "--data-purchase-limit", "1000"])
+    result = CliRunner().invoke(lean, ["backtest", "Python Project", "--data-provider-historical", "QuantConnect",
+                                       "--data-purchase-limit", "1000"])
 
     assert result.exit_code == 0
 
@@ -684,4 +694,25 @@ def test_backtest_calls_lean_runner_with_extra_docker_config() -> None:
                                                                "volumes": {
                                                                    "extra/path": {"bind": "/extra/path", "mode": "rw"}
                                                                }
-                                                           })
+                                                           },
+                                                           {})
+
+
+def test_backtest_calls_lean_runner_with_paths_to_mount() -> None:
+    create_fake_lean_cli_directory()
+
+    with mock.patch.object(JsonModule, "get_paths_to_mount", return_value={"some-config": "/path/to/file.json"}):
+        result = CliRunner().invoke(lean, ["backtest", "Python Project", "--data-provider-historical", "QuantConnect"])
+
+    assert result.exit_code == 0
+
+    container.lean_runner.run_lean.assert_called_once_with(mock.ANY,
+                                                           "backtesting",
+                                                           Path("Python Project/main.py").resolve(),
+                                                           mock.ANY,
+                                                           ENGINE_IMAGE,
+                                                           None,
+                                                           False,
+                                                           False,
+                                                           {},
+                                                           {"some-config": "/path/to/file.json"})
diff --git a/tests/commands/test_live.py b/tests/commands/test_live.py
index e9c78c9..191e5f2 100644
--- a/tests/commands/test_live.py
+++ b/tests/commands/test_live.py
@@ -24,6 +24,7 @@ from lean.commands import lean
 from lean.constants import DEFAULT_ENGINE_IMAGE
 from lean.container import container
 from lean.models.docker import DockerImage
+from lean.models.json_module import JsonModule
 from tests.test_helpers import create_fake_lean_cli_directory, reset_state_installed_modules
 from tests.conftest import initialize_container
 from click.testing import Result
@@ -114,6 +115,7 @@ def test_live_calls_lean_runner_with_correct_algorithm_file() -> None:
                                                  None,
                                                  False,
                                                  False,
+                                                 {},
                                                  {})
 
 
@@ -148,7 +150,31 @@ def test_live_calls_lean_runner_with_extra_docker_config() -> None:
                                                                "volumes": {
                                                                    "extra/path": {"bind": "/extra/path", "mode": "rw"}
                                                                }
-                                                           })
+                                                           },
+                                                           {})
+
+
+def test_live_calls_lean_runner_with_paths_to_mount() -> None:
+    create_fake_lean_cli_directory()
+    create_fake_environment("live-paper", True)
+
+    with mock.patch.object(JsonModule, "get_paths_to_mount", return_value={"some-config": "/path/to/file.json"}):
+        result = CliRunner().invoke(lean, ["live", "Python Project",
+                                           "--environment", "live-paper",
+                                           "--data-provider-historical", "QuantConnect"])
+
+    assert result.exit_code == 0
+
+    container.lean_runner.run_lean.assert_called_once_with(mock.ANY,
+                                                           "live-paper",
+                                                           Path("Python Project/main.py").resolve(),
+                                                           mock.ANY,
+                                                           ENGINE_IMAGE,
+                                                           None,
+                                                           False,
+                                                           False,
+                                                           {},
+                                                           {"some-config": "/path/to/file.json"})
 
 
 def test_live_aborts_when_environment_does_not_exist() -> None:
@@ -224,6 +250,7 @@ def test_live_calls_lean_runner_with_release_mode() -> None:
                                                  None,
                                                  True,
                                                  False,
+                                                 {},
                                                  {})
 
 
@@ -244,6 +271,7 @@ def test_live_calls_lean_runner_with_detach() -> None:
                                                  None,
                                                  False,
                                                  True,
+                                                 {},
                                                  {})
 
 
@@ -467,6 +495,7 @@ def test_live_calls_lean_runner_with_data_provider(data_provider: str) -> None:
                                                      None,
                                                      False,
                                                      False,
+                                                     {},
                                                      {})
 
 
@@ -570,6 +599,7 @@ def test_live_non_interactive_do_not_store_non_persistent_properties_in_lean_con
                                                  None,
                                                  False,
                                                  False,
+                                                 {},
                                                  {})
 
     config = container.lean_config_manager.get_lean_config()
@@ -612,6 +642,7 @@ def test_live_non_interactive_calls_run_lean_when_all_options_given(brokerage: s
                                                  None,
                                                  False,
                                                  False,
+                                                 {},
                                                  {})
 
 @pytest.mark.parametrize("brokerage,data_feed1,data_feed2",[(brokerage, *data_feeds) for brokerage, data_feeds in
@@ -652,6 +683,7 @@ def test_live_non_interactive_calls_run_lean_when_all_options_given_with_multipl
                                                  None,
                                                  False,
                                                  False,
+                                                 {},
                                                  {})
 
 
@@ -809,6 +841,7 @@ def test_live_non_interactive_falls_back_to_lean_config_for_multiple_data_feed_s
                                                          None,
                                                          False,
                                                          False,
+                                                         {},
                                                          {})
 
 
@@ -829,6 +862,7 @@ def test_live_forces_update_when_update_option_given() -> None:
                                                  None,
                                                  False,
                                                  False,
+                                                 {},
                                                  {})
 
 
@@ -850,6 +884,7 @@ def test_live_passes_custom_image_to_lean_runner_when_set_in_config() -> None:
                                                  None,
                                                  False,
                                                  False,
+                                                 {},
                                                  {})
 
 
@@ -872,6 +907,7 @@ def test_live_passes_custom_image_to_lean_runner_when_given_as_option() -> None:
                                                  None,
                                                  False,
                                                  False,
+                                                 {},
                                                  {})
 
 
diff --git a/tests/commands/test_optimize.py b/tests/commands/test_optimize.py
index c85c22a..34d6707 100644
--- a/tests/commands/test_optimize.py
+++ b/tests/commands/test_optimize.py
@@ -24,6 +24,7 @@ from lean.components.config.storage import Storage
 from lean.constants import DEFAULT_ENGINE_IMAGE, LEAN_ROOT_PATH
 from lean.container import container
 from lean.models.docker import DockerImage
+from lean.models.json_module import JsonModule
 from lean.models.optimizer import (OptimizationConstraint, OptimizationExtremum, OptimizationParameter,
                                    OptimizationTarget)
 from tests.test_helpers import create_fake_lean_cli_directory
@@ -816,3 +817,30 @@ def test_optimize_used_data_downloader_specified_with_data_provider_option() ->
     config = json.loads(Path(lean_config_filename).read_text(encoding="utf-8"))
     assert "data-downloader" in config['environments']['backtesting']
     assert config['environments']['backtesting']["data-downloader"] == "QuantConnect.Lean.DataSource.Polygon.PolygonDataDownloader"
+
+
+def test_optimize_runs_lean_container_with_paths_to_mount() -> None:
+    create_fake_lean_cli_directory()
+
+    docker_manager = mock.MagicMock()
+    docker_manager.run_image.side_effect = run_image
+    container.initialize(docker_manager=docker_manager)
+    container.optimizer_config_manager = _get_optimizer_config_manager_mock()
+
+    Storage(str(Path.cwd() / "Python Project" / "config.json")).set("parameters", {"param1": "1"})
+
+    with mock.patch.object(JsonModule, "get_paths_to_mount", return_value={"some-config": "/path/to/file.json"}):
+        result = CliRunner().invoke(lean, ["optimize", "Python Project", "--data-provider-historical", "QuantConnect"])
+
+    assert result.exit_code == 0
+
+    docker_manager.run_image.assert_called_once()
+    args, kwargs = docker_manager.run_image.call_args
+
+    assert args[0] == ENGINE_IMAGE
+
+    expected_source = str(Path("/path/to/file.json").resolve())
+    mount = next((m for m in kwargs["mounts"] if m["Source"] == expected_source), None)
+
+    assert mount is not None
+    assert mount["Target"] == "/Files/file.json"
diff --git a/tests/commands/test_research.py b/tests/commands/test_research.py
index a6a2281..9dd57e8 100644
--- a/tests/commands/test_research.py
+++ b/tests/commands/test_research.py
@@ -21,6 +21,7 @@ from lean.commands import lean
 from lean.constants import DEFAULT_RESEARCH_IMAGE, LEAN_ROOT_PATH
 from lean.container import container
 from lean.models.docker import DockerImage
+from lean.models.json_module import JsonModule
 from tests.test_helpers import create_fake_lean_cli_directory
 
 RESEARCH_IMAGE = DockerImage.parse(DEFAULT_RESEARCH_IMAGE)
@@ -229,7 +230,7 @@ def test_research_runs_custom_image_when_given_as_option() -> None:
     assert args[0] == DockerImage(name="custom/research", tag="456")
 
 
-def test_optimize_runs_lean_container_with_extra_docker_config() -> None:
+def test_research_runs_lean_container_with_extra_docker_config() -> None:
     import docker.types
 
     create_fake_lean_cli_directory()
@@ -256,3 +257,26 @@ def test_optimize_runs_lean_container_with_extra_docker_config() -> None:
     volumes = kwargs["volumes"]
     assert "extra/path" in volumes
     assert volumes["extra/path"] == {"bind": "/extra/path", "mode": "rw"}
+
+
+def test_research_runs_lean_container_with_paths_to_mount() -> None:
+    create_fake_lean_cli_directory()
+
+    docker_manager = mock.MagicMock()
+    container.initialize(docker_manager)
+
+    with mock.patch.object(JsonModule, "get_paths_to_mount", return_value={"some-config": "/path/to/file.json"}):
+        result = CliRunner().invoke(lean, ["research", "Python Project", "--data-provider-historical", "QuantConnect"])
+
+    assert result.exit_code == 0
+
+    docker_manager.run_image.assert_called_once()
+    args, kwargs = docker_manager.run_image.call_args
+
+    assert args[0] == RESEARCH_IMAGE
+
+    expected_source = str(Path("/path/to/file.json").resolve())
+    mount = next((m for m in kwargs["mounts"] if m["Source"] == expected_source), None)
+
+    assert mount is not None
+    assert mount["Target"] == "/Files/file.json"
diff --git a/tests/components/docker/test_lean_runner.py b/tests/components/docker/test_lean_runner.py
index 1520b5f..ff2e713 100644
--- a/tests/components/docker/test_lean_runner.py
+++ b/tests/components/docker/test_lean_runner.py
@@ -690,3 +690,66 @@ def test_run_lean_passes_extra_volumes() -> None:
     assert "extra/path" in volumes
     assert volumes["extra/path"]["bind"] == "/extra/bound/path"
     assert volumes["extra/path"]["mode"] == "rw"
+
+
+def test_run_lean_mounts_additional_paths() -> None:
+    create_fake_lean_cli_directory()
+
+    docker_manager = mock.Mock()
+    docker_manager.run_image.return_value = True
+
+    lean_runner = create_lean_runner(docker_manager)
+
+    lean_config = {
+        "transaction-log": "transaction-log.log",
+        "environment": "backtesting",
+        "environments": {
+            "backtesting": {}
+        }
+    }
+    paths_to_mount = {
+        "file-to-mount-key": "../some/path/to/mount/file.json",
+        "directory-to-mount-key": "../some/path/to/mount"
+    }
+
+    lean_runner.run_lean(lean_config,
+                         "backtesting",
+                         Path.cwd() / "Python Project" / "main.py",
+                         Path.cwd() / "output",
+                         ENGINE_IMAGE,
+                         None,
+                         False,
+                         False,
+                         {},
+                         paths_to_mount)
+
+    docker_manager.run_image.assert_called_once()
+    _, kwargs = docker_manager.run_image.call_args
+
+    assert "mounts" in kwargs
+    mounts = kwargs["mounts"]
+
+    def source_to_target(path):
+        return f"/Files/{str(Path(path).name)}"
+
+    def path_is_in_mounts(path):
+        return any([mount["Source"] == str(Path(path).resolve()) for mount in mounts])
+
+    def path_is_mounted_in_files(path):
+        return any([mount["Target"] == source_to_target(path) for mount in mounts])
+
+    assert all([path_is_in_mounts(path) and path_is_mounted_in_files(path) for path in paths_to_mount.values()])
+
+    # The target paths should have been added to the lean config
+    lean_config_path = next((mount["Source"] for mount in mounts if mount["Target"].endswith("config.json")), None)
+    assert lean_config_path is not None
+
+    # Read temporal lean config
+    with open(lean_config_path, "r") as f:
+        import json
+        lean_config = json.load(f)
+
+    backtesting_env = lean_config["environments"]["backtesting"]
+
+    assert all([key in backtesting_env and backtesting_env[key] == source_to_target(value)
+                for key, value in paths_to_mount.items()])