Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ Changelog = "https://github.com/initial-d/ml-quant-trading/blob/main/CHANGELOG.m
[tool.setuptools.packages.find]
where = ["src"]

[tool.setuptools.package-data]
"mlquant.configs" = ["*.yaml"]

[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-ra -q --strict-markers"
Expand Down
19 changes: 16 additions & 3 deletions src/mlquant/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import json
import math
import pickle
from importlib.resources import as_file, files
from pathlib import Path

import click
Expand Down Expand Up @@ -104,13 +105,25 @@ def cli() -> None:
@click.option(
"--config",
"config_path",
default="configs/small.yaml",
show_default=True,
default=None,
show_default="packaged small config",
type=click.Path(exists=True),
)
@click.pass_context
def cmd_demo(ctx: click.Context, config_path: str) -> None:
def cmd_demo(ctx: click.Context, config_path: str | None) -> None:
"""Run the complete synthetic factor-to-backtest pipeline."""
if config_path is not None:
_run_demo(ctx, config_path)
return

default_config = files("mlquant.configs").joinpath("small.yaml")
with as_file(default_config) as default_path:
click.echo("Using packaged default config (override with --config PATH).\n")
_run_demo(ctx, str(default_path))


def _run_demo(ctx: click.Context, config_path: str) -> None:
"""Run all demo stages with an explicit, existing config path."""
stages = (
("1/5 Generate deterministic synthetic data", cmd_gen_data),
("2/5 Compute the 213-factor tensor", cmd_features),
Expand Down
1 change: 1 addition & 0 deletions src/mlquant/configs/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Packaged configuration files for zero-setup CLI entry points."""
41 changes: 41 additions & 0 deletions src/mlquant/configs/small.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Small synthetic config — runs end-to-end in <1 minute on a laptop CPU.
# This is the configuration that `make paper` and CI exercise.

seed: 42
artifacts_dir: artifacts/small
limit_pct: 0.098
costs_bps: 5.0
cov_lookback: 60
hidden: 64
loss_gamma: 0.1

synthetic:
n_stocks: 200
n_dates: 500
start_date: "2022-01-04"
annual_drift: 0.05
annual_vol: 0.30
market_beta: 0.6
halt_prob: 0.005
limit_pct: 0.10
seed: 42
device: cpu

train:
epochs: 8
batch_size: 1024
lr: 1.0e-3
weight_decay: 1.0e-5
grad_clip: 1.0
device: cpu
log_every: 50
save_dir: artifacts/small/checkpoints
val_split: 0.1

portfolio:
risk_aversion: 5.0
weight_cap: 0.05
long_only: true
cash_weight: 0.0
solver: SCS
shrinkage: true
29 changes: 29 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import json
from pathlib import Path

from click.testing import CliRunner

Expand Down Expand Up @@ -47,6 +48,34 @@ def test_demo_is_visible_in_cli_help():
assert "demo" in result.output


def test_demo_uses_packaged_config_outside_repository(monkeypatch, tmp_path):
calls = []

for command in (cmd_gen_data, cmd_features, cmd_train, cmd_portfolio, cmd_backtest):
name = command.name

def callback(config_path, stage=name):
calls.append((stage, config_path, Path(config_path).read_text()))

monkeypatch.setattr(command, "callback", callback)

with CliRunner().isolated_filesystem(temp_dir=tmp_path):
result = CliRunner().invoke(cli, ["demo"])

assert result.exit_code == 0
assert len(calls) == 5
assert all(Path(path).name == "small.yaml" for _, path, _ in calls)
assert all("n_stocks: 200" in content for _, _, content in calls)
assert "Using packaged default config" in result.output


def test_packaged_demo_config_matches_repository_config():
packaged = Path(__file__).parents[1] / "src" / "mlquant" / "configs" / "small.yaml"
repository = Path(__file__).parents[1] / "configs" / "small.yaml"

assert packaged.read_text() == repository.read_text()


def test_backtest_summary_is_shareable_and_strict_json(tmp_path):
markdown_path, json_path = _write_backtest_summary(
tmp_path,
Expand Down
Loading