Skip to content
Open
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
8 changes: 7 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,20 @@ jobs:

# Static analysis tools
- name: Static Code Analysis
if: runner.os == 'Linux' && matrix.python-version != '3.8'
if: runner.os == 'Linux'
run: |
pip install mypy==1.19.1 flake8==7.3.0
python static_analysis.py

- name: Run tests
run: python -m pytest -s -rs

- name: Check README
if: runner.os == 'Linux'
run: |
python scripts/readme.py
git diff --exit-code README.md

release:
runs-on: ubuntu-latest

Expand Down
18 changes: 16 additions & 2 deletions lean/click.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from pathlib import Path
from typing import Optional, List, Callable

from click import Command, Context, Parameter, ParamType, Option as ClickOption
from click import Choice, Command, Context, Parameter, ParamType, Option as ClickOption
from click.decorators import FC, option

from lean.constants import DEFAULT_LEAN_CONFIG_FILE_NAME, CONTAINER_LABEL_LEAN_VERSION_NAME
Expand Down Expand Up @@ -305,6 +305,20 @@ def _parse_config_option(self, ctx: Context, param: Parameter, value: Optional[P



class CaseInsensitiveChoice(Choice):
"""A click.Choice subclass that matches case-insensitively but displays original casing in help text."""

def __init__(self, choices, **kwargs):
super().__init__(choices, case_sensitive=False, **kwargs)

def get_metavar(self, param, ctx=None) -> str:
import enum
choices_str = "|".join(c.value if isinstance(c, enum.Enum) else str(c) for c in self.choices)
if param is not None and param.required and param.param_type_name == "argument":
return f"{{{choices_str}}}"
return f"[{choices_str}]"


class PathParameter(ParamType):
"""A limited version of click.Path which uses pathlib.Path."""

Expand Down Expand Up @@ -352,7 +366,7 @@ class DateParameter(ParamType):

name = "date"

def get_metavar(self, param: Parameter) -> str:
def get_metavar(self, param: Parameter, ctx=None) -> str:
return "[yyyyMMdd]"

def convert(self, value: str, param: Parameter, ctx: Context):
Expand Down
8 changes: 4 additions & 4 deletions lean/commands/backtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@

from pathlib import Path
from typing import List, Optional, Tuple
from click import command, option, argument, Choice
from click import command, option, argument

from lean.click import LeanCommand, PathParameter, backtest_parameter_option
from lean.click import LeanCommand, PathParameter, backtest_parameter_option, CaseInsensitiveChoice
from lean.constants import DEFAULT_ENGINE_IMAGE, LEAN_ROOT_PATH
from lean.container import container, Logger
from lean.models.utils import DebuggingMethod
Expand Down Expand Up @@ -234,10 +234,10 @@ def _migrate_csharp_csproj(project_dir: Path) -> None:
default=False,
help="Run the backtest in a detached Docker container and return immediately")
@option("--debug",
type=Choice(["pycharm", "ptvsd", "debugpy", "vsdbg", "rider", "local-platform"], case_sensitive=False),
type=CaseInsensitiveChoice(["pycharm", "ptvsd", "debugpy", "vsdbg", "rider", "local-platform"]),
help="Enable a certain debugging method (see --help for more information)")
@option("--data-provider-historical",
type=Choice([dp.get_name() for dp in cli_data_downloaders], case_sensitive=False),
type=CaseInsensitiveChoice([dp.get_name() for dp in cli_data_downloaders]),
default="Local",
help="Update the Lean configuration file to retrieve data from the given historical provider")
@options_from_json(get_configs_for_options("backtest"))
Expand Down
8 changes: 4 additions & 4 deletions lean/commands/cloud/live/deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
# limitations under the License.

from typing import List, Tuple, Optional
from click import prompt, option, argument, Choice, confirm
from lean.click import LeanCommand, ensure_options
from click import prompt, option, argument, confirm
from lean.click import LeanCommand, ensure_options, CaseInsensitiveChoice
from lean.components.api.api_client import APIClient
from lean.components.util.json_modules_handler import non_interactive_config_build_for_name, \
interactive_config_build
Expand Down Expand Up @@ -166,10 +166,10 @@ def _configure_auto_restart(logger: Logger) -> bool:
@live.command(cls=LeanCommand, default_command=True, name="deploy")
@argument("project", type=str)
@option("--brokerage",
type=Choice([b.get_name() for b in cloud_brokerages], case_sensitive=False),
type=CaseInsensitiveChoice([b.get_name() for b in cloud_brokerages]),
help="The brokerage to use")
@option("--data-provider-live",
type=Choice([d.get_name() for d in cloud_data_queue_handlers], case_sensitive=False),
type=CaseInsensitiveChoice([d.get_name() for d in cloud_data_queue_handlers]),
multiple=True,
help="The live data provider to use")
@options_from_json(get_configs_for_options("live-cloud"))
Expand Down
8 changes: 4 additions & 4 deletions lean/commands/cloud/optimize.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@

from typing import List, Optional, Tuple

from click import command, option, Choice, argument, confirm
from click import command, option, argument, confirm

from lean.click import LeanCommand, ensure_options
from lean.click import LeanCommand, ensure_options, CaseInsensitiveChoice
from lean.components.config.optimizer_config_manager import NodeType, available_nodes
from lean.container import container
from lean.models.api import QCOptimizationBacktest, QCProject, QCCompileWithLogs, QCFullOrganization
Expand Down Expand Up @@ -156,7 +156,7 @@ def _display_estimate(cloud_project: QCProject,
type=str,
help="The target statistic of the optimization")
@option("--target-direction",
type=Choice(["min", "max"], case_sensitive=False),
type=CaseInsensitiveChoice(["min", "max"]),
help="Whether the target must be minimized or maximized")
@option("--parameter",
type=(str, float, float, float),
Expand All @@ -167,7 +167,7 @@ def _display_estimate(cloud_project: QCProject,
multiple=True,
help="The 'statistic operator value' pairs configuring the constraints of the optimization")
@option("--node",
type=Choice([node.name for node in available_nodes], case_sensitive=False),
type=CaseInsensitiveChoice([node.name for node in available_nodes]),
help="The node type to run the optimization on")
@option("--parallel-nodes",
type=int,
Expand Down
6 changes: 3 additions & 3 deletions lean/commands/create_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
# limitations under the License.

from pathlib import Path
from click import Choice, option, argument
from click import option, argument

from lean.click import LeanCommand
from lean.click import LeanCommand, CaseInsensitiveChoice
from lean.commands import lean
from lean.container import container
from lean.models.api import QCLanguage
Expand Down Expand Up @@ -398,7 +398,7 @@ def _not_identifier_char(text):
@lean.command(cls=LeanCommand, name="project-create", aliases=["create-project"])
@argument("name", type=str)
@option("--language", "-l",
type=Choice(container.cli_config_manager.default_language.allowed_values, case_sensitive=False),
type=CaseInsensitiveChoice(container.cli_config_manager.default_language.allowed_values),
help="The language of the project to create")
def create_project(name: str, language: str) -> None:
"""Create a new project containing starter code.
Expand Down
16 changes: 8 additions & 8 deletions lean/commands/data/download.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@

from docker.types import Mount
from typing import Any, Dict, Iterable, List, Optional
from click import command, option, confirm, pass_context, Context, Choice, prompt
from lean.click import LeanCommand, ensure_options
from click import command, option, confirm, pass_context, Context, prompt
from lean.click import LeanCommand, ensure_options, CaseInsensitiveChoice
from lean.components.util.json_modules_handler import config_build_for_name
from lean.constants import DEFAULT_ENGINE_IMAGE
from lean.container import container
Expand Down Expand Up @@ -500,8 +500,8 @@ def _configure_date_option(date_value: str, option_id: str, option_label: str) -
return date_option.configure_non_interactive(date_value)


class QCDataTypeCustomChoice(Choice):
def get_metavar(self, param) -> str:
class QCDataTypeCustomChoice(CaseInsensitiveChoice):
def get_metavar(self, param, ctx=None) -> str:
choices_str = "|".join(QCDataType.get_all_members_except('Open Interest'))

# Use square braces to indicate an option or optional argument.
Expand All @@ -521,7 +521,7 @@ def _replace_data_type(ctx, param, value):

@command(cls=LeanCommand, requires_lean_config=True, allow_unknown_options=True, name="download")
@option("--data-provider-historical",
type=Choice([data_downloader.get_name() for data_downloader in cli_data_downloaders], case_sensitive=False),
type=CaseInsensitiveChoice([data_downloader.get_name() for data_downloader in cli_data_downloaders]),
help="The name of the downloader data provider.")
@options_from_json(get_configs_for_options("download"))
@option("--dataset", type=str, help="The name of the dataset to download non-interactively")
Expand All @@ -530,11 +530,11 @@ def _replace_data_type(ctx, param, value):
@option("--yes", "-y", "auto_confirm", is_flag=True, default=False,
help="Automatically confirm payment confirmation prompts")
@option("--data-type", callback=_replace_data_type,
type=QCDataTypeCustomChoice(QCDataType.get_all_members(), case_sensitive=False),
type=QCDataTypeCustomChoice(QCDataType.get_all_members()),
help="Specify the type of historical data")
@option("--resolution", type=Choice(QCResolution.get_all_members(), case_sensitive=False),
@option("--resolution", type=CaseInsensitiveChoice(QCResolution.get_all_members()),
help="Specify the resolution of the historical data")
@option("--security-type", type=Choice(QCSecurityType.get_all_members(), case_sensitive=False),
@option("--security-type", type=CaseInsensitiveChoice(QCSecurityType.get_all_members()),
help="Specify the security type of the historical data")
@option("--market", type=str,
help="Specify the market name for tickers (e.g., 'USA', 'NYMEX', 'Binance')"
Expand Down
12 changes: 6 additions & 6 deletions lean/commands/data/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
from datetime import datetime
from typing import Optional

from click import command, option, Choice, IntRange
from click import command, option, IntRange

from lean.click import DateParameter, LeanCommand, ensure_options
from lean.click import DateParameter, LeanCommand, ensure_options, CaseInsensitiveChoice
from lean.constants import DEFAULT_ENGINE_IMAGE
from lean.container import container

Expand All @@ -39,15 +39,15 @@
default="",
help="Comma separated list of tickers to use for generated data")
@option("--security-type",
type=Choice(["Equity", "Forex", "Cfd", "Future", "Crypto", "Option"], case_sensitive=False),
type=CaseInsensitiveChoice(["Equity", "Forex", "Cfd", "Future", "Crypto", "Option"]),
default="Equity",
help="The security type to generate data for (defaults to Equity)")
@option("--resolution",
type=Choice(["Tick", "Second", "Minute", "Hour", "Daily"], case_sensitive=False),
type=CaseInsensitiveChoice(["Tick", "Second", "Minute", "Hour", "Daily"]),
default="Minute",
help="The resolution of the generated data (defaults to Minute)")
@option("--data-density",
type=Choice(["Dense", "Sparse", "VerySparse"], case_sensitive=False),
type=CaseInsensitiveChoice(["Dense", "Sparse", "VerySparse"]),
default="Dense",
help="The density of the generated data (defaults to Dense)")
@option("--include-coarse",
Expand Down Expand Up @@ -98,7 +98,7 @@
help="The stochastic process, and returns new pricing engine to run calculations for that option "
"(defaults to BaroneAdesiWhaleyApproximationEngine)")
@option("--volatility-model-resolution",
type=Choice(["Tick", "Second", "Minute", "Hour", "Daily"], case_sensitive=False),
type=CaseInsensitiveChoice(["Tick", "Second", "Minute", "Hour", "Daily"]),
default="Daily",
help="The volatility model period span (defaults to Daily)")
@option("--chain-symbol-count",
Expand Down
4 changes: 2 additions & 2 deletions lean/commands/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

from click import command, option, Choice, confirm, prompt

from lean.click import LeanCommand
from lean.click import LeanCommand, CaseInsensitiveChoice
from lean.commands.login import get_credentials, validate_credentials, get_lean_config_credentials
from lean.constants import DEFAULT_DATA_DIRECTORY_NAME, DEFAULT_LEAN_CONFIG_FILE_NAME
from lean.container import container
Expand Down Expand Up @@ -121,7 +121,7 @@ def _download_repository(output_path: Path) -> None:
@command(cls=LeanCommand)
@option("--organization", type=str, help="The name or id of the organization the Lean CLI will be scaffolded for")
@option("--language", "-l",
type=Choice(container.cli_config_manager.default_language.allowed_values, case_sensitive=False),
type=CaseInsensitiveChoice(container.cli_config_manager.default_language.allowed_values),
help="The default language to use for new projects")
def init(organization: Optional[str], language: Optional[str]) -> None:
"""Scaffold a Lean configuration file and data directory."""
Expand Down
10 changes: 5 additions & 5 deletions lean/commands/live/deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@

from pathlib import Path
from typing import List, Optional, Tuple
from click import option, argument, Choice
from lean.click import LeanCommand, PathParameter
from click import option, argument
from lean.click import LeanCommand, PathParameter, CaseInsensitiveChoice
from lean.components.util.name_rename import rename_internal_config_to_user_friendly_format
from lean.constants import DEFAULT_ENGINE_IMAGE
from lean.container import container
Expand Down Expand Up @@ -64,14 +64,14 @@ def _get_history_provider_name(data_provider_live_names: [str]) -> [str]:
default=False,
help="Run the live deployment in a detached Docker container and return immediately")
@option("--brokerage",
type=Choice([b.get_name() for b in cli_brokerages], case_sensitive=False),
type=CaseInsensitiveChoice([b.get_name() for b in cli_brokerages]),
help="The brokerage to use")
@option("--data-provider-live",
type=Choice([d.get_name() for d in cli_data_queue_handlers], case_sensitive=False),
type=CaseInsensitiveChoice([d.get_name() for d in cli_data_queue_handlers]),
multiple=True,
help="The live data provider to use")
@option("--data-provider-historical",
type=Choice([dp.get_name() for dp in cli_data_downloaders if dp.get_id() != "TerminalLinkBrokerage"], case_sensitive=False),
type=CaseInsensitiveChoice([dp.get_name() for dp in cli_data_downloaders if dp.get_id() != "TerminalLinkBrokerage"]),
help="Update the Lean configuration file to retrieve data from the given historical provider")
@options_from_json(get_configs_for_options("live-cli"))
@option("--release",
Expand Down
10 changes: 5 additions & 5 deletions lean/commands/optimize.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
from typing import Optional, List, Tuple
from datetime import datetime, timedelta

from click import command, argument, option, Choice, IntRange
from click import command, argument, option, IntRange

from lean.click import LeanCommand, PathParameter, ensure_options
from lean.click import LeanCommand, PathParameter, ensure_options, CaseInsensitiveChoice
from lean.components.docker.lean_runner import LeanRunner
from lean.constants import DEFAULT_ENGINE_IMAGE
from lean.container import container
Expand Down Expand Up @@ -81,13 +81,13 @@ def get_filename_timestamp(path: Path) -> datetime:
type=PathParameter(exists=True, file_okay=True, dir_okay=False),
help=f"The optimizer configuration file that should be used")
@option("--strategy",
type=Choice(["Grid Search", "Euler Search"], case_sensitive=False),
type=CaseInsensitiveChoice(["Grid Search", "Euler Search"]),
help="The optimization strategy to use")
@option("--target",
type=str,
help="The target statistic of the optimization")
@option("--target-direction",
type=Choice(["min", "max"], case_sensitive=False),
type=CaseInsensitiveChoice(["min", "max"]),
help="Whether the target must be minimized or maximized")
@option("--parameter",
type=(str, float, float, float),
Expand All @@ -98,7 +98,7 @@ def get_filename_timestamp(path: Path) -> datetime:
multiple=True,
help="The 'statistic operator value' pairs configuring the constraints of the optimization")
@option("--data-provider-historical",
type=Choice([dp.get_name() for dp in cli_data_downloaders], case_sensitive=False),
type=CaseInsensitiveChoice([dp.get_name() for dp in cli_data_downloaders]),
default="Local",
help="Update the Lean configuration file to retrieve data from the given historical provider")
@option("--download-data",
Expand Down
6 changes: 3 additions & 3 deletions lean/commands/research.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@

from pathlib import Path
from typing import Optional, Tuple
from click import command, argument, option, Choice
from lean.click import LeanCommand, PathParameter
from click import command, argument, option
from lean.click import LeanCommand, PathParameter, CaseInsensitiveChoice
from lean.components.docker.lean_runner import LeanRunner
from lean.constants import DEFAULT_RESEARCH_IMAGE, LEAN_ROOT_PATH
from lean.container import container
Expand All @@ -38,7 +38,7 @@ def _check_docker_output(chunk: str, port: int) -> None:
@argument("project", type=PathParameter(exists=True, file_okay=False, dir_okay=True))
@option("--port", type=int, default=8888, help="The port to run Jupyter Lab on (defaults to 8888)")
@option("--data-provider-historical",
type=Choice([dp.get_name() for dp in cli_data_downloaders], case_sensitive=False),
type=CaseInsensitiveChoice([dp.get_name() for dp in cli_data_downloaders]),
default="Local",
help="Update the Lean configuration file to retrieve data from the given historical provider")
@options_from_json(get_configs_for_options("research"))
Expand Down
6 changes: 3 additions & 3 deletions lean/models/click_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@


from typing import List, Dict
from click import option, Choice
from lean.click import PathParameter
from click import option
from lean.click import PathParameter, CaseInsensitiveChoice
from lean.models.cli import cli_brokerages, cli_data_downloaders, cli_data_queue_handlers
from lean.models.cloud import cloud_brokerages, cloud_data_queue_handlers
from lean.models.configuration import Configuration, InfoConfiguration, InternalInputUserInput
Expand Down Expand Up @@ -59,7 +59,7 @@ def get_click_option_type(configuration: Configuration):
# 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)
return CaseInsensitiveChoice(configuration._choices)
elif configuration._input_method == "prompt":
return configuration.get_input_type()
elif configuration._input_method == "prompt-password":
Expand Down
5 changes: 3 additions & 2 deletions lean/models/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@

from pathlib import Path
from typing import Any, Dict, List
from click import prompt, Choice
from click import prompt
from lean.click import CaseInsensitiveChoice
from abc import ABC, abstractmethod
from lean.components.util.logger import Logger
from lean.click import PathParameter
Expand Down Expand Up @@ -294,7 +295,7 @@ def ask_user_for_input(self, default_value, logger: Logger, hide_input: bool = F
return prompt(
self._prompt_info,
default_value,
type=Choice(self._choices, case_sensitive=False)
type=CaseInsensitiveChoice(self._choices)
)


Expand Down
Loading
Loading