marketlab 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- marketlab-0.1.0/LICENSE +21 -0
- marketlab-0.1.0/PKG-INFO +210 -0
- marketlab-0.1.0/README.md +176 -0
- marketlab-0.1.0/pyproject.toml +86 -0
- marketlab-0.1.0/setup.cfg +4 -0
- marketlab-0.1.0/src/marketlab/__init__.py +5 -0
- marketlab-0.1.0/src/marketlab/_version.py +16 -0
- marketlab-0.1.0/src/marketlab/backtest/__init__.py +1 -0
- marketlab-0.1.0/src/marketlab/backtest/engine.py +61 -0
- marketlab-0.1.0/src/marketlab/backtest/metrics.py +44 -0
- marketlab-0.1.0/src/marketlab/cli.py +76 -0
- marketlab-0.1.0/src/marketlab/config.py +181 -0
- marketlab-0.1.0/src/marketlab/data/__init__.py +1 -0
- marketlab-0.1.0/src/marketlab/data/market.py +99 -0
- marketlab-0.1.0/src/marketlab/data/panel.py +124 -0
- marketlab-0.1.0/src/marketlab/evaluation/__init__.py +13 -0
- marketlab-0.1.0/src/marketlab/evaluation/walk_forward.py +173 -0
- marketlab-0.1.0/src/marketlab/features/__init__.py +1 -0
- marketlab-0.1.0/src/marketlab/features/engineering.py +42 -0
- marketlab-0.1.0/src/marketlab/log.py +10 -0
- marketlab-0.1.0/src/marketlab/models/__init__.py +16 -0
- marketlab-0.1.0/src/marketlab/models/registry.py +98 -0
- marketlab-0.1.0/src/marketlab/models/training.py +197 -0
- marketlab-0.1.0/src/marketlab/pipeline.py +444 -0
- marketlab-0.1.0/src/marketlab/rebalance.py +54 -0
- marketlab-0.1.0/src/marketlab/reports/__init__.py +1 -0
- marketlab-0.1.0/src/marketlab/reports/analytics.py +146 -0
- marketlab-0.1.0/src/marketlab/reports/markdown.py +158 -0
- marketlab-0.1.0/src/marketlab/reports/plots.py +68 -0
- marketlab-0.1.0/src/marketlab/reports/summary.py +215 -0
- marketlab-0.1.0/src/marketlab/resources/__init__.py +13 -0
- marketlab-0.1.0/src/marketlab/resources/config_templates/__init__.py +1 -0
- marketlab-0.1.0/src/marketlab/resources/config_templates/weekly_rank.yaml +53 -0
- marketlab-0.1.0/src/marketlab/resources/config_templates/weekly_rank_smoke.yaml +53 -0
- marketlab-0.1.0/src/marketlab/resources/templates.py +34 -0
- marketlab-0.1.0/src/marketlab/strategies/__init__.py +1 -0
- marketlab-0.1.0/src/marketlab/strategies/buy_hold.py +17 -0
- marketlab-0.1.0/src/marketlab/strategies/ranking.py +184 -0
- marketlab-0.1.0/src/marketlab/strategies/sma.py +50 -0
- marketlab-0.1.0/src/marketlab/targets/__init__.py +11 -0
- marketlab-0.1.0/src/marketlab/targets/weekly.py +143 -0
- marketlab-0.1.0/src/marketlab.egg-info/PKG-INFO +210 -0
- marketlab-0.1.0/src/marketlab.egg-info/SOURCES.txt +45 -0
- marketlab-0.1.0/src/marketlab.egg-info/dependency_links.txt +1 -0
- marketlab-0.1.0/src/marketlab.egg-info/entry_points.txt +2 -0
- marketlab-0.1.0/src/marketlab.egg-info/requires.txt +13 -0
- marketlab-0.1.0/src/marketlab.egg-info/top_level.txt +1 -0
marketlab-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ricardogr07
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
marketlab-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: marketlab
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Reproducible market research toolkit for weekly modeling, walk-forward evaluation, and baseline-plus-ML experiments.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/ricardogr07/market-lab
|
|
7
|
+
Project-URL: Repository, https://github.com/ricardogr07/market-lab
|
|
8
|
+
Project-URL: Issues, https://github.com/ricardogr07/market-lab/issues
|
|
9
|
+
Keywords: market-research,backtesting,walk-forward,machine-learning,portfolio
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Requires-Python: >=3.12
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: matplotlib>=3.8
|
|
22
|
+
Requires-Dist: pandas>=2.2
|
|
23
|
+
Requires-Dist: PyYAML>=6.0
|
|
24
|
+
Requires-Dist: yfinance>=0.2
|
|
25
|
+
Requires-Dist: scikit-learn>=1.4
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: build>=1.2; extra == "dev"
|
|
28
|
+
Requires-Dist: mkdocs>=1.6; extra == "dev"
|
|
29
|
+
Requires-Dist: mkdocs-include-markdown-plugin>=7.1; extra == "dev"
|
|
30
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
31
|
+
Requires-Dist: ruff>=0.11; extra == "dev"
|
|
32
|
+
Requires-Dist: tox>=4.0; extra == "dev"
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
# MarketLab
|
|
36
|
+
|
|
37
|
+
MarketLab is a package-first research toolkit for reproducible market experiments over a fixed ETF universe. The current implementation includes a working baseline-plus-ML workflow: weekly supervised modeling rows, walk-forward folds, trained models, rank-based ML strategies, shared out-of-sample experiments, and reviewable artifact summaries.
|
|
38
|
+
|
|
39
|
+
See [ARCHITECTURE.md](ARCHITECTURE.md) for the system map, data contracts, execution flow, and extension rules.
|
|
40
|
+
|
|
41
|
+
## Current Commands
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
python scripts/run_marketlab.py prepare-data --config configs/experiment.weekly_rank.yaml
|
|
45
|
+
python scripts/run_marketlab.py backtest --config configs/experiment.weekly_rank.yaml
|
|
46
|
+
python scripts/run_marketlab.py train-models --config configs/experiment.weekly_rank.yaml
|
|
47
|
+
python scripts/run_marketlab.py run-experiment --config configs/experiment.weekly_rank.yaml
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`python scripts/run_marketlab.py ...` is the canonical local invocation path because it always resolves to the source tree under `src/`.
|
|
51
|
+
|
|
52
|
+
## What Each Command Does
|
|
53
|
+
|
|
54
|
+
- `prepare-data`: build or reuse the cached prepared panel.
|
|
55
|
+
- `backtest`: run the rule baselines only (`buy_hold` and `sma`) and write performance, analytics summaries, report, and plots.
|
|
56
|
+
- `train-models`: fit the configured models across walk-forward folds and write raw training artifacts plus fold/model summary CSVs.
|
|
57
|
+
- `run-experiment`: run baselines and ML strategies together on the shared out-of-sample window and write the experiment outputs, analytics summaries, and ML summary CSVs.
|
|
58
|
+
|
|
59
|
+
## Artifact Outputs
|
|
60
|
+
|
|
61
|
+
### `train-models`
|
|
62
|
+
|
|
63
|
+
Writes a timestamped folder under `artifacts/runs/<experiment_name>/` containing:
|
|
64
|
+
|
|
65
|
+
- `folds.csv`
|
|
66
|
+
- `model_manifest.csv`
|
|
67
|
+
- `model_metrics.csv`
|
|
68
|
+
- `predictions.csv`
|
|
69
|
+
- `model_summary.csv`
|
|
70
|
+
- `fold_summary.csv`
|
|
71
|
+
- per-fold model pickles under `models/`
|
|
72
|
+
|
|
73
|
+
### `backtest`
|
|
74
|
+
|
|
75
|
+
Writes a timestamped folder under `artifacts/runs/<experiment_name>/` containing:
|
|
76
|
+
|
|
77
|
+
- `metrics.csv`
|
|
78
|
+
- `performance.csv`
|
|
79
|
+
- `strategy_summary.csv`
|
|
80
|
+
- `monthly_returns.csv`
|
|
81
|
+
- `turnover_costs.csv`
|
|
82
|
+
- `report.md`
|
|
83
|
+
- `cumulative_returns.png`
|
|
84
|
+
- `drawdown.png`
|
|
85
|
+
- `turnover.png`
|
|
86
|
+
|
|
87
|
+
### `run-experiment`
|
|
88
|
+
|
|
89
|
+
Writes a timestamped folder under `artifacts/runs/<experiment_name>/` containing:
|
|
90
|
+
|
|
91
|
+
- `metrics.csv`
|
|
92
|
+
- `performance.csv`
|
|
93
|
+
- `strategy_summary.csv`
|
|
94
|
+
- `monthly_returns.csv`
|
|
95
|
+
- `turnover_costs.csv`
|
|
96
|
+
- `report.md`
|
|
97
|
+
- `cumulative_returns.png`
|
|
98
|
+
- `drawdown.png`
|
|
99
|
+
- `turnover.png`
|
|
100
|
+
- `model_summary.csv`
|
|
101
|
+
- `fold_summary.csv`
|
|
102
|
+
- optional per-fold model pickles under `models/`
|
|
103
|
+
|
|
104
|
+
## Environment
|
|
105
|
+
|
|
106
|
+
- Python 3.12+
|
|
107
|
+
- Installed packages:
|
|
108
|
+
- `pandas`
|
|
109
|
+
- `PyYAML`
|
|
110
|
+
- `matplotlib`
|
|
111
|
+
- `yfinance`
|
|
112
|
+
- `scikit-learn`
|
|
113
|
+
|
|
114
|
+
## Quickstart
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
python -m pip install -e .[dev]
|
|
118
|
+
python scripts/run_marketlab.py run-experiment --config configs/experiment.weekly_rank.yaml
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
If `artifacts/data/panel.csv` already exists, the pipeline uses it and does not attempt a network download.
|
|
122
|
+
|
|
123
|
+
## Installed Package Quickstart
|
|
124
|
+
|
|
125
|
+
If you install MarketLab from PyPI or a built wheel, use the packaged CLI bootstrap flow instead of the repo launcher:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
marketlab --version
|
|
129
|
+
marketlab list-configs
|
|
130
|
+
marketlab write-config --name weekly_rank --output weekly_rank.yaml
|
|
131
|
+
marketlab run-experiment --config weekly_rank.yaml
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`list-configs` shows the bundled example templates, and `write-config` exports one of those templates into your working directory. That keeps the installed package self-contained without requiring a checkout of this repository.
|
|
135
|
+
|
|
136
|
+
## Local Validation
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
python -m pytest -q --basetemp .pytest_tmp
|
|
140
|
+
powershell -ExecutionPolicy Bypass -File scripts/run-e2e.ps1
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Local CI Entry Points
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
python -m uv sync --group dev
|
|
147
|
+
python -m uv run tox -e lint
|
|
148
|
+
python -m uv run tox -e docs
|
|
149
|
+
python -m uv run tox -e package
|
|
150
|
+
python -m uv run tox -e py312
|
|
151
|
+
python -m tox -e integration
|
|
152
|
+
python -m tox -e preflight
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Use `python -m tox -e preflight` as the canonical local pre-push gate. It runs the same lint, docs, packaging, unit-test, and offline integration checks that Phase 3 CI expects through one local entrypoint after the dev dependencies are installed.
|
|
156
|
+
|
|
157
|
+
The MkDocs site renders the current root Markdown docs through `mkdocs-include-markdown-plugin`, so the documentation build stays aligned with `README.md`, `ARCHITECTURE.md`, `Phase2-results.md`, and `PLAN.md`.
|
|
158
|
+
|
|
159
|
+
## Contribution Workflow
|
|
160
|
+
|
|
161
|
+
- Branch from a refreshed `master` instead of working directly on the default branch.
|
|
162
|
+
- Keep changes in small intentional commits so review scope stays clear.
|
|
163
|
+
- Run `python -m tox -e preflight` before pushing.
|
|
164
|
+
- Open a pull request for review instead of pushing directly to `master`.
|
|
165
|
+
- Treat the `Docker Runner` workflow as an optional manual smoke path, not as a required pre-push step.
|
|
166
|
+
- Keep Codex skills and other personal automation assets in the user-local Codex home rather than in the public repository or package surface.
|
|
167
|
+
- Expect `master` to move ahead of the last public release between monthly release batches.
|
|
168
|
+
|
|
169
|
+
## Dockerized CLI
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
docker build -t marketlab-cli .
|
|
173
|
+
docker run --rm marketlab-cli --help
|
|
174
|
+
docker run --rm marketlab-cli backtest --config configs/experiment.weekly_rank.smoke.yaml
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
The container uses the installed `marketlab` console script as its entrypoint. Keep using `python scripts/run_marketlab.py ...` for local source-tree development; the Docker image exists to validate the installed package path and to support manual GitHub Actions runs.
|
|
178
|
+
|
|
179
|
+
## Manual Docker Runner Workflow
|
|
180
|
+
|
|
181
|
+
GitHub Actions now includes a manual workflow named `Docker Runner` with these inputs:
|
|
182
|
+
|
|
183
|
+
- `command`: `backtest`, `train-models`, or `run-experiment`
|
|
184
|
+
- `config_path`: repo-relative config path inside the image, defaulting to `configs/experiment.weekly_rank.smoke.yaml`
|
|
185
|
+
|
|
186
|
+
The workflow defaults to `backtest`, builds the Docker image, runs the selected command inside the container, writes the resolved run directory into the job summary, and uploads the copied `artifacts/` tree as an Actions artifact.
|
|
187
|
+
|
|
188
|
+
This workflow is not part of the required PR CI checks. It is a manual historical real-data smoke runner around the checked-in smoke config, not a rolling weekly market automation job.
|
|
189
|
+
|
|
190
|
+
## Release Automation
|
|
191
|
+
|
|
192
|
+
GitHub Actions now includes a release workflow at `.github/workflows/release.yml`.
|
|
193
|
+
|
|
194
|
+
- Normal feature PRs still merge to `master` in sequence.
|
|
195
|
+
- Each merge to `master` updates the open Release PR managed by release-please.
|
|
196
|
+
- The Release PR accumulates the unreleased monthly or feature batch over time.
|
|
197
|
+
- Nothing is tagged or published when a normal feature PR lands on `master`.
|
|
198
|
+
- The actual Git tag, GitHub Release, and PyPI publish path run only when you merge the Release PR.
|
|
199
|
+
|
|
200
|
+
This means `master` can intentionally contain unreleased work while you continue merging feature PRs. The Release PR is the public-release gate.
|
|
201
|
+
|
|
202
|
+
### Manual Prerequisites
|
|
203
|
+
|
|
204
|
+
Before the first automated public release:
|
|
205
|
+
|
|
206
|
+
- verify that the `marketlab` package name is available on PyPI
|
|
207
|
+
- configure PyPI Trusted Publishing for this repository and the `pypi` environment
|
|
208
|
+
- create the GitHub Actions environment named `pypi`
|
|
209
|
+
|
|
210
|
+
The first automated public release target remains `v0.1.0`.
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# MarketLab
|
|
2
|
+
|
|
3
|
+
MarketLab is a package-first research toolkit for reproducible market experiments over a fixed ETF universe. The current implementation includes a working baseline-plus-ML workflow: weekly supervised modeling rows, walk-forward folds, trained models, rank-based ML strategies, shared out-of-sample experiments, and reviewable artifact summaries.
|
|
4
|
+
|
|
5
|
+
See [ARCHITECTURE.md](ARCHITECTURE.md) for the system map, data contracts, execution flow, and extension rules.
|
|
6
|
+
|
|
7
|
+
## Current Commands
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
python scripts/run_marketlab.py prepare-data --config configs/experiment.weekly_rank.yaml
|
|
11
|
+
python scripts/run_marketlab.py backtest --config configs/experiment.weekly_rank.yaml
|
|
12
|
+
python scripts/run_marketlab.py train-models --config configs/experiment.weekly_rank.yaml
|
|
13
|
+
python scripts/run_marketlab.py run-experiment --config configs/experiment.weekly_rank.yaml
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`python scripts/run_marketlab.py ...` is the canonical local invocation path because it always resolves to the source tree under `src/`.
|
|
17
|
+
|
|
18
|
+
## What Each Command Does
|
|
19
|
+
|
|
20
|
+
- `prepare-data`: build or reuse the cached prepared panel.
|
|
21
|
+
- `backtest`: run the rule baselines only (`buy_hold` and `sma`) and write performance, analytics summaries, report, and plots.
|
|
22
|
+
- `train-models`: fit the configured models across walk-forward folds and write raw training artifacts plus fold/model summary CSVs.
|
|
23
|
+
- `run-experiment`: run baselines and ML strategies together on the shared out-of-sample window and write the experiment outputs, analytics summaries, and ML summary CSVs.
|
|
24
|
+
|
|
25
|
+
## Artifact Outputs
|
|
26
|
+
|
|
27
|
+
### `train-models`
|
|
28
|
+
|
|
29
|
+
Writes a timestamped folder under `artifacts/runs/<experiment_name>/` containing:
|
|
30
|
+
|
|
31
|
+
- `folds.csv`
|
|
32
|
+
- `model_manifest.csv`
|
|
33
|
+
- `model_metrics.csv`
|
|
34
|
+
- `predictions.csv`
|
|
35
|
+
- `model_summary.csv`
|
|
36
|
+
- `fold_summary.csv`
|
|
37
|
+
- per-fold model pickles under `models/`
|
|
38
|
+
|
|
39
|
+
### `backtest`
|
|
40
|
+
|
|
41
|
+
Writes a timestamped folder under `artifacts/runs/<experiment_name>/` containing:
|
|
42
|
+
|
|
43
|
+
- `metrics.csv`
|
|
44
|
+
- `performance.csv`
|
|
45
|
+
- `strategy_summary.csv`
|
|
46
|
+
- `monthly_returns.csv`
|
|
47
|
+
- `turnover_costs.csv`
|
|
48
|
+
- `report.md`
|
|
49
|
+
- `cumulative_returns.png`
|
|
50
|
+
- `drawdown.png`
|
|
51
|
+
- `turnover.png`
|
|
52
|
+
|
|
53
|
+
### `run-experiment`
|
|
54
|
+
|
|
55
|
+
Writes a timestamped folder under `artifacts/runs/<experiment_name>/` containing:
|
|
56
|
+
|
|
57
|
+
- `metrics.csv`
|
|
58
|
+
- `performance.csv`
|
|
59
|
+
- `strategy_summary.csv`
|
|
60
|
+
- `monthly_returns.csv`
|
|
61
|
+
- `turnover_costs.csv`
|
|
62
|
+
- `report.md`
|
|
63
|
+
- `cumulative_returns.png`
|
|
64
|
+
- `drawdown.png`
|
|
65
|
+
- `turnover.png`
|
|
66
|
+
- `model_summary.csv`
|
|
67
|
+
- `fold_summary.csv`
|
|
68
|
+
- optional per-fold model pickles under `models/`
|
|
69
|
+
|
|
70
|
+
## Environment
|
|
71
|
+
|
|
72
|
+
- Python 3.12+
|
|
73
|
+
- Installed packages:
|
|
74
|
+
- `pandas`
|
|
75
|
+
- `PyYAML`
|
|
76
|
+
- `matplotlib`
|
|
77
|
+
- `yfinance`
|
|
78
|
+
- `scikit-learn`
|
|
79
|
+
|
|
80
|
+
## Quickstart
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
python -m pip install -e .[dev]
|
|
84
|
+
python scripts/run_marketlab.py run-experiment --config configs/experiment.weekly_rank.yaml
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
If `artifacts/data/panel.csv` already exists, the pipeline uses it and does not attempt a network download.
|
|
88
|
+
|
|
89
|
+
## Installed Package Quickstart
|
|
90
|
+
|
|
91
|
+
If you install MarketLab from PyPI or a built wheel, use the packaged CLI bootstrap flow instead of the repo launcher:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
marketlab --version
|
|
95
|
+
marketlab list-configs
|
|
96
|
+
marketlab write-config --name weekly_rank --output weekly_rank.yaml
|
|
97
|
+
marketlab run-experiment --config weekly_rank.yaml
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`list-configs` shows the bundled example templates, and `write-config` exports one of those templates into your working directory. That keeps the installed package self-contained without requiring a checkout of this repository.
|
|
101
|
+
|
|
102
|
+
## Local Validation
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
python -m pytest -q --basetemp .pytest_tmp
|
|
106
|
+
powershell -ExecutionPolicy Bypass -File scripts/run-e2e.ps1
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Local CI Entry Points
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
python -m uv sync --group dev
|
|
113
|
+
python -m uv run tox -e lint
|
|
114
|
+
python -m uv run tox -e docs
|
|
115
|
+
python -m uv run tox -e package
|
|
116
|
+
python -m uv run tox -e py312
|
|
117
|
+
python -m tox -e integration
|
|
118
|
+
python -m tox -e preflight
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Use `python -m tox -e preflight` as the canonical local pre-push gate. It runs the same lint, docs, packaging, unit-test, and offline integration checks that Phase 3 CI expects through one local entrypoint after the dev dependencies are installed.
|
|
122
|
+
|
|
123
|
+
The MkDocs site renders the current root Markdown docs through `mkdocs-include-markdown-plugin`, so the documentation build stays aligned with `README.md`, `ARCHITECTURE.md`, `Phase2-results.md`, and `PLAN.md`.
|
|
124
|
+
|
|
125
|
+
## Contribution Workflow
|
|
126
|
+
|
|
127
|
+
- Branch from a refreshed `master` instead of working directly on the default branch.
|
|
128
|
+
- Keep changes in small intentional commits so review scope stays clear.
|
|
129
|
+
- Run `python -m tox -e preflight` before pushing.
|
|
130
|
+
- Open a pull request for review instead of pushing directly to `master`.
|
|
131
|
+
- Treat the `Docker Runner` workflow as an optional manual smoke path, not as a required pre-push step.
|
|
132
|
+
- Keep Codex skills and other personal automation assets in the user-local Codex home rather than in the public repository or package surface.
|
|
133
|
+
- Expect `master` to move ahead of the last public release between monthly release batches.
|
|
134
|
+
|
|
135
|
+
## Dockerized CLI
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
docker build -t marketlab-cli .
|
|
139
|
+
docker run --rm marketlab-cli --help
|
|
140
|
+
docker run --rm marketlab-cli backtest --config configs/experiment.weekly_rank.smoke.yaml
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
The container uses the installed `marketlab` console script as its entrypoint. Keep using `python scripts/run_marketlab.py ...` for local source-tree development; the Docker image exists to validate the installed package path and to support manual GitHub Actions runs.
|
|
144
|
+
|
|
145
|
+
## Manual Docker Runner Workflow
|
|
146
|
+
|
|
147
|
+
GitHub Actions now includes a manual workflow named `Docker Runner` with these inputs:
|
|
148
|
+
|
|
149
|
+
- `command`: `backtest`, `train-models`, or `run-experiment`
|
|
150
|
+
- `config_path`: repo-relative config path inside the image, defaulting to `configs/experiment.weekly_rank.smoke.yaml`
|
|
151
|
+
|
|
152
|
+
The workflow defaults to `backtest`, builds the Docker image, runs the selected command inside the container, writes the resolved run directory into the job summary, and uploads the copied `artifacts/` tree as an Actions artifact.
|
|
153
|
+
|
|
154
|
+
This workflow is not part of the required PR CI checks. It is a manual historical real-data smoke runner around the checked-in smoke config, not a rolling weekly market automation job.
|
|
155
|
+
|
|
156
|
+
## Release Automation
|
|
157
|
+
|
|
158
|
+
GitHub Actions now includes a release workflow at `.github/workflows/release.yml`.
|
|
159
|
+
|
|
160
|
+
- Normal feature PRs still merge to `master` in sequence.
|
|
161
|
+
- Each merge to `master` updates the open Release PR managed by release-please.
|
|
162
|
+
- The Release PR accumulates the unreleased monthly or feature batch over time.
|
|
163
|
+
- Nothing is tagged or published when a normal feature PR lands on `master`.
|
|
164
|
+
- The actual Git tag, GitHub Release, and PyPI publish path run only when you merge the Release PR.
|
|
165
|
+
|
|
166
|
+
This means `master` can intentionally contain unreleased work while you continue merging feature PRs. The Release PR is the public-release gate.
|
|
167
|
+
|
|
168
|
+
### Manual Prerequisites
|
|
169
|
+
|
|
170
|
+
Before the first automated public release:
|
|
171
|
+
|
|
172
|
+
- verify that the `marketlab` package name is available on PyPI
|
|
173
|
+
- configure PyPI Trusted Publishing for this repository and the `pypi` environment
|
|
174
|
+
- create the GitHub Actions environment named `pypi`
|
|
175
|
+
|
|
176
|
+
The first automated public release target remains `v0.1.0`.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "marketlab"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Reproducible market research toolkit for weekly modeling, walk-forward evaluation, and baseline-plus-ML experiments."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
keywords = ["market-research", "backtesting", "walk-forward", "machine-learning", "portfolio"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"Intended Audience :: Financial and Insurance Industry",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Topic :: Office/Business :: Financial :: Investment",
|
|
21
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"matplotlib>=3.8",
|
|
25
|
+
"pandas>=2.2",
|
|
26
|
+
"PyYAML>=6.0",
|
|
27
|
+
"yfinance>=0.2",
|
|
28
|
+
"scikit-learn>=1.4",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
dev = [
|
|
33
|
+
"build>=1.2",
|
|
34
|
+
"mkdocs>=1.6",
|
|
35
|
+
"mkdocs-include-markdown-plugin>=7.1",
|
|
36
|
+
"pytest>=8.0",
|
|
37
|
+
"ruff>=0.11",
|
|
38
|
+
"tox>=4.0",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[project.urls]
|
|
42
|
+
Homepage = "https://github.com/ricardogr07/market-lab"
|
|
43
|
+
Repository = "https://github.com/ricardogr07/market-lab"
|
|
44
|
+
Issues = "https://github.com/ricardogr07/market-lab/issues"
|
|
45
|
+
|
|
46
|
+
[dependency-groups]
|
|
47
|
+
dev = [
|
|
48
|
+
"build>=1.2",
|
|
49
|
+
"mkdocs>=1.6",
|
|
50
|
+
"mkdocs-include-markdown-plugin>=7.1",
|
|
51
|
+
"pytest>=8.0",
|
|
52
|
+
"ruff>=0.11",
|
|
53
|
+
"tox>=4.0",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
[project.scripts]
|
|
57
|
+
marketlab = "marketlab.cli:main"
|
|
58
|
+
|
|
59
|
+
[tool.setuptools]
|
|
60
|
+
package-dir = {"" = "src"}
|
|
61
|
+
include-package-data = true
|
|
62
|
+
|
|
63
|
+
[tool.setuptools.package-data]
|
|
64
|
+
"marketlab.resources.config_templates" = ["*.yaml"]
|
|
65
|
+
|
|
66
|
+
[tool.setuptools.packages.find]
|
|
67
|
+
where = ["src"]
|
|
68
|
+
|
|
69
|
+
[tool.uv]
|
|
70
|
+
default-groups = ["dev"]
|
|
71
|
+
|
|
72
|
+
[tool.ruff]
|
|
73
|
+
target-version = "py312"
|
|
74
|
+
src = ["src", "tests", "scripts"]
|
|
75
|
+
extend-exclude = ["artifacts", "build", "dist", "site", ".pytest_tmp*", "tmpk_x17fq8"]
|
|
76
|
+
|
|
77
|
+
[tool.ruff.lint]
|
|
78
|
+
select = ["E4", "E7", "E9", "F", "I"]
|
|
79
|
+
|
|
80
|
+
[tool.pytest.ini_options]
|
|
81
|
+
testpaths = ["tests"]
|
|
82
|
+
addopts = "--basetemp=.pytest_tmp -p no:cacheprovider"
|
|
83
|
+
markers = [
|
|
84
|
+
"real_data: opt-in tests that download real market data.",
|
|
85
|
+
"network: tests that depend on external network access.",
|
|
86
|
+
]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from importlib import metadata
|
|
4
|
+
|
|
5
|
+
FALLBACK_VERSION = "0.0.0+local"
|
|
6
|
+
DIST_NAME = "marketlab"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_version(dist_name: str = DIST_NAME, fallback: str = FALLBACK_VERSION) -> str:
|
|
10
|
+
try:
|
|
11
|
+
return metadata.version(dist_name)
|
|
12
|
+
except metadata.PackageNotFoundError:
|
|
13
|
+
return fallback
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
__version__ = get_version()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Backtest engine and metrics."""
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def run_backtest(
|
|
7
|
+
panel: pd.DataFrame,
|
|
8
|
+
weights: pd.DataFrame,
|
|
9
|
+
cost_bps: float,
|
|
10
|
+
) -> pd.DataFrame:
|
|
11
|
+
required_weight_columns = {"strategy", "effective_date", "symbol", "weight"}
|
|
12
|
+
missing = required_weight_columns - set(weights.columns)
|
|
13
|
+
if missing:
|
|
14
|
+
joined = ", ".join(sorted(missing))
|
|
15
|
+
raise ValueError(f"Weights frame is missing required columns: {joined}")
|
|
16
|
+
|
|
17
|
+
strategy_names = weights["strategy"].drop_duplicates().tolist()
|
|
18
|
+
if len(strategy_names) != 1:
|
|
19
|
+
raise ValueError("Backtest engine expects weights for exactly one strategy.")
|
|
20
|
+
strategy_name = strategy_names[0]
|
|
21
|
+
|
|
22
|
+
working_panel = panel.sort_values(["timestamp", "symbol"]).copy()
|
|
23
|
+
unique_dates = pd.Index(sorted(working_panel["timestamp"].drop_duplicates()))
|
|
24
|
+
symbols = sorted(working_panel["symbol"].unique())
|
|
25
|
+
|
|
26
|
+
adj_close = working_panel.pivot(index="timestamp", columns="symbol", values="adj_close")
|
|
27
|
+
adj_open = working_panel.pivot(index="timestamp", columns="symbol", values="adj_open")
|
|
28
|
+
|
|
29
|
+
overnight_returns = (adj_open / adj_close.shift(1)) - 1.0
|
|
30
|
+
intraday_returns = (adj_close / adj_open) - 1.0
|
|
31
|
+
|
|
32
|
+
weights_pivot = (
|
|
33
|
+
weights.copy()
|
|
34
|
+
.assign(effective_date=lambda frame: pd.to_datetime(frame["effective_date"]))
|
|
35
|
+
.pivot(index="effective_date", columns="symbol", values="weight")
|
|
36
|
+
.reindex(columns=symbols, fill_value=0.0)
|
|
37
|
+
)
|
|
38
|
+
weights_pivot = weights_pivot.reindex(unique_dates).ffill().fillna(0.0)
|
|
39
|
+
|
|
40
|
+
post_open_weights = weights_pivot
|
|
41
|
+
pre_open_weights = post_open_weights.shift(1).fillna(0.0)
|
|
42
|
+
turnover = (post_open_weights - pre_open_weights).abs().sum(axis=1)
|
|
43
|
+
|
|
44
|
+
gross_returns = (
|
|
45
|
+
(pre_open_weights * overnight_returns.fillna(0.0)).sum(axis=1)
|
|
46
|
+
+ (post_open_weights * intraday_returns.fillna(0.0)).sum(axis=1)
|
|
47
|
+
)
|
|
48
|
+
costs = turnover * (cost_bps / 10_000.0)
|
|
49
|
+
net_returns = gross_returns - costs
|
|
50
|
+
equity = (1.0 + net_returns).cumprod()
|
|
51
|
+
|
|
52
|
+
return pd.DataFrame(
|
|
53
|
+
{
|
|
54
|
+
"date": unique_dates,
|
|
55
|
+
"strategy": strategy_name,
|
|
56
|
+
"gross_return": gross_returns.values,
|
|
57
|
+
"net_return": net_returns.values,
|
|
58
|
+
"turnover": turnover.values,
|
|
59
|
+
"equity": equity.values,
|
|
60
|
+
}
|
|
61
|
+
)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def compute_strategy_metrics(performance: pd.DataFrame) -> pd.DataFrame:
|
|
9
|
+
rows: list[dict[str, float | str]] = []
|
|
10
|
+
for strategy, frame in performance.groupby("strategy", sort=False):
|
|
11
|
+
ordered = frame.sort_values("date").reset_index(drop=True)
|
|
12
|
+
returns = ordered["net_return"]
|
|
13
|
+
equity = ordered["equity"]
|
|
14
|
+
periods = len(ordered)
|
|
15
|
+
|
|
16
|
+
cumulative_return = float(equity.iloc[-1] - 1.0) if periods else 0.0
|
|
17
|
+
annualized_return = (
|
|
18
|
+
float((equity.iloc[-1] ** (252.0 / periods)) - 1.0) if periods else 0.0
|
|
19
|
+
)
|
|
20
|
+
annualized_volatility = float(returns.std(ddof=0) * math.sqrt(252.0))
|
|
21
|
+
sharpe_like = (
|
|
22
|
+
annualized_return / annualized_volatility if annualized_volatility else 0.0
|
|
23
|
+
)
|
|
24
|
+
drawdown = (equity / equity.cummax()) - 1.0
|
|
25
|
+
max_drawdown = float(drawdown.min()) if not drawdown.empty else 0.0
|
|
26
|
+
hit_rate = float((returns > 0.0).mean()) if periods else 0.0
|
|
27
|
+
avg_turnover = float(ordered["turnover"].mean()) if periods else 0.0
|
|
28
|
+
total_turnover = float(ordered["turnover"].sum()) if periods else 0.0
|
|
29
|
+
|
|
30
|
+
rows.append(
|
|
31
|
+
{
|
|
32
|
+
"strategy": strategy,
|
|
33
|
+
"cumulative_return": cumulative_return,
|
|
34
|
+
"annualized_return": annualized_return,
|
|
35
|
+
"annualized_volatility": annualized_volatility,
|
|
36
|
+
"sharpe_like": sharpe_like,
|
|
37
|
+
"max_drawdown": max_drawdown,
|
|
38
|
+
"hit_rate": hit_rate,
|
|
39
|
+
"avg_turnover": avg_turnover,
|
|
40
|
+
"total_turnover": total_turnover,
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
return pd.DataFrame(rows)
|