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
4 changes: 2 additions & 2 deletions .github/workflows-config/python-versions.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
3.8
3.9
3.10
3.11
3.12
3.13
3.13
3.14
2 changes: 1 addition & 1 deletion .github/workflows/regression-testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
echo "python=$(jq -Rsc 'split("\n") | map(select(length > 0))' .github/workflows-config/python-versions.txt)" >> $GITHUB_OUTPUT
echo "os=$(jq -Rsc 'split("\n") | map(select(length > 0))' .github/workflows-config/os-versions.txt)" >> $GITHUB_OUTPUT
else
echo 'python=["3.13"]' >> $GITHUB_OUTPUT
echo 'python=["3.14"]' >> $GITHUB_OUTPUT
echo 'os=["ubuntu-24.04","macos-15","windows-latest"]' >> $GITHUB_OUTPUT
fi

Expand Down
4 changes: 2 additions & 2 deletions lean/commands/optimize.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,8 +267,8 @@ def optimize(project: Path,
"target": optimization_target.target,
"extremum": optimization_target.extremum.value
},
"parameters": [parameter.dict() for parameter in optimization_parameters],
"constraints": [constraint.dict(by_alias=True) for constraint in optimization_constraints]
"parameters": [parameter.model_dump() for parameter in optimization_parameters],
"constraints": [constraint.model_dump(by_alias=True) for constraint in optimization_constraints]
}

if max_concurrent_backtests is not None:
Expand Down
2 changes: 1 addition & 1 deletion lean/components/api/live_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ def start(self,

parameters["notification"] = {
"events": events,
"targets": [{x: y for x, y in method.dict().items() if y} for method in notify_methods]
"targets": [{x: y for x, y in method.model_dump().items() if y} for method in notify_methods]
}

data = self._api.post("live/create", parameters)
Expand Down
5 changes: 2 additions & 3 deletions lean/components/util/library_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,11 +119,10 @@ def add_lean_library_reference_to_project(self, project_dir: Path, library_dir:
raise RuntimeError("Circular dependency detected between "
f"{project_relative_path} and {library_relative_path}")

from json import loads
project_libraries.append(loads(LeanLibraryReference(
project_libraries.append(LeanLibraryReference(
name=library_dir.name,
path=library_relative_path
).json()))
).model_dump(mode="json"))
project_config.set("libraries", project_libraries)

return False
Expand Down
47 changes: 25 additions & 22 deletions lean/models/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@
from enum import Enum
from typing import Any, Dict, List, Optional, Union

from lean.models.pydantic import WrappedBaseModel, validator
from lean.models.pydantic import WrappedBaseModel, field_validator


# The models in this module are all parts of responses from the QuantConnect API
# The keys of properties are not changed, so they don't obey the rest of the project's naming conventions


class QCAuth0Authorization(WrappedBaseModel):
authorization: Optional[Dict[str, Any]]
authorization: Optional[Dict[str, Any]] = None

def get_account_ids(self) -> List[str]:
"""
Expand Down Expand Up @@ -55,7 +55,7 @@ class ProjectEncryptionKey(WrappedBaseModel):
name: str

class QCCollaborator(WrappedBaseModel):
uid: Optional[int]
uid: Optional[int] = None
liveControl: bool
permission: str
profileImage: str
Expand All @@ -66,10 +66,10 @@ class QCCollaborator(WrappedBaseModel):
class QCParameter(WrappedBaseModel):
key: str
value: str
min: Optional[float]
max: Optional[float]
step: Optional[float]
type: Optional[str]
min: Optional[float] = None
max: Optional[float] = None
step: Optional[float] = None
type: Optional[str] = None


class QCLanguage(str, Enum):
Expand Down Expand Up @@ -112,7 +112,8 @@ class QCProject(WrappedBaseModel):
encrypted: Optional[bool] = False
encryptionKey: Optional[ProjectEncryptionKey] = None

@validator("parameters", pre=True)
@field_validator("parameters", mode="before")
@classmethod
def process_parameters_dict(cls, value: Any) -> Any:
if isinstance(value, dict):
return list(value.values())
Expand Down Expand Up @@ -181,9 +182,9 @@ class QCBacktest(WrappedBaseModel):
result: Optional[Any] = None
error: Optional[str] = None
stacktrace: Optional[str] = None
runtimeStatistics: Optional[Dict[str, str]]
statistics: Optional[Union[Dict[str, str], List[Any]]]
totalPerformance: Optional[Any]
runtimeStatistics: Optional[Dict[str, str]] = None
statistics: Optional[Union[Dict[str, str], List[Any]]] = None
totalPerformance: Optional[Any] = None

def is_complete(self) -> bool:
"""Returns whether the backtest has completed in the cloud.
Expand Down Expand Up @@ -271,7 +272,7 @@ class QCNode(WrappedBaseModel):
cpu: int
ram: float
assets: int
host: Optional[str]
host: Optional[str] = None


class QCNodeList(WrappedBaseModel):
Expand All @@ -297,7 +298,7 @@ class QCLiveAlgorithmStatus(str, Enum):

class QCRestResponse(WrappedBaseModel):
success: bool
error: Optional[List[str]]
error: Optional[List[str]] = None

class QCMinimalLiveAlgorithm(WrappedBaseModel):
projectId: int
Expand All @@ -317,7 +318,7 @@ class QCFullLiveAlgorithm(QCMinimalLiveAlgorithm):
deployId: str
status: Optional[QCLiveAlgorithmStatus] = None
launched: datetime
stopped: Optional[datetime]
stopped: Optional[datetime] = None
brokerage: str


Expand Down Expand Up @@ -389,7 +390,7 @@ class QCOrganizationProduct(WrappedBaseModel):


class QCOrganizationData(WrappedBaseModel):
signedTime: Optional[int]
signedTime: Optional[int] = None
current: bool


Expand Down Expand Up @@ -544,7 +545,8 @@ class QCOptimization(WrappedBaseModel):
backtests: Dict[str, QCOptimizationBacktest] = {}
runtimeStatistics: Dict[str, str] = {}

@validator("backtests", "runtimeStatistics", pre=True)
@field_validator("backtests", "runtimeStatistics", mode="before")
@classmethod
def parse_empty_lists(cls, value: Any) -> Any:
# If these fields have no data, they are assigned an array by default
# For consistency we convert those empty arrays to empty dicts
Expand Down Expand Up @@ -576,9 +578,10 @@ class QCDataVendor(WrappedBaseModel):
regex: Any

# Price in QCC
price: Optional[float]
price: Optional[float] = None

@validator("regex", pre=True)
@field_validator("regex", mode="before")
@classmethod
def parse_regex(cls, value: Any) -> Any:
from re import compile
if isinstance(value, str):
Expand Down Expand Up @@ -614,7 +617,7 @@ class QCDataset(WrappedBaseModel):
class QCUser(WrappedBaseModel):
name: str
profile: str
badge: Optional[str]
badge: Optional[str] = None


class QCTerminalNewsItem(WrappedBaseModel):
Expand All @@ -625,15 +628,15 @@ class QCTerminalNewsItem(WrappedBaseModel):
content: str
image: str
link: str
year_deleted: Optional[Any]
week_deleted: Optional[Any]
year_deleted: Optional[Any] = None
week_deleted: Optional[Any] = None
created: datetime
date: datetime


class QCLeanEnvironment(WrappedBaseModel):
id: int
name: str
path: Optional[str]
path: Optional[str] = None
description: str
public: bool
16 changes: 10 additions & 6 deletions lean/models/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,16 @@
from abc import ABC
from datetime import datetime
from enum import Enum
from typing import List, Any, Optional, Dict, Set, Tuple, Pattern
import re
from typing import List, Any, Optional, Dict, Set, Tuple

from click import prompt

from lean.click import DateParameter
from lean.container import container
from lean.models.api import QCDataVendor
from lean.models.logger import Option
from lean.models.pydantic import WrappedBaseModel, validator
from lean.models.pydantic import WrappedBaseModel, field_validator

class OptionResult(WrappedBaseModel):
"""The OptionResult class represents an option's result with an internal value and a display-friendly label."""
Expand Down Expand Up @@ -84,7 +85,8 @@ class DatasetOption(WrappedBaseModel, ABC):
description: str
condition: Optional[DatasetCondition] = None

@validator("condition", pre=True)
@field_validator("condition", mode="before")
@classmethod
def parse_condition(cls, value: Optional[Any]) -> Any:
if value is None or isinstance(value, DatasetCondition):
return value
Expand Down Expand Up @@ -258,7 +260,8 @@ class DatasetPath(WrappedBaseModel):
condition: Optional[DatasetCondition] = None
templates: DatasetPathTemplates

@validator("condition", pre=True)
@field_validator("condition", mode="before")
@classmethod
def parse_condition(cls, value: Optional[Any]) -> Any:
if value is None or isinstance(value, DatasetCondition):
return value
Expand Down Expand Up @@ -290,7 +293,8 @@ class Dataset(WrappedBaseModel):
paths: List[DatasetPath]
requirements: Dict[int, str]

@validator("options", pre=True)
@field_validator("options", mode="before")
@classmethod
def parse_options(cls, values: List[Any]) -> List[Any]:
option_types = {
"text": DatasetTextOption,
Expand Down Expand Up @@ -358,7 +362,7 @@ def get_valid_files(self, files_with_prefix: Optional[List[str]]) -> Set[str]:


class DataFileLatestGroup(DataFileGroup):
regex: Pattern
regex: re.Pattern

def get_valid_files(self, files_with_prefix: Optional[List[str]]) -> Set[str]:
if files_with_prefix is not None:
Expand Down
5 changes: 3 additions & 2 deletions lean/models/market_hours_database.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from datetime import datetime
from typing import List, Dict, Any

from lean.models.pydantic import WrappedBaseModel, validator
from lean.models.pydantic import WrappedBaseModel, field_validator

class MarketHoursSegment(WrappedBaseModel):
start: str
Expand All @@ -36,7 +36,8 @@ class MarketHoursDatabaseEntry(WrappedBaseModel):
earlyCloses: Dict[str, str] = {}
lateOpens: Dict[str, str] = {}

@validator("holidays", pre=True)
@field_validator("holidays", mode="before")
@classmethod
def parse_holidays(cls, value: Any) -> Any:
if isinstance(value, list):
return [datetime.strptime(x, "%m/%d/%Y") for x in value]
Expand Down
29 changes: 10 additions & 19 deletions lean/models/pydantic.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,17 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from pydantic import __version__ as pydantic_version
if pydantic_version.startswith("1."):
# We keep all this imports here, even if not used like validator, so other files can import them through this file
# to avoid having to check the pydantic version in every file.
# All imports should be done through this file to avoid pydantic version related errors.
from pydantic import BaseModel, ValidationError, Field, validator
else:
from pydantic.v1 import BaseModel, ValidationError, Field, validator
from pathlib import Path
from typing import Annotated
from pydantic import BaseModel, BeforeValidator, ConfigDict, ValidationError, Field, field_validator

# Path field that accepts str | Path. Converts in Python before pydantic-core runs,
# avoiding issues when pathlib is patched at runtime (e.g. pyfakefs).
SafePath = Annotated[Path, BeforeValidator(lambda v: v if isinstance(v, Path) else Path(v))]


class WrappedBaseModel(BaseModel):
"""A version of Pydantic's BaseModel which makes the input data accessible in case of a validation error."""

def __init__(self, *args, **kwargs) -> None:
"""Creates a new WrappedBaseModel instance.

:param args: args to pass on to the BaseModel constructor
:param kwargs: kwargs to pass on to the BaseModel constructor
"""
try:
super().__init__(*args, **kwargs)
except ValidationError as error:
error.input_value = kwargs
raise error
# Ensures backward compatibility: automatically converts numeric inputs to strings for string fields
model_config = ConfigDict(coerce_numbers_to_str=True)
5 changes: 2 additions & 3 deletions lean/models/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,8 @@
# limitations under the License.

from enum import Enum
from pathlib import Path

from lean.models.pydantic import WrappedBaseModel
from lean.models.pydantic import WrappedBaseModel, SafePath

class DebuggingMethod(Enum):
"""The debugging methods supported by the CLI."""
Expand Down Expand Up @@ -47,4 +46,4 @@ class CSharpLibrary(WrappedBaseModel):
class LeanLibraryReference(WrappedBaseModel):
"""The information of a library reference in a project's config.json file"""
name: str
path: Path
path: SafePath
5 changes: 3 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
# This file contains development dependencies
# Production dependencies are stored in setup.py

setuptools==69.5.1
-e .

wheel
pytest>=7.1.2,<8
pyfakefs>=5.4.1,<6
responses~=0.21
lxml-stubs==0.4.0
# Match pandas version to use same as Lean. Except that 2.1.* is only supported on Python 3.9+.
pandas==2.2.3;python_version>='3.9'
# Match pandas version used by LEAN. No prebuilt wheel for Python 3.14+, quantconnect-stubs will install a compatible version.
pandas==2.2.3;python_version>='3.9' and python_version<'3.14'
10 changes: 5 additions & 5 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,9 @@ def get_stubs_version_range() -> str:
"json5>=0.9.8",
"docker>=6.0.0",
"rich>=9.10.0",
"pydantic>=1.8.2",
"pydantic>=2.0.0",
"python-dateutil>=2.8.2",
"pytz",
"lxml>=4.9.0",
"joblib>=1.1.0",
"setuptools",
Expand All @@ -77,20 +78,19 @@ def get_stubs_version_range() -> str:
"console_scripts": ["lean=lean.main:main"]
},
install_requires=install_requires,
python_requires=">= 3.7",
python_requires=">= 3.9",
classifiers=[
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"Intended Audience :: Financial and Insurance Industry",
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13"
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14"
],
project_urls={
"Documentation": "https://www.lean.io/docs/v2/lean-cli/key-concepts/getting-started",
Expand Down