diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8910c54f..f1f82e91 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,15 +14,15 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Cache pip - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('pyproject.toml') }} @@ -43,8 +43,31 @@ jobs: - name: isort run: isort --check-only . + - name: Mypy + if: matrix.python-version == '3.12' + run: >- + mypy + --follow-imports=skip + src/microalpha/audit_lab.py + src/microalpha/multiple_testing.py + src/microalpha/engine.py + src/microalpha/execution.py + src/microalpha/reporting/factors.py + - name: detect-secrets - run: detect-secrets scan --baseline .secrets.baseline + run: | + git ls-files -z README.md PROJECT.md pyproject.toml LICENSE CHANGELOG.md \ + Makefile 'src/**' 'tests/**' 'scripts/**' '.github/**' \ + docs/index.md docs/audit-lab.md docs/architecture.md docs/api.md \ + docs/examples.md docs/reproducibility.md docs/leakage-safety.md \ + docs/benchmarks.md docs/limitations.md docs/portfolio_evidence_2026-07-11.md \ + docs/wrds.md docs/flagship_momentum_wrds.md docs/results_wrds.md docs/factors.md \ + 'docs/assets/audit_lab/**' \ + | xargs -0 detect-secrets-hook --baseline .secrets.baseline + + - name: Licensed-data policy + if: matrix.python-version == '3.12' + run: python scripts/check_data_policy.py - name: Pytest (unit) run: pytest -m "not wrds" --cov=microalpha --cov-report=xml --cov-report=html @@ -54,11 +77,16 @@ jobs: - name: Upload HTML coverage if: matrix.python-version == '3.12' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: htmlcov path: htmlcov + - name: Reproduce Audit Lab receipt + run: | + microalpha audit-demo + git diff --exit-code -- docs/assets/audit_lab + - name: MkDocs build if: matrix.python-version == '3.12' - run: mkdocs build + run: mkdocs build --strict diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 29a4fae1..e413cf23 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -16,10 +16,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.12' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f4fbcabe..7758f747 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,28 +3,57 @@ name: Release on: push: tags: - - 'v*' + - "v*" workflow_dispatch: +permissions: + contents: write + id-token: write + attestations: write + jobs: - build-and-publish: + build-verify-release: runs-on: ubuntu-latest - permissions: - contents: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 + - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: - python-version: '3.11' - - name: Install build tools + python-version: "3.12" + + - name: Build distributions run: | python -m pip install --upgrade pip - pip install build twine - - name: Build sdist/wheel - run: python -m build - - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 + python -m pip install build twine + python -m build + python -m twine check dist/* + + - name: Smoke-test wheel in a clean environment + run: | + python -m venv /tmp/microalpha-wheel-smoke + /tmp/microalpha-wheel-smoke/bin/python -m pip install dist/*.whl + /tmp/microalpha-wheel-smoke/bin/microalpha audit-demo --out /tmp/audit-lab + test "$(sha256sum /tmp/audit-lab/receipt.json | cut -d' ' -f1)" = \ + "6e36c2397696d7e9eecbd058cbfc1ba522c8ffba7e5798224de86b20457b6575" + + - name: Attest release artifacts + uses: actions/attest@v4 + with: + subject-path: "dist/*" + + - name: Upload workflow artifact + uses: actions/upload-artifact@v7 with: - password: ${{ secrets.PYPI_API_TOKEN }} + name: microalpha-distributions + path: dist/* + if-no-files-found: error + + - name: Create GitHub release + if: github.ref_type == 'tag' + env: + GH_TOKEN: ${{ github.token }} + run: gh release create "${GITHUB_REF_NAME}" dist/* --verify-tag --generate-notes + # The "microalpha" name on PyPI belongs to an unrelated project. + # Releases intentionally remain GitHub-only unless the distribution is renamed. diff --git a/.secrets.baseline b/.secrets.baseline index c30fc7b0..d2f02e89 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -124,7 +124,7 @@ { "path": "detect_secrets.filters.regex.should_exclude_file", "pattern": [ - "(artifacts|data_sp500|data_sp500_enriched|site)" + "(artifacts|data_sp500|data_sp500_enriched|site|docs/assets/audit_lab|docs/audit-lab.md)" ] } ], diff --git a/CHANGELOG.md b/CHANGELOG.md index dedbf962..17e40cc1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,26 @@ All notable changes to this project will be documented in this file. The format follows [Keep a Changelog](https://keepachangelog.com) and the project adheres to [Semantic Versioning](https://semver.org). ## [Unreleased] + +## [0.2.0] - 2026-07-15 + ### Added +- Deterministic synthetic Audit Lab covering point-in-time availability, + event-time execution, cost reconciliation, and selection correction. +- Null-centered synchronous benchmark-differential max-statistic test. +- Byte-stable JSON, CSV, and SVG evidence with a SHA-256 receipt. + +### Fixed +- Future fills are planned and materialized only when the engine reaches their + market timestamp; positions, cash, P&L, turnover, and logs no longer mutate + early. +- Public product, package metadata, documentation navigation, and claim + boundaries now agree. +- Release automation no longer attempts to publish to an unrelated PyPI + namespace; wheels are clean-installed, smoke-tested, attested, and attached to + GitHub releases. + +### Earlier project history - A public-safe validation-frontier chart with its source CSV and an evidence note binding each published aggregate metric to the accepted 2026-07-11 research artifacts. - Repository guardrails: pytest marker config, WRDS detection helpers, log fan-out to `artifacts/logs/`. - Pre-commit automation (black, isort, ruff, detect-secrets) plus tightened `.gitignore`. diff --git a/LICENSE b/LICENSE index e69de29b..85238267 100644 --- a/LICENSE +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Mateo Bodon + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile index 3156ea19..99f1dd04 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ WRDS_ARTIFACT_DIR ?= artifacts/wrds_flagship WRDS_SMOKE_CONFIG ?= configs/wfv_flagship_wrds_smoke.yaml WRDS_SMOKE_ARTIFACT_DIR ?= artifacts/wrds_flagship_smoke -.PHONY: dev test test-fast test-wrds sample wfv wfv-wrds wfv-wrds-smoke wrds wrds-flagship report report-wrds report-wrds-smoke docs clean export-wrds report-wfv gpt-bundle check-data-policy validate-runlogs runs-index +.PHONY: dev test test-fast test-wrds sample audit-demo wfv wfv-wrds wfv-wrds-smoke wrds wrds-flagship report report-wrds report-wrds-smoke docs clean export-wrds report-wfv gpt-bundle check-data-policy validate-runlogs runs-index dev: pip install -e '.[dev]' @@ -25,6 +25,9 @@ test-wrds: sample: microalpha run --config $(SAMPLE_CONFIG) --out $(ARTIFACT_DIR) +audit-demo: + microalpha audit-demo --out docs/assets/audit_lab + wfv: microalpha wfv --config $(SAMPLE_WFV_CONFIG) --out $(WFV_ARTIFACT_DIR) diff --git a/PROJECT.md b/PROJECT.md index 6278a7a8..e1e6640a 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -2,37 +2,38 @@ ## Project Profile - Name: microalpha -- One-liner: Leakage-safe, event-driven backtesting engine with walk-forward cross-validation and reporting. -- Type: research/trading +- One-liner: Quant research audit lab that makes leakage, impossible execution, omitted costs, and selection overfitting visibly fail. +- Type: quantitative engineering / research infrastructure - Risk tier: high - Primary languages: Python - External dependencies / services: WRDS/CRSP exports (optional), MkDocs (docs site) ## Goals (what “done” looks like) -- Leakage-safe backtesting and walk-forward validation with reproducible artifacts. -- Sample/public data runs plus an optional WRDS pipeline for real data. -- Report generation (plots + Markdown summaries) suitable for audit/review. +- One-command, deterministic Audit Lab evidence with known ground truth and a SHA-256 receipt. +- Event-scheduled execution, point-in-time availability, explicit cost reconciliation, and benchmark-differential selection correction. +- Clean-clone install, usable CLI/API, green multi-version CI, and product-first docs. +- Honest public case studies; a negative research result is preserved instead of tuned away. ## Non-goals (explicitly out of scope) - Live trading execution or brokerage integration. - Guaranteed alpha discovery or performance claims. ## Current state -- What works: sample/public configs, WFV runs, reporting, docs + tests. -- What’s missing: real-data runs require local WRDS exports and credentials. -- What’s broken: see `project_state/KNOWN_ISSUES.md` for open issues. -- Biggest risks: leakage/survivorship bias, missing WRDS data, misreported results. +- What works: deterministic Audit Lab, sample/public configs, WFV runs, reporting, docs, CLI/API, and tests. +- Optional: licensed-data workflows require authorized local exports and never ship raw rows. +- Historical research: six frozen mechanisms failed promotion gates; 2023–2025 remains sealed. +- Biggest risks: incorrect source availability metadata, survivorship bias, uncalibrated simulation costs, and claims stronger than receipts. ## Quickstart (how to run) -- `python -m venv .venv && source .venv/bin/activate && pip install -e '.[dev]'` -- `make sample && make report` -- `make wfv && make report-wfv` -- `pytest -q` +- `python -m venv .venv && source .venv/bin/activate && pip install .` +- `microalpha audit-demo` +- `git diff --exit-code -- docs/assets/audit_lab` +- Contributors: `pip install -e '.[dev]' && pytest -q` ## Architecture (high-level) - Modules: `src/microalpha/` (engine, data, strategies, reporting, CLI). -- Data flow: DataHandler -> Engine -> Strategy -> Portfolio -> Broker -> Trades. -- Key invariants: strict chronology, t+1 execution, point-in-time universe. +- Data flow: DataHandler -> Engine clock -> Strategy -> Portfolio -> ExecutionPlan -> Broker/materialized FillEvent -> Evidence receipt. +- Key invariants: availability at decision time, no early fill mutation, exact cost reconciliation, isolated test/holdout windows, synchronous null-centered selection correction. ## Constraints / preferences - Performance constraints: deterministic runs; prefer reproducible pipelines. diff --git a/README.md b/README.md index d012b630..5ffd113b 100644 --- a/README.md +++ b/README.md @@ -1,222 +1,212 @@ # microalpha +[![CI](https://github.com/MateoBodon/microalpha/actions/workflows/ci.yml/badge.svg)](https://github.com/MateoBodon/microalpha/actions/workflows/ci.yml) [![Documentation](https://img.shields.io/badge/docs-live-2563eb)](https://mateobodon.github.io/microalpha/) +[![Python 3.10–3.12](https://img.shields.io/badge/python-3.10–3.12-0ea5e9)](pyproject.toml) +[![MIT License](https://img.shields.io/badge/license-MIT-22c55e)](LICENSE) -**A leakage-aware, event-driven research engine for turning quantitative ideas -into timestamped, costed, reproducible evidence.** +**A quant research audit lab that catches four ways a backtest lies: data +leakage, impossible execution, omitted costs, and selection overfitting.** -microalpha is built for the part of backtesting that is easiest to get wrong: -chronology, point-in-time data, execution timing, model selection, transaction -costs, and the boundary between an interesting validation result and a claim -that is actually ready to publish. +Microalpha is an event-driven Python system for turning a quantitative idea +into timestamped, costed, walk-forward evidence. Its flagship is not a profitable +strategy. It is a deterministic, known-ground-truth fixture that proves the +research pipeline rejects attractive results when they are invalid. -> **Status — research infrastructure, not a live trading system.** The public -> sample workflows validate the engine and reporting path. The latest reviewed -> licensed-data campaign remains pre-holdout: its 2023–2025 final holdout is -> sealed, and no alpha or live-performance claim is made. +![Four paired Audit Lab results: leaky versus point-in-time data, same-tick versus queued execution, gross versus costed returns, and naive versus corrected selection](docs/assets/audit_lab/audit_lab.svg) -| Completed evidence | Scope | Claim boundary | -| --- | --- | --- | -| Six frozen mechanisms | 2017–2022 validation | Every candidate was rejected by at least one preregistered gate | -| Immutable run manifests | Config, data identity, code state, outputs | Aggregate public receipts; licensed rows remain local | -| Final holdout | 2023–2025 | Sealed and not used in the reported economic evidence | - -## What it makes auditable - -| Research risk | microalpha control | -| --- | --- | -| Lookahead and same-period execution | Timestamp validation, explicit signal/fill clocks, tested `t+1` fills | -| Selection overfitting | Walk-forward folds, preregistered candidate sets, stationary-bootstrap reality checks | -| Frictionless backtests | Commission, slippage, borrow, turnover, capacity, and stress-cost accounting | -| Unreproducible results | Resolved configs, dataset/artifact manifests, run IDs, immutable metrics and trades | -| Licensed-data leakage | Raw WRDS/CRSP data stays local; only reviewed aggregate evidence is publishable | - -## Quickstart - -Requires Python 3.12+. +## The 30-second proof ```bash git clone https://github.com/MateoBodon/microalpha.git cd microalpha -python -m venv .venv -source .venv/bin/activate -python -m pip install -e '.[dev]' - -make sample -make report +python -m venv .venv && source .venv/bin/activate +python -m pip install . +microalpha audit-demo ``` -The sample run writes a self-describing artifact directory containing the -resolved config, metrics, trades, exposures, equity curve, bootstrap result, -and manifest. Run the walk-forward path with: - -```bash -make wfv -make report-wfv -``` - -These bundled inputs are for deterministic software validation—not evidence of -tradable performance. - -## Research flow +The command uses no network, provider, licensed data, or hidden holdout. It +recreates the tracked evidence under `docs/assets/audit_lab/`. + +| Failure injected into the synthetic fixture | Naive result | Audited result | What stopped it | +| --- | ---: | ---: | --- | +| Revised value used before availability | Sharpe `+20.53` | `−0.17` | 756 unavailable rows blocked | +| Same-tick signal and fill | Sharpe `+21.89` | `+0.17` | Fill queued until the t+1 market event | +| Costs omitted from a planted control | Sharpe `+0.57` | `−0.68` | Commission, spread, impact, and borrow reconciled | +| Best of 128 noise models | Sharpe `+1.38` | OOS `−1.28` | Walk-forward split; max-stat `p=0.601` | + +The separately labeled planted positive control passes at `p=0.001`, showing +that the correction is capable of detecting a known effect rather than merely +rejecting everything. These values are software-test outputs, not market +performance or evidence of alpha. + +Canonical receipt SHA-256: +`6e36c2397696d7e9eecbd058cbfc1ba522c8ffba7e5798224de86b20457b6575`. +The [receipt](docs/assets/audit_lab/receipt.json) binds the input fixture, +generator version and source, and every JSON, CSV, and SVG artifact by hash. + +On the current Apple arm64 benchmark host, Audit Lab completed in a median +`1.3745 s` across five runs and the no-op event loop processed `1,464,231` +events/s. These are host-dependent engineering baselines, documented in the +[benchmark receipt](docs/assets/audit_lab/benchmark.json), not correctness or +performance promises. + +## What is engineered, not asserted + +![Audit lineage from synthetic oracle through availability gate, event queue, cost ledger, and SHA-256 receipt](docs/assets/audit_lab/data_lineage.svg) + +- **Point-in-time discipline** — `require_point_in_time` fails closed on + `available_at > decision_at` and reports exact violating row IDs and counts; + production data manifests retain source lineage. +- **Event-time execution** — orders become planned execution slices; future + fills cannot change cash, positions, turnover, P&L, or logs before their + market event is processed. +- **Configurable execution costs** — commission, spread, slippage/impact, + borrow, turnover, exposure, and capacity controls. These are simulation + models, not claims of venue calibration. +- **Walk-forward evaluation** — parameter selection is isolated from test and + holdout windows, with fold-level manifests and artifacts. +- **Selection control** — candidate-minus-benchmark returns are null-centered + and synchronously resampled for a max-statistic test that preserves + cross-model dependence. +- **Artifact provenance** — generator source, version, seed, schema, inputs, + and canonical outputs are hash-bound. The Audit Lab excludes clocks, hosts, + and absolute paths, so clean directories reproduce identical bytes. + +Audit Lab uses transparent NumPy oracle constructions so each injected failure +has known ground truth. Production event scheduling is exercised separately by +the [future-fill regression test](tests/test_tplus1_execution.py); the shared +max-statistic implementation is covered by +[selection-control tests](tests/test_multiple_testing.py). + +## Architecture ```mermaid flowchart LR - A["Research question
and frozen protocol"] --> B["Point-in-time data
and source manifest"] - B --> C["Chronology guards
and signal clock"] - C --> D["Event loop
Strategy → Portfolio → Broker"] - D --> E["Walk-forward selection
costs and stress tests"] - E --> F["Evidence bundle
metrics · trades · config · hashes"] - F --> G{"Claim gate"} - G -->|pass| H["Reviewed aggregate result"] - G -->|fail| I["Preserved negative result"] + A["Point-in-time data
availability + lineage"] --> B["Strategy
signal at event t"] + B --> C["Portfolio + risk
sizing and constraints"] + C --> D["Execution plan
queued slices"] + D --> E["Broker fill
only when event arrives"] + E --> F["Cost + P&L ledger
exact reconciliation"] + F --> G["Walk-forward evidence
benchmark + max statistic"] + G --> H["Artifact receipt
schemas + SHA-256"] ``` -The engine keeps data access, signal formation, portfolio construction, and -execution as separate steps so their timing assumptions can be tested directly. - -## Evidence, including negative results - -The latest completed economic ledger (as of **2026-07-11**) is intentionally -more useful than a single best backtest. Six frozen mechanisms were evaluated -on a 2017–2022 validation window while the 2023–2025 final holdout remained -sealed. Newer SEC 13F pipeline work is infrastructure progress, not newer -economic evidence. - -![Validation HAC Sharpe for six preregistered mechanisms; only the SEC cash-earnings candidate approaches the 0.50 promotion gate and it still fails the full gate set](docs/assets/portfolio/validation_frontier.svg) - -| Frozen mechanism | Net HAC Sharpe | Decision | -| --- | ---: | --- | -| Classic momentum baseline | 0.2407 | Baseline; validation proxy only | -| Industry-residual momentum | 0.3198 | Rejected: improvement `0.0791` < required `0.10` | -| Low volatility | -0.0906 | Rejected on return and drawdown gates | -| One-month reversal | -0.4542 | Rejected; `63.27×` one-way turnover | -| Annual QVPI composite | -0.0234 | Rejected; restatement/vintage caveat remains | -| SEC cash-earnings acceleration | 0.4736 | Rejected: below `0.50`; harsh-cost Sharpe `-0.1034` | - -This is **validation evidence, not a final-holdout or alpha claim**. The strongest -candidate was still rejected because the complete preregistered gate set did not -pass. Exact windows, costs, uncertainty, manifest digests, and caveats are in the -[public-safe research note](docs/portfolio_evidence_2026-07-11.md); chart values -are also available as [CSV](docs/assets/portfolio/validation_frontier.csv). - -## Core capabilities - -- **Event-driven engine** — explicit data, strategy, portfolio, risk, broker, - and execution components with deterministic clocks. -- **Walk-forward validation** — training/test folds, parameter selection, - per-fold outputs, and aggregate out-of-sample metrics. -- **Inference** — HAC statistics, Politis–White stationary bootstrap, reality - checks, SPA tooling, and factor regressions. -- **Execution realism** — `t+1` fills, commissions, slippage/impact models, - borrow costs, turnover controls, sector/industry caps, and capacity checks. -- **Evidence packaging** — resolved YAML, data IDs, manifests, metrics, trades, - plots, and Markdown summaries designed to survive handoff and review. -- **Data boundaries** — deterministic synthetic samples, a small public-data - path, and guarded adapters for local licensed research data. - -## CLI +Data access, signal formation, portfolio construction, execution, inference, +and claim gating remain separate so each timing assumption has a testable +boundary. See the [architecture guide](docs/architecture.md) and +[Audit Lab methodology](docs/audit-lab.md). + +## CLI and Python API | Command | Purpose | | --- | --- | -| `microalpha run --config --out ` | Run one event-driven backtest | -| `microalpha wfv --config --out ` | Run walk-forward validation | -| `microalpha report --artifact-dir ` | Render plots and a Markdown result summary | +| `microalpha audit-demo` | Rebuild the deterministic correctness fixture and receipt | +| `microalpha --version` | Print the installed distribution version | +| `microalpha run --config --out ` | Run one event-driven simulation | +| `microalpha wfv --config --out ` | Run walk-forward selection and evaluation | +| `microalpha report --artifact-dir ` | Render a report from an existing artifact set | | `microalpha info` | Print environment and package metadata as JSON | -Example public-data workflow: +The same demo is available as a Python API: -```bash -microalpha wfv \ - --config configs/wfv_flagship_public.yaml \ - --out artifacts/public_wfv -microalpha report --artifact-dir artifacts/public_wfv/ +```python +from microalpha.audit_lab import run_audit_lab + +result = run_audit_lab("my-audit-evidence") +print(result["receipt_sha256"]) ``` -The tiny public panel is a wiring/demo surface. Its latest audited run had zero -trades and must not be used as a performance claim. +Core extension points cover strategies, data handlers, portfolio policies, +execution models, slippage, reporting, and statistical controls. See the +[API guide](docs/api.md) and [examples](docs/examples.md). -## Output contract +## Reproduce and verify -A typical run includes: +```bash +# User-facing proof +microalpha audit-demo +git diff --exit-code -- docs/assets/audit_lab -```text -/ -├── config_resolved.yaml -├── manifest.json -├── metrics.json -├── trades.jsonl -├── exposures.csv -├── equity_curve.csv -├── equity_curve.png -├── bootstrap.json -└── folds.json # walk-forward runs +# Contributor gates +python -m pip install -e '.[dev]' +ruff check . +black --check . +isort --check-only . +mypy --follow-imports=skip \ + src/microalpha/audit_lab.py src/microalpha/multiple_testing.py \ + src/microalpha/engine.py src/microalpha/execution.py \ + src/microalpha/reporting/factors.py +pytest -m "not wrds" --cov=microalpha --cov-report=term-missing +python scripts/check_data_policy.py +git ls-files -z README.md PROJECT.md pyproject.toml LICENSE CHANGELOG.md \ + Makefile 'src/**' 'tests/**' 'scripts/**' '.github/**' \ + docs/index.md docs/audit-lab.md docs/architecture.md docs/api.md \ + docs/examples.md docs/reproducibility.md docs/leakage-safety.md \ + docs/benchmarks.md docs/limitations.md docs/portfolio_evidence_2026-07-11.md \ + docs/wrds.md docs/flagship_momentum_wrds.md docs/results_wrds.md docs/factors.md \ + 'docs/assets/audit_lab/**' \ + | xargs -0 detect-secrets-hook --baseline .secrets.baseline +mkdocs build --strict ``` -The manifest binds the run to its config, software state, and dataset identity; -reporting reads these artifacts rather than reconstructing results from prose. -Committed examples are available under -[`artifacts/sample_flagship`](artifacts/sample_flagship/) and -[`artifacts/sample_wfv`](artifacts/sample_wfv/). +CI runs the supported Python matrix and enforces lint, format, types, secret +scanning, tests, coverage, deterministic Audit Lab regeneration, and strict docs. -## Licensed-data workflow +Two earlier synthetic example bundles remain available for schema and reporting +inspection: [`artifacts/sample_flagship`](artifacts/sample_flagship) and +[`artifacts/sample_wfv`](artifacts/sample_wfv). They are historical examples; +the Audit Lab above is the canonical product demonstration. -WRDS/CRSP exports are never committed. A local user can point the guarded config -at `WRDS_DATA_ROOT`, run the pipeline, and publish only reviewed aggregate -artifacts: +## Honest research case study -```bash -make wfv-wrds -make report-wrds -python reports/analytics.py artifacts/wrds_flagship/ -python reports/spa.py --grid artifacts/wrds_flagship//grid_returns.csv -``` +Microalpha was also used for a preregistered 2017–2022 licensed-data research +campaign. Six frozen mechanisms—including momentum, residual momentum, low +volatility, reversal, a fundamentals composite, and an SEC cash-earnings +candidate—each failed at least one promotion gate. The strongest development +candidate reached net HAC Sharpe `0.4736`, missed the `0.50` threshold, and +failed harsh costs at `−0.1034`. The 2023–2025 confirmation set remains sealed. -See [the WRDS guide](docs/wrds.md) for schema, licensing, point-in-time, and -survivorship requirements. +That is a feature of the project, not an embarrassing footnote: the system +preserved a negative result instead of retuning until a chart looked good. Read +the [public-safe case study](docs/portfolio_evidence_2026-07-11.md). Licensed +rows are not distributed. ## Repository map | Path | Role | | --- | --- | -| `src/microalpha/` | Engine, data, strategies, execution, portfolio, risk, reporting | +| `src/microalpha/` | Engine, events, data, strategies, execution, portfolio, risk, inference, reporting | +| `tests/` | Chronology, execution, holdout, statistics, artifact, CLI, and data-policy contracts | +| `docs/assets/audit_lab/` | Generated public correctness evidence and SHA-256 receipt | | `configs/` | Reproducible sample, public, and local licensed-data workflows | -| `tests/` | Chronology, execution, reporting, artifact, and data-policy contracts | -| `artifacts/` | Committed deterministic evidence used by docs and tests | -| `docs/` | User documentation, methods, data rules, and evidence notes | -| `reports/` | Analytics, factor, SPA, and summary entry points | - -## Validation - -```bash -ruff check -mypy src/microalpha/reporting/factors.py -pytest -q -mkdocs build --strict -``` - -Focused tests cover no-lookahead behavior, `t+1` execution, artifact schemas, -CLI contracts, factor alignment, data policy, and documentation links. - -## Limitations - -- microalpha does not connect to a broker or claim live execution. -- Public sample and mini-panel runs validate software behavior, not alpha. -- Licensed-data results are reproducible only for authorized users with the - exact source snapshot; the repository publishes aggregates, not raw rows. -- The latest pre-holdout research candidates all failed at least one frozen - promotion gate. The final 2023–2025 holdout remains sealed. -- Historical artifacts can become stale; trust a result only when its manifest, - status label, and evidence note agree. - -## Contributing and citation - -Issues and focused pull requests are welcome. Preserve chronology, add a test -for any changed timing assumption, and bind result claims to generated artifacts. -For research use, cite the repository URL and the exact commit/artifact manifest -used in the analysis. +| `docs/` | Product guides, methodology, API, limitations, and historical case study | +| `artifacts/` | Run-scoped simulation outputs; most generated paths remain untracked | + +Historical project logs remain available for provenance, but a new user should +start with **Audit Lab → Architecture → API → Reproducibility → Limitations**. + +## Limits and claim boundary + +- Microalpha is research software, not a broker, execution venue, or live + trading system. +- The Audit Lab is synthetic and deliberately adversarial. Its positive controls + are not evidence of market predictability. +- Cost and impact models are configurable simulations; they are not calibrated + to every asset, venue, or order type. +- Point-in-time safety ultimately depends on correct source availability + metadata. A manifest cannot repair an incorrectly labeled dataset. +- Licensed-data research is reproducible only for authorized users with the + exact source snapshot; raw WRDS/CRSP/OptionMetrics data is never published. +- No public package is published to PyPI because that distribution name belongs + to an unrelated project. Install this repository from source or a GitHub + release artifact. + +More detail: [limitations](docs/limitations.md), [data policy](docs/wrds.md), and +[reproducibility](docs/reproducibility.md). ## License -No open-source license is currently declared for this repository. Data sources -may carry separate restrictions; WRDS/CRSP data are not redistributed here. +Code is available under the [MIT License](LICENSE). Data sources and generated +research inputs may carry separate restrictions; the license does not grant +rights to third-party datasets. diff --git a/artifacts/sample_flagship/2025-10-30T18-04-23Z-b2dcbb6/manifest.json b/artifacts/sample_flagship/2025-10-30T18-04-23Z-b2dcbb6/manifest.json index ee35f55b..d071a3d5 100644 --- a/artifacts/sample_flagship/2025-10-30T18-04-23Z-b2dcbb6/manifest.json +++ b/artifacts/sample_flagship/2025-10-30T18-04-23Z-b2dcbb6/manifest.json @@ -7,6 +7,6 @@ "numpy_version": "2.2.6", "pandas_version": "2.2.3", "seed": 123, - "config_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/configs/flagship_sample.yaml", + "config_path": "/Documents/Programming/Projects/microalpha/configs/flagship_sample.yaml", "config_sha256": "c2cf8578eb736c050dbe5d89eb6db276feca7ec3af9aaba373945fd52bcc8e33" } \ No newline at end of file diff --git a/artifacts/sample_flagship/2025-10-30T18-39-31Z-a4ab8e7/manifest.json b/artifacts/sample_flagship/2025-10-30T18-39-31Z-a4ab8e7/manifest.json index 844e9f56..2b0cd296 100644 --- a/artifacts/sample_flagship/2025-10-30T18-39-31Z-a4ab8e7/manifest.json +++ b/artifacts/sample_flagship/2025-10-30T18-39-31Z-a4ab8e7/manifest.json @@ -7,6 +7,6 @@ "numpy_version": "2.2.6", "pandas_version": "2.2.3", "seed": 123, - "config_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/configs/flagship_sample.yaml", + "config_path": "/Documents/Programming/Projects/microalpha/configs/flagship_sample.yaml", "config_sha256": "c2cf8578eb736c050dbe5d89eb6db276feca7ec3af9aaba373945fd52bcc8e33" } \ No newline at end of file diff --git a/artifacts/sample_flagship/2025-10-30T18-39-31Z-a4ab8e7/metadata_coverage.json b/artifacts/sample_flagship/2025-10-30T18-39-31Z-a4ab8e7/metadata_coverage.json index d30ef6bc..a5bcca13 100644 --- a/artifacts/sample_flagship/2025-10-30T18-39-31Z-a4ab8e7/metadata_coverage.json +++ b/artifacts/sample_flagship/2025-10-30T18-39-31Z-a4ab8e7/metadata_coverage.json @@ -1,5 +1,5 @@ { - "meta_source": "/Users/mateobodon/Documents/Programming/Projects/microalpha/data/sample/meta_sample.csv", + "meta_source": "/Documents/Programming/Projects/microalpha/data/sample/meta_sample.csv", "coverage": { "pct_notional_with_adv": 1.0, "pct_notional_with_spread": 1.0, diff --git a/artifacts/sample_wfv/2025-10-30T18-04-06Z-b2dcbb6/folds.json b/artifacts/sample_wfv/2025-10-30T18-04-06Z-b2dcbb6/folds.json index 36b28354..ba94e81e 100644 --- a/artifacts/sample_wfv/2025-10-30T18-04-06Z-b2dcbb6/folds.json +++ b/artifacts/sample_wfv/2025-10-30T18-04-06Z-b2dcbb6/folds.json @@ -5,7 +5,7 @@ "test_start": "2020-07-10", "test_end": "2020-09-11", "best_params": { - "universe_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/data/sample/universe_sample.csv", + "universe_path": "/Documents/Programming/Projects/microalpha/data/sample/universe_sample.csv", "lookback_months": 6, "skip_months": 1, "top_frac": 0.35, @@ -92,8 +92,8 @@ "block_length": 20, "num_bootstrap": 800 }, - "exposures_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-04-06Z-b2dcbb6/exposures_fold_0.csv", - "factor_exposure_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-04-06Z-b2dcbb6/factor_exposure_fold_0.csv", + "exposures_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-04-06Z-b2dcbb6/exposures_fold_0.csv", + "factor_exposure_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-04-06Z-b2dcbb6/factor_exposure_fold_0.csv", "spa_pvalue": null }, { @@ -102,7 +102,7 @@ "test_start": "2020-09-11", "test_end": "2020-11-13", "best_params": { - "universe_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/data/sample/universe_sample.csv", + "universe_path": "/Documents/Programming/Projects/microalpha/data/sample/universe_sample.csv", "lookback_months": 6, "skip_months": 1, "top_frac": 0.35, @@ -189,8 +189,8 @@ "block_length": 20, "num_bootstrap": 800 }, - "exposures_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-04-06Z-b2dcbb6/exposures_fold_1.csv", - "factor_exposure_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-04-06Z-b2dcbb6/factor_exposure_fold_1.csv", + "exposures_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-04-06Z-b2dcbb6/exposures_fold_1.csv", + "factor_exposure_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-04-06Z-b2dcbb6/factor_exposure_fold_1.csv", "spa_pvalue": null }, { @@ -199,7 +199,7 @@ "test_start": "2020-11-13", "test_end": "2021-01-15", "best_params": { - "universe_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/data/sample/universe_sample.csv", + "universe_path": "/Documents/Programming/Projects/microalpha/data/sample/universe_sample.csv", "lookback_months": 6, "skip_months": 1, "top_frac": 0.35, @@ -286,8 +286,8 @@ "block_length": 20, "num_bootstrap": 800 }, - "exposures_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-04-06Z-b2dcbb6/exposures_fold_2.csv", - "factor_exposure_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-04-06Z-b2dcbb6/factor_exposure_fold_2.csv", + "exposures_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-04-06Z-b2dcbb6/exposures_fold_2.csv", + "factor_exposure_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-04-06Z-b2dcbb6/factor_exposure_fold_2.csv", "spa_pvalue": null }, { @@ -296,7 +296,7 @@ "test_start": "2021-01-15", "test_end": "2021-03-19", "best_params": { - "universe_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/data/sample/universe_sample.csv", + "universe_path": "/Documents/Programming/Projects/microalpha/data/sample/universe_sample.csv", "lookback_months": 6, "skip_months": 1, "top_frac": 0.35, @@ -383,8 +383,8 @@ "block_length": 20, "num_bootstrap": 800 }, - "exposures_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-04-06Z-b2dcbb6/exposures_fold_3.csv", - "factor_exposure_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-04-06Z-b2dcbb6/factor_exposure_fold_3.csv", + "exposures_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-04-06Z-b2dcbb6/exposures_fold_3.csv", + "factor_exposure_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-04-06Z-b2dcbb6/factor_exposure_fold_3.csv", "spa_pvalue": null }, { @@ -393,7 +393,7 @@ "test_start": "2021-03-19", "test_end": "2021-05-21", "best_params": { - "universe_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/data/sample/universe_sample.csv", + "universe_path": "/Documents/Programming/Projects/microalpha/data/sample/universe_sample.csv", "lookback_months": 6, "skip_months": 1, "top_frac": 0.35, @@ -480,8 +480,8 @@ "block_length": 20, "num_bootstrap": 800 }, - "exposures_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-04-06Z-b2dcbb6/exposures_fold_4.csv", - "factor_exposure_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-04-06Z-b2dcbb6/factor_exposure_fold_4.csv", + "exposures_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-04-06Z-b2dcbb6/exposures_fold_4.csv", + "factor_exposure_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-04-06Z-b2dcbb6/factor_exposure_fold_4.csv", "spa_pvalue": null }, { @@ -490,7 +490,7 @@ "test_start": "2021-05-21", "test_end": "2021-07-23", "best_params": { - "universe_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/data/sample/universe_sample.csv", + "universe_path": "/Documents/Programming/Projects/microalpha/data/sample/universe_sample.csv", "lookback_months": 6, "skip_months": 1, "top_frac": 0.35, @@ -577,8 +577,8 @@ "block_length": 20, "num_bootstrap": 800 }, - "exposures_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-04-06Z-b2dcbb6/exposures_fold_5.csv", - "factor_exposure_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-04-06Z-b2dcbb6/factor_exposure_fold_5.csv", + "exposures_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-04-06Z-b2dcbb6/exposures_fold_5.csv", + "factor_exposure_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-04-06Z-b2dcbb6/factor_exposure_fold_5.csv", "spa_pvalue": null }, { @@ -587,7 +587,7 @@ "test_start": "2021-07-23", "test_end": "2021-09-24", "best_params": { - "universe_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/data/sample/universe_sample.csv", + "universe_path": "/Documents/Programming/Projects/microalpha/data/sample/universe_sample.csv", "lookback_months": 6, "skip_months": 1, "top_frac": 0.35, @@ -674,8 +674,8 @@ "block_length": 20, "num_bootstrap": 800 }, - "exposures_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-04-06Z-b2dcbb6/exposures_fold_6.csv", - "factor_exposure_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-04-06Z-b2dcbb6/factor_exposure_fold_6.csv", + "exposures_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-04-06Z-b2dcbb6/exposures_fold_6.csv", + "factor_exposure_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-04-06Z-b2dcbb6/factor_exposure_fold_6.csv", "spa_pvalue": null }, { @@ -684,7 +684,7 @@ "test_start": "2021-09-24", "test_end": "2021-11-26", "best_params": { - "universe_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/data/sample/universe_sample.csv", + "universe_path": "/Documents/Programming/Projects/microalpha/data/sample/universe_sample.csv", "lookback_months": 6, "skip_months": 1, "top_frac": 0.35, @@ -771,8 +771,8 @@ "block_length": 20, "num_bootstrap": 800 }, - "exposures_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-04-06Z-b2dcbb6/exposures_fold_7.csv", - "factor_exposure_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-04-06Z-b2dcbb6/factor_exposure_fold_7.csv", + "exposures_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-04-06Z-b2dcbb6/exposures_fold_7.csv", + "factor_exposure_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-04-06Z-b2dcbb6/factor_exposure_fold_7.csv", "spa_pvalue": null } ] \ No newline at end of file diff --git a/artifacts/sample_wfv/2025-10-30T18-04-06Z-b2dcbb6/manifest.json b/artifacts/sample_wfv/2025-10-30T18-04-06Z-b2dcbb6/manifest.json index ac1c5cc4..9b18dda1 100644 --- a/artifacts/sample_wfv/2025-10-30T18-04-06Z-b2dcbb6/manifest.json +++ b/artifacts/sample_wfv/2025-10-30T18-04-06Z-b2dcbb6/manifest.json @@ -7,6 +7,6 @@ "numpy_version": "2.2.6", "pandas_version": "2.2.3", "seed": 321, - "config_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/configs/wfv_flagship_sample.yaml", + "config_path": "/Documents/Programming/Projects/microalpha/configs/wfv_flagship_sample.yaml", "config_sha256": "2ed595b438a01052f3e29e629b1081804e66d46655017f7d8248170d69380fea" } \ No newline at end of file diff --git a/artifacts/sample_wfv/2025-10-30T18-39-47Z-a4ab8e7/folds.json b/artifacts/sample_wfv/2025-10-30T18-39-47Z-a4ab8e7/folds.json index a3eca0c6..c0cdcd64 100644 --- a/artifacts/sample_wfv/2025-10-30T18-39-47Z-a4ab8e7/folds.json +++ b/artifacts/sample_wfv/2025-10-30T18-39-47Z-a4ab8e7/folds.json @@ -5,7 +5,7 @@ "test_start": "2020-07-10", "test_end": "2020-09-11", "best_params": { - "universe_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/data/sample/universe_sample.csv", + "universe_path": "/Documents/Programming/Projects/microalpha/data/sample/universe_sample.csv", "lookback_months": 6, "skip_months": 1, "top_frac": 0.35, @@ -93,8 +93,8 @@ "num_bootstrap": 800, "num_models": 4 }, - "exposures_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-39-47Z-a4ab8e7/exposures_fold_0.csv", - "factor_exposure_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-39-47Z-a4ab8e7/factor_exposure_fold_0.csv", + "exposures_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-39-47Z-a4ab8e7/exposures_fold_0.csv", + "factor_exposure_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-39-47Z-a4ab8e7/factor_exposure_fold_0.csv", "spa_pvalue": null }, { @@ -103,7 +103,7 @@ "test_start": "2020-09-11", "test_end": "2020-11-13", "best_params": { - "universe_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/data/sample/universe_sample.csv", + "universe_path": "/Documents/Programming/Projects/microalpha/data/sample/universe_sample.csv", "lookback_months": 6, "skip_months": 1, "top_frac": 0.35, @@ -191,8 +191,8 @@ "num_bootstrap": 800, "num_models": 4 }, - "exposures_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-39-47Z-a4ab8e7/exposures_fold_1.csv", - "factor_exposure_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-39-47Z-a4ab8e7/factor_exposure_fold_1.csv", + "exposures_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-39-47Z-a4ab8e7/exposures_fold_1.csv", + "factor_exposure_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-39-47Z-a4ab8e7/factor_exposure_fold_1.csv", "spa_pvalue": null }, { @@ -201,7 +201,7 @@ "test_start": "2020-11-13", "test_end": "2021-01-15", "best_params": { - "universe_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/data/sample/universe_sample.csv", + "universe_path": "/Documents/Programming/Projects/microalpha/data/sample/universe_sample.csv", "lookback_months": 6, "skip_months": 1, "top_frac": 0.35, @@ -289,8 +289,8 @@ "num_bootstrap": 800, "num_models": 4 }, - "exposures_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-39-47Z-a4ab8e7/exposures_fold_2.csv", - "factor_exposure_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-39-47Z-a4ab8e7/factor_exposure_fold_2.csv", + "exposures_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-39-47Z-a4ab8e7/exposures_fold_2.csv", + "factor_exposure_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-39-47Z-a4ab8e7/factor_exposure_fold_2.csv", "spa_pvalue": null }, { @@ -299,7 +299,7 @@ "test_start": "2021-01-15", "test_end": "2021-03-19", "best_params": { - "universe_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/data/sample/universe_sample.csv", + "universe_path": "/Documents/Programming/Projects/microalpha/data/sample/universe_sample.csv", "lookback_months": 6, "skip_months": 1, "top_frac": 0.35, @@ -387,8 +387,8 @@ "num_bootstrap": 800, "num_models": 4 }, - "exposures_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-39-47Z-a4ab8e7/exposures_fold_3.csv", - "factor_exposure_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-39-47Z-a4ab8e7/factor_exposure_fold_3.csv", + "exposures_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-39-47Z-a4ab8e7/exposures_fold_3.csv", + "factor_exposure_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-39-47Z-a4ab8e7/factor_exposure_fold_3.csv", "spa_pvalue": null }, { @@ -397,7 +397,7 @@ "test_start": "2021-03-19", "test_end": "2021-05-21", "best_params": { - "universe_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/data/sample/universe_sample.csv", + "universe_path": "/Documents/Programming/Projects/microalpha/data/sample/universe_sample.csv", "lookback_months": 6, "skip_months": 1, "top_frac": 0.35, @@ -485,8 +485,8 @@ "num_bootstrap": 800, "num_models": 4 }, - "exposures_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-39-47Z-a4ab8e7/exposures_fold_4.csv", - "factor_exposure_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-39-47Z-a4ab8e7/factor_exposure_fold_4.csv", + "exposures_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-39-47Z-a4ab8e7/exposures_fold_4.csv", + "factor_exposure_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-39-47Z-a4ab8e7/factor_exposure_fold_4.csv", "spa_pvalue": null }, { @@ -495,7 +495,7 @@ "test_start": "2021-05-21", "test_end": "2021-07-23", "best_params": { - "universe_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/data/sample/universe_sample.csv", + "universe_path": "/Documents/Programming/Projects/microalpha/data/sample/universe_sample.csv", "lookback_months": 6, "skip_months": 1, "top_frac": 0.35, @@ -583,8 +583,8 @@ "num_bootstrap": 800, "num_models": 4 }, - "exposures_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-39-47Z-a4ab8e7/exposures_fold_5.csv", - "factor_exposure_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-39-47Z-a4ab8e7/factor_exposure_fold_5.csv", + "exposures_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-39-47Z-a4ab8e7/exposures_fold_5.csv", + "factor_exposure_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-39-47Z-a4ab8e7/factor_exposure_fold_5.csv", "spa_pvalue": null }, { @@ -593,7 +593,7 @@ "test_start": "2021-07-23", "test_end": "2021-09-24", "best_params": { - "universe_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/data/sample/universe_sample.csv", + "universe_path": "/Documents/Programming/Projects/microalpha/data/sample/universe_sample.csv", "lookback_months": 6, "skip_months": 1, "top_frac": 0.35, @@ -681,8 +681,8 @@ "num_bootstrap": 800, "num_models": 4 }, - "exposures_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-39-47Z-a4ab8e7/exposures_fold_6.csv", - "factor_exposure_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-39-47Z-a4ab8e7/factor_exposure_fold_6.csv", + "exposures_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-39-47Z-a4ab8e7/exposures_fold_6.csv", + "factor_exposure_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-39-47Z-a4ab8e7/factor_exposure_fold_6.csv", "spa_pvalue": null }, { @@ -691,7 +691,7 @@ "test_start": "2021-09-24", "test_end": "2021-11-26", "best_params": { - "universe_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/data/sample/universe_sample.csv", + "universe_path": "/Documents/Programming/Projects/microalpha/data/sample/universe_sample.csv", "lookback_months": 6, "skip_months": 1, "top_frac": 0.35, @@ -779,8 +779,8 @@ "num_bootstrap": 800, "num_models": 4 }, - "exposures_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-39-47Z-a4ab8e7/exposures_fold_7.csv", - "factor_exposure_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-39-47Z-a4ab8e7/factor_exposure_fold_7.csv", + "exposures_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-39-47Z-a4ab8e7/exposures_fold_7.csv", + "factor_exposure_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv/2025-10-30T18-39-47Z-a4ab8e7/factor_exposure_fold_7.csv", "spa_pvalue": null } ] \ No newline at end of file diff --git a/artifacts/sample_wfv/2025-10-30T18-39-47Z-a4ab8e7/manifest.json b/artifacts/sample_wfv/2025-10-30T18-39-47Z-a4ab8e7/manifest.json index b4843b7b..53106591 100644 --- a/artifacts/sample_wfv/2025-10-30T18-39-47Z-a4ab8e7/manifest.json +++ b/artifacts/sample_wfv/2025-10-30T18-39-47Z-a4ab8e7/manifest.json @@ -7,6 +7,6 @@ "numpy_version": "2.2.6", "pandas_version": "2.2.3", "seed": 321, - "config_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/configs/wfv_flagship_sample.yaml", + "config_path": "/Documents/Programming/Projects/microalpha/configs/wfv_flagship_sample.yaml", "config_sha256": "2ed595b438a01052f3e29e629b1081804e66d46655017f7d8248170d69380fea" } \ No newline at end of file diff --git a/artifacts/sample_wfv/2025-10-30T18-39-47Z-a4ab8e7/metadata_coverage.json b/artifacts/sample_wfv/2025-10-30T18-39-47Z-a4ab8e7/metadata_coverage.json index 3b65fae2..82a22e96 100644 --- a/artifacts/sample_wfv/2025-10-30T18-39-47Z-a4ab8e7/metadata_coverage.json +++ b/artifacts/sample_wfv/2025-10-30T18-39-47Z-a4ab8e7/metadata_coverage.json @@ -1,5 +1,5 @@ { - "meta_source": "/Users/mateobodon/Documents/Programming/Projects/microalpha/data/sample/meta_sample.csv", + "meta_source": "/Documents/Programming/Projects/microalpha/data/sample/meta_sample.csv", "coverage": { "pct_notional_with_adv": 1.0, "pct_notional_with_spread": 1.0, diff --git a/artifacts/sample_wfv_holdout/2025-12-21T20-53-05Z-4457b33/folds.json b/artifacts/sample_wfv_holdout/2025-12-21T20-53-05Z-4457b33/folds.json index 9fee28ba..61cf54b2 100644 --- a/artifacts/sample_wfv_holdout/2025-12-21T20-53-05Z-4457b33/folds.json +++ b/artifacts/sample_wfv_holdout/2025-12-21T20-53-05Z-4457b33/folds.json @@ -5,7 +5,7 @@ "test_start": "2020-07-10", "test_end": "2020-09-11", "best_params": { - "universe_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/data/sample/universe_sample.csv", + "universe_path": "/Documents/Programming/Projects/microalpha/data/sample/universe_sample.csv", "lookback_months": 6, "skip_months": 1, "top_frac": 0.35, @@ -101,8 +101,8 @@ "num_bootstrap": 800, "num_models": 4 }, - "exposures_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv_holdout/2025-12-21T20-53-05Z-4457b33/exposures_fold_0.csv", - "factor_exposure_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv_holdout/2025-12-21T20-53-05Z-4457b33/factor_exposure_fold_0.csv", + "exposures_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv_holdout/2025-12-21T20-53-05Z-4457b33/exposures_fold_0.csv", + "factor_exposure_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv_holdout/2025-12-21T20-53-05Z-4457b33/factor_exposure_fold_0.csv", "spa_pvalue": null, "grid_summary": [ { @@ -153,7 +153,7 @@ "test_start": "2020-09-11", "test_end": "2020-11-13", "best_params": { - "universe_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/data/sample/universe_sample.csv", + "universe_path": "/Documents/Programming/Projects/microalpha/data/sample/universe_sample.csv", "lookback_months": 6, "skip_months": 1, "top_frac": 0.35, @@ -249,8 +249,8 @@ "num_bootstrap": 800, "num_models": 4 }, - "exposures_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv_holdout/2025-12-21T20-53-05Z-4457b33/exposures_fold_1.csv", - "factor_exposure_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv_holdout/2025-12-21T20-53-05Z-4457b33/factor_exposure_fold_1.csv", + "exposures_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv_holdout/2025-12-21T20-53-05Z-4457b33/exposures_fold_1.csv", + "factor_exposure_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv_holdout/2025-12-21T20-53-05Z-4457b33/factor_exposure_fold_1.csv", "spa_pvalue": null, "grid_summary": [ { @@ -301,7 +301,7 @@ "test_start": "2020-11-13", "test_end": "2021-01-15", "best_params": { - "universe_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/data/sample/universe_sample.csv", + "universe_path": "/Documents/Programming/Projects/microalpha/data/sample/universe_sample.csv", "lookback_months": 6, "skip_months": 1, "top_frac": 0.35, @@ -397,8 +397,8 @@ "num_bootstrap": 800, "num_models": 4 }, - "exposures_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv_holdout/2025-12-21T20-53-05Z-4457b33/exposures_fold_2.csv", - "factor_exposure_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv_holdout/2025-12-21T20-53-05Z-4457b33/factor_exposure_fold_2.csv", + "exposures_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv_holdout/2025-12-21T20-53-05Z-4457b33/exposures_fold_2.csv", + "factor_exposure_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv_holdout/2025-12-21T20-53-05Z-4457b33/factor_exposure_fold_2.csv", "spa_pvalue": null, "grid_summary": [ { @@ -449,7 +449,7 @@ "test_start": "2021-01-15", "test_end": "2021-03-19", "best_params": { - "universe_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/data/sample/universe_sample.csv", + "universe_path": "/Documents/Programming/Projects/microalpha/data/sample/universe_sample.csv", "lookback_months": 6, "skip_months": 1, "top_frac": 0.35, @@ -545,8 +545,8 @@ "num_bootstrap": 800, "num_models": 4 }, - "exposures_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv_holdout/2025-12-21T20-53-05Z-4457b33/exposures_fold_3.csv", - "factor_exposure_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv_holdout/2025-12-21T20-53-05Z-4457b33/factor_exposure_fold_3.csv", + "exposures_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv_holdout/2025-12-21T20-53-05Z-4457b33/exposures_fold_3.csv", + "factor_exposure_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv_holdout/2025-12-21T20-53-05Z-4457b33/factor_exposure_fold_3.csv", "spa_pvalue": null, "grid_summary": [ { @@ -597,7 +597,7 @@ "test_start": "2021-03-19", "test_end": "2021-05-21", "best_params": { - "universe_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/data/sample/universe_sample.csv", + "universe_path": "/Documents/Programming/Projects/microalpha/data/sample/universe_sample.csv", "lookback_months": 6, "skip_months": 1, "top_frac": 0.35, @@ -693,8 +693,8 @@ "num_bootstrap": 800, "num_models": 4 }, - "exposures_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv_holdout/2025-12-21T20-53-05Z-4457b33/exposures_fold_4.csv", - "factor_exposure_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv_holdout/2025-12-21T20-53-05Z-4457b33/factor_exposure_fold_4.csv", + "exposures_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv_holdout/2025-12-21T20-53-05Z-4457b33/exposures_fold_4.csv", + "factor_exposure_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv_holdout/2025-12-21T20-53-05Z-4457b33/factor_exposure_fold_4.csv", "spa_pvalue": null, "grid_summary": [ { diff --git a/artifacts/sample_wfv_holdout/2025-12-21T20-53-05Z-4457b33/holdout_manifest.json b/artifacts/sample_wfv_holdout/2025-12-21T20-53-05Z-4457b33/holdout_manifest.json index 92e530f2..750a42fc 100644 --- a/artifacts/sample_wfv_holdout/2025-12-21T20-53-05Z-4457b33/holdout_manifest.json +++ b/artifacts/sample_wfv_holdout/2025-12-21T20-53-05Z-4457b33/holdout_manifest.json @@ -11,7 +11,7 @@ "skip_months": 1 }, "selected_params_full": { - "universe_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/data/sample/universe_sample.csv", + "universe_path": "/Documents/Programming/Projects/microalpha/data/sample/universe_sample.csv", "lookback_months": 6, "skip_months": 1, "top_frac": 0.35, @@ -31,9 +31,9 @@ ] }, "selection_metric": 0.0, - "selection_summary_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv_holdout/2025-12-21T20-53-05Z-4457b33/selection_summary.json", - "holdout_metrics_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv_holdout/2025-12-21T20-53-05Z-4457b33/holdout_metrics.json", - "holdout_equity_curve_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv_holdout/2025-12-21T20-53-05Z-4457b33/holdout_equity_curve.csv", - "holdout_returns_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv_holdout/2025-12-21T20-53-05Z-4457b33/holdout_returns.csv", - "holdout_trades_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv_holdout/2025-12-21T20-53-05Z-4457b33/holdout_trades.jsonl" + "selection_summary_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv_holdout/2025-12-21T20-53-05Z-4457b33/selection_summary.json", + "holdout_metrics_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv_holdout/2025-12-21T20-53-05Z-4457b33/holdout_metrics.json", + "holdout_equity_curve_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv_holdout/2025-12-21T20-53-05Z-4457b33/holdout_equity_curve.csv", + "holdout_returns_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv_holdout/2025-12-21T20-53-05Z-4457b33/holdout_returns.csv", + "holdout_trades_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv_holdout/2025-12-21T20-53-05Z-4457b33/holdout_trades.jsonl" } \ No newline at end of file diff --git a/artifacts/sample_wfv_holdout/2025-12-21T20-53-05Z-4457b33/manifest.json b/artifacts/sample_wfv_holdout/2025-12-21T20-53-05Z-4457b33/manifest.json index f8941324..adfde7f6 100644 --- a/artifacts/sample_wfv_holdout/2025-12-21T20-53-05Z-4457b33/manifest.json +++ b/artifacts/sample_wfv_holdout/2025-12-21T20-53-05Z-4457b33/manifest.json @@ -7,7 +7,7 @@ "numpy_version": "2.2.6", "pandas_version": "2.2.3", "seed": 321, - "config_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/configs/wfv_flagship_sample_holdout.yaml", + "config_path": "/Documents/Programming/Projects/microalpha/configs/wfv_flagship_sample_holdout.yaml", "config_sha256": "d07891662ad83d8447eabd803a80f6616b101f3fc4334bd7c43052cb1552ed6a", "config_summary": { "risk_caps": { @@ -51,7 +51,7 @@ "skip_months": 1 }, "selected_params_full": { - "universe_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/data/sample/universe_sample.csv", + "universe_path": "/Documents/Programming/Projects/microalpha/data/sample/universe_sample.csv", "lookback_months": 6, "skip_months": 1, "top_frac": 0.35, @@ -70,8 +70,8 @@ "ZETA" ] }, - "selection_summary_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv_holdout/2025-12-21T20-53-05Z-4457b33/selection_summary.json", - "holdout_metrics_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv_holdout/2025-12-21T20-53-05Z-4457b33/holdout_metrics.json", - "holdout_manifest_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv_holdout/2025-12-21T20-53-05Z-4457b33/holdout_manifest.json" + "selection_summary_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv_holdout/2025-12-21T20-53-05Z-4457b33/selection_summary.json", + "holdout_metrics_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv_holdout/2025-12-21T20-53-05Z-4457b33/holdout_metrics.json", + "holdout_manifest_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv_holdout/2025-12-21T20-53-05Z-4457b33/holdout_manifest.json" } } \ No newline at end of file diff --git a/artifacts/wrds_flagship/2025-11-12T18-50-58Z-b2eaf50/manifest.json b/artifacts/wrds_flagship/2025-11-12T18-50-58Z-b2eaf50/manifest.json index 34b4feb4..85968131 100644 --- a/artifacts/wrds_flagship/2025-11-12T18-50-58Z-b2eaf50/manifest.json +++ b/artifacts/wrds_flagship/2025-11-12T18-50-58Z-b2eaf50/manifest.json @@ -7,6 +7,6 @@ "numpy_version": "2.2.6", "pandas_version": "2.2.3", "seed": 20251010, - "config_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/configs/wfv_flagship_wrds.yaml", + "config_path": "/Documents/Programming/Projects/microalpha/configs/wfv_flagship_wrds.yaml", "config_sha256": "e8ca0b39d592c33159540d9ba2997ce4d9887c524928b289bec5e9a093ee9eb1" } \ No newline at end of file diff --git a/docs/agent_runs/20251221_154039_ticket-02_holdout-wfv/COMMANDS.md b/docs/agent_runs/20251221_154039_ticket-02_holdout-wfv/COMMANDS.md index 15a21c29..5dc4eb0a 100644 --- a/docs/agent_runs/20251221_154039_ticket-02_holdout-wfv/COMMANDS.md +++ b/docs/agent_runs/20251221_154039_ticket-02_holdout-wfv/COMMANDS.md @@ -53,7 +53,7 @@ sed -n '1,200p' PROGRESS.md sed -n '1,200p' CHANGELOG.md mkdir -p docs/agent_runs/20251221_154039_ticket-02_holdout-wfv cat <<'EOF' > docs/agent_runs/20251221_154039_ticket-02_holdout-wfv/PROMPT.md -# AGENTS.md instructions for /Users/mateobodon/Documents/Programming/Projects/microalpha +# AGENTS.md instructions for /Documents/Programming/Projects/microalpha # AGENTS.md — microalpha (Repo Instructions) @@ -169,8 +169,8 @@ See: `docs/DOCS_AND_LOGGING_SYSTEM.md` ## Skills These skills are discovered at startup from multiple local sources. Each entry includes a name, description, and file path so you can open the source for full instructions. -- skill-creator: Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Codex's capabilities with specialized knowledge, workflows, or tool integrations. (file: /Users/mateobodon/.codex/skills/.system/skill-creator/SKILL.md) -- skill-installer: Install Codex skills into $CODEX_HOME/skills from a curated list or a GitHub repo path. Use when a user asks to list installable skills, install a curated skill, or install a skill from another repo (including private repos). (file: /Users/mateobodon/.codex/skills/.system/skill-installer/SKILL.md) +- skill-creator: Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Codex's capabilities with specialized knowledge, workflows, or tool integrations. (file: /.codex/skills/.system/skill-creator/SKILL.md) +- skill-installer: Install Codex skills into $CODEX_HOME/skills from a curated list or a GitHub repo path. Use when a user asks to list installable skills, install a curated skill, or install a skill from another repo (including private repos). (file: /.codex/skills/.system/skill-installer/SKILL.md) - Discovery: Available skills are listed in project docs and may also appear in a runtime "## Skills" section (name + description + file path). These are the sources of truth; skill bodies live on disk at the listed paths. - Trigger rules: If the user names a skill (with `$SkillName` or plain text) OR the task clearly matches a skill's description, you must use that skill for that turn. Multiple mentions mean use them all. Do not carry skills across turns unless re-mentioned. - Missing/blocked: If a named skill isn't in the list or the path can't be read, say so briefly and continue with the best fallback. @@ -191,7 +191,7 @@ These skills are discovered at startup from multiple local sources. Each entry i - /Users/mateobodon/Documents/Programming/Projects/microalpha + /Documents/Programming/Projects/microalpha never danger-full-access enabled diff --git a/docs/agent_runs/20251221_154039_ticket-02_holdout-wfv/PROMPT.md b/docs/agent_runs/20251221_154039_ticket-02_holdout-wfv/PROMPT.md index 9c99d4db..1251b100 100644 --- a/docs/agent_runs/20251221_154039_ticket-02_holdout-wfv/PROMPT.md +++ b/docs/agent_runs/20251221_154039_ticket-02_holdout-wfv/PROMPT.md @@ -1,4 +1,4 @@ -# AGENTS.md instructions for /Users/mateobodon/Documents/Programming/Projects/microalpha +# AGENTS.md instructions for /Documents/Programming/Projects/microalpha # AGENTS.md — microalpha (Repo Instructions) @@ -114,8 +114,8 @@ See: `docs/DOCS_AND_LOGGING_SYSTEM.md` ## Skills These skills are discovered at startup from multiple local sources. Each entry includes a name, description, and file path so you can open the source for full instructions. -- skill-creator: Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Codex's capabilities with specialized knowledge, workflows, or tool integrations. (file: /Users/mateobodon/.codex/skills/.system/skill-creator/SKILL.md) -- skill-installer: Install Codex skills into $CODEX_HOME/skills from a curated list or a GitHub repo path. Use when a user asks to list installable skills, install a curated skill, or install a skill from another repo (including private repos). (file: /Users/mateobodon/.codex/skills/.system/skill-installer/SKILL.md) +- skill-creator: Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Codex's capabilities with specialized knowledge, workflows, or tool integrations. (file: /.codex/skills/.system/skill-creator/SKILL.md) +- skill-installer: Install Codex skills into $CODEX_HOME/skills from a curated list or a GitHub repo path. Use when a user asks to list installable skills, install a curated skill, or install a skill from another repo (including private repos). (file: /.codex/skills/.system/skill-installer/SKILL.md) - Discovery: Available skills are listed in project docs and may also appear in a runtime "## Skills" section (name + description + file path). These are the sources of truth; skill bodies live on disk at the listed paths. - Trigger rules: If the user names a skill (with `$SkillName` or plain text) OR the task clearly matches a skill's description, you must use that skill for that turn. Multiple mentions mean use them all. Do not carry skills across turns unless re-mentioned. - Missing/blocked: If a named skill isn't in the list or the path can't be read, say so briefly and continue with the best fallback. @@ -136,7 +136,7 @@ These skills are discovered at startup from multiple local sources. Each entry i - /Users/mateobodon/Documents/Programming/Projects/microalpha + /Documents/Programming/Projects/microalpha never danger-full-access enabled diff --git a/docs/agent_runs/20251221_162711_ticket-02_holdout-wfv-wrds/COMMANDS.md b/docs/agent_runs/20251221_162711_ticket-02_holdout-wfv-wrds/COMMANDS.md index 84faa0f9..93377901 100644 --- a/docs/agent_runs/20251221_162711_ticket-02_holdout-wfv-wrds/COMMANDS.md +++ b/docs/agent_runs/20251221_162711_ticket-02_holdout-wfv-wrds/COMMANDS.md @@ -3,20 +3,20 @@ ``` sed -n '1,200p' docs/results_wrds_smoke.md sed -n '1,240p' Makefile -ls /Volumes/Storage/Data -ls /Volumes/Storage/Data/wrds -ls /Volumes/Storage/Data/wrds/crsp -ls /Volumes/Storage/Data/wrds/meta -ls /Volumes/Storage/Data/wrds/universes +ls /Data +ls /Data/wrds +ls /Data/wrds/crsp +ls /Data/wrds/meta +ls /Data/wrds/universes date '+%Y%m%d_%H%M%S' mkdir -p docs/agent_runs/20251221_162711_ticket-02_holdout-wfv-wrds cat <<'EOF' > docs/agent_runs/20251221_162711_ticket-02_holdout-wfv-wrds/PROMPT.md User request: -WRDS is available, i keep all WRDS raw data at /Volumes/Storage/Data ,look through, find, then continue +WRDS is available, i keep all WRDS raw data at /Data ,look through, find, then continue EOF -WRDS_DATA_ROOT=/Volumes/Storage/Data/wrds make wfv-wrds-smoke -WRDS_DATA_ROOT=/Volumes/Storage/Data/wrds make report-wrds-smoke +WRDS_DATA_ROOT=/Data/wrds make wfv-wrds-smoke +WRDS_DATA_ROOT=/Data/wrds make report-wrds-smoke ls artifacts/wrds_flagship_smoke/2025-12-21T21-28-14Z-33c9c2a | rg holdout rg -n "Latest run" -n docs/results_wrds_smoke.md python3 tools/build_project_state.py @@ -52,7 +52,7 @@ cat <<'EOF' > docs/agent_runs/20251221_162711_ticket-02_holdout-wfv-wrds/RESULTS # Results Summary: -- Located WRDS exports under `/Volumes/Storage/Data/wrds` with expected `crsp`, `meta`, and `universes` paths. +- Located WRDS exports under `/Data/wrds` with expected `crsp`, `meta`, and `universes` paths. - Ran WRDS holdout-capable smoke WFV; holdout artifacts were written alongside WFV outputs. - Rendered WRDS smoke report outputs and updated `docs/results_wrds_smoke.md` plus summary assets; regenerated project_state docs. @@ -76,8 +76,8 @@ EOF cat <<'EOF' > docs/agent_runs/20251221_162711_ticket-02_holdout-wfv-wrds/TESTS.md # Tests -- `WRDS_DATA_ROOT=/Volumes/Storage/Data/wrds make wfv-wrds-smoke` (pass) -- `WRDS_DATA_ROOT=/Volumes/Storage/Data/wrds make report-wrds-smoke` (pass; warnings about pandas fillna and matplotlib tight_layout) +- `WRDS_DATA_ROOT=/Data/wrds make wfv-wrds-smoke` (pass) +- `WRDS_DATA_ROOT=/Data/wrds make report-wrds-smoke` (pass; warnings about pandas fillna and matplotlib tight_layout) - `pytest -q` (not run in this follow-up) EOF python3 - <<'PY' @@ -95,12 +95,12 @@ meta = { "os": "macOS-26.0.1-arm64-arm-64bit", "python": "3.12.2", "pip_freeze_hash": "d155c3d7c2b41ef4ce242bfec462edd0db5be747333f4495a1e96c8a0252e525", - "notes": "WRDS exports located under /Volumes/Storage/Data/wrds", + "notes": "WRDS exports located under /Data/wrds", }, "data": { "mode": "wrds", "dataset_id": "wrds_smoke", - "wrds_data_root": "/Volumes/Storage/Data/wrds", + "wrds_data_root": "/Data/wrds", "license_notes": "No raw WRDS data committed.", }, "config": { @@ -128,6 +128,6 @@ Path("docs/agent_runs/20251221_162711_ticket-02_holdout-wfv-wrds/META.json").wri ) PY git add PROGRESS.md docs/results_wrds_smoke.md reports/summaries/wrds_flagship_smoke.md reports/summaries/wrds_flagship_smoke_factors.md reports/summaries/wrds_flagship_smoke_metrics.json docs/img/wrds_flagship_smoke/2025-12-21T21-28-14Z-33c9c2a project_state docs/agent_runs/20251221_162711_ticket-02_holdout-wfv-wrds -git commit -m "ticket-02: run WRDS holdout smoke" -m "Tests: WRDS_DATA_ROOT=/Volumes/Storage/Data/wrds make wfv-wrds-smoke; WRDS_DATA_ROOT=/Volumes/Storage/Data/wrds make report-wrds-smoke" -m "Artifacts: artifacts/wrds_flagship_smoke/2025-12-21T21-28-14Z-33c9c2a/ (local only)" -m "Docs: PROGRESS.md, docs/results_wrds_smoke.md, reports/summaries/wrds_flagship_smoke*, docs/img/wrds_flagship_smoke/2025-12-21T21-28-14Z-33c9c2a/, docs/agent_runs/20251221_162711_ticket-02_holdout-wfv-wrds/" +git commit -m "ticket-02: run WRDS holdout smoke" -m "Tests: WRDS_DATA_ROOT=/Data/wrds make wfv-wrds-smoke; WRDS_DATA_ROOT=/Data/wrds make report-wrds-smoke" -m "Artifacts: artifacts/wrds_flagship_smoke/2025-12-21T21-28-14Z-33c9c2a/ (local only)" -m "Docs: PROGRESS.md, docs/results_wrds_smoke.md, reports/summaries/wrds_flagship_smoke*, docs/img/wrds_flagship_smoke/2025-12-21T21-28-14Z-33c9c2a/, docs/agent_runs/20251221_162711_ticket-02_holdout-wfv-wrds/" git status -sb ``` diff --git a/docs/agent_runs/20251221_162711_ticket-02_holdout-wfv-wrds/META.json b/docs/agent_runs/20251221_162711_ticket-02_holdout-wfv-wrds/META.json index b43082a7..975635bf 100644 --- a/docs/agent_runs/20251221_162711_ticket-02_holdout-wfv-wrds/META.json +++ b/docs/agent_runs/20251221_162711_ticket-02_holdout-wfv-wrds/META.json @@ -9,12 +9,12 @@ "os": "macOS-26.0.1-arm64-arm-64bit", "python": "3.12.2", "pip_freeze_hash": "d155c3d7c2b41ef4ce242bfec462edd0db5be747333f4495a1e96c8a0252e525", - "notes": "WRDS exports located under /Volumes/Storage/Data/wrds" + "notes": "WRDS exports located under /Data/wrds" }, "data": { "mode": "wrds", "dataset_id": "wrds_smoke", - "wrds_data_root": "/Volumes/Storage/Data/wrds", + "wrds_data_root": "/Data/wrds", "license_notes": "No raw WRDS data committed." }, "config": { @@ -60,5 +60,5 @@ "docs/results_wrds_smoke.md" ], "web_sources": [], - "host_env_notes": "macOS-26.0.1-arm64-arm-64bit / 3.12.2 / notes: WRDS exports located under /Volumes/Storage/Data/wrds / timestamps derived from timestamp_local" + "host_env_notes": "macOS-26.0.1-arm64-arm-64bit / 3.12.2 / notes: WRDS exports located under /Data/wrds / timestamps derived from timestamp_local" } diff --git a/docs/agent_runs/20251221_162711_ticket-02_holdout-wfv-wrds/PROMPT.md b/docs/agent_runs/20251221_162711_ticket-02_holdout-wfv-wrds/PROMPT.md index ffac7b98..02c0a539 100644 --- a/docs/agent_runs/20251221_162711_ticket-02_holdout-wfv-wrds/PROMPT.md +++ b/docs/agent_runs/20251221_162711_ticket-02_holdout-wfv-wrds/PROMPT.md @@ -1,3 +1,3 @@ User request: -WRDS is available, i keep all WRDS raw data at /Volumes/Storage/Data ,look through, find, then continue +WRDS is available, i keep all WRDS raw data at /Data ,look through, find, then continue diff --git a/docs/agent_runs/20251221_162711_ticket-02_holdout-wfv-wrds/RESULTS.md b/docs/agent_runs/20251221_162711_ticket-02_holdout-wfv-wrds/RESULTS.md index 3bf78825..501a7f1e 100644 --- a/docs/agent_runs/20251221_162711_ticket-02_holdout-wfv-wrds/RESULTS.md +++ b/docs/agent_runs/20251221_162711_ticket-02_holdout-wfv-wrds/RESULTS.md @@ -1,7 +1,7 @@ # Results Summary: -- Located WRDS exports under `/Volumes/Storage/Data/wrds` with expected `crsp`, `meta`, and `universes` paths. +- Located WRDS exports under `/Data/wrds` with expected `crsp`, `meta`, and `universes` paths. - Ran WRDS holdout-capable smoke WFV; holdout artifacts were written alongside WFV outputs. - Rendered WRDS smoke report outputs and updated `docs/results_wrds_smoke.md` plus summary assets; regenerated project_state docs. diff --git a/docs/agent_runs/20251221_162711_ticket-02_holdout-wfv-wrds/TESTS.md b/docs/agent_runs/20251221_162711_ticket-02_holdout-wfv-wrds/TESTS.md index 4222a218..c004e4ec 100644 --- a/docs/agent_runs/20251221_162711_ticket-02_holdout-wfv-wrds/TESTS.md +++ b/docs/agent_runs/20251221_162711_ticket-02_holdout-wfv-wrds/TESTS.md @@ -1,5 +1,5 @@ # Tests -- `WRDS_DATA_ROOT=/Volumes/Storage/Data/wrds make wfv-wrds-smoke` (pass) -- `WRDS_DATA_ROOT=/Volumes/Storage/Data/wrds make report-wrds-smoke` (pass; warnings about pandas fillna and matplotlib tight_layout) +- `WRDS_DATA_ROOT=/Data/wrds make wfv-wrds-smoke` (pass) +- `WRDS_DATA_ROOT=/Data/wrds make report-wrds-smoke` (pass; warnings about pandas fillna and matplotlib tight_layout) - `pytest -q` (not run in this follow-up) diff --git a/docs/agent_runs/20251221_173223_ticket-02_holdout-wfv-wrds-full/COMMANDS.md b/docs/agent_runs/20251221_173223_ticket-02_holdout-wfv-wrds-full/COMMANDS.md index b707b029..326352c9 100644 --- a/docs/agent_runs/20251221_173223_ticket-02_holdout-wfv-wrds-full/COMMANDS.md +++ b/docs/agent_runs/20251221_173223_ticket-02_holdout-wfv-wrds-full/COMMANDS.md @@ -6,10 +6,10 @@ mkdir -p docs/agent_runs/20251221_173223_ticket-02_holdout-wfv-wrds-full cat <<'EOF' > docs/agent_runs/20251221_173223_ticket-02_holdout-wfv-wrds-full/PROMPT.md User request: -1) Run the full WRDS holdout walk-forward (WRDS_DATA_ROOT=/Volumes/Storage/Data/wrds make wfv-wrds or make wrds-flagship). +1) Run the full WRDS holdout walk-forward (WRDS_DATA_ROOT=/Data/wrds make wfv-wrds or make wrds-flagship). 2) Run pytest -q. EOF -WRDS_DATA_ROOT=/Volumes/Storage/Data/wrds make wfv-wrds +WRDS_DATA_ROOT=/Data/wrds make wfv-wrds pytest -q sed -n '1,200p' project_state/KNOWN_ISSUES.md python3 tools/build_project_state.py @@ -42,6 +42,6 @@ tail -n 40 PROGRESS.md rg -n "zero trades|zero-trade|wrds" project_state/KNOWN_ISSUES.md git add PROGRESS.md project_state tools/render_project_state_docs.py docs/agent_runs/20251221_173223_ticket-02_holdout-wfv-wrds-full git status -sb -git commit -m "ticket-02: run full WRDS holdout WFV" -m "Tests: WRDS_DATA_ROOT=/Volumes/Storage/Data/wrds make wfv-wrds; pytest -q" -m "Artifacts: artifacts/wrds_flagship/2025-12-21T22-32-44Z-2b48ef7 (local only)" -m "Docs: PROGRESS.md, project_state/*, docs/agent_runs/20251221_173223_ticket-02_holdout-wfv-wrds-full/, tools/render_project_state_docs.py" +git commit -m "ticket-02: run full WRDS holdout WFV" -m "Tests: WRDS_DATA_ROOT=/Data/wrds make wfv-wrds; pytest -q" -m "Artifacts: artifacts/wrds_flagship/2025-12-21T22-32-44Z-2b48ef7 (local only)" -m "Docs: PROGRESS.md, project_state/*, docs/agent_runs/20251221_173223_ticket-02_holdout-wfv-wrds-full/, tools/render_project_state_docs.py" git rev-parse HEAD ``` diff --git a/docs/agent_runs/20251221_173223_ticket-02_holdout-wfv-wrds-full/META.json b/docs/agent_runs/20251221_173223_ticket-02_holdout-wfv-wrds-full/META.json index 82b67e99..8e8b0ac2 100644 --- a/docs/agent_runs/20251221_173223_ticket-02_holdout-wfv-wrds-full/META.json +++ b/docs/agent_runs/20251221_173223_ticket-02_holdout-wfv-wrds-full/META.json @@ -14,7 +14,7 @@ "data": { "mode": "wrds", "dataset_id": "wrds_full", - "wrds_data_root": "/Volumes/Storage/Data/wrds", + "wrds_data_root": "/Data/wrds", "license_notes": "No raw WRDS data committed." }, "config": { diff --git a/docs/agent_runs/20251221_173223_ticket-02_holdout-wfv-wrds-full/PROMPT.md b/docs/agent_runs/20251221_173223_ticket-02_holdout-wfv-wrds-full/PROMPT.md index 0192e6c3..826a7e9a 100644 --- a/docs/agent_runs/20251221_173223_ticket-02_holdout-wfv-wrds-full/PROMPT.md +++ b/docs/agent_runs/20251221_173223_ticket-02_holdout-wfv-wrds-full/PROMPT.md @@ -1,4 +1,4 @@ User request: -1) Run the full WRDS holdout walk-forward (WRDS_DATA_ROOT=/Volumes/Storage/Data/wrds make wfv-wrds or make wrds-flagship). +1) Run the full WRDS holdout walk-forward (WRDS_DATA_ROOT=/Data/wrds make wfv-wrds or make wrds-flagship). 2) Run pytest -q. diff --git a/docs/agent_runs/20251221_173223_ticket-02_holdout-wfv-wrds-full/TESTS.md b/docs/agent_runs/20251221_173223_ticket-02_holdout-wfv-wrds-full/TESTS.md index 5eede58b..a68d86bd 100644 --- a/docs/agent_runs/20251221_173223_ticket-02_holdout-wfv-wrds-full/TESTS.md +++ b/docs/agent_runs/20251221_173223_ticket-02_holdout-wfv-wrds-full/TESTS.md @@ -1,4 +1,4 @@ # Tests -- `WRDS_DATA_ROOT=/Volumes/Storage/Data/wrds make wfv-wrds` (pass; runtime ~463s) +- `WRDS_DATA_ROOT=/Data/wrds make wfv-wrds` (pass; runtime ~463s) - `pytest -q` (pass: 100 passed, 1 skipped) diff --git a/docs/agent_runs/20251221_175417_ticket-02_holdout-wfv-wrds-report/COMMANDS.md b/docs/agent_runs/20251221_175417_ticket-02_holdout-wfv-wrds-report/COMMANDS.md index b76a331d..d5659fac 100644 --- a/docs/agent_runs/20251221_175417_ticket-02_holdout-wfv-wrds-report/COMMANDS.md +++ b/docs/agent_runs/20251221_175417_ticket-02_holdout-wfv-wrds-report/COMMANDS.md @@ -6,9 +6,9 @@ mkdir -p docs/agent_runs/20251221_175417_ticket-02_holdout-wfv-wrds-report cat <<'PROMPT' > docs/agent_runs/20251221_175417_ticket-02_holdout-wfv-wrds-report/PROMPT.md User request: -Run WRDS report: WRDS_DATA_ROOT=/Volumes/Storage/Data/wrds make report-wrds. +Run WRDS report: WRDS_DATA_ROOT=/Data/wrds make report-wrds. PROMPT -WRDS_DATA_ROOT=/Volumes/Storage/Data/wrds make report-wrds +WRDS_DATA_ROOT=/Data/wrds make report-wrds rg -n "report-wrds" Makefile sed -n '55,95p' Makefile git rev-parse HEAD @@ -27,7 +27,7 @@ EOF_RESULTS cat <<'EOF_TESTS' > docs/agent_runs/20251221_175417_ticket-02_holdout-wfv-wrds-report/TESTS.md # Tests -- `WRDS_DATA_ROOT=/Volumes/Storage/Data/wrds make report-wrds` (failed: SPA comparator t-stats all zero) +- `WRDS_DATA_ROOT=/Data/wrds make report-wrds` (failed: SPA comparator t-stats all zero) EOF_TESTS python3 - <<'PY' import subprocess, hashlib, sys @@ -61,7 +61,7 @@ cat <<'EOF_META' > docs/agent_runs/20251221_175417_ticket-02_holdout-wfv-wrds-re "data": { "mode": "wrds", "dataset_id": "wrds_full", - "wrds_data_root": "/Volumes/Storage/Data/wrds", + "wrds_data_root": "/Data/wrds", "license_notes": "No raw WRDS data committed." }, "config": { @@ -86,7 +86,7 @@ cat <<'EOF_META' > docs/agent_runs/20251221_175417_ticket-02_holdout-wfv-wrds-re EOF_META git status -sb git add PROGRESS.md reports/summaries/wrds_flagship_factors.md docs/agent_runs/20251221_175417_ticket-02_holdout-wfv-wrds-report -git commit -m "ticket-02: attempt WRDS report" -m "Tests: WRDS_DATA_ROOT=/Volumes/Storage/Data/wrds make report-wrds (failed: SPA comparator t-stats all zero)" -m "Artifacts: artifacts/wrds_flagship/2025-12-21T22-32-44Z-2b48ef7 (local only)" -m "Docs: PROGRESS.md, docs/agent_runs/20251221_175417_ticket-02_holdout-wfv-wrds-report/, reports/summaries/wrds_flagship_factors.md" +git commit -m "ticket-02: attempt WRDS report" -m "Tests: WRDS_DATA_ROOT=/Data/wrds make report-wrds (failed: SPA comparator t-stats all zero)" -m "Artifacts: artifacts/wrds_flagship/2025-12-21T22-32-44Z-2b48ef7 (local only)" -m "Docs: PROGRESS.md, docs/agent_runs/20251221_175417_ticket-02_holdout-wfv-wrds-report/, reports/summaries/wrds_flagship_factors.md" git rev-parse HEAD cat <<'EOF_META_UPDATE' > docs/agent_runs/20251221_175417_ticket-02_holdout-wfv-wrds-report/META.json { @@ -105,7 +105,7 @@ cat <<'EOF_META_UPDATE' > docs/agent_runs/20251221_175417_ticket-02_holdout-wfv- "data": { "mode": "wrds", "dataset_id": "wrds_full", - "wrds_data_root": "/Volumes/Storage/Data/wrds", + "wrds_data_root": "/Data/wrds", "license_notes": "No raw WRDS data committed." }, "config": { @@ -153,7 +153,7 @@ cat <<'EOF_META' > docs/agent_runs/20251221_175417_ticket-02_holdout-wfv-wrds-re "data": { "mode": "wrds", "dataset_id": "wrds_full", - "wrds_data_root": "/Volumes/Storage/Data/wrds", + "wrds_data_root": "/Data/wrds", "license_notes": "No raw WRDS data committed." }, "config": { diff --git a/docs/agent_runs/20251221_175417_ticket-02_holdout-wfv-wrds-report/META.json b/docs/agent_runs/20251221_175417_ticket-02_holdout-wfv-wrds-report/META.json index 893db535..e06df488 100644 --- a/docs/agent_runs/20251221_175417_ticket-02_holdout-wfv-wrds-report/META.json +++ b/docs/agent_runs/20251221_175417_ticket-02_holdout-wfv-wrds-report/META.json @@ -14,7 +14,7 @@ "data": { "mode": "wrds", "dataset_id": "wrds_full", - "wrds_data_root": "/Volumes/Storage/Data/wrds", + "wrds_data_root": "/Data/wrds", "license_notes": "No raw WRDS data committed." }, "config": { diff --git a/docs/agent_runs/20251221_175417_ticket-02_holdout-wfv-wrds-report/PROMPT.md b/docs/agent_runs/20251221_175417_ticket-02_holdout-wfv-wrds-report/PROMPT.md index 696b8578..1f9d5d93 100644 --- a/docs/agent_runs/20251221_175417_ticket-02_holdout-wfv-wrds-report/PROMPT.md +++ b/docs/agent_runs/20251221_175417_ticket-02_holdout-wfv-wrds-report/PROMPT.md @@ -1,3 +1,3 @@ User request: -Run WRDS report: WRDS_DATA_ROOT=/Volumes/Storage/Data/wrds make report-wrds. +Run WRDS report: WRDS_DATA_ROOT=/Data/wrds make report-wrds. diff --git a/docs/agent_runs/20251221_175417_ticket-02_holdout-wfv-wrds-report/TESTS.md b/docs/agent_runs/20251221_175417_ticket-02_holdout-wfv-wrds-report/TESTS.md index e300810d..ae5312c1 100644 --- a/docs/agent_runs/20251221_175417_ticket-02_holdout-wfv-wrds-report/TESTS.md +++ b/docs/agent_runs/20251221_175417_ticket-02_holdout-wfv-wrds-report/TESTS.md @@ -1,3 +1,3 @@ # Tests -- `WRDS_DATA_ROOT=/Volumes/Storage/Data/wrds make report-wrds` (failed: SPA comparator t-stats all zero) +- `WRDS_DATA_ROOT=/Data/wrds make report-wrds` (failed: SPA comparator t-stats all zero) diff --git a/docs/agent_runs/20251222_013000_ticket-08_unblock-wrds-report-spa/TESTS.md b/docs/agent_runs/20251222_013000_ticket-08_unblock-wrds-report-spa/TESTS.md index d53ce13a..b915a7af 100644 --- a/docs/agent_runs/20251222_013000_ticket-08_unblock-wrds-report-spa/TESTS.md +++ b/docs/agent_runs/20251222_013000_ticket-08_unblock-wrds-report-spa/TESTS.md @@ -5,15 +5,15 @@ Command: Output: ``` -/Users/mateobodon/Documents/Programming/Projects/microalpha/src/microalpha/reporting/tearsheet.py:174: UserWarning: This figure includes Axes that are not compatible with tight_layout, so results might be incorrect. +/Documents/Programming/Projects/microalpha/src/microalpha/reporting/tearsheet.py:174: UserWarning: This figure includes Axes that are not compatible with tight_layout, so results might be incorrect. fig_equity.tight_layout() { - "artifact_dir": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv_holdout/2025-12-22T00-40-53Z-99a072e", - "summary_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/reports/summaries/flagship_mom.md", - "equity_curve_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv_holdout/2025-12-22T00-40-53Z-99a072e/equity_curve.png", - "bootstrap_hist_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv_holdout/2025-12-22T00-40-53Z-99a072e/bootstrap_hist.png", - "cost_sensitivity_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv_holdout/2025-12-22T00-40-53Z-99a072e/cost_sensitivity.json", - "metadata_coverage_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv_holdout/2025-12-22T00-40-53Z-99a072e/metadata_coverage.json", + "artifact_dir": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv_holdout/2025-12-22T00-40-53Z-99a072e", + "summary_path": "/Documents/Programming/Projects/microalpha/reports/summaries/flagship_mom.md", + "equity_curve_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv_holdout/2025-12-22T00-40-53Z-99a072e/equity_curve.png", + "bootstrap_hist_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv_holdout/2025-12-22T00-40-53Z-99a072e/bootstrap_hist.png", + "cost_sensitivity_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv_holdout/2025-12-22T00-40-53Z-99a072e/cost_sensitivity.json", + "metadata_coverage_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv_holdout/2025-12-22T00-40-53Z-99a072e/metadata_coverage.json", "runtime_sec": 0.641, "version": "0.1.0" } @@ -24,15 +24,15 @@ Command: Output: ``` -/Users/mateobodon/Documents/Programming/Projects/microalpha/src/microalpha/reporting/tearsheet.py:174: UserWarning: This figure includes Axes that are not compatible with tight_layout, so results might be incorrect. +/Documents/Programming/Projects/microalpha/src/microalpha/reporting/tearsheet.py:174: UserWarning: This figure includes Axes that are not compatible with tight_layout, so results might be incorrect. fig_equity.tight_layout() { - "artifact_dir": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/wrds_flagship/2025-12-21T22-32-44Z-2b48ef7", - "summary_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/reports/summaries/flagship_mom.md", - "equity_curve_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/wrds_flagship/2025-12-21T22-32-44Z-2b48ef7/equity_curve.png", - "bootstrap_hist_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/wrds_flagship/2025-12-21T22-32-44Z-2b48ef7/bootstrap_hist.png", - "cost_sensitivity_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/wrds_flagship/2025-12-21T22-32-44Z-2b48ef7/cost_sensitivity.json", - "metadata_coverage_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/wrds_flagship/2025-12-21T22-32-44Z-2b48ef7/metadata_coverage.json", + "artifact_dir": "/Documents/Programming/Projects/microalpha/artifacts/wrds_flagship/2025-12-21T22-32-44Z-2b48ef7", + "summary_path": "/Documents/Programming/Projects/microalpha/reports/summaries/flagship_mom.md", + "equity_curve_path": "/Documents/Programming/Projects/microalpha/artifacts/wrds_flagship/2025-12-21T22-32-44Z-2b48ef7/equity_curve.png", + "bootstrap_hist_path": "/Documents/Programming/Projects/microalpha/artifacts/wrds_flagship/2025-12-21T22-32-44Z-2b48ef7/bootstrap_hist.png", + "cost_sensitivity_path": "/Documents/Programming/Projects/microalpha/artifacts/wrds_flagship/2025-12-21T22-32-44Z-2b48ef7/cost_sensitivity.json", + "metadata_coverage_path": "/Documents/Programming/Projects/microalpha/artifacts/wrds_flagship/2025-12-21T22-32-44Z-2b48ef7/metadata_coverage.json", "runtime_sec": 0.513, "version": "0.1.0" } @@ -59,15 +59,15 @@ Command: Output: ``` -/Users/mateobodon/Documents/Programming/Projects/microalpha/src/microalpha/reporting/tearsheet.py:174: UserWarning: This figure includes Axes that are not compatible with tight_layout, so results might be incorrect. +/Documents/Programming/Projects/microalpha/src/microalpha/reporting/tearsheet.py:174: UserWarning: This figure includes Axes that are not compatible with tight_layout, so results might be incorrect. fig_equity.tight_layout() { - "artifact_dir": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv_holdout/2025-12-22T00-40-53Z-99a072e", - "summary_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/reports/summaries/flagship_mom.md", - "equity_curve_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv_holdout/2025-12-22T00-40-53Z-99a072e/equity_curve.png", - "bootstrap_hist_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv_holdout/2025-12-22T00-40-53Z-99a072e/bootstrap_hist.png", - "cost_sensitivity_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv_holdout/2025-12-22T00-40-53Z-99a072e/cost_sensitivity.json", - "metadata_coverage_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_wfv_holdout/2025-12-22T00-40-53Z-99a072e/metadata_coverage.json", + "artifact_dir": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv_holdout/2025-12-22T00-40-53Z-99a072e", + "summary_path": "/Documents/Programming/Projects/microalpha/reports/summaries/flagship_mom.md", + "equity_curve_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv_holdout/2025-12-22T00-40-53Z-99a072e/equity_curve.png", + "bootstrap_hist_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv_holdout/2025-12-22T00-40-53Z-99a072e/bootstrap_hist.png", + "cost_sensitivity_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv_holdout/2025-12-22T00-40-53Z-99a072e/cost_sensitivity.json", + "metadata_coverage_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_wfv_holdout/2025-12-22T00-40-53Z-99a072e/metadata_coverage.json", "runtime_sec": 0.538, "version": "0.1.0" } @@ -78,15 +78,15 @@ Command: Output: ``` -/Users/mateobodon/Documents/Programming/Projects/microalpha/src/microalpha/reporting/tearsheet.py:174: UserWarning: This figure includes Axes that are not compatible with tight_layout, so results might be incorrect. +/Documents/Programming/Projects/microalpha/src/microalpha/reporting/tearsheet.py:174: UserWarning: This figure includes Axes that are not compatible with tight_layout, so results might be incorrect. fig_equity.tight_layout() { - "artifact_dir": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/wrds_flagship/2025-12-21T22-32-44Z-2b48ef7", - "summary_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/reports/summaries/flagship_mom.md", - "equity_curve_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/wrds_flagship/2025-12-21T22-32-44Z-2b48ef7/equity_curve.png", - "bootstrap_hist_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/wrds_flagship/2025-12-21T22-32-44Z-2b48ef7/bootstrap_hist.png", - "cost_sensitivity_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/wrds_flagship/2025-12-21T22-32-44Z-2b48ef7/cost_sensitivity.json", - "metadata_coverage_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/wrds_flagship/2025-12-21T22-32-44Z-2b48ef7/metadata_coverage.json", + "artifact_dir": "/Documents/Programming/Projects/microalpha/artifacts/wrds_flagship/2025-12-21T22-32-44Z-2b48ef7", + "summary_path": "/Documents/Programming/Projects/microalpha/reports/summaries/flagship_mom.md", + "equity_curve_path": "/Documents/Programming/Projects/microalpha/artifacts/wrds_flagship/2025-12-21T22-32-44Z-2b48ef7/equity_curve.png", + "bootstrap_hist_path": "/Documents/Programming/Projects/microalpha/artifacts/wrds_flagship/2025-12-21T22-32-44Z-2b48ef7/bootstrap_hist.png", + "cost_sensitivity_path": "/Documents/Programming/Projects/microalpha/artifacts/wrds_flagship/2025-12-21T22-32-44Z-2b48ef7/cost_sensitivity.json", + "metadata_coverage_path": "/Documents/Programming/Projects/microalpha/artifacts/wrds_flagship/2025-12-21T22-32-44Z-2b48ef7/metadata_coverage.json", "runtime_sec": 0.502, "version": "0.1.0" } @@ -99,15 +99,15 @@ Command: Output: ``` -/Users/mateobodon/Documents/Programming/Projects/microalpha/src/microalpha/reporting/tearsheet.py:174: UserWarning: This figure includes Axes that are not compatible with tight_layout, so results might be incorrect. +/Documents/Programming/Projects/microalpha/src/microalpha/reporting/tearsheet.py:174: UserWarning: This figure includes Axes that are not compatible with tight_layout, so results might be incorrect. fig_equity.tight_layout() { - "artifact_dir": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_flagship/2025-12-20T23-30-48Z-f8b316f", - "summary_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/reports/summaries/flagship_mom.md", - "equity_curve_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_flagship/2025-12-20T23-30-48Z-f8b316f/equity_curve.png", - "bootstrap_hist_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_flagship/2025-12-20T23-30-48Z-f8b316f/bootstrap_hist.png", - "cost_sensitivity_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_flagship/2025-12-20T23-30-48Z-f8b316f/cost_sensitivity.json", - "metadata_coverage_path": "/Users/mateobodon/Documents/Programming/Projects/microalpha/artifacts/sample_flagship/2025-12-20T23-30-48Z-f8b316f/metadata_coverage.json", + "artifact_dir": "/Documents/Programming/Projects/microalpha/artifacts/sample_flagship/2025-12-20T23-30-48Z-f8b316f", + "summary_path": "/Documents/Programming/Projects/microalpha/reports/summaries/flagship_mom.md", + "equity_curve_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_flagship/2025-12-20T23-30-48Z-f8b316f/equity_curve.png", + "bootstrap_hist_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_flagship/2025-12-20T23-30-48Z-f8b316f/bootstrap_hist.png", + "cost_sensitivity_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_flagship/2025-12-20T23-30-48Z-f8b316f/cost_sensitivity.json", + "metadata_coverage_path": "/Documents/Programming/Projects/microalpha/artifacts/sample_flagship/2025-12-20T23-30-48Z-f8b316f/metadata_coverage.json", "runtime_sec": 0.541, "version": "0.1.0" } diff --git a/docs/agent_runs/20251222_191759_ticket-00_project_state_rebuild/PROMPT.md b/docs/agent_runs/20251222_191759_ticket-00_project_state_rebuild/PROMPT.md index 99e31737..c9017950 100644 --- a/docs/agent_runs/20251222_191759_ticket-00_project_state_rebuild/PROMPT.md +++ b/docs/agent_runs/20251222_191759_ticket-00_project_state_rebuild/PROMPT.md @@ -2,7 +2,7 @@ ## User-provided repo instructions (verbatim) -# AGENTS.md instructions for /Users/mateobodon/Documents/Programming/Projects/microalpha +# AGENTS.md instructions for /Documents/Programming/Projects/microalpha # AGENTS.md — microalpha (Repo Instructions) @@ -119,7 +119,7 @@ See: `docs/DOCS_AND_LOGGING_SYSTEM.md` ## Environment context (verbatim) - /Users/mateobodon/Documents/Programming/Projects/microalpha + /Documents/Programming/Projects/microalpha never danger-full-access enabled diff --git a/docs/agent_runs/20251222_200000_ticket-01_fix-spa-robustness/COMMANDS.md b/docs/agent_runs/20251222_200000_ticket-01_fix-spa-robustness/COMMANDS.md index d567de28..eef9acf6 100644 --- a/docs/agent_runs/20251222_200000_ticket-01_fix-spa-robustness/COMMANDS.md +++ b/docs/agent_runs/20251222_200000_ticket-01_fix-spa-robustness/COMMANDS.md @@ -26,7 +26,7 @@ 24. printenv WRDS_DATA_ROOT 25. rg -n "WRDS_DATA_ROOT" -S . 26. sed -n '1,200p' metadata/codex_sessions.md -27. ls -la /Users/mateobodon/wrds_cache | head -n 20 +27. ls -la /wrds_cache | head -n 20 28. cat .git/info/exclude 29. git ls-files -v docs | head -n 5 30. mkdir -p docs/agent_runs/20251222_200000_ticket-01_fix-spa-robustness @@ -41,9 +41,9 @@ 39. apply_patch (update tests/test_wrds_summary_render.py) 40. mkdir -p docs/local; cat > docs/local/WRDS_DATA_ROOT.md <<'EOF' ... 41. pytest -q -42. WRDS_DATA_ROOT=/Users/mateobodon/wrds_cache make wfv-wrds-smoke -43. WRDS_DATA_ROOT=/Users/mateobodon/wrds_cache make report-wrds-smoke -44. WRDS_DATA_ROOT=/Users/mateobodon/wrds_cache make report-wrds +42. WRDS_DATA_ROOT=/wrds_cache make wfv-wrds-smoke +43. WRDS_DATA_ROOT=/wrds_cache make report-wrds-smoke +44. WRDS_DATA_ROOT=/wrds_cache make report-wrds 45. python - <<'PY' ... (failed: python not found) 46. python3 - <<'PY' ... (inspect spa.json smoke) 47. python3 - <<'PY' ... (inspect spa.json wrds) diff --git a/docs/agent_runs/20251222_200000_ticket-01_fix-spa-robustness/META.json b/docs/agent_runs/20251222_200000_ticket-01_fix-spa-robustness/META.json index 714e18aa..bbdf0597 100644 --- a/docs/agent_runs/20251222_200000_ticket-01_fix-spa-robustness/META.json +++ b/docs/agent_runs/20251222_200000_ticket-01_fix-spa-robustness/META.json @@ -5,7 +5,7 @@ "git_sha_after": "12b63a22117a3a5acf1784eac2df4418c3037ba9", "branch": "main", "env_notes": { - "wrds_data_root": "/Users/mateobodon/wrds_cache", + "wrds_data_root": "/wrds_cache", "python": "python3 (see artifacts/manifest.json for full runtime metadata)", "wrds_available": true }, @@ -64,5 +64,5 @@ "reports/summaries/wrds_flagship_spa.md", "reports/summaries/wrds_flagship_metrics.json" ], - "host_env_notes": "wrds_data_root=/Users/mateobodon/wrds_cache / python=python3 (see artifacts/manifest.json for full runtime metadata) / wrds_available=True" + "host_env_notes": "wrds_data_root=/wrds_cache / python=python3 (see artifacts/manifest.json for full runtime metadata) / wrds_available=True" } diff --git a/docs/agent_runs/20251222_200000_ticket-01_fix-spa-robustness/RESULTS.md b/docs/agent_runs/20251222_200000_ticket-01_fix-spa-robustness/RESULTS.md index 74b518d4..c215f289 100644 --- a/docs/agent_runs/20251222_200000_ticket-01_fix-spa-robustness/RESULTS.md +++ b/docs/agent_runs/20251222_200000_ticket-01_fix-spa-robustness/RESULTS.md @@ -28,6 +28,6 @@ ## Notes -- WRDS exports available at `/Users/mateobodon/wrds_cache` (documented locally per user request). +- WRDS exports available at `/wrds_cache` (documented locally per user request). - SPA outputs for both WRDS smoke and flagship runs are degenerate (“all strategies have zero variance”); reports now show the reason instead of crashing. - Report commands emitted FutureWarning (pandas fillna method) and a matplotlib tight_layout warning; no failures. diff --git a/docs/agent_runs/20251223_214840_ticket-04_leakage-tests-unsafe-manifest/PROMPT.md b/docs/agent_runs/20251223_214840_ticket-04_leakage-tests-unsafe-manifest/PROMPT.md index a61bc516..94a76c1e 100644 --- a/docs/agent_runs/20251223_214840_ticket-04_leakage-tests-unsafe-manifest/PROMPT.md +++ b/docs/agent_runs/20251223_214840_ticket-04_leakage-tests-unsafe-manifest/PROMPT.md @@ -1,4 +1,4 @@ -# AGENTS.md instructions for /Users/mateobodon/Documents/Programming/Projects/microalpha +# AGENTS.md instructions for /Documents/Programming/Projects/microalpha # AGENTS.md — microalpha @@ -88,8 +88,8 @@ Living docs: ## Skills These skills are discovered at startup from multiple local sources. Each entry includes a name, description, and file path so you can open the source for full instructions. -- skill-creator: Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Codex's capabilities with specialized knowledge, workflows, or tool integrations. (file: /Users/mateobodon/.codex/skills/.system/skill-creator/SKILL.md) -- skill-installer: Install Codex skills into $CODEX_HOME/skills from a curated list or a GitHub repo path. Use when a user asks to list installable skills, install a curated skill, or install a skill from another repo (including private repos). (file: /Users/mateobodon/.codex/skills/.system/skill-installer/SKILL.md) +- skill-creator: Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Codex's capabilities with specialized knowledge, workflows, or tool integrations. (file: /.codex/skills/.system/skill-creator/SKILL.md) +- skill-installer: Install Codex skills into $CODEX_HOME/skills from a curated list or a GitHub repo path. Use when a user asks to list installable skills, install a curated skill, or install a skill from another repo (including private repos). (file: /.codex/skills/.system/skill-installer/SKILL.md) - Discovery: Available skills are listed in project docs and may also appear in a runtime "## Skills" section (name + description + file path). These are the sources of truth; skill bodies live on disk at the listed paths. - Trigger rules: If the user names a skill (with `$SkillName` or plain text) OR the task clearly matches a skill's description, you must use that skill for that turn. Multiple mentions mean use them all. Do not carry skills across turns unless re-mentioned. - Missing/blocked: If a named skill isn't in the list or the path can't be read, say so briefly and continue with the best fallback. @@ -110,7 +110,7 @@ These skills are discovered at startup from multiple local sources. Each entry i - /Users/mateobodon/Documents/Programming/Projects/microalpha + /Documents/Programming/Projects/microalpha never danger-full-access enabled diff --git a/docs/agent_runs/20251224_051508_ticket-04_wrds-smoke-check/COMMANDS.md b/docs/agent_runs/20251224_051508_ticket-04_wrds-smoke-check/COMMANDS.md index ceed4060..b7171b2b 100644 --- a/docs/agent_runs/20251224_051508_ticket-04_wrds-smoke-check/COMMANDS.md +++ b/docs/agent_runs/20251224_051508_ticket-04_wrds-smoke-check/COMMANDS.md @@ -4,8 +4,8 @@ 2. `echo $WRDS_DATA_ROOT` 3. `cat docs/local/WRDS_DATA_ROOT.md` 4. `RUN_NAME=20251224_051508_ticket-04_wrds-smoke-check mkdir -p docs/agent_runs/$RUN_NAME` (and write prompt files) -5. `WRDS_DATA_ROOT=/Volumes/Storage/Data/wrds make wfv-wrds-smoke` -6. `WRDS_DATA_ROOT=/Volumes/Storage/Data/wrds make report-wrds-smoke` +5. `WRDS_DATA_ROOT=/Data/wrds make wfv-wrds-smoke` +6. `WRDS_DATA_ROOT=/Data/wrds make report-wrds-smoke` 7. `ls -t reports/summaries/wrds_flagship_smoke* | head -n 5` 8. `ls -t docs/results_wrds_smoke.md` 9. `ls -t docs/img/wrds_flagship_smoke | head -n 3` diff --git a/docs/agent_runs/20251224_051508_ticket-04_wrds-smoke-check/META.json b/docs/agent_runs/20251224_051508_ticket-04_wrds-smoke-check/META.json index 2161230e..e7a1d957 100644 --- a/docs/agent_runs/20251224_051508_ticket-04_wrds-smoke-check/META.json +++ b/docs/agent_runs/20251224_051508_ticket-04_wrds-smoke-check/META.json @@ -6,7 +6,7 @@ "git_sha_before": "559a99e401d416b942b77fbf4b0bba10095cae9a", "git_sha_after": "385e9c75c371933dbc074dd5a94430f513362862", "branch_name": "codex/ticket-04-leakage-tests-unsafe-manifest", - "host_env_notes": "macOS 26.0.1 / Python 3.12.2 (system) / WRDS_DATA_ROOT=/Volumes/Storage/Data/wrds / started_at_utc and finished_at_utc approximate", + "host_env_notes": "macOS 26.0.1 / Python 3.12.2 (system) / WRDS_DATA_ROOT=/Data/wrds / started_at_utc and finished_at_utc approximate", "dataset_id": "wrds_crsp_local_exports_20251224", "config_paths": [ "configs/wfv_flagship_wrds_smoke.yaml" diff --git a/docs/agent_runs/20251226_023453_ticket-13_fix-wrds-degeneracy/COMMANDS.md b/docs/agent_runs/20251226_023453_ticket-13_fix-wrds-degeneracy/COMMANDS.md index 4851e768..fe8245da 100644 --- a/docs/agent_runs/20251226_023453_ticket-13_fix-wrds-degeneracy/COMMANDS.md +++ b/docs/agent_runs/20251226_023453_ticket-13_fix-wrds-degeneracy/COMMANDS.md @@ -118,18 +118,18 @@ 116. `git add -f docs/agent_runs/20251226_023453_ticket-13_fix-wrds-degeneracy/COMMANDS.md` 117. `git commit -m "ticket-13: log bundle rerun" -m "Tests: not run (log update)" -m "Artifacts: docs/gpt_bundles/2025-12-26T02-53-08Z_ticket-13_20251226_023453_ticket-13_fix-wrds-degeneracy.zip" -m "Docs: docs/agent_runs/20251226_023453_ticket-13_fix-wrds-degeneracy/COMMANDS.md"` 118. `BUNDLE_TIMESTAMP=2025-12-26T02-53-08Z make gpt-bundle TICKET=ticket-13 RUN_NAME=20251226_023453_ticket-13_fix-wrds-degeneracy` -119. `ls /Volumes/Storage/Data` -120. `ls /Volumes/Storage/Data/wrds` +119. `ls /Data` +120. `ls /Data/wrds` 121. `rg -n "WRDS_DATA_ROOT" -S src configs docs | head -n 50` 122. `sed -n '1,120p' docs/wrds.md` 123. `rg -n "docs/local" -S .gitignore .git/info/exclude` 124. `cat .git/info/exclude` -125. `ls /Volumes/Storage/Data/wrds/crsp | head -n 20` +125. `ls /Data/wrds/crsp | head -n 20` 126. `sed -n '1,140p' src/microalpha/wrds/__init__.py` 127. `mkdir -p docs/local` 128. `cat <<'EOF' > docs/local/WRDS_DATA_ROOT.md` (set WRDS_DATA_ROOT) 129. `cat docs/local/WRDS_DATA_ROOT.md` -130. `WRDS_DATA_ROOT=/Volumes/Storage/Data/wrds microalpha wfv --config configs/wfv_flagship_wrds_smoke.yaml` (failed: non-degenerate rejected all candidates) +130. `WRDS_DATA_ROOT=/Data/wrds microalpha wfv --config configs/wfv_flagship_wrds_smoke.yaml` (failed: non-degenerate rejected all candidates) 131. `ls -td artifacts/wrds_flagship_smoke/* 2>/dev/null | head -1` 132. `sed -n '1,120p' configs/wfv_flagship_wrds_smoke.yaml` 133. `python3 - <<'PY'` (inspect smoke metrics) @@ -137,13 +137,13 @@ 135. `python3 - <<'PY'` (inspect smoke manifest) 136. `python3 - <<'PY'` (inspect smoke manifest keys) 137. `python3 - <<'PY'` (inspect smoke config_summary) -138. `WRDS_DATA_ROOT=/Volumes/Storage/Data/wrds microalpha wfv --config configs/wfv_flagship_wrds.yaml` (failed: non-degenerate rejected all candidates) +138. `WRDS_DATA_ROOT=/Data/wrds microalpha wfv --config configs/wfv_flagship_wrds.yaml` (failed: non-degenerate rejected all candidates) 139. `ls -td artifacts/wrds_flagship/* 2>/dev/null | head -1` 140. `python3 - <<'PY'` (inspect flagship metrics) 141. `python3 - <<'PY'` (inspect flagship manifest) -142. `ls /Volumes/Storage/Data/wrds/manifests | head -n 20` -143. `ls -td /Volumes/Storage/Data/wrds/manifests/* | head -n 1` -144. `ls /Volumes/Storage/Data/wrds/manifests/20251221_001618 | head -n 20` +142. `ls /Data/wrds/manifests | head -n 20` +143. `ls -td /Data/wrds/manifests/* | head -n 1` +144. `ls /Data/wrds/manifests/20251221_001618 | head -n 20` 145. `python3 - <<'PY'` (inspect WRDS manifest keys) 146. `python3 - <<'PY'` (inspect WRDS manifest timestamps) 147. `date -u +"%Y-%m-%dT%H:%M:%SZ"` diff --git a/docs/agent_runs/20251226_023453_ticket-13_fix-wrds-degeneracy/PROMPT.md b/docs/agent_runs/20251226_023453_ticket-13_fix-wrds-degeneracy/PROMPT.md index dbe2e252..f8755871 100644 --- a/docs/agent_runs/20251226_023453_ticket-13_fix-wrds-degeneracy/PROMPT.md +++ b/docs/agent_runs/20251226_023453_ticket-13_fix-wrds-degeneracy/PROMPT.md @@ -1,4 +1,4 @@ -# AGENTS.md instructions for /Users/mateobodon/Documents/Programming/Projects/microalpha +# AGENTS.md instructions for /Documents/Programming/Projects/microalpha # AGENTS.md — microalpha @@ -89,8 +89,8 @@ Living docs: ## Skills These skills are discovered at startup from multiple local sources. Each entry includes a name, description, and file path so you can open the source for full instructions. -- skill-creator: Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Codex's capabilities with specialized knowledge, workflows, or tool integrations. (file: /Users/mateobodon/.codex/skills/.system/skill-creator/SKILL.md) -- skill-installer: Install Codex skills into $CODEX_HOME/skills from a curated list or a GitHub repo path. Use when a user asks to list installable skills, install a curated skill, or install a skill from another repo (including private repos). (file: /Users/mateobodon/.codex/skills/.system/skill-installer/SKILL.md) +- skill-creator: Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Codex's capabilities with specialized knowledge, workflows, or tool integrations. (file: /.codex/skills/.system/skill-creator/SKILL.md) +- skill-installer: Install Codex skills into $CODEX_HOME/skills from a curated list or a GitHub repo path. Use when a user asks to list installable skills, install a curated skill, or install a skill from another repo (including private repos). (file: /.codex/skills/.system/skill-installer/SKILL.md) - Discovery: Available skills are listed in project docs and may also appear in a runtime "## Skills" section (name + description + file path). These are the sources of truth; skill bodies live on disk at the listed paths. - Trigger rules: If the user names a skill (with `$SkillName` or plain text) OR the task clearly matches a skill's description, you must use that skill for that turn. Multiple mentions mean use them all. Do not carry skills across turns unless re-mentioned. - Missing/blocked: If a named skill isn't in the list or the path can't be read, say so briefly and continue with the best fallback. @@ -111,7 +111,7 @@ These skills are discovered at startup from multiple local sources. Each entry i - /Users/mateobodon/Documents/Programming/Projects/microalpha + /Documents/Programming/Projects/microalpha never danger-full-access enabled diff --git a/docs/agent_runs/20251226_091335_ticket-14_order-flow-trace/COMMANDS.md b/docs/agent_runs/20251226_091335_ticket-14_order-flow-trace/COMMANDS.md index 4d1882f1..12ed7ba8 100644 --- a/docs/agent_runs/20251226_091335_ticket-14_order-flow-trace/COMMANDS.md +++ b/docs/agent_runs/20251226_091335_ticket-14_order-flow-trace/COMMANDS.md @@ -64,7 +64,7 @@ 62. `cp docs/prompts/20251226_091335_ticket-14_order-flow-trace.md docs/agent_runs/20251226_091335_ticket-14_order-flow-trace/PROMPT.md` 63. `cp docs/agent_runs/20251226_065226_ticket-13_fix-wrds-degeneracy/wfv_flagship_wrds_single_fold.yaml docs/agent_runs/20251226_091335_ticket-14_order-flow-trace/wfv_flagship_wrds_single_fold.yaml` 64. `apply_patch` (enable `order_flow_diagnostics` in debug config) -65. `WRDS_DATA_ROOT=/Volumes/Storage/Data/wrds microalpha wfv --config docs/agent_runs/20251226_091335_ticket-14_order-flow-trace/wfv_flagship_wrds_single_fold.yaml` (failed: non-degenerate rejected all candidates) +65. `WRDS_DATA_ROOT=/Data/wrds microalpha wfv --config docs/agent_runs/20251226_091335_ticket-14_order-flow-trace/wfv_flagship_wrds_single_fold.yaml` (failed: non-degenerate rejected all candidates) 66. `ls -td artifacts/wrds_flagship_debug/* 2>/dev/null | head -1` 67. `python3 - <<'PY'` (summarize order-flow diagnostics + rejection reasons) 68. `python3 - <<'PY'` (inspect one rebalance entry with weights) @@ -78,7 +78,7 @@ 76. `apply_patch` (add `clipped_by_caps` to order-flow summary) 77. `make test-fast` 78. `pytest -q tests/test_order_flow_diagnostics.py` -79. `WRDS_DATA_ROOT=/Volumes/Storage/Data/wrds microalpha wfv --config docs/agent_runs/20251226_091335_ticket-14_order-flow-trace/wfv_flagship_wrds_single_fold.yaml` +79. `WRDS_DATA_ROOT=/Data/wrds microalpha wfv --config docs/agent_runs/20251226_091335_ticket-14_order-flow-trace/wfv_flagship_wrds_single_fold.yaml` 80. `python3 - <<'PY'` (inspect order-flow clip/drop summary) 81. `cat project_state/CURRENT_RESULTS.md` 82. `cat project_state/KNOWN_ISSUES.md` diff --git a/docs/agent_runs/20251226_122058_ticket-14_wrds-full-run/COMMANDS.md b/docs/agent_runs/20251226_122058_ticket-14_wrds-full-run/COMMANDS.md index 557d9e06..15412e0d 100644 --- a/docs/agent_runs/20251226_122058_ticket-14_wrds-full-run/COMMANDS.md +++ b/docs/agent_runs/20251226_122058_ticket-14_wrds-full-run/COMMANDS.md @@ -19,7 +19,7 @@ go ahead and do this, 1. Run a full WRDS WFV (configs/wfv_flagship_wrds.yaml) EOF ``` 8. `cat docs/DOCS_AND_LOGGING_SYSTEM.md` -9. `WRDS_DATA_ROOT=/Volumes/Storage/Data/wrds make wrds-flagship` +9. `WRDS_DATA_ROOT=/Data/wrds make wrds-flagship` 10. `ls -td artifacts/wrds_flagship/*/metrics.json | head -1` 11. `sed -n '1,200p' project_state/CURRENT_RESULTS.md` 12. `sed -n '1,200p' docs/results_wrds.md` diff --git a/docs/agent_runs/20260124_235038_ticket-19_finish-agentic-scaffold-cleanup/COMMANDS.md b/docs/agent_runs/20260124_235038_ticket-19_finish-agentic-scaffold-cleanup/COMMANDS.md index 545459eb..7519c9b3 100644 --- a/docs/agent_runs/20260124_235038_ticket-19_finish-agentic-scaffold-cleanup/COMMANDS.md +++ b/docs/agent_runs/20260124_235038_ticket-19_finish-agentic-scaffold-cleanup/COMMANDS.md @@ -18,7 +18,7 @@ ls docs/agent_runs | head ls docs/agent_runs/20260110_110259_ticket-18_agentic-scaffold-bootstrap cat docs/agent_runs/20260110_110259_ticket-18_agentic-scaffold-bootstrap/META.json cat docs/agent_runs/20260110_110259_ticket-18_agentic-scaffold-bootstrap/COMMANDS.md -cat /Users/mateobodon/.codex/skills/gpt-bundle/SKILL.md +cat /.codex/skills/gpt-bundle/SKILL.md sed -n '1,200p' tools/agentic/gpt_bundle.py sed -n '1,200p' tools/agentic/project_state_refresh.py sed -n '1,120p' CHANGELOG.md diff --git a/docs/agent_runs/20260124_235038_ticket-19_finish-agentic-scaffold-cleanup/PROMPT.md b/docs/agent_runs/20260124_235038_ticket-19_finish-agentic-scaffold-cleanup/PROMPT.md index 8b60586a..7d45115e 100644 --- a/docs/agent_runs/20260124_235038_ticket-19_finish-agentic-scaffold-cleanup/PROMPT.md +++ b/docs/agent_runs/20260124_235038_ticket-19_finish-agentic-scaffold-cleanup/PROMPT.md @@ -48,7 +48,7 @@ Inputs: gpt-bundle -/Users/mateobodon/.codex/skills/gpt-bundle/SKILL.md +/.codex/skills/gpt-bundle/SKILL.md --- name: gpt-bundle description: Create a gpt_bundle.zip for GPT review (status + diffs + key docs). diff --git a/docs/api.md b/docs/api.md index d10366af..c8bcde91 100644 --- a/docs/api.md +++ b/docs/api.md @@ -2,6 +2,47 @@ This page summarises the primary extension points for building strategies and tooling on top of Microalpha. +## Audit Lab (`microalpha.audit_lab`) + +```python +from microalpha.audit_lab import run_audit_lab + +result = run_audit_lab("evidence", seed=20260715) +``` + +`run_audit_lab` returns the output directory, receipt SHA-256, and exact result +payload. Canonical files exclude clocks, hosts, and absolute paths. + +## Multiple-testing control (`microalpha.multiple_testing`) + +```python +from microalpha.multiple_testing import centered_max_statistic_test + +result = centered_max_statistic_test( + candidate_returns, + benchmark_returns=benchmark_returns, + candidate_names=["model_a", "model_b"], + seed=7, + num_bootstrap=2000, +) +``` + +The function tests whether the best candidate has a positive expected return +differential to an explicit benchmark. It null-centers the differential matrix +and synchronously resamples timestamps across all candidates. + +## Point-in-time gate (`microalpha.point_in_time`) + +```python +from microalpha.point_in_time import require_point_in_time + +require_point_in_time(decision_at, available_at, row_ids=observation_ids) +``` + +The gate fails closed when any feature row becomes available after its decision +timestamp. `PointInTimeViolation` exposes exact `count` and `row_ids` fields for +auditable failure reports. + ## Runner (`microalpha.runner`) - `run_from_config(path: str, override_artifacts_dir: str | None = None) -> dict` @@ -49,10 +90,14 @@ Portfolio(data_handler, initial_cash, *, max_exposure=None, max_drawdown_stop=No ## Broker & Execution (`microalpha.broker`, `microalpha.execution`) -- `SimulatedBroker(executor)` – wraps an `Executor` and enforces t+1 semantics before returning fills. +- `SimulatedBroker(executor)` – plans execution slices and materializes each fill only when the engine reaches its scheduled market event. +- `ExecutionPlan(timestamp, order, qty)` – immutable pending slice. Market-data executors do not read a future price when creating it. - `Executor` – base class implementing simple price-impact + commission fills against the `DataHandler`. - `TWAP` – splits orders evenly across future timestamps supplied by the data handler. -- `VWAP` – splits by future-tick volumes; uses `DataHandler.get_volume_at` if available (requires `volume` column). +- `VWAP` – the safe Engine planning path uses equal ex-ante slices because + realized future volume is not available at order time. Its direct `execute()` + compatibility method can reproduce an offline realized-volume benchmark, but + must not be used as a chronology-safe simulation path. - `ImplementationShortfall` – front-loaded geometric schedule controlled by `urgency`. - `SquareRootImpact` / `KyleLambda` – stylised impact models for execution cost studies. - `LOBExecution` – routes orders to the in-memory level-2 book (`microalpha.lob.LimitOrderBook`) with latency simulation. @@ -75,6 +120,7 @@ Refer to the module docstrings and tests for deeper examples of composing these ## CLI (`microalpha.cli`) +- `microalpha audit-demo [--out DIR] [--seed INT]` - `microalpha run -c [--out DIR] [--profile]` - `microalpha wfv -c [--out DIR] [--profile]` diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 00000000..8566536f --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,58 @@ +# Architecture + +Microalpha is event-driven so timing assumptions remain explicit and testable. + +![Audit lineage](assets/audit_lab/data_lineage.svg) + +## Event lifecycle + +```mermaid +sequenceDiagram + participant D as Data handler + participant E as Engine clock + participant S as Strategy + participant P as Portfolio + participant X as Execution planner + participant B as Broker/materializer + + D->>E: MarketEvent(t) + E->>P: mark current state at t + E->>B: materialize plans due at t + B->>P: FillEvent(t) + E->>S: observe state and market at t + S->>P: SignalEvent(t) + P->>X: OrderEvent(t) + X-->>E: ExecutionPlan(t+1, qty) + Note over E,P: No future price or state mutation occurs at t +``` + +The built-in market-data executors plan timestamps and quantities without +reading future prices or volumes. When the matching symbol and timestamp arrive, +the broker materializes the slice using information available at that event. +TWAP and implementation-shortfall schedules are fixed ex ante. The safe VWAP +path uses equal ex-ante slices until an explicit historical volume profile is +provided; it never sizes from realized future volume. + +## Component boundaries + +| Component | Owns | Must not own | +| --- | --- | --- | +| Data handler | ordered observations and availability metadata | strategy selection | +| Strategy | signals at the current event | fills or future prices | +| Portfolio | sizing, cash, positions, exposure, turnover, risk | market-data revision logic | +| Execution planner | timestamps and slice quantities | future observed price/volume | +| Broker/materializer | fill at a due event | model selection | +| Walk-forward evaluator | train/test/holdout isolation | post-hoc strategy mutation | +| Evidence layer | schemas, hashes, reports, claim gate | reconstructing missing facts from prose | + +## Statistical control + +`centered_max_statistic_test` accepts an aligned candidate-return matrix and an +explicit benchmark series. It computes candidate-minus-benchmark statistics, +recenters all differentials under the null, and synchronously resamples rows. +This is the selection correction used by Audit Lab and walk-forward grid +evaluation. + +The older relative "best versus other candidates" SPA interpretation is not a +claim that any model beats a benchmark. Public claims use the explicit +benchmark-differential test. diff --git a/docs/assets/audit_lab/audit_lab.svg b/docs/assets/audit_lab/audit_lab.svg new file mode 100644 index 00000000..6c1020ac --- /dev/null +++ b/docs/assets/audit_lab/audit_lab.svg @@ -0,0 +1,44 @@ + +Microalpha Audit Lab correctness results +Four comparisons show inflation from data leakage, same-tick execution, omitted costs, and naive model selection. + +Four ways a backtest lies +Known-ground-truth synthetic fixture · correctness test, not alpha + +1 POINT-IN-TIME DATA +Leaky revised value ++20.53 + +PIT-safe value +-0.17 + +756 unavailable rows blocked + +2 EVENT-TIME EXECUTION +Same-tick oracle ++21.89 + +Queued t+1 ++0.17 + +State changes only when the fill event arrives + +3 COST RECONCILIATION +Gross planted control ++0.57 + +Commission + spread + impact + borrow +-0.68 + +Exact P&L residual 0.0e+00 + +4 SELECTION CONTROL +Best of 128 noise models ++1.38 + +Walk-forward OOS +-1.28 + +Max-stat p=0.601; planted control p=0.001 +Generated by microalpha audit-demo · all values are receipt-bound + diff --git a/docs/assets/audit_lab/audit_results.json b/docs/assets/audit_lab/audit_results.json new file mode 100644 index 00000000..28cd5d15 --- /dev/null +++ b/docs/assets/audit_lab/audit_results.json @@ -0,0 +1,39 @@ +{ + "claim_boundary": "software correctness demonstration; not alpha or market evidence", + "costs": { + "borrow_total": 0.0069, + "commission_total": 0.0781, + "gross_sharpe": 0.5668, + "half_spread_total": 0.1562, + "impact_total": 0.1171, + "net_sharpe": -0.6753, + "reconciliation_error": 0.0 + }, + "execution": { + "inflation_removed": 21.7121, + "same_tick_sharpe": 21.886, + "state_rule": "no fill mutation before scheduled market event", + "t_plus_1_sharpe": 0.1739 + }, + "fixture": "deterministic_synthetic_known_ground_truth", + "leakage": { + "availability_rule": "available_at <= decision_at", + "inflation_removed": 20.7004, + "leaky_sharpe": 20.5276, + "pit_safe_sharpe": -0.1728, + "unavailable_rows_blocked": 756 + }, + "observations": 756, + "schema_version": "microalpha.audit-lab.v1", + "seed": 20260715, + "selection": { + "candidate_count": 128, + "naive_full_sample_winner": "noise_102", + "naive_in_sample_sharpe": 1.3802, + "noise_family_p_value": 0.601, + "planted_control_p_value": 0.001, + "test": "centered synchronous max statistic vs zero-return benchmark", + "walk_forward_oos_sharpe": -1.2781, + "walk_forward_selected": "noise_089" + } +} diff --git a/docs/assets/audit_lab/benchmark.json b/docs/assets/audit_lab/benchmark.json new file mode 100644 index 00000000..ddc0e1bd --- /dev/null +++ b/docs/assets/audit_lab/benchmark.json @@ -0,0 +1,26 @@ +{ + "audit_demo": { + "median_seconds": 1.3745, + "minimum_seconds": 1.3363, + "runs_seconds": [1.434, 1.4238, 1.3363, 1.3554, 1.3745] + }, + "engine": { + "events": 1000000, + "events_per_second": 1464231, + "seconds": 0.683 + }, + "environment": { + "platform": "macOS-26.5.1-arm64-arm-64bit", + "python": "3.12.2" + }, + "measured_on": "2026-07-15", + "scope": "Host-dependent benchmark receipt; values are not deterministic correctness claims.", + "source_sha256": { + "benchmarks/bench_engine.py": "5e65cabeeda7b26e8e8e84573128d88c778dd682ba3f7a6234cd1ef88df6c13a", + "src/microalpha/audit_lab.py": "4be05bf2de2926cd88a0ffbe8a65b6ebc134492c658272684fca98bd98a5bb18", + "src/microalpha/engine.py": "7d10f35f55bb1ec1e72fd53202381049798d43db31e5dadfe8d3bfdc618fc816", + "src/microalpha/execution.py": "8e65b1cb1019dd9ebd7cd49cbc81493c11ab52cbe207450d2ba42d1519f11474", + "src/microalpha/multiple_testing.py": "71ea17a709cffd3edc9bf01634b60ee8955efba3e939b1d64c8f88f2947faf89", + "src/microalpha/point_in_time.py": "38ec0e23233e05373554b867a727fbe84e880d951c5510b0928de9a6df78870d" + } +} diff --git a/docs/assets/audit_lab/comparison.csv b/docs/assets/audit_lab/comparison.csv new file mode 100644 index 00000000..49487493 --- /dev/null +++ b/docs/assets/audit_lab/comparison.csv @@ -0,0 +1,5 @@ +audit,unsafe_or_naive,safe_or_corrected,unit,verdict +point_in_time_data,20.5276,-0.1728,annualized_sharpe,unavailable_rows_blocked +event_time_execution,21.886,0.1739,annualized_sharpe,future_fill_queued +cost_reconciliation,0.5668,-0.6753,annualized_sharpe,all_cost_components_reconciled +selection_control,1.3802,-1.2781,annualized_sharpe,noise_family_not_promoted diff --git a/docs/assets/audit_lab/data_lineage.svg b/docs/assets/audit_lab/data_lineage.svg new file mode 100644 index 00000000..3bbabbf3 --- /dev/null +++ b/docs/assets/audit_lab/data_lineage.svg @@ -0,0 +1,29 @@ + +Microalpha audit data lineage +A safe pipeline moves a synthetic oracle through availability, event scheduling, cost reconciliation, and an artifact receipt. + +Audit lineage: every claim has a clock and a hash + +Synthetic oracle +seed + schema + + +Availability gate +available_at ≤ decision_at + + +Event queue +fill only at market time + + +Cost ledger +gross − 4 components + + +Receipt +SHA-256 + + +Fail closed: unavailable rows or early fills never enter state +Synthetic fixture only · no provider, licensed dataset, holdout, or network access + diff --git a/docs/assets/audit_lab/receipt.json b/docs/assets/audit_lab/receipt.json new file mode 100644 index 00000000..a40e368a --- /dev/null +++ b/docs/assets/audit_lab/receipt.json @@ -0,0 +1,21 @@ +{ + "artifacts": { + "audit_lab.svg": "a06f2e66d453dce90cdaf2569fdfd5e261cd6ef945f0a74c728f99f60ca34c47", + "audit_results.json": "eeeec15f5efe6329019cdbd1831d84e765591bcdc3c2a3309afdd8653c34de44", + "comparison.csv": "c735b39518860cb1d75f8057742b44426c97ae54791ea4df30df1799f7c304f9", + "data_lineage.svg": "c2a272feb1f2300043fc33f7887cdf0bbe095283bf841003f9a1a9ddc96af240" + }, + "claim_boundary": "software correctness demonstration; not alpha or market evidence", + "fixture": "deterministic_synthetic_known_ground_truth", + "generator": { + "source_sha256": { + "audit_lab.py": "4be05bf2de2926cd88a0ffbe8a65b6ebc134492c658272684fca98bd98a5bb18", + "multiple_testing.py": "71ea17a709cffd3edc9bf01634b60ee8955efba3e939b1d64c8f88f2947faf89", + "point_in_time.py": "38ec0e23233e05373554b867a727fbe84e880d951c5510b0928de9a6df78870d" + }, + "version": "0.2.0" + }, + "input_sha256": "2b12a408b131bcb30bdf0169eed34e0ab050d0872ccf8b6c0229c7bf03924882", + "schema_version": "microalpha.audit-lab.v1", + "seed": 20260715 +} diff --git a/docs/audit-lab.md b/docs/audit-lab.md new file mode 100644 index 00000000..f8f2fad2 --- /dev/null +++ b/docs/audit-lab.md @@ -0,0 +1,96 @@ +# Audit Lab + +Audit Lab is a deterministic, known-ground-truth correctness fixture. It creates +invalid positive results on purpose, runs the corresponding safe controls, and +writes a byte-stable evidence set. + +```bash +microalpha audit-demo +``` + +No network, provider, credential, licensed row, or hidden holdout is accessed. +The only inputs are schema `microalpha.audit-lab.v1`, seed `20260715`, and +NumPy-generated arrays bound by `input_sha256`. + +The fixture uses transparent NumPy oracle constructions so every injected +failure has known ground truth. The production `Engine` and `ExecutionPlan` +path is guarded by +[`test_tplus1_execution.py`](https://github.com/MateoBodon/microalpha/blob/main/tests/test_tplus1_execution.py), +while the shared max-statistic implementation is guarded by +[`test_multiple_testing.py`](https://github.com/MateoBodon/microalpha/blob/main/tests/test_multiple_testing.py). + +## Four paired audits + +### 1. Point-in-time availability + +The unsafe feature is a revised value that contains the contemporaneous return +but is not available until two sessions later. Using it at the decision time +creates Sharpe `+20.5276`. The safe path enforces +`available_at <= decision_at`, blocks all 756 unavailable rows, and produces +Sharpe `−0.1728` from the information actually available. + +The same public `require_point_in_time` gate raises `PointInTimeViolation` with +the exact violating row IDs and count instead of silently dropping late data. + +### 2. Event-time execution + +The unsafe oracle observes a return and fills on the same tick, producing Sharpe +`+21.8860`. The safe vector fixture shifts the signal to the next observation; +its Sharpe is `+0.1739`. + +The separate production regression test uses a clock-guarded data handler: any attempt to read a +future price before its event raises immediately. It also records cash and +positions at each strategy callback, proving the future fill cannot mutate +state early. + +### 3. Cost reconciliation + +A labeled planted control has gross Sharpe `+0.5668`. Commission, half-spread, +impact, and borrow costs reduce it to `−0.6753`. Net returns reconcile exactly: + +```text +net = gross - commission - half_spread - impact - borrow +max absolute residual = 0.0 +``` + +These costs are fixture parameters, not empirical venue calibration. + +### 4. Model-selection control + +Searching 128 noise strategies over the full sample finds an apparent winner at +Sharpe `+1.3802`. A model selected only on the first 504 observations scores +`−1.2781` on the untouched final 252 observations. + +The max-statistic test evaluates every candidate as a return differential to an +explicit zero-return benchmark. It recenters differentials under the null and +uses the same stationary-bootstrap timestamps for every candidate, preserving +cross-model dependence. The noise family is not promoted (`p=0.601`); a labeled +planted positive control is detected (`p=0.001`). + +## Canonical artifacts + +| File | Purpose | +| --- | --- | +| [`audit_results.json`](assets/audit_lab/audit_results.json) | Exact metrics and claim boundary | +| [`comparison.csv`](assets/audit_lab/comparison.csv) | Four-row machine-readable comparison | +| [`audit_lab.svg`](assets/audit_lab/audit_lab.svg) | Reviewer-facing result graphic | +| [`data_lineage.svg`](assets/audit_lab/data_lineage.svg) | Architecture and data-lineage graphic | +| [`receipt.json`](assets/audit_lab/receipt.json) | Input and artifact SHA-256 hashes | + +The receipt itself has SHA-256 +`6e36c2397696d7e9eecbd058cbfc1ba522c8ffba7e5798224de86b20457b6575`. +Paths, clocks, hostnames, and Git metadata are excluded from canonical bytes. + +## Python API + +```python +from microalpha.audit_lab import run_audit_lab + +result = run_audit_lab("evidence") +assert result["receipt_sha256"] == ( + "6e36c2397696d7e9eecbd058cbfc1ba522c8ffba7e5798224de86b20457b6575" +) +``` + +This equality verifies the fixture and generator version used by this release; +it is not a universal hash across future schema versions. diff --git a/docs/benchmarks.md b/docs/benchmarks.md index 300dabbe..fd697f41 100644 --- a/docs/benchmarks.md +++ b/docs/benchmarks.md @@ -2,6 +2,20 @@ This page documents how to run the bundled micro-benchmark and interpret the results. +## Current receipt + +The 2026-07-15 host-dependent receipt is tracked as +[`benchmark.json`](assets/audit_lab/benchmark.json): + +| Benchmark | Result | Environment | +| --- | ---: | --- | +| Audit Lab, median of 5 clean output directories | `1.3745 s` | Python 3.12.2, Apple arm64 | +| Event loop, 1,000,000 no-op events | `1,464,231 events/s` | Python 3.12.2, Apple arm64 | + +The receipt hashes every benchmark source file. Runtime varies by hardware, +Python, power state, and background load; it is an engineering baseline, not a +deterministic correctness artifact. + - Script: `benchmarks/bench_engine.py` - Purpose: Measures raw event throughput of the engine and Portfolio wiring under a no-op strategy and zero-cost execution model. @@ -13,7 +27,7 @@ python benchmarks/bench_engine.py The harness prints a small JSON with the number of processed events, wall-clock seconds, and events/sec. -Example on Apple M2 Pro (32GB, macOS 14.6.1): +Historical example on Apple M2 Pro (32GB, macOS 14.6.1): ``` {"events": 1000000, "sec": 0.773, "evps": 1294141} diff --git a/docs/examples.md b/docs/examples.md index 0b36b08f..73b8354d 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -5,8 +5,8 @@ Kick off experiments quickly using the bundled configuration files and the repro ## 1. Flagship momentum quickstart ```bash -microalpha run --config configs/flagship_sample.yaml --out artifacts/sample_flagship -microalpha report --artifact-dir artifacts/sample_flagship --summary-out reports/summaries/flagship_mom.md +make sample +make report ``` The run command generates deterministic artifacts (metrics, bootstrap distribution, exposures, trades) using the new linear+sqrt slippage model, IOC queueing, and covariance-aware allocator. The report command renders a PNG tear sheet plus a Markdown case study. @@ -14,10 +14,13 @@ The run command generates deterministic artifacts (metrics, bootstrap distributi ## 2. Walk-forward reality check on the sample universe ```bash -microalpha wfv --config configs/wfv_flagship_sample.yaml --out artifacts/sample_wfv -microalpha report --artifact-dir artifacts/sample_wfv --summary-out reports/summaries/flagship_mom_wfv.md --title "Flagship Walk-Forward" +make wfv +make report-wfv ``` +The run commands create a run-ID child directory. If you call the CLI directly, +pass the `artifact_dir` printed by `run` or `wfv` to `microalpha report`. + This executes a rolling walk-forward with Politis–White bootstrap and writes `folds.json`, `bootstrap.json`, `exposures.csv`, and aggregated metrics for the flagship strategy. ## 3. Classic single-asset examples diff --git a/docs/index.md b/docs/index.md index 3332f875..2ba7041e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,48 +1,60 @@ # Microalpha -Microalpha is an event-driven research platform for reproducible quantitative strategy development. The engine prioritises leakage-safety, deterministic execution, and rich analytics so researchers can iterate quickly without sacrificing rigor. +**A quantitative research audit lab that makes invalid backtests visibly fail.** -## Why Microalpha? +Microalpha separates market-data availability, signal time, portfolio state, +execution events, cost accounting, model selection, and artifact provenance. +The flagship Audit Lab injects four common research failures into deterministic +synthetic data and proves the safe path removes them. -- **Leakage-safe core**: strict timestamp ordering, FIFO broker interactions, and lookahead guards. -- **Execution realism**: TWAP/impact models and a configurable level-2 limit order book. -- **Reproducible pipelines**: manifest metadata, trade logs, and automation-ready CLI. -- **Extensible design**: users can plug in new strategies, data handlers, and execution layers. -- **Documentation-first**: MkDocs site with invariants, manifests, API references, and runnable demos. +![Audit Lab paired results](assets/audit_lab/audit_lab.svg) -## Quickstart +## Run the proof -1. **Install this repository from source** +```bash +git clone https://github.com/MateoBodon/microalpha.git +cd microalpha +python -m venv .venv +source .venv/bin/activate +python -m pip install . +microalpha audit-demo +``` - ```bash - git clone https://github.com/MateoBodon/microalpha.git - cd microalpha - python -m venv .venv - source .venv/bin/activate - pip install -e ".[dev]" - ``` +The output is tracked under `docs/assets/audit_lab/`. A correct clean run leaves +that directory unchanged: - > The namesake package on PyPI is an unrelated third-party project. This - > repository has no public package release; use the source checkout above. +```bash +git diff --exit-code -- docs/assets/audit_lab +``` -2. **Run the bundled mean-reversion backtest** +Receipt SHA-256: +`6e36c2397696d7e9eecbd058cbfc1ba522c8ffba7e5798224de86b20457b6575`. - ```bash - microalpha run -c configs/meanrev.yaml - ``` +Install from this repository: the namesake package on PyPI is an unrelated third-party project. - The CLI writes manifests, metrics, equity curves, and trade logs under `artifacts//`. +## What the fixture proves -3. **Explore further** +| Audit | Naive | Safe | Enforcement | +| --- | ---: | ---: | --- | +| Revised value before availability | Sharpe `+20.53` | `−0.17` | 756 rows blocked | +| Same-tick signal and fill | Sharpe `+21.89` | `+0.17` | queued t+1 fill | +| Omitted costs | Sharpe `+0.57` | `−0.68` | four-part cost ledger | +| Best of 128 noise models | Sharpe `+1.38` | OOS `−1.28` | corrected `p=0.601` | - - Inspect leakage invariants in [Leakage Safety](leakage-safety.md). - - Review reproducibility workflows in [Reproducibility](reproducibility.md). - - Extend components using the [API Reference](api.md). - - Try the scenarios in [Examples](examples.md). +The planted positive control is detected at `p=0.001`. All results are synthetic +software fixtures, never market or alpha claims. -Use the navigation to dive into leakage guarantees, reproducibility tooling, API surfaces, and runnable examples. +![Clock and hash lineage](assets/audit_lab/data_lineage.svg) ---- +## Product path -These docs are deployed from the public `main` branch. The deployment commit is -recorded in the repository's [Docs workflow](https://github.com/MateoBodon/microalpha/actions/workflows/docs.yml). +1. [Audit Lab](audit-lab.md) — exact fixture, method, schemas, and hashes. +2. [Architecture](architecture.md) — event scheduling and component boundaries. +3. [API](api.md) — CLI and Python extension points. +4. [Reproducibility](reproducibility.md) — manifests, deterministic evidence, + and clean-run checks. +5. [Limitations](limitations.md) — what the system does not prove. + +The [research case study](portfolio_evidence_2026-07-11.md) shows the other side +of the same discipline: six licensed-data mechanisms failed frozen promotion +gates while the 2023–2025 confirmation set remained sealed. diff --git a/docs/leakage-safety.md b/docs/leakage-safety.md index 44eeb488..72bdc6c8 100644 --- a/docs/leakage-safety.md +++ b/docs/leakage-safety.md @@ -5,7 +5,12 @@ Microalpha enforces a strict "no-peek" discipline at every layer of the simulati ## Engine invariants - **Monotonic clocks** – the `Engine` raises `LookaheadError` if market events arrive out of order. See [`tests/test_time_ordering.py`](https://github.com/MateoBodon/microalpha/blob/main/tests/test_time_ordering.py). -- **t+1 execution** – strategies submit intents at time *t* and executions occur no earlier than the next event. Verified in [`tests/test_tplus1_execution.py`](https://github.com/MateoBodon/microalpha/blob/main/tests/test_tplus1_execution.py). +- **t+1 execution** – strategies submit intents at time *t*. Built-in executors + plan timestamps and quantities without reading future prices; the fill is + materialized only when the matching market event arrives. A clock-guarded + regression test fails on any early future-price read and proves cash and + positions remain unchanged before t+1. See + [`tests/test_tplus1_execution.py`](https://github.com/MateoBodon/microalpha/blob/main/tests/test_tplus1_execution.py). - **Fill ordering** – brokers acknowledge fills only after the active market event has been processed. ## Portfolio guards @@ -19,7 +24,10 @@ The `LimitOrderBook` keeps per-level FIFO queues to ensure first-in-first-out fi ### LOB t+1 semantics -By default, LOB execution enforces t+1 semantics by shifting the reported `FillEvent.timestamp` to the next available market timestamp while retaining measured latency fields. This preserves the global no-peek invariant. You can disable this behavior per config with: +By default, LOB execution schedules its prepared fill for the next market +timestamp while retaining measured latency fields. The engine holds that fill +outside portfolio state until the event arrives. You can disable this behavior +only through the explicitly unsafe same-bar configuration: ```yaml exec: @@ -34,6 +42,9 @@ During walk-forward validation, the optimizer only uses in-sample data to select ## Statistical inference invariants - **Sharpe statistics** use the same deterministic return stream as performance metrics, with optional HAC adjustments (`METRICS_HAC_LAGS`) that never peek beyond the evaluated window. [`tests/test_risk_stats.py`](https://github.com/MateoBodon/microalpha/blob/main/tests/test_risk_stats.py) asserts IID vs HAC behaviour on synthetic AR(1) data and validates block bootstrap coverage. -- **Reality check bootstraps** in walk-forward mode rely on stationary/circular block resampling, seeded from the configuration manifest so repeated runs reproduce identical `reality_check_pvalue` results. +- **Selection-corrected max statistics** compare every candidate with an + explicit benchmark, recenter candidate differentials under the null, and use + the same stationary/circular bootstrap indices for every model to preserve + cross-model dependence. The seed and block length are persisted. Together, these invariants provide strong protection against accidentally leaking future information into historical tests. diff --git a/docs/limitations.md b/docs/limitations.md new file mode 100644 index 00000000..2c7ccfe3 --- /dev/null +++ b/docs/limitations.md @@ -0,0 +1,38 @@ +# Limitations and claim boundary + +Microalpha demonstrates research-engineering controls. It does not certify that +a strategy is profitable, data is correctly licensed, or a simulation matches a +specific live venue. + +## What Audit Lab proves + +- the shipped generator blocks a known unavailable-data fixture; +- built-in engine execution plans cannot mutate portfolio state before the due + market event; +- the fixture's net returns reconcile to four explicit cost components; +- the shipped benchmark-differential max-statistic test rejects a frozen noise + family and detects a frozen planted control; +- canonical artifacts reproduce byte-for-byte for the same schema and seed. + +## What it does not prove + +- market predictability or alpha; +- calibration of commission, spread, impact, borrow, or capacity for a real + asset and venue; +- correctness of provider availability metadata supplied by a user; +- survivorship-free coverage for arbitrary external universes; +- statistical power for every dependence structure or sample size; +- live trading, broker connectivity, or operational risk management. + +## Data boundaries + +Synthetic fixtures are public and generated locally. Small public examples are +wiring demonstrations. Licensed WRDS/CRSP/OptionMetrics rows stay outside the +repository; only reviewed aggregates may be published. The 2023–2025 research +confirmation set remains sealed and is not used by Audit Lab. + +## Packaging boundary + +The `microalpha` name on PyPI belongs to an unrelated project. This repository +does not publish there. Install from the source checkout or an attached GitHub +release wheel and verify its GitHub artifact attestation. diff --git a/docs/portfolio/SHOWCASE_HUB.md b/docs/portfolio/SHOWCASE_HUB.md new file mode 100644 index 00000000..778d2df3 --- /dev/null +++ b/docs/portfolio/SHOWCASE_HUB.md @@ -0,0 +1,106 @@ +# Microalpha showcase hub + +Release candidate: `v0.2.0` + +Reviewed product commit: `1fe57117ebcf3fcabce9048002265c914ecd28aa` + +Clean-package commit: `90323a349036c17cdffef2bb97bdc0874a8aae3b` + +Baseline public commit: `9abb834117820dc479246fc9486ae7301d309110` + +## Positioning + +Microalpha is a **quant research audit lab**: an event-driven Python system that +makes attractive but invalid backtests fail visibly. Its flagship is a +deterministic, synthetic correctness fixture—not an alpha claim. + +Public targets: + +- [GitHub repository](https://github.com/MateoBodon/microalpha) +- [Documentation](https://mateobodon.github.io/microalpha/) +- [CI](https://github.com/MateoBodon/microalpha/actions/workflows/ci.yml) +- [Release](https://github.com/MateoBodon/microalpha/releases/tag/v0.2.0) + +## The proof + +![Four paired Audit Lab results](../assets/audit_lab/audit_lab.svg) + +| Failure injected | Naive result | Audited result | Guardrail | +| --- | ---: | ---: | --- | +| Revised value used too early | Sharpe `+20.5276` | `−0.1728` | 756 rows rejected by point-in-time gate | +| Same-tick signal and fill | Sharpe `+21.8860` | `+0.1739` | production plans wait for the next symbol event | +| Costs omitted | Sharpe `+0.5668` | `−0.6753` | exact commission/spread/impact/borrow reconciliation | +| Best of 128 noise models | Sharpe `+1.3802` | OOS `−1.2781` | centered max-statistic `p=0.601` | + +The labeled planted control is detected at `p=0.001`. Receipt SHA-256: +`6e36c2397696d7e9eecbd058cbfc1ba522c8ffba7e5798224de86b20457b6575`. +The receipt binds the seed, input arrays, generator version, generator source, +and every canonical output. + +![Audit lineage](../assets/audit_lab/data_lineage.svg) + +## Before and after + +| Dimension | Previous public main | Reviewed product | +| --- | ---: | ---: | +| First-30-second clarity | `2.0 / 5` | `4.9 / 5` | +| Engineering proof | `3.0 / 5` | `4.7 / 5` | +| Scientific safeguards | `2.4 / 5` | `4.8 / 5` | +| Interview memorability | `2.0 / 5` | `4.9 / 5` | +| Claim honesty | `4.0 / 5` | `5.0 / 5` | +| Full tests | 128 public-CI tests | 146 local tests, all passing | +| Coverage | 77% | 77.73% | +| Deterministic product receipt | none | source-bound SHA-256 receipt | +| Clean install to first proof | sample/report path `27.161 s` | final clean clone: install `12.183 s`, cold demo `10.488 s`, total `22.671 s` | + +The final independent reviews found no remaining P0 or P1 product/scientific +issue. Hiring-manager scores were clarity `4.9`, depth `4.7`, memorability `4.9`, +honesty `5.0`, and resume usefulness `4.9`. + +## Verification receipt + +- 146 tests pass with 77.73% line coverage. +- Ruff, Black, isort, focused Mypy, strict MkDocs, and link tests pass. +- Secret scanning gates the product surface; the licensed-data/private-path + policy scans all 1,696 tracked files and 1,073 data files. +- The Audit Lab regenerates byte-for-byte from clean directories; CI repeats it + on Python 3.10, 3.11, and 3.12. +- A clean-built `0.2.0` wheel installs, reports its version, and reproduces the + canonical receipt without licensed data. +- A fresh clone stays clean after source installation and Audit Lab regeneration; + generated package metadata is ignored rather than versioned. +- Terminal-tick orders fail closed, asynchronous multi-asset plans wait for the + ordered symbol, and deferred fills retain originating-rebalance diagnostics. + +## Honest boundaries + +- Audit Lab is a synthetic oracle fixture. Its positive controls are software + tests, not evidence of market predictability. +- Cost models are configurable simulations, not universal venue calibration. +- Point-in-time safety still depends on correct upstream availability metadata. +- The licensed 2017–2022 case study is negative evidence of disciplined + falsification; the 2023–2025 confirmation set remains sealed. +- Full-package Mypy still has legacy debt; CI types the new and correctness- + critical surface rather than claiming full static coverage. + +## Resume and interview explanation + +**Resume:** Built an event-driven quantitative research audit lab that injects +four common backtest failures—data leakage, impossible execution, omitted costs, +and model-selection bias—and proves chronology, point-in-time, walk-forward, and +multiple-testing controls remove the illusion with deterministic, source-bound +evidence. + +**Interview:** Start with the paired Audit Lab visual, explain why a positive +Sharpe is not the evaluator, then walk through the clock boundary +(`signal → plan → matching market event → fill`), the explicit benchmark- +differential null, exact cost reconciliation, and the receipt. End with the +negative licensed-data case study as evidence that the same system refuses weak +claims. + +## Highest-value next improvement + +Add an end-to-end point-in-time tabular join API that carries availability +metadata from ingestion through feature construction, then expose one small +public dataset adapter. The current gate is explicit and tested; this would make +the provenance contract easier to adopt outside the synthetic fixture. diff --git a/docs/portfolio/SHOWCASE_SCORECARD.md b/docs/portfolio/SHOWCASE_SCORECARD.md new file mode 100644 index 00000000..ee20a62f --- /dev/null +++ b/docs/portfolio/SHOWCASE_SCORECARD.md @@ -0,0 +1,119 @@ +# Microalpha showcase scorecard + +Frozen: 2026-07-15 +Baseline public commit: `9abb834117820dc479246fc9486ae7301d309110` +Evaluator: senior quant engineer, open-source user, scientific red team, and +portfolio reviewer +Objective: make the repository an interview-ready quantitative-engineering +product. Positive investment performance is not an objective. + +## Positioning decision + +**Microalpha is a quant research audit laboratory:** an event-driven Python +system that makes chronology, execution, cost, selection, and provenance errors +observable before a backtest is allowed to become a claim. + +The public flagship must be a deterministic synthetic correctness fixture with +known ground truth. It may deliberately create inflated positive results only +to prove that the safe pipeline detects and removes the inflation. It must never +be presented as alpha. + +## Independent baseline audit + +| Perspective | Baseline | Most material gap | +| --- | ---: | --- | +| Hiring manager | 2.7 / 5 | Strong engineering is buried; there is no memorable, executable proof. | +| Open-source user | 2.9 / 5 | Clean-clone works, but there is no compact correctness receipt, Mypy fails, and release/license surfaces conflict. | +| Scientific red team | 2.4 / 5 | Future fills mutate state early; the existing reality check is not a valid centered multiple-testing correction. | +| Visual/portfolio | 2.6 / 5 | README and Pages are readable but text-heavy, generic, and missing a flagship demo visual. | + +### Direct baseline evidence + +- Clean temporary checkout from the public commit: editable source install + `10.341 s`; `make sample` `14.916 s`; `make report` `1.903 s`; total + `27.161 s` on the manager host. +- The sample uses a wall-clock run id and writes absolute local paths, so its + complete output tree is not byte-stable across clean locations. +- Latest public CI: 128 tests passed on Python 3.10, 3.11, and 3.12; 77% line + coverage; Ruff, Black, isort, detect-secrets, and MkDocs passed. +- Independent clean-clone verification found 129 focused tests passing, the + data-policy scan and strict docs build passing, but the README-advertised Mypy + command failing with five pandas index-type errors. +- `LICENSE` is empty and GitHub reports no asserted license. The release workflow + can publish the `microalpha` distribution to PyPI even though the docs warn + that the PyPI namesake is an unrelated project. +- Public GitHub `main` and Pages were green at the baseline commit. The Pages + deployment commit was `34163e141941638670a99fe3a2f5d2d6380bbc36`. +- Desktop and narrow screenshots taken after that deployment show no broken + layout, but the first fold contains no generated correctness proof; the Pages + home is generic and gives disproportionate navigation weight to WRDS history. + +## Ranked gaps frozen before implementation + +1. Correct the engine so a future fill cannot change positions, cash, logs, or + risk state before the corresponding market timestamp is processed. +2. Replace the current pseudo reality-check claim with a centered, synchronous, + benchmark-differential max-statistic correction and tests. +3. Add one deterministic `microalpha audit-demo` command that contrasts: + leaky versus point-in-time data; same-tick versus queued t+1 execution; gross + versus reconciled costs; naive selection versus walk-forward plus the valid + max-statistic correction. +4. Produce one compact JSON/CSV/SVG evidence set and a SHA-256 receipt that is + byte-identical across clean paths and repeated runs. +5. Rebuild the README and Pages first fold around the generated demo, architecture + and data lineage, one command, exact proof, and explicit limitations. +6. Disable unsafe PyPI publication, adopt a real license if ownership intent is + supported, fix the advertised type check, and align package metadata, + repository description/topics, docs navigation, version/release language, + and the public negative-research case study. + +## Frozen acceptance scorecard + +| Dimension | Baseline | Done threshold | Required direct evidence | +| --- | ---: | ---: | --- | +| 30-second product clarity | 2.0 / 5 | 4.5 / 5 | Top fold states problem, mechanism, exact demo result, one command, and non-alpha boundary. | +| Chronology correctness | 2.0 / 5 | 4.5 / 5 | Regression test proves future fills cannot mutate state early; same-timestamp ordering is deterministic. | +| Statistical safeguards | 1.0 / 5 | 4.0 / 5 | Centered synchronous max-statistic test includes a benchmark, controls a frozen null canary, and detects a planted positive control. | +| Deterministic flagship demo | 0.5 / 5 | 5.0 / 5 | Two clean runs have identical canonical files and receipt hash; no clock, host, or absolute path enters the receipt. | +| Install-to-proof usability | 2.5 / 5 | 4.5 / 5 | Clean clone/source install and one demo command pass in a reasonable time with no licensed data. | +| API/CLI coherence | 3.0 / 5 | 4.0 / 5 | Public Python API and CLI share the same implementation and schemas; help and errors are tested. | +| Evidence and provenance | 2.5 / 5 | 4.5 / 5 | Input, config, schema, code version, and every canonical output are hash-bound with relative paths. | +| Tests/static/security | 3.8 / 5 | 4.5 / 5 | Focused and full tests, lint, format, type/static checks, coverage, docs, links, data-policy, and secret scan pass. | +| Docs/navigation | 2.5 / 5 | 4.5 / 5 | Demo, architecture, API, reproducibility, and limitations form the primary path; process history is secondary. | +| Visual/mobile quality | 2.6 / 5 | 4.5 / 5 | README and live Pages inspected at desktop and narrow widths; charts are legible and no overflow/broken images remain. | +| Claim honesty | 3.0 / 5 | 5.0 / 5 | No alpha claim; configurable costs are not called calibrated; licensed/public/synthetic boundaries are explicit. | +| Live publication | 3.5 / 5 | 5.0 / 5 | Reviewed commit is on public `main`; CI/Docs green; Pages and repository metadata read back and match. | + +## Non-negotiable scientific tests + +- Every feature row used at a decision satisfies `available_at <= decision_at`; + an unsafe join fails closed with row identifiers and counts. +- A scheduled fill remains pending until its timestamp; no position, cash, + turnover, realized P&L, trade log, or risk state changes early. +- Net P&L reconciles exactly to gross P&L minus commission, spread, impact, and + borrow components in the correctness fixture. +- The multiple-testing correction recenters candidate-minus-benchmark returns + under the null and resamples candidates synchronously to preserve dependence. +- The demo includes both a noise-only family and a clearly labeled planted-signal + positive control; neither is evidence of market alpha. +- Generated public artifacts contain no private absolute path, credential, + licensed row, or sealed 2023–2025 confirmation observation. + +## Publication comparison + +The final report must compare the shipped public commit directly with +`9abb834117820dc479246fc9486ae7301d309110`, including changed evaluator scores, +demo runtime and hashes, test/coverage counts, rendered screenshots, live CI and +Pages URLs, remaining weaknesses, and the next highest-value improvement. + +## Reviewed outcome before publication + +- Product commit: `1fe57117ebcf3fcabce9048002265c914ecd28aa`. +- Final receipt: `6e36c2397696d7e9eecbd058cbfc1ba522c8ffba7e5798224de86b20457b6575`. +- Full verification: 146 tests, 77.73% coverage, all quality/static/security/docs + gates passing, clean wheel proof passing. +- Independent final hiring, open-source, and scientific reviews report no + remaining P0/P1 product issue. +- Final hiring scores: clarity 4.9, depth 4.7, memorability 4.9, honesty 5.0, + resume usefulness 4.9. +- Publication and live desktop/narrow readback remain the final release gate. diff --git a/docs/prompts/20251226_023453_ticket-13_fix-wrds-degeneracy.md b/docs/prompts/20251226_023453_ticket-13_fix-wrds-degeneracy.md index dbe2e252..f8755871 100644 --- a/docs/prompts/20251226_023453_ticket-13_fix-wrds-degeneracy.md +++ b/docs/prompts/20251226_023453_ticket-13_fix-wrds-degeneracy.md @@ -1,4 +1,4 @@ -# AGENTS.md instructions for /Users/mateobodon/Documents/Programming/Projects/microalpha +# AGENTS.md instructions for /Documents/Programming/Projects/microalpha # AGENTS.md — microalpha @@ -89,8 +89,8 @@ Living docs: ## Skills These skills are discovered at startup from multiple local sources. Each entry includes a name, description, and file path so you can open the source for full instructions. -- skill-creator: Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Codex's capabilities with specialized knowledge, workflows, or tool integrations. (file: /Users/mateobodon/.codex/skills/.system/skill-creator/SKILL.md) -- skill-installer: Install Codex skills into $CODEX_HOME/skills from a curated list or a GitHub repo path. Use when a user asks to list installable skills, install a curated skill, or install a skill from another repo (including private repos). (file: /Users/mateobodon/.codex/skills/.system/skill-installer/SKILL.md) +- skill-creator: Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Codex's capabilities with specialized knowledge, workflows, or tool integrations. (file: /.codex/skills/.system/skill-creator/SKILL.md) +- skill-installer: Install Codex skills into $CODEX_HOME/skills from a curated list or a GitHub repo path. Use when a user asks to list installable skills, install a curated skill, or install a skill from another repo (including private repos). (file: /.codex/skills/.system/skill-installer/SKILL.md) - Discovery: Available skills are listed in project docs and may also appear in a runtime "## Skills" section (name + description + file path). These are the sources of truth; skill bodies live on disk at the listed paths. - Trigger rules: If the user names a skill (with `$SkillName` or plain text) OR the task clearly matches a skill's description, you must use that skill for that turn. Multiple mentions mean use them all. Do not carry skills across turns unless re-mentioned. - Missing/blocked: If a named skill isn't in the list or the path can't be read, say so briefly and continue with the best fallback. @@ -111,7 +111,7 @@ These skills are discovered at startup from multiple local sources. Each entry i - /Users/mateobodon/Documents/Programming/Projects/microalpha + /Documents/Programming/Projects/microalpha never danger-full-access enabled diff --git a/docs/prompts/20260124_235038_ticket-19_finish-agentic-scaffold-cleanup_ticket-19_finish-agentic-scaffold-cleanup.md b/docs/prompts/20260124_235038_ticket-19_finish-agentic-scaffold-cleanup_ticket-19_finish-agentic-scaffold-cleanup.md index 8b60586a..7d45115e 100644 --- a/docs/prompts/20260124_235038_ticket-19_finish-agentic-scaffold-cleanup_ticket-19_finish-agentic-scaffold-cleanup.md +++ b/docs/prompts/20260124_235038_ticket-19_finish-agentic-scaffold-cleanup_ticket-19_finish-agentic-scaffold-cleanup.md @@ -48,7 +48,7 @@ Inputs: gpt-bundle -/Users/mateobodon/.codex/skills/gpt-bundle/SKILL.md +/.codex/skills/gpt-bundle/SKILL.md --- name: gpt-bundle description: Create a gpt_bundle.zip for GPT review (status + diffs + key docs). diff --git a/docs/reproducibility.md b/docs/reproducibility.md index 824e9ab7..dca42cc5 100644 --- a/docs/reproducibility.md +++ b/docs/reproducibility.md @@ -2,6 +2,27 @@ Microalpha emits a manifest for every run so you can replay results and audit configuration drift. +## Byte-stable Audit Lab evidence + +The flagship correctness fixture is stricter than a normal simulation run: + +```bash +microalpha audit-demo +git diff --exit-code -- docs/assets/audit_lab +``` + +Its canonical JSON, CSV, and SVG files contain no clock, host, absolute path, or +Git worktree location. `receipt.json` hashes the generator source, generated +input arrays, and every canonical artifact. The receipt file for schema +`microalpha.audit-lab.v1` hashes to +`6e36c2397696d7e9eecbd058cbfc1ba522c8ffba7e5798224de86b20457b6575`. +CI reproduces the same bytes on every supported Python version (3.10–3.12), so +dependency drift fails before merge rather than silently changing the evidence. + +General research runs retain timestamps and environment metadata because those +facts are part of their operational provenance; they are replayable but not +necessarily byte-identical across output directories. + ## Manifest fields Each backtest stores `artifacts//manifest.json` with: diff --git a/docs/results_wrds.md b/docs/results_wrds.md index 144c1abb..5bc72959 100644 --- a/docs/results_wrds.md +++ b/docs/results_wrds.md @@ -1,5 +1,12 @@ # WRDS Walk-Forward Results (Flagship Momentum) +> **Historical artifact interpretation.** This page predates the current +> benchmark-differential max-statistic correction. Its field named "SPA" was a +> relative best-versus-other-candidates comparison and must not be interpreted +> as evidence that a strategy beat cash or another investable benchmark. The +> current public correctness proof is [Audit Lab](audit-lab.md); the sealed +> confirmation set was not opened. + > Latest run: **2026-01-26T01-22-23Z-e76eb4d** (`configs/wfv_flagship_wrds.yaml`, 2013-01-02 -> 2017-11-02, 4 folds with 252-day forward tests (~12.0 months)) ## Performance Snapshot @@ -65,7 +72,10 @@ _Exposure time series is recorded in equity_curve.csv._ ## SPA & Factor Highlights -- Hansen SPA best model: **allocator_kwargs={'risk_model': 'equal'}|lookback_months=9|skip_months=1|top_frac=0.2000** with p-value **0.015** (2000 stationary bootstrap draws, block=63). See `reports/summaries/wrds_flagship_spa.md`. +- Legacy relative-comparison best model: + **allocator_kwargs={'risk_model': 'equal'}|lookback_months=9|skip_months=1|top_frac=0.2000** + with historical p-value **0.015** (2000 stationary bootstrap draws, block=63). + This was not a benchmark-superiority test and is not a current claim. - FF5 + MOM regression (HAC lags=5): ``` diff --git a/metadata/codex_sessions.md b/metadata/codex_sessions.md index 643a2975..0c416be4 100644 --- a/metadata/codex_sessions.md +++ b/metadata/codex_sessions.md @@ -6,6 +6,6 @@ - Notes: Added heat/turnover cap tests and fixed `Portfolio._sized_quantity` to enforce heat caps when `current_time=0`; created `configs/wfv_flagship_wrds_smoke.yaml` for quick validation; WRDS rerun not executed (no `WRDS_DATA_ROOT` locally). - 2025-11-22 (GPT-5.1-Codex-Max) - Tasks: S2 (smoke WFV with tightened caps; full WFV attempts), S3 (notebook visualisations). - - Commands: `WRDS_CONFIG=configs/wfv_flagship_wrds_smoke.yaml WRDS_DATA_ROOT=/Users/mateobodon/wrds_cache make wfv-wrds` (success, run `2025-11-22T00-21-14Z-c792b44`), three full-run attempts `WRDS_DATA_ROOT=/Users/mateobodon/wrds_cache make wfv-wrds` (timed out at 15m, 30m, 120m; partial artefacts only), notebook edits; no report rerun. + - Commands: `WRDS_CONFIG=configs/wfv_flagship_wrds_smoke.yaml WRDS_DATA_ROOT=/wrds_cache make wfv-wrds` (success, run `2025-11-22T00-21-14Z-c792b44`), three full-run attempts `WRDS_DATA_ROOT=/wrds_cache make wfv-wrds` (timed out at 15m, 30m, 120m; partial artefacts only), notebook edits; no report rerun. - Metrics (smoke run): Sharpe_HAC ≈ 0.057, MaxDD ≈ 40.2%, MAR ≈ -0.134, total turnover ≈ $360MM, RC p ≈ 0.745. Drawdown cap now binding vs prior 82% DD. - Notes: Full 2005–2024 WFV still pending; requires longer wall-clock execution. Notebook now plots equity+drawdown, rolling Sharpe, and per-fold test metrics while selecting the latest complete run (has `metrics.json`). diff --git a/mkdocs.yml b/mkdocs.yml index 42e32baf..8b59924f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -7,16 +7,40 @@ theme: - navigation.sections nav: - Home: index.md - - Flagship Strategy: flagship_strategy.md + - Audit Lab: audit-lab.md + - Architecture: architecture.md + - API & CLI: api.md - Examples: examples.md - Reproducibility: reproducibility.md - Leakage Safety: leakage-safety.md - - WRDS & Real Data: wrds.md - - WRDS Flagship Spec: flagship_momentum_wrds.md - - WRDS Results: results_wrds.md - - Factors: factors.md + - Benchmarks: benchmarks.md + - Limitations: limitations.md + - Research case study: portfolio_evidence_2026-07-11.md + - Licensed-data research: + - Data policy: wrds.md + - Strategy specification: flagship_momentum_wrds.md + - Historical results: results_wrds.md + - Factor analysis: factors.md extra: social: - icon: fontawesome/brands/github link: https://github.com/MateoBodon/microalpha docs_dir: docs +not_in_nav: | + /agent_runs/** + /gpt_outputs/** + /prompts/** + /portfolio/** + /tickets/** + /CODEX_SPRINT_TICKETS.md + /DECISIONS.md + /DOCS_AND_LOGGING_SYSTEM.md + /NOW.md + /PLAN_OF_RECORD.md + /RUNBOOK.md + /RUN_REGISTRY.md + /TICKETS.md + /data_sp500.md + /flagship_strategy.md + /results_wrds_resume.md + /results_wrds_smoke.md diff --git a/notebooks/tearsheet.ipynb b/notebooks/tearsheet.ipynb index c0d26f89..11da6d6f 100644 --- a/notebooks/tearsheet.ipynb +++ b/notebooks/tearsheet.ipynb @@ -16,13 +16,13 @@ "outputs": [ { "ename": "ImportError", - "evalue": "cannot import name 'bootstrap_sharpe_ratio' from 'microalpha.risk' (/Users/mateobodon/Documents/Programming/Projects/microalpha/notebooks/../microalpha/risk.py)", + "evalue": "cannot import name 'bootstrap_sharpe_ratio' from 'microalpha.risk' (/Documents/Programming/Projects/microalpha/notebooks/../microalpha/risk.py)", "output_type": "error", "traceback": [ "\u001b[31m---------------------------------------------------------------------------\u001b[39m", "\u001b[31mImportError\u001b[39m Traceback (most recent call last)", "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[2]\u001b[39m\u001b[32m, line 7\u001b[39m\n\u001b[32m 5\u001b[39m \u001b[38;5;28;01mimport\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01msys\u001b[39;00m\n\u001b[32m 6\u001b[39m sys.path.append(\u001b[33m'\u001b[39m\u001b[33m..\u001b[39m\u001b[33m'\u001b[39m)\n\u001b[32m----> \u001b[39m\u001b[32m7\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mmicroalpha\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mrisk\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m create_sharpe_ratio, create_drawdowns, bootstrap_sharpe_ratio\n\u001b[32m 9\u001b[39m \u001b[38;5;66;03m# Set plot style\u001b[39;00m\n\u001b[32m 10\u001b[39m plt.style.use(\u001b[33m'\u001b[39m\u001b[33mseaborn-v0_8-darkgrid\u001b[39m\u001b[33m'\u001b[39m)\n", - "\u001b[31mImportError\u001b[39m: cannot import name 'bootstrap_sharpe_ratio' from 'microalpha.risk' (/Users/mateobodon/Documents/Programming/Projects/microalpha/notebooks/../microalpha/risk.py)" + "\u001b[31mImportError\u001b[39m: cannot import name 'bootstrap_sharpe_ratio' from 'microalpha.risk' (/Documents/Programming/Projects/microalpha/notebooks/../microalpha/risk.py)" ] } ], diff --git a/pyproject.toml b/pyproject.toml index 704fe01a..9ac81a97 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,11 +1,20 @@ [project] name = "microalpha" -version = "0.1.0" -description = "A event-driven backtesting engine for quantitative strategies." +version = "0.2.0" +description = "An event-driven quantitative research audit lab for chronology, costs, walk-forward evidence, and reproducibility." authors = [ { name = "Mateo Bodon", email = "mateo.bodon@yale.edu" }, ] -requires-python = ">=3.9" +requires-python = ">=3.10,<3.13" +license = "MIT" +classifiers = [ + "Development Status :: 3 - Alpha", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Scientific/Engineering", +] dependencies = [ "numpy", "pandas", @@ -69,3 +78,8 @@ where = ["src"] [project.scripts] microalpha = "microalpha.cli:main" + +[project.urls] +Homepage = "https://github.com/MateoBodon/microalpha" +Documentation = "https://mateobodon.github.io/microalpha/" +Issues = "https://github.com/MateoBodon/microalpha/issues" diff --git a/scripts/check_data_policy.py b/scripts/check_data_policy.py index fca0cabe..f9459a0a 100644 --- a/scripts/check_data_policy.py +++ b/scripts/check_data_policy.py @@ -33,6 +33,21 @@ (re.compile(r"(^|/)[^/]*option_[^/]*", re.IGNORECASE), "path: option_*"), ] +PRIVATE_PATH_PATTERNS = [ + ( + re.compile(r"/Users/(?!\.\.\.)[A-Za-z0-9._-]+/"), + "private path: macOS home", + ), + ( + re.compile(r"/Volumes/(?!\.\.\.)[A-Za-z0-9._-]+/"), + "private path: mounted volume", + ), + ( + re.compile(r"[A-Za-z]:\\\\Users\\\\(?!\.\.\.)[A-Za-z0-9._-]+\\\\"), + "private path: Windows home", + ), +] + def load_allowlist() -> list[str]: patterns: list[str] = [] @@ -87,13 +102,28 @@ def scan_file(rel_path: Path) -> list[str]: return matches +def scan_private_paths(rel_path: Path) -> list[str]: + """Reject machine-specific absolute paths in any tracked text file.""" + text = read_head_text(REPO_ROOT / rel_path) + return [label for pattern, label in PRIVATE_PATH_PATTERNS if pattern.search(text)] + + def main() -> int: allowlist = load_allowlist() scanned = 0 skipped = 0 + privacy_scanned = 0 violations: list[tuple[Path, list[str]]] = [] for rel_path in iter_tracked_files(): + try: + private_matches = scan_private_paths(rel_path) + except RuntimeError as exc: + private_matches = [str(exc)] + privacy_scanned += 1 + if private_matches: + violations.append((rel_path, private_matches)) + if rel_path.suffix.lower() not in DATA_EXTENSIONS: continue if is_allowlisted(rel_path, allowlist): @@ -112,11 +142,18 @@ def main() -> int: for path, matches in violations: unique = ", ".join(sorted(set(matches))) print(f"- {path.as_posix()}: {unique}") - print(f"Scanned {scanned} files; allowlisted {skipped}.") + print( + f"Scanned {scanned} data files and {privacy_scanned} tracked files " + f"for private paths; allowlisted {skipped} data files." + ) print(f"Allowlist path: {ALLOWLIST_PATH.as_posix()}") return 2 - print(f"Data policy check passed. Scanned {scanned} files; allowlisted {skipped}.") + print( + f"Data policy check passed. Scanned {scanned} data files and " + f"{privacy_scanned} tracked files for private paths; " + f"allowlisted {skipped} data files." + ) return 0 diff --git a/src/microalpha.egg-info/PKG-INFO b/src/microalpha.egg-info/PKG-INFO deleted file mode 100644 index 73dd9565..00000000 --- a/src/microalpha.egg-info/PKG-INFO +++ /dev/null @@ -1,30 +0,0 @@ -Metadata-Version: 2.4 -Name: microalpha -Version: 0.1.0 -Summary: A event-driven backtesting engine for quantitative strategies. -Author-email: Mateo Bodon -Requires-Python: >=3.9 -License-File: LICENSE -Requires-Dist: numpy -Requires-Dist: pandas -Requires-Dist: pyyaml -Requires-Dist: pydantic>=2 -Requires-Dist: matplotlib>=3.7 -Provides-Extra: dev -Requires-Dist: pytest; extra == "dev" -Requires-Dist: pytest-cov; extra == "dev" -Requires-Dist: hypothesis; extra == "dev" -Requires-Dist: mypy; extra == "dev" -Requires-Dist: ruff>=0.4.0; extra == "dev" -Requires-Dist: black>=23.7; extra == "dev" -Requires-Dist: matplotlib; extra == "dev" -Requires-Dist: plotly>=5.15; extra == "dev" -Requires-Dist: pyarrow; extra == "dev" -Requires-Dist: pydantic; extra == "dev" -Requires-Dist: pandas-stubs; extra == "dev" -Requires-Dist: types-PyYAML; extra == "dev" -Requires-Dist: mkdocs>=1.6; extra == "dev" -Requires-Dist: mkdocs-material>=9.5; extra == "dev" -Requires-Dist: isort>=5.13; extra == "dev" -Requires-Dist: detect-secrets>=1.5; extra == "dev" -Dynamic: license-file diff --git a/src/microalpha.egg-info/SOURCES.txt b/src/microalpha.egg-info/SOURCES.txt deleted file mode 100644 index 92ab54b0..00000000 --- a/src/microalpha.egg-info/SOURCES.txt +++ /dev/null @@ -1,118 +0,0 @@ -LICENSE -README.md -pyproject.toml -src/microalpha/__init__.py -src/microalpha/allocators.py -src/microalpha/broker.py -src/microalpha/capital.py -src/microalpha/cli.py -src/microalpha/config.py -src/microalpha/config_wfv.py -src/microalpha/data.py -src/microalpha/engine.py -src/microalpha/events.py -src/microalpha/execution.py -src/microalpha/execution_safety.py -src/microalpha/integrity.py -src/microalpha/lob.py -src/microalpha/logging.py -src/microalpha/manifest.py -src/microalpha/market_metadata.py -src/microalpha/metrics.py -src/microalpha/order_flow.py -src/microalpha/portfolio.py -src/microalpha/risk.py -src/microalpha/risk_stats.py -src/microalpha/runner.py -src/microalpha/slippage.py -src/microalpha/walkforward.py -src/microalpha.egg-info/PKG-INFO -src/microalpha.egg-info/PKG-INFO 2 -src/microalpha.egg-info/SOURCES 2.txt -src/microalpha.egg-info/SOURCES.txt -src/microalpha.egg-info/dependency_links 2.txt -src/microalpha.egg-info/dependency_links.txt -src/microalpha.egg-info/entry_points 2.txt -src/microalpha.egg-info/entry_points.txt -src/microalpha.egg-info/requires 2.txt -src/microalpha.egg-info/requires.txt -src/microalpha.egg-info/top_level 2.txt -src/microalpha.egg-info/top_level.txt -src/microalpha/reporting/__init__.py -src/microalpha/reporting/analytics.py -src/microalpha/reporting/baselines.py -src/microalpha/reporting/factors.py -src/microalpha/reporting/robustness.py -src/microalpha/reporting/spa.py -src/microalpha/reporting/summary.py -src/microalpha/reporting/tearsheet.py -src/microalpha/reporting/wrds_summary.py -src/microalpha/strategies/breakout.py -src/microalpha/strategies/cs_momentum.py -src/microalpha/strategies/flagship_mom.py -src/microalpha/strategies/flagship_momentum.py -src/microalpha/strategies/meanrev.py -src/microalpha/strategies/mm.py -src/microalpha/wrds/__init__.py -tests/test_allocators.py -tests/test_artifacts_schema.py -tests/test_baselines.py -tests/test_benchmarks.py -tests/test_borrow_costs.py -tests/test_build_wrds_signals.py -tests/test_capital_and_slippage_integration.py -tests/test_cfg_unify.py -tests/test_cli_help.py -tests/test_cli_info.py -tests/test_data.py -tests/test_data_policy.py -tests/test_degeneracy_constraints.py -tests/test_determinism.py -tests/test_docs_links.py -tests/test_execution.py -tests/test_execution_models.py -tests/test_factor_alignment.py -tests/test_factor_regression.py -tests/test_flagship_filter_diagnostics.py -tests/test_flagship_momentum.py -tests/test_limit_order_execution.py -tests/test_lob.py -tests/test_lob_cancel_latency.py -tests/test_lob_fifo.py -tests/test_lob_modes.py -tests/test_manifest_written.py -tests/test_metrics_hac.py -tests/test_metrics_invariant.py -tests/test_multiasset_cs_momentum.py -tests/test_multiasset_data_handler.py -tests/test_no_lookahead.py -tests/test_order_flow_diagnostics.py -tests/test_pnl_attribution.py -tests/test_pnl_integrity.py -tests/test_portfolio_risk_caps.py -tests/test_portfolio_risk_sizing.py -tests/test_portfolio_turnover_cap.py -tests/test_portfolio_weight_sizing.py -tests/test_price_lookup.py -tests/test_profile_output.py -tests/test_reality_check_store.py -tests/test_reporting_analytics.py -tests/test_reporting_robustness.py -tests/test_reporting_spa.py -tests/test_risk_and_slippage.py -tests/test_risk_controls.py -tests/test_risk_stats.py -tests/test_runner_flagship.py -tests/test_runs_index.py -tests/test_slippage_models.py -tests/test_spa_regression_keyerror.py -tests/test_strategies.py -tests/test_time_ordering.py -tests/test_tplus1_execution.py -tests/test_trades_jsonl.py -tests/test_vwap_is.py -tests/test_walkforward.py -tests/test_wrds_detection.py -tests/test_wrds_flagship_spec.py -tests/test_wrds_markers.py -tests/test_wrds_summary_render.py \ No newline at end of file diff --git a/src/microalpha.egg-info/dependency_links.txt b/src/microalpha.egg-info/dependency_links.txt deleted file mode 100644 index 8b137891..00000000 --- a/src/microalpha.egg-info/dependency_links.txt +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/microalpha.egg-info/entry_points.txt b/src/microalpha.egg-info/entry_points.txt deleted file mode 100644 index c2635389..00000000 --- a/src/microalpha.egg-info/entry_points.txt +++ /dev/null @@ -1,2 +0,0 @@ -[console_scripts] -microalpha = microalpha.cli:main diff --git a/src/microalpha.egg-info/requires.txt b/src/microalpha.egg-info/requires.txt deleted file mode 100644 index 178cd384..00000000 --- a/src/microalpha.egg-info/requires.txt +++ /dev/null @@ -1,23 +0,0 @@ -numpy -pandas -pyyaml -pydantic>=2 -matplotlib>=3.7 - -[dev] -pytest -pytest-cov -hypothesis -mypy -ruff>=0.4.0 -black>=23.7 -matplotlib -plotly>=5.15 -pyarrow -pydantic -pandas-stubs -types-PyYAML -mkdocs>=1.6 -mkdocs-material>=9.5 -isort>=5.13 -detect-secrets>=1.5 diff --git a/src/microalpha.egg-info/top_level.txt b/src/microalpha.egg-info/top_level.txt deleted file mode 100644 index f82d92fc..00000000 --- a/src/microalpha.egg-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -microalpha diff --git a/src/microalpha/audit_lab.py b/src/microalpha/audit_lab.py new file mode 100644 index 00000000..65be6440 --- /dev/null +++ b/src/microalpha/audit_lab.py @@ -0,0 +1,439 @@ +"""Deterministic synthetic correctness fixture for the public showcase. + +The fixture intentionally creates invalid positive results and a labeled planted +control. It demonstrates audit behavior; it is not market evidence or alpha. +""" + +from __future__ import annotations + +import csv +import hashlib +import io +import json +from pathlib import Path +from typing import Mapping, cast + +import numpy as np + +from .multiple_testing import centered_max_statistic_test +from .point_in_time import PointInTimeViolation, require_point_in_time + +SCHEMA_VERSION = "microalpha.audit-lab.v1" +GENERATOR_VERSION = "0.2.0" +DEFAULT_SEED = 20260715 +PERIODS_PER_YEAR = 252 + + +def _sha256(data: bytes) -> str: + return hashlib.sha256(data).hexdigest() + + +def _json_bytes(payload: object) -> bytes: + return (json.dumps(payload, indent=2, sort_keys=True) + "\n").encode("utf-8") + + +def _sharpe(returns: np.ndarray) -> float: + values = np.asarray(returns, dtype=float) + std = float(values.std(ddof=0)) + if values.size < 2 or std == 0.0: + return 0.0 + return float(np.sqrt(PERIODS_PER_YEAR) * values.mean() / std) + + +def _round(value: float) -> float: + return round(float(value), 4) + + +def _array_digest(arrays: Mapping[str, np.ndarray]) -> str: + """Hash a platform-stable representation of the synthetic fixture inputs.""" + + digest = hashlib.sha256() + for name in sorted(arrays): + value = np.ascontiguousarray(arrays[name], dtype=" dict[str, str]: + """Bind the receipt to the source modules that generate its evidence.""" + module_dir = Path(__file__).resolve().parent + return { + name: _sha256((module_dir / name).read_bytes()) + for name in ("audit_lab.py", "multiple_testing.py", "point_in_time.py") + } + + +def _comparison_csv(rows: list[dict[str, object]]) -> bytes: + buffer = io.StringIO(newline="") + writer = csv.DictWriter( + buffer, + fieldnames=["audit", "unsafe_or_naive", "safe_or_corrected", "unit", "verdict"], + lineterminator="\n", + ) + writer.writeheader() + writer.writerows(rows) + return buffer.getvalue().encode("utf-8") + + +def _bar( + *, x: float, y: float, width: float, value: float, scale: float, color: str +) -> str: + normalized = min(abs(value) / scale, 1.0) + bar_width = max(2.0, width * normalized) + return ( + f'' + ) + + +def _comparison_svg(results: dict[str, object]) -> bytes: + leakage = results["leakage"] + execution = results["execution"] + costs = results["costs"] + selection = results["selection"] + assert isinstance(leakage, dict) + assert isinstance(execution, dict) + assert isinstance(costs, dict) + assert isinstance(selection, dict) + + cards = [ + ( + "1 POINT-IN-TIME DATA", + "Leaky revised value", + float(leakage["leaky_sharpe"]), + "PIT-safe value", + float(leakage["pit_safe_sharpe"]), + f'{leakage["unavailable_rows_blocked"]} unavailable rows blocked', + ), + ( + "2 EVENT-TIME EXECUTION", + "Same-tick oracle", + float(execution["same_tick_sharpe"]), + "Queued t+1", + float(execution["t_plus_1_sharpe"]), + "State changes only when the fill event arrives", + ), + ( + "3 COST RECONCILIATION", + "Gross planted control", + float(costs["gross_sharpe"]), + "Commission + spread + impact + borrow", + float(costs["net_sharpe"]), + f'Exact P&L residual {float(costs["reconciliation_error"]):.1e}', + ), + ( + "4 SELECTION CONTROL", + "Best of 128 noise models", + float(selection["naive_in_sample_sharpe"]), + "Walk-forward OOS", + float(selection["walk_forward_oos_sharpe"]), + f'Max-stat p={float(selection["noise_family_p_value"]):.3f}; planted control p={float(selection["planted_control_p_value"]):.3f}', + ), + ] + + parts = [ + '', + 'Microalpha Audit Lab correctness results', + 'Four comparisons show inflation from data leakage, same-tick execution, omitted costs, and naive model selection.', + '', + 'Four ways a backtest lies', + 'Known-ground-truth synthetic fixture · correctness test, not alpha', + ] + for index, ( + heading, + bad_label, + bad_value, + good_label, + good_value, + note, + ) in enumerate(cards): + row = index // 2 + col = index % 2 + x = 64 + col * 548 + y = 150 + row * 276 + parts.extend( + [ + f'', + f'{heading}', + f'{bad_label}', + f'{bad_value:+.2f}', + _bar( + x=x + 28, + y=y + 92, + width=434, + value=bad_value, + scale=20.0, + color="#e11d48", + ), + f'{good_label}', + f'{good_value:+.2f}', + _bar( + x=x + 28, + y=y + 156, + width=434, + value=good_value, + scale=20.0, + color="#22c55e", + ), + f'{note}', + ] + ) + parts.extend( + [ + 'Generated by microalpha audit-demo · all values are receipt-bound', + "", + ] + ) + return ("\n".join(parts) + "\n").encode("utf-8") + + +def _lineage_svg() -> bytes: + nodes = [ + (55, 116, 190, 78, "Synthetic oracle", "seed + schema"), + (302, 116, 190, 78, "Availability gate", "available_at ≤ decision_at"), + (549, 116, 190, 78, "Event queue", "fill only at market time"), + (796, 116, 190, 78, "Cost ledger", "gross − 4 components"), + (1043, 116, 102, 78, "Receipt", "SHA-256"), + ] + parts = [ + '', + 'Microalpha audit data lineage', + 'A safe pipeline moves a synthetic oracle through availability, event scheduling, cost reconciliation, and an artifact receipt.', + '', + 'Audit lineage: every claim has a clock and a hash', + ] + for idx, (x, y, width, height, title, subtitle) in enumerate(nodes): + parts.extend( + [ + f'', + f'{title}', + f'{subtitle}', + ] + ) + if idx < len(nodes) - 1: + next_x = nodes[idx + 1][0] + parts.append( + f'' + ) + parts.extend( + [ + '', + '', + 'Fail closed: unavailable rows or early fills never enter state', + 'Synthetic fixture only · no provider, licensed dataset, holdout, or network access', + "", + ] + ) + return ("\n".join(parts) + "\n").encode("utf-8") + + +def _build_results(seed: int) -> tuple[dict[str, object], dict[str, np.ndarray]]: + rng = np.random.default_rng(seed) + n_days = 756 + + # A: a revised value contains the contemporaneous return but is not available + # until two sessions later. The safe feature is independent at decision time. + market_returns = rng.normal(0.0, 0.01, size=n_days) + initial_feature = rng.normal(0.0, 1.0, size=n_days) + revised_feature = market_returns.copy() + decision_day: np.ndarray = np.arange(n_days, dtype=float) + revised_available_day = decision_day + 2.0 + row_ids = [f"fixture_row_{index:03d}" for index in range(n_days)] + try: + require_point_in_time(decision_day, revised_available_day, row_ids=row_ids) + except PointInTimeViolation as exc: + unavailable_count = exc.count + else: # pragma: no cover - fixture deliberately violates availability + raise AssertionError("leaky fixture unexpectedly passed point-in-time gate") + require_point_in_time(decision_day, decision_day, row_ids=row_ids) + leaky_returns = np.sign(revised_feature) * market_returns + pit_positions = np.sign(initial_feature) + pit_returns = np.roll(pit_positions, 1) * market_returns + pit_returns[0] = 0.0 + + # B: a same-close oracle sees the return it is meant to trade. The queued + # t+1 version uses the same signal only after the next event arrives. + execution_returns = rng.normal(0.0, 0.009, size=n_days) + same_tick = np.sign(execution_returns) * execution_returns + queued_positions = np.roll(np.sign(execution_returns), 1) + queued_positions[0] = 0.0 + queued_t1 = queued_positions * execution_returns + + # C: a small planted software-control edge is consumed by explicit costs. + cost_positions = rng.choice(np.array([-1.0, 1.0]), size=n_days) + gross_cost_fixture = 0.00045 + rng.normal(0.0, 0.006, size=n_days) + turnover = np.abs(np.diff(np.r_[0.0, cost_positions])) + commission = turnover * 0.00010 + half_spread = turnover * 0.00020 + impact = turnover * 0.00015 + borrow = (cost_positions < 0.0).astype(float) * 0.00002 + total_cost = commission + half_spread + impact + borrow + net_cost_fixture = gross_cost_fixture - total_cost + reconciliation = gross_cost_fixture - total_cost - net_cost_fixture + + # D: full-sample selection finds a winner in noise. Walk-forward selection + # uses only the first two thirds and is evaluated on untouched final data. + selection_rng = np.random.default_rng(seed + 1) + base_noise_returns = selection_rng.normal(0.0, 0.01, size=n_days) + noise_signals = selection_rng.choice(np.array([-1.0, 1.0]), size=(n_days, 128)) + noise_candidates = noise_signals * base_noise_returns[:, None] + split = 504 + in_sample_sharpes = np.array( + [_sharpe(noise_candidates[:split, idx]) for idx in range(128)] + ) + selected_index = int(np.argmax(in_sample_sharpes)) + naive_full_sharpes = np.array( + [_sharpe(noise_candidates[:, idx]) for idx in range(128)] + ) + naive_index = int(np.argmax(naive_full_sharpes)) + walk_forward_oos = noise_candidates[split:, selected_index] + + noise_test = centered_max_statistic_test( + noise_candidates, + benchmark_returns=np.zeros(n_days), + candidate_names=[f"noise_{idx:03d}" for idx in range(128)], + seed=seed + 101, + num_bootstrap=999, + block_length=8, + ) + planted = noise_candidates[:, :31].copy() + planted_control = selection_rng.normal(0.0025, 0.01, size=n_days) + planted = np.column_stack([planted_control, planted]) + planted_test = centered_max_statistic_test( + planted, + benchmark_returns=np.zeros(n_days), + candidate_names=["planted_control", *[f"noise_{idx:03d}" for idx in range(31)]], + seed=seed + 102, + num_bootstrap=999, + block_length=8, + ) + + results: dict[str, object] = { + "schema_version": SCHEMA_VERSION, + "fixture": "deterministic_synthetic_known_ground_truth", + "claim_boundary": "software correctness demonstration; not alpha or market evidence", + "seed": seed, + "observations": n_days, + "leakage": { + "leaky_sharpe": _round(_sharpe(leaky_returns)), + "pit_safe_sharpe": _round(_sharpe(pit_returns)), + "inflation_removed": _round(_sharpe(leaky_returns) - _sharpe(pit_returns)), + "unavailable_rows_blocked": unavailable_count, + "availability_rule": "available_at <= decision_at", + }, + "execution": { + "same_tick_sharpe": _round(_sharpe(same_tick)), + "t_plus_1_sharpe": _round(_sharpe(queued_t1)), + "inflation_removed": _round(_sharpe(same_tick) - _sharpe(queued_t1)), + "state_rule": "no fill mutation before scheduled market event", + }, + "costs": { + "gross_sharpe": _round(_sharpe(gross_cost_fixture)), + "net_sharpe": _round(_sharpe(net_cost_fixture)), + "commission_total": _round(commission.sum()), + "half_spread_total": _round(half_spread.sum()), + "impact_total": _round(impact.sum()), + "borrow_total": _round(borrow.sum()), + "reconciliation_error": float(np.max(np.abs(reconciliation))), + }, + "selection": { + "candidate_count": 128, + "naive_full_sample_winner": f"noise_{naive_index:03d}", + "naive_in_sample_sharpe": _round(naive_full_sharpes[naive_index]), + "walk_forward_selected": f"noise_{selected_index:03d}", + "walk_forward_oos_sharpe": _round(_sharpe(walk_forward_oos)), + "noise_family_p_value": _round(float(cast(float, noise_test["p_value"]))), + "planted_control_p_value": _round( + float(cast(float, planted_test["p_value"])) + ), + "test": "centered synchronous max statistic vs zero-return benchmark", + }, + } + arrays = { + "market_returns": market_returns, + "initial_feature": initial_feature, + "revised_feature": revised_feature, + "execution_returns": execution_returns, + "gross_cost_fixture": gross_cost_fixture, + "cost_positions": cost_positions, + "noise_candidates": noise_candidates, + "planted_control": planted_control, + } + return results, arrays + + +def run_audit_lab( + output_dir: str | Path, *, seed: int = DEFAULT_SEED +) -> dict[str, object]: + """Generate the canonical Audit Lab evidence set and SHA-256 receipt.""" + + out = Path(output_dir) + out.mkdir(parents=True, exist_ok=True) + results, arrays = _build_results(seed) + rows = [ + { + "audit": "point_in_time_data", + "unsafe_or_naive": results["leakage"]["leaky_sharpe"], # type: ignore[index] + "safe_or_corrected": results["leakage"]["pit_safe_sharpe"], # type: ignore[index] + "unit": "annualized_sharpe", + "verdict": "unavailable_rows_blocked", + }, + { + "audit": "event_time_execution", + "unsafe_or_naive": results["execution"]["same_tick_sharpe"], # type: ignore[index] + "safe_or_corrected": results["execution"]["t_plus_1_sharpe"], # type: ignore[index] + "unit": "annualized_sharpe", + "verdict": "future_fill_queued", + }, + { + "audit": "cost_reconciliation", + "unsafe_or_naive": results["costs"]["gross_sharpe"], # type: ignore[index] + "safe_or_corrected": results["costs"]["net_sharpe"], # type: ignore[index] + "unit": "annualized_sharpe", + "verdict": "all_cost_components_reconciled", + }, + { + "audit": "selection_control", + "unsafe_or_naive": results["selection"]["naive_in_sample_sharpe"], # type: ignore[index] + "safe_or_corrected": results["selection"]["walk_forward_oos_sharpe"], # type: ignore[index] + "unit": "annualized_sharpe", + "verdict": "noise_family_not_promoted", + }, + ] + files = { + "audit_results.json": _json_bytes(results), + "comparison.csv": _comparison_csv(rows), + "audit_lab.svg": _comparison_svg(results), + "data_lineage.svg": _lineage_svg(), + } + for name, content in files.items(): + (out / name).write_bytes(content) + + receipt = { + "schema_version": SCHEMA_VERSION, + "generator": { + "version": GENERATOR_VERSION, + "source_sha256": _generator_source_hashes(), + }, + "fixture": "deterministic_synthetic_known_ground_truth", + "claim_boundary": "software correctness demonstration; not alpha or market evidence", + "seed": seed, + "input_sha256": _array_digest(arrays), + "artifacts": { + name: _sha256(content) for name, content in sorted(files.items()) + }, + } + receipt_bytes = _json_bytes(receipt) + (out / "receipt.json").write_bytes(receipt_bytes) + return { + "artifact_dir": str(out), + "receipt_sha256": _sha256(receipt_bytes), + "results": results, + } diff --git a/src/microalpha/broker.py b/src/microalpha/broker.py index 46f45b6f..cb74f079 100644 --- a/src/microalpha/broker.py +++ b/src/microalpha/broker.py @@ -5,7 +5,7 @@ from typing import Optional from .events import FillEvent, OrderEvent -from .execution import Executor +from .execution import ExecutionPlan, Executor class SimulatedBroker: @@ -14,3 +14,9 @@ def __init__(self, executor: Executor): def execute(self, order: OrderEvent, market_timestamp: int) -> Optional[FillEvent]: return self.executor.execute(order, market_timestamp) + + def plan(self, order: OrderEvent, market_timestamp: int) -> list[ExecutionPlan]: + return self.executor.plan(order, market_timestamp) + + def materialize(self, plan: ExecutionPlan) -> Optional[FillEvent]: + return self.executor.materialize(plan) diff --git a/src/microalpha/cli.py b/src/microalpha/cli.py index 7d8faf3f..c0b3caf0 100644 --- a/src/microalpha/cli.py +++ b/src/microalpha/cli.py @@ -18,12 +18,16 @@ render_tearsheet, ) +from .audit_lab import DEFAULT_SEED, run_audit_lab from .runner import run_from_config from .walkforward import run_walk_forward def main() -> None: parser = argparse.ArgumentParser() + parser.add_argument( + "--version", action="version", version=f"%(prog)s {_resolve_version()}" + ) subparsers = parser.add_subparsers(dest="cmd", required=True) run_parser = subparsers.add_parser("run") @@ -106,12 +110,28 @@ def main() -> None: subparsers.add_parser("info") + audit_parser = subparsers.add_parser( + "audit-demo", + help="Generate the deterministic synthetic correctness showcase.", + ) + audit_parser.add_argument( + "--out", + dest="outdir", + default="docs/assets/audit_lab", + help="Output directory (default: docs/assets/audit_lab).", + ) + audit_parser.add_argument("--seed", type=int, default=DEFAULT_SEED) + args = parser.parse_args() if args.cmd == "info": print(json.dumps(_build_info(), indent=2)) return + if args.cmd == "audit-demo": + print(json.dumps(run_audit_lab(args.outdir, seed=args.seed), indent=2)) + return + t0 = time.time() if args.cmd == "run": diff --git a/src/microalpha/data.py b/src/microalpha/data.py index 4c031169..c3ffb3ab 100644 --- a/src/microalpha/data.py +++ b/src/microalpha/data.py @@ -93,12 +93,14 @@ def get_latest_price(self, symbol: str, timestamp: int): close_value = cast(float, self.data.iloc[idx]["close"]) return close_value - def get_future_timestamps(self, start_timestamp: int, n: int) -> List[int]: + def get_future_timestamps( + self, start_timestamp: int, n: int, symbol: str | None = None + ) -> List[int]: """ Gets the next `n` timestamps from the data starting after a given timestamp. Used by the TWAP execution handler to schedule child orders. """ - if self.data is None: + if self.data is None or (symbol is not None and symbol != self.symbol): return [] # Get the index of all future dates @@ -249,9 +251,17 @@ def get_latest_price(self, symbol: str, timestamp: int): ts = CsvDataHandler._to_datetime(timestamp) return self._lookup_price(df, ts) - def get_future_timestamps(self, start_timestamp: int, n: int) -> List[int]: - # Use the union index to determine the next times globally + def get_future_timestamps( + self, start_timestamp: int, n: int, symbol: str | None = None + ) -> List[int]: + # Execution schedules must use events actually observed for the order symbol. ts = CsvDataHandler._to_datetime(start_timestamp) + if symbol is not None: + frame = self.frames.get(symbol) + if frame is None: + return [] + future = frame.index[frame.index > ts] + return [CsvDataHandler._to_int_timestamp(t) for t in future[:n]] union = list(self._iter_union_index()) idx = pd.Index(union).searchsorted(ts, side="right") return [CsvDataHandler._to_int_timestamp(t) for t in union[idx : idx + n]] diff --git a/src/microalpha/engine.py b/src/microalpha/engine.py index 74cc4ccc..775dc562 100644 --- a/src/microalpha/engine.py +++ b/src/microalpha/engine.py @@ -10,6 +10,7 @@ import numpy as np from .events import FillEvent, LookaheadError, MarketEvent, OrderEvent, SignalEvent +from .execution import ExecutionPlan class Engine: @@ -23,6 +24,7 @@ def __init__( self.broker = broker self.rng = rng or np.random.default_rng() self._pending_equity_refresh_ts: int | None = None + self._pending_executions: list[ExecutionPlan] = [] def run(self) -> None: profiler = None @@ -58,6 +60,7 @@ def _on_market(self, market_event: MarketEvent) -> None: self.clock = market_event.timestamp self.portfolio.on_market(market_event) + self._materialize_due(market_event) signals_iter: Iterable[SignalEvent] = self.strategy.on_market(market_event) signals = list(signals_iter) @@ -80,10 +83,8 @@ def _on_market(self, market_event: MarketEvent) -> None: orders: Iterable[OrderEvent] = self.portfolio.on_signal(signal) for order in orders: - fill: FillEvent | None = self.broker.execute( - order, market_event.timestamp - ) - if fill is None: + plans = self._plan_execution(order, market_event.timestamp) + if not plans: if order_flow: reason = getattr( getattr(self.broker, "executor", None), @@ -94,12 +95,18 @@ def _on_market(self, market_event: MarketEvent) -> None: continue if order_flow: order_flow.record_broker_accept(order) - order_flow.record_fill(fill) - if fill.timestamp < market_event.timestamp: - raise LookaheadError("fill before current market event") - if fill.timestamp == market_event.timestamp: - same_day_fill = True - self.portfolio.on_fill(fill) + for plan in plans: + if plan.timestamp < market_event.timestamp: + raise LookaheadError("planned fill before current market event") + if plan.timestamp == market_event.timestamp: + fill = self._materialize(plan) + if fill is not None: + if order_flow: + order_flow.record_fill(fill, order=plan.order) + self.portfolio.on_fill(fill) + same_day_fill = True + else: + self._pending_executions.append(plan) if same_day_fill: self._pending_equity_refresh_ts = market_event.timestamp if order_flow and signals: @@ -111,3 +118,64 @@ def _on_market(self, market_event: MarketEvent) -> None: order_flow.record_error( f"end_rebalance_error: {type(exc).__name__}: {exc}" ) + + def _plan_execution( + self, order: OrderEvent, market_timestamp: int + ) -> list[ExecutionPlan]: + planner = getattr(self.broker, "plan", None) + if callable(planner): + return list(planner(order, market_timestamp)) + + # Compatibility for simple third-party test brokers. Built-in brokers + # use the safe planning API above and never read a future market price + # before the engine reaches it. + fill = self.broker.execute(order, market_timestamp) + if fill is None: + return [] + return [ExecutionPlan(fill.timestamp, order, abs(fill.qty), fill)] + + def _materialize(self, plan: ExecutionPlan) -> FillEvent | None: + materializer = getattr(self.broker, "materialize", None) + if callable(materializer): + return materializer(plan) + return plan.prepared_fill + + def _materialize_due(self, market_event: MarketEvent) -> None: + overdue = [ + plan + for plan in self._pending_executions + if plan.timestamp < market_event.timestamp + and plan.order.symbol == market_event.symbol + ] + if overdue: + raise LookaheadError("scheduled execution timestamp was not observed") + + due = [ + plan + for plan in self._pending_executions + if plan.timestamp == market_event.timestamp + and plan.order.symbol == market_event.symbol + ] + if not due: + return + + due_ids = {id(plan) for plan in due} + self._pending_executions = [ + plan for plan in self._pending_executions if id(plan) not in due_ids + ] + order_flow = getattr(self.portfolio, "order_flow", None) + applied = False + for plan in due: + fill = self._materialize(plan) + if fill is None: + continue + if fill.timestamp != market_event.timestamp: + raise LookaheadError( + "materialized fill timestamp differs from schedule" + ) + if order_flow: + order_flow.record_fill(fill, order=plan.order) + self.portfolio.on_fill(fill) + applied = True + if applied: + self.portfolio.refresh_equity_after_fills(market_event.timestamp) diff --git a/src/microalpha/execution.py b/src/microalpha/execution.py index d77c7ae0..8cdeb830 100644 --- a/src/microalpha/execution.py +++ b/src/microalpha/execution.py @@ -5,7 +5,7 @@ import math from collections import Counter from dataclasses import dataclass, field -from typing import Any, Dict, Literal, Mapping, Optional, Protocol, Sequence +from typing import Any, Dict, Literal, Mapping, Optional, Protocol, Sequence, cast import numpy as np @@ -16,7 +16,9 @@ class DataHandlerProtocol(Protocol): - def get_future_timestamps(self, start_timestamp: int, n: int) -> Sequence[int]: ... + def get_future_timestamps( + self, start_timestamp: int, n: int, symbol: str | None = None + ) -> Sequence[int]: ... def get_latest_price(self, symbol: str, timestamp: int) -> float | None: ... @@ -27,6 +29,22 @@ def get_recent_prices( ) -> Sequence[float]: ... +@dataclass(frozen=True) +class ExecutionPlan: + """An order slice that may be materialized only at ``timestamp``. + + ``prepared_fill`` is reserved for execution venues, such as the in-memory + limit-order book, whose price is known at submission time. Market-data based + executors leave it unset so future prices and volumes are not read while the + engine clock is still in the past. + """ + + timestamp: int + order: OrderEvent + qty: int + prepared_fill: FillEvent | None = None + + @dataclass class Executor: data_handler: DataHandlerProtocol @@ -80,14 +98,48 @@ def _summarize_reject_reasons(self) -> str: def execute(self, order: OrderEvent, current_ts: int) -> Optional[FillEvent]: self._reset_reject_tracking() fill_ts = self._fill_timestamp(order, current_ts) + if fill_ts is None: + self._record_reject("no_future_market_event") + return None fill = self._build_fill(order, fill_ts, order.qty) if fill is None: self.last_reject_reason = self._summarize_reject_reasons() return fill - def _fill_timestamp(self, order: OrderEvent, current_ts: int) -> int: - future = self.data_handler.get_future_timestamps(current_ts, 1) - return future[0] if future else current_ts + def plan(self, order: OrderEvent, current_ts: int) -> list[ExecutionPlan]: + """Schedule execution without reading future market observations.""" + self._reset_reject_tracking() + fill_ts = self._fill_timestamp(order, current_ts) + if fill_ts is None: + self._record_reject("no_future_market_event") + return [] + return [ExecutionPlan(fill_ts, order, order.qty)] + + def materialize(self, plan: ExecutionPlan) -> Optional[FillEvent]: + """Create a fill when the engine has reached the planned timestamp.""" + self._reset_reject_tracking() + if plan.prepared_fill is not None: + return plan.prepared_fill + fill = self._build_fill(plan.order, plan.timestamp, plan.qty) + if fill is None: + self.last_reject_reason = self._summarize_reject_reasons() + return fill + + def _future_timestamps( + self, symbol: str, start_timestamp: int, n: int + ) -> list[int]: + """Return symbol-specific future events, with legacy-handler compatibility.""" + try: + values = self.data_handler.get_future_timestamps( + start_timestamp, n, symbol=symbol + ) + except TypeError: + values = self.data_handler.get_future_timestamps(start_timestamp, n) + return list(values) + + def _fill_timestamp(self, order: OrderEvent, current_ts: int) -> int | None: + future = self._future_timestamps(order.symbol, current_ts, 1) + return future[0] if future else None def _slippage(self, symbol: str, qty: int, price: float) -> float: if self.slippage_model is not None: @@ -173,7 +225,7 @@ def _normalise_mode( mode_str = mode.upper() if mode_str not in {"IOC", "PO"}: raise ValueError(f"Unsupported limit execution mode '{mode}'") - return mode_str + return cast(Literal["IOC", "PO"], mode_str) def _limit_crossable( self, @@ -247,7 +299,7 @@ def _resolve_volatility_bps( ) -> float: if meta.volatility_bps and meta.volatility_bps > 0: return float(meta.volatility_bps) - prices = [] + prices: list[float] = [] try: prices = list( self.data_handler.get_recent_prices( @@ -259,7 +311,7 @@ def _resolve_volatility_bps( if len(prices) < 2: spread = meta.spread_bps if meta.spread_bps else 0.0 return max(spread, 1.0) - prices_arr = np.asarray(prices, dtype=float) + prices_arr: np.ndarray = np.asarray(prices, dtype=float) returns = np.diff(prices_arr) / prices_arr[:-1] if returns.size == 0: return max(meta.spread_bps or 0.0, 1.0) @@ -309,9 +361,10 @@ def __init__( def execute(self, order: OrderEvent, current_ts: int) -> Optional[FillEvent]: self._reset_reject_tracking() - future = self.data_handler.get_future_timestamps(current_ts, self.slices) + future = self._future_timestamps(order.symbol, current_ts, self.slices) if not future: - future = [current_ts] + self._record_reject("no_future_market_event") + return None base = order.qty // len(future) remainder = order.qty % len(future) @@ -350,6 +403,20 @@ def execute(self, order: OrderEvent, current_ts: int) -> Optional[FillEvent]: slippage=avg_slippage, ) + def plan(self, order: OrderEvent, current_ts: int) -> list[ExecutionPlan]: + self._reset_reject_tracking() + future = self._future_timestamps(order.symbol, current_ts, self.slices) + if not future: + self._record_reject("no_future_market_event") + return [] + base = order.qty // len(future) + remainder = order.qty % len(future) + return [ + ExecutionPlan(ts, order, base + (1 if idx < remainder else 0)) + for idx, ts in enumerate(future) + if base + (1 if idx < remainder else 0) > 0 + ] + class VWAP(Executor): """Volume-weighted execution across future timestamps. @@ -380,9 +447,10 @@ def __init__( def execute(self, order: OrderEvent, current_ts: int) -> Optional[FillEvent]: self._reset_reject_tracking() - future = list(self.data_handler.get_future_timestamps(current_ts, self.slices)) + future = self._future_timestamps(order.symbol, current_ts, self.slices) if not future: - future = [current_ts] + self._record_reject("no_future_market_event") + return None vols = [] for ts in future: @@ -439,6 +507,27 @@ def execute(self, order: OrderEvent, current_ts: int) -> Optional[FillEvent]: slippage=avg_slippage, ) + def plan(self, order: OrderEvent, current_ts: int) -> list[ExecutionPlan]: + """Schedule ex-ante slices without peeking at realized future volume. + + Realized future volume cannot be used to size an order while the engine + is still at ``current_ts``. Until a historical intraday volume profile is + supplied explicitly, the safe engine path uses equal planned slices and + lets each slice's slippage model observe only its current tick. + """ + self._reset_reject_tracking() + future = self._future_timestamps(order.symbol, current_ts, self.slices) + if not future: + self._record_reject("no_future_market_event") + return [] + base = order.qty // len(future) + remainder = order.qty % len(future) + return [ + ExecutionPlan(ts, order, base + (1 if idx < remainder else 0)) + for idx, ts in enumerate(future) + if base + (1 if idx < remainder else 0) > 0 + ] + class ImplementationShortfall(Executor): """Front-loaded schedule approximating IS minimisation. @@ -472,9 +561,10 @@ def __init__( def execute(self, order: OrderEvent, current_ts: int) -> Optional[FillEvent]: self._reset_reject_tracking() - future = list(self.data_handler.get_future_timestamps(current_ts, self.slices)) + future = self._future_timestamps(order.symbol, current_ts, self.slices) if not future: - future = [current_ts] + self._record_reject("no_future_market_event") + return None # Geometric weights, normalised weights = np.array([self.urgency**i for i in range(len(future))], dtype=float) s = float(weights.sum()) or 1.0 @@ -519,6 +609,28 @@ def execute(self, order: OrderEvent, current_ts: int) -> Optional[FillEvent]: slippage=avg_slippage, ) + def plan(self, order: OrderEvent, current_ts: int) -> list[ExecutionPlan]: + self._reset_reject_tracking() + future = self._future_timestamps(order.symbol, current_ts, self.slices) + if not future: + self._record_reject("no_future_market_event") + return [] + weights = np.array([self.urgency**i for i in range(len(future))], dtype=float) + weights /= float(weights.sum()) or 1.0 + raw = [float(weight) * order.qty for weight in weights] + quantities = [int(value) for value in raw] + remainder = order.qty - sum(quantities) + fractions = sorted( + range(len(raw)), key=lambda idx: raw[idx] - quantities[idx], reverse=True + ) + for idx in range(remainder): + quantities[fractions[idx % len(fractions)]] += 1 + return [ + ExecutionPlan(ts, order, qty) + for ts, qty in zip(future, quantities) + if qty > 0 + ] + class SquareRootImpact(Executor): def _slippage(self, symbol: str, qty: int, price: float) -> float: @@ -559,6 +671,14 @@ def __init__( def execute(self, order: OrderEvent, current_ts: int) -> Optional[FillEvent]: self._reset_reject_tracking() + next_timestamp: int | None = None + if self.lob_tplus1: + future = self._future_timestamps(order.symbol, current_ts, 1) + if not future: + self._record_reject("no_future_market_event") + self.last_reject_reason = self._summarize_reject_reasons() + return None + next_timestamp = future[0] fills = self.book.submit(order) if not fills: self._record_reject("lob_no_fills") @@ -567,19 +687,17 @@ def execute(self, order: OrderEvent, current_ts: int) -> Optional[FillEvent]: if len(fills) == 1: fill = fills[0] # Enforce t+1 semantics by moving fill timestamp to next tick if requested - if self.lob_tplus1: - next_ts = self.data_handler.get_future_timestamps(order.timestamp, 1) - if next_ts: - fill = FillEvent( - timestamp=next_ts[0], - symbol=fill.symbol, - qty=fill.qty, - price=fill.price, - commission=fill.commission, - slippage=fill.slippage, - latency_ack=fill.latency_ack, - latency_fill=fill.latency_fill, - ) + if next_timestamp is not None: + fill = FillEvent( + timestamp=next_timestamp, + symbol=fill.symbol, + qty=fill.qty, + price=fill.price, + commission=fill.commission, + slippage=fill.slippage, + latency_ack=fill.latency_ack, + latency_fill=fill.latency_fill, + ) return fill total_qty = sum(f.qty for f in fills) @@ -591,10 +709,8 @@ def execute(self, order: OrderEvent, current_ts: int) -> Optional[FillEvent]: latency_fill = max(f.latency_fill for f in fills) ts = fills[-1].timestamp - if self.lob_tplus1: - future = self.data_handler.get_future_timestamps(order.timestamp, 1) - if future: - ts = future[0] + if next_timestamp is not None: + ts = next_timestamp return FillEvent( timestamp=ts, symbol=fills[0].symbol, @@ -605,3 +721,17 @@ def execute(self, order: OrderEvent, current_ts: int) -> Optional[FillEvent]: latency_ack=latency_ack, latency_fill=latency_fill, ) + + def plan(self, order: OrderEvent, current_ts: int) -> list[ExecutionPlan]: + """Submit to the current book, deferring any t+1 state mutation.""" + fill = self.execute(order, current_ts) + if fill is None: + return [] + return [ + ExecutionPlan( + timestamp=fill.timestamp, + order=order, + qty=abs(fill.qty), + prepared_fill=fill, + ) + ] diff --git a/src/microalpha/multiple_testing.py b/src/microalpha/multiple_testing.py new file mode 100644 index 00000000..ad4c5dfd --- /dev/null +++ b/src/microalpha/multiple_testing.py @@ -0,0 +1,147 @@ +"""Benchmark-differential multiple-testing controls. + +The public entry point implements a null-centered, synchronous max-statistic +bootstrap. Candidate returns are always evaluated as differentials to an +explicit benchmark. A single resampled time index is shared across candidates +on every draw, preserving their cross-sectional dependence. +""" + +from __future__ import annotations + +from typing import Literal, Sequence + +import numpy as np + + +def _bootstrap_indices( + n: int, + *, + method: Literal["stationary", "circular", "iid"], + block_length: int, + rng: np.random.Generator, +) -> np.ndarray: + if method == "iid": + return rng.integers(0, n, size=n) + if method == "circular": + indices: np.ndarray = np.empty(n, dtype=int) + position = 0 + while position < n: + start = int(rng.integers(0, n)) + width = min(block_length, n - position) + indices[position : position + width] = np.arange(start, start + width) % n + position += width + return indices + + probability = 1.0 / block_length + indices = np.empty(n, dtype=int) + current = int(rng.integers(0, n)) + for position in range(n): + indices[position] = current + if rng.random() < probability: + current = int(rng.integers(0, n)) + else: + current = (current + 1) % n + return indices + + +def _studentized_means(matrix: np.ndarray) -> np.ndarray: + n = matrix.shape[0] + means = matrix.mean(axis=0) + std = matrix.std(axis=0, ddof=1) + return np.divide( + np.sqrt(n) * means, + std, + out=np.zeros_like(means, dtype=float), + where=std > 0.0, + ) + + +def centered_max_statistic_test( + candidate_returns: np.ndarray, + *, + benchmark_returns: np.ndarray, + candidate_names: Sequence[str] | None = None, + seed: int = 0, + num_bootstrap: int = 2000, + method: Literal["stationary", "circular", "iid"] = "stationary", + block_length: int | None = None, +) -> dict[str, object]: + """Test whether the best candidate beats a benchmark after selection. + + The null is that no candidate has positive expected differential return. + Candidate differentials are recentered before resampling, and every + candidate uses the same resampled timestamps on a given draw. + """ + + candidates = np.asarray(candidate_returns, dtype=float) + benchmark = np.asarray(benchmark_returns, dtype=float) + if candidates.ndim == 1: + candidates = candidates[:, None] + if candidates.ndim != 2: + raise ValueError("candidate_returns must be a 1D or 2D array") + if benchmark.ndim != 1: + raise ValueError("benchmark_returns must be one-dimensional") + if candidates.shape[0] != benchmark.shape[0]: + raise ValueError("candidate and benchmark observations must align") + if candidates.shape[0] < 5: + raise ValueError("at least five aligned observations are required") + if candidates.shape[1] < 1: + raise ValueError("at least one candidate is required") + if not np.isfinite(candidates).all() or not np.isfinite(benchmark).all(): + raise ValueError("returns must be finite") + if num_bootstrap < 1: + raise ValueError("num_bootstrap must be positive") + if method not in {"stationary", "circular", "iid"}: + raise ValueError("unsupported bootstrap method") + + n_observations, n_candidates = candidates.shape + names = list(candidate_names or [f"candidate_{idx}" for idx in range(n_candidates)]) + if len(names) != n_candidates or len(set(names)) != len(names): + raise ValueError("candidate_names must be unique and match candidate columns") + + if block_length is None: + block_length = max(1, int(round(1.1447 * n_observations ** (1.0 / 3.0)))) + block_length = int(block_length) + if block_length < 1: + raise ValueError("block_length must be positive") + + differentials = candidates - benchmark[:, None] + observed_components = _studentized_means(differentials) + observed_statistic = float(max(0.0, float(np.max(observed_components)))) + centered = differentials - differentials.mean(axis=0, keepdims=True) + + rng = np.random.default_rng(seed) + distribution: np.ndarray = np.empty(num_bootstrap, dtype=float) + for draw in range(num_bootstrap): + indices = _bootstrap_indices( + n_observations, + method=method, + block_length=block_length, + rng=rng, + ) + components = _studentized_means(centered[indices, :]) + distribution[draw] = max(0.0, float(np.max(components))) + + exceedances = int(np.count_nonzero(distribution >= observed_statistic)) + p_value = float((exceedances + 1) / (num_bootstrap + 1)) + best_index = int(np.argmax(observed_components)) + + return { + "test": "centered_synchronous_max_statistic", + "null": "no_candidate_outperforms_benchmark", + "benchmark": "explicit_return_series", + "p_value": p_value, + "observed_statistic": observed_statistic, + "best_candidate": names[best_index], + "candidate_statistics": { + name: float(value) for name, value in zip(names, observed_components) + }, + "distribution": [float(value) for value in distribution], + "method": method, + "block_length": block_length, + "num_bootstrap": num_bootstrap, + "num_observations": n_observations, + "num_candidates": n_candidates, + "null_centered": True, + "synchronous_resampling": True, + } diff --git a/src/microalpha/order_flow.py b/src/microalpha/order_flow.py index e33defb2..91b164a2 100644 --- a/src/microalpha/order_flow.py +++ b/src/microalpha/order_flow.py @@ -57,6 +57,7 @@ class OrderFlowDiagnostics: _entries: Dict[str, Dict[str, Any]] = field(default_factory=dict) _active_key: str | None = None + _order_keys: Dict[int, str] = field(default_factory=dict) _errors: List[str] = field(default_factory=list) def _entry_for_key(self, key: str) -> Dict[str, Any]: @@ -75,6 +76,8 @@ def _resolve_key( ) -> str: if self._active_key is not None: return self._active_key + if order is not None and id(order) in self._order_keys: + return self._order_keys[id(order)] if signal is not None: return _signal_rebalance_key(signal, signal.timestamp) if order is not None: @@ -148,6 +151,7 @@ def record_order_created( self, order: OrderEvent, signal: SignalEvent | None = None ) -> None: key = self._resolve_key(signal=signal, order=order) + self._order_keys[id(order)] = key entry = self._entry_for_key(key) entry["orders_created_count"] = int(entry["orders_created_count"]) + 1 if abs(int(getattr(order, "qty", 0) or 0)) > 0: @@ -196,8 +200,10 @@ def record_broker_reject(self, order: OrderEvent, reason: str | None) -> None: buckets = entry["orders_rejected_reason_counts"] buckets[reason_key] = int(buckets.get(reason_key, 0)) + 1 - def record_fill(self, fill: FillEvent) -> None: - key = self._resolve_key(timestamp=fill.timestamp) + def record_fill(self, fill: FillEvent, order: OrderEvent | None = None) -> None: + # Deferred fills belong to the originating rebalance, not the later + # market timestamp at which their execution plan is materialized. + key = self._resolve_key(order=order, timestamp=fill.timestamp) entry = self._entry_for_key(key) entry["fills_count"] = int(entry["fills_count"]) + 1 try: diff --git a/src/microalpha/point_in_time.py b/src/microalpha/point_in_time.py new file mode 100644 index 00000000..97044f9d --- /dev/null +++ b/src/microalpha/point_in_time.py @@ -0,0 +1,58 @@ +"""Point-in-time availability checks for research features.""" + +from __future__ import annotations + +from collections.abc import Sequence + +import numpy as np + +from .events import LookaheadError + + +class PointInTimeViolation(LookaheadError): + """Raised when feature rows are used before they become available.""" + + def __init__(self, row_ids: Sequence[str]): + self.row_ids = tuple(str(row_id) for row_id in row_ids) + self.count = len(self.row_ids) + preview = ", ".join(self.row_ids[:5]) + suffix = "..." if self.count > 5 else "" + super().__init__( + f"{self.count} feature rows violate available_at <= decision_at: " + f"{preview}{suffix}" + ) + + +def require_point_in_time( + decision_at: Sequence[float] | np.ndarray, + available_at: Sequence[float] | np.ndarray, + *, + row_ids: Sequence[str] | None = None, +) -> None: + """Fail closed unless every row is available at its decision timestamp.""" + + decisions = np.asarray(decision_at) + availability = np.asarray(available_at) + if decisions.shape != availability.shape: + raise ValueError("decision_at and available_at must have identical shapes") + if decisions.ndim != 1: + raise ValueError("point-in-time timestamps must be one-dimensional") + if row_ids is None: + identifiers = [str(index) for index in range(decisions.size)] + else: + identifiers = [str(row_id) for row_id in row_ids] + if len(identifiers) != decisions.size: + raise ValueError("row_ids must align with timestamp rows") + if np.issubdtype(decisions.dtype, np.datetime64): + missing_decisions = np.isnat(decisions) + else: + missing_decisions = ~np.isfinite(decisions.astype(float)) + if np.issubdtype(availability.dtype, np.datetime64): + missing_availability = np.isnat(availability) + else: + missing_availability = ~np.isfinite(availability.astype(float)) + violations = np.flatnonzero( + missing_decisions | missing_availability | (availability > decisions) + ) + if violations.size: + raise PointInTimeViolation([identifiers[index] for index in violations]) diff --git a/src/microalpha/reporting/factors.py b/src/microalpha/reporting/factors.py index 17b25457..d966d2ef 100644 --- a/src/microalpha/reporting/factors.py +++ b/src/microalpha/reporting/factors.py @@ -5,7 +5,7 @@ import argparse from dataclasses import dataclass from pathlib import Path -from typing import Iterable, Sequence +from typing import Iterable, Sequence, cast import numpy as np import pandas as pd @@ -97,7 +97,8 @@ def _infer_index_frequency(index: pd.DatetimeIndex) -> tuple[str, str | None]: deltas = index.to_series().diff().dropna() if deltas.empty: return "unknown", None - return _label_from_timedelta(deltas.median()), None + median_delta = cast(pd.Timedelta, deltas.median()) + return _label_from_timedelta(median_delta), None def _compound_returns(returns: pd.Series) -> float: @@ -118,7 +119,7 @@ def _resample_returns_to_factor_index( if overlap.empty: raise ValueError("No overlapping dates between factors and returns") if rule: - resampled = returns.resample(rule).apply(_compound_returns) + resampled = cast(pd.Series, returns.resample(rule).apply(_compound_returns)) resampled = resampled.reindex(overlap) if resampled.isna().any(): missing = resampled[resampled.isna()].index @@ -151,11 +152,13 @@ def align_factor_panel( allow_resample: bool = False, resample_rule: str | None = None, ) -> tuple[pd.Series, pd.DataFrame, FactorRegressionMeta]: - _validate_datetime_index(returns.index, "returns") - _validate_datetime_index(factors.index, "factors") + returns_datetime_index = cast(pd.DatetimeIndex, returns.index) + factors_datetime_index = cast(pd.DatetimeIndex, factors.index) + _validate_datetime_index(returns_datetime_index, "returns") + _validate_datetime_index(factors_datetime_index, "factors") - returns_freq, returns_freq_inferred = _infer_index_frequency(returns.index) - factors_freq, factors_freq_inferred = _infer_index_frequency(factors.index) + returns_freq, returns_freq_inferred = _infer_index_frequency(returns_datetime_index) + factors_freq, factors_freq_inferred = _infer_index_frequency(factors_datetime_index) resampled = False resample_used = resample_rule @@ -172,7 +175,7 @@ def align_factor_panel( if resample_used is None: resample_used = factors_freq_inferred aligned_returns = _resample_returns_to_factor_index( - returns, factors.index, resample_used + returns, factors_datetime_index, resample_used ) aligned_factors = factors.loc[aligned_returns.index] resampled = True @@ -239,7 +242,7 @@ def _design_matrix( raise ValueError("No overlapping dates between factors and returns") y = aligned["excess"].to_numpy(dtype=float) X = aligned[list(factor_names)].to_numpy(dtype=float) - intercept = np.ones((X.shape[0], 1), dtype=float) + intercept: np.ndarray = np.ones((X.shape[0], 1), dtype=float) X_design = np.hstack((intercept, X)) return X_design, y diff --git a/src/microalpha/reporting/spa.py b/src/microalpha/reporting/spa.py index fd999460..4911b41e 100644 --- a/src/microalpha/reporting/spa.py +++ b/src/microalpha/reporting/spa.py @@ -1,10 +1,14 @@ -"""Hansen SPA test utilities for Microalpha parameter grids.""" +"""Benchmark-differential selection tests for parameter grids. + +The historical ``spa`` module name remains for artifact compatibility. Current +results use the null-centered synchronous max-statistic implementation from the +public Audit Lab and compare candidates with an explicit zero-return benchmark. +""" from __future__ import annotations import argparse import json -import math from dataclasses import dataclass from pathlib import Path from typing import Sequence @@ -12,6 +16,8 @@ import numpy as np import pandas as pd +from microalpha.multiple_testing import centered_max_statistic_test + @dataclass class SpaSummary: @@ -132,38 +138,6 @@ def load_grid_returns(grid_path: Path) -> pd.DataFrame: return pivot -def _stationary_bootstrap_indices( - n: int, avg_block: int, rng: np.random.Generator -) -> np.ndarray: - p = 1.0 / max(1, avg_block) - indices = np.empty(n, dtype=int) - current = int(rng.integers(0, n)) - for t in range(n): - indices[t] = current - if rng.random() < p: - current = int(rng.integers(0, n)) - else: - current = (current + 1) % n - return indices - - -def _spa_stat(diff_matrix: np.ndarray) -> tuple[float, list[float]]: - if diff_matrix.size == 0: - return 0.0, [] - T, k = diff_matrix.shape - stats: list[float] = [] - for j in range(k): - series = diff_matrix[:, j] - mean = float(np.mean(series)) - std = float(np.std(series, ddof=1)) - if std <= 0.0: - stats.append(0.0) - continue - t_val = np.sqrt(T) * max(0.0, mean) / std - stats.append(float(t_val)) - return float(max(stats)) if stats else 0.0, stats - - def compute_spa( pivot: pd.DataFrame, *, @@ -258,77 +232,36 @@ def compute_spa( diagnostics=diagnostics, ) - model_names = list(cleaned.columns) - best_idx = int(np.argmax(means)) - best_model = str(model_names[best_idx]) - diff_matrix = matrix[:, [best_idx]] - matrix - diff_matrix = np.delete(diff_matrix, best_idx, axis=1) - comparator_names = [name for i, name in enumerate(model_names) if i != best_idx] - observed_stat, component_stats = _spa_stat(diff_matrix) - - if not math.isfinite(observed_stat): - return _degenerate_summary( - "non-finite observed SPA statistic", - n_obs=n_obs, - n_strategies=n_strategies, - avg_block=avg_block, - num_bootstrap=num_bootstrap, - diagnostics=diagnostics, - ) - if any(not math.isfinite(stat) for stat in component_stats): - return _degenerate_summary( - "non-finite comparator t-stats", - n_obs=n_obs, - n_strategies=n_strategies, - avg_block=avg_block, - num_bootstrap=num_bootstrap, - diagnostics=diagnostics, - ) - - rng = np.random.default_rng(seed) - boot_stats = np.zeros(num_bootstrap, dtype=float) - centered = diff_matrix - diff_matrix.mean(axis=0) - for b in range(num_bootstrap): - indices = _stationary_bootstrap_indices(len(matrix), avg_block, rng) - boot_slice = centered[indices, :] - boot_stats[b], _ = _spa_stat(boot_slice) - finite_boot = boot_stats[np.isfinite(boot_stats)] - if finite_boot.size == 0: - return _degenerate_summary( - "bootstrap statistics are all NaN/inf", - n_obs=n_obs, - n_strategies=n_strategies, - avg_block=avg_block, - num_bootstrap=num_bootstrap, - diagnostics=diagnostics, - ) - if finite_boot.size != boot_stats.size: - diagnostics.append("dropped non-finite bootstrap statistics") - p_value = float(np.mean(finite_boot >= observed_stat)) - if not math.isfinite(p_value): - return _degenerate_summary( - "non-finite SPA p-value", + model_names = [str(name) for name in cleaned.columns] + correction = centered_max_statistic_test( + matrix, + benchmark_returns=np.zeros(n_obs, dtype=float), + candidate_names=model_names, + seed=seed, + num_bootstrap=num_bootstrap, + block_length=avg_block, + ) + best_model = str(correction["best_candidate"]) + observed_stat = float(correction["observed_statistic"]) + p_value = float(correction["p_value"]) + raw_statistics = correction["candidate_statistics"] + if not isinstance(raw_statistics, dict): + return _error_summary( + "candidate statistics missing from correction", n_obs=n_obs, n_strategies=n_strategies, avg_block=avg_block, num_bootstrap=num_bootstrap, diagnostics=diagnostics, ) - p_value = min(max(p_value, 0.0), 1.0) - - candidate_stats: list[dict[str, float | str]] = [] - for name, stat, series in zip( - comparator_names, - component_stats, - diff_matrix.T, - ): - candidate_stats.append( - { - "model": str(name), - "mean_diff": float(np.mean(series)), - "t_stat": float(stat), - } - ) + candidate_stats: list[dict[str, float | str]] = [ + { + "model": name, + "mean_diff": float(means[index]), + "t_stat": float(raw_statistics[name]), + } + for index, name in enumerate(model_names) + ] return SpaSummary( status="ok", reason=None, @@ -364,7 +297,7 @@ def write_outputs(summary: SpaSummary, json_path: Path, markdown_path: Path) -> } json_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") - lines = ["# Hansen SPA Summary", ""] + lines = ["# Benchmark-differential max-statistic summary", ""] if summary.status != "ok": lines.append(f"- **Status:** {summary.status}") if summary.status == "error": diff --git a/src/microalpha/walkforward.py b/src/microalpha/walkforward.py index 5784056f..0b5eccb0 100644 --- a/src/microalpha/walkforward.py +++ b/src/microalpha/walkforward.py @@ -40,9 +40,9 @@ ) from .market_metadata import load_symbol_meta from .metrics import compute_metrics +from .multiple_testing import centered_max_statistic_test from .order_flow import OrderFlowDiagnostics, infer_non_degenerate_reason from .portfolio import Portfolio -from .risk_stats import block_bootstrap from .runner import ( persist_config, persist_exposures, @@ -1661,7 +1661,6 @@ def bootstrap_reality_check( if not np.isfinite(best_sharpe): return None - rng = np.random.default_rng(seed) method_lower = method.lower() use_iid = method_lower == "iid" if use_iid: @@ -1672,39 +1671,55 @@ def bootstrap_reality_check( stacklevel=2, ) - exceed = 0 - distribution: List[float] = [] - for _ in range(max(1, n_bootstrap)): - max_sharpe = float("-inf") - for entry in valid: - returns = entry["returns"] - if use_iid: - sample = rng.choice(returns, size=returns.size, replace=True) - else: - sample = next( - block_bootstrap( - returns, - B=1, - method=method_lower, # type: ignore[arg-type] - block_len=block_len, - rng=rng, - ) - ) - sharpe = _annualised_sharpe(sample) - max_sharpe = max(max_sharpe, sharpe) - distribution.append(float(max_sharpe)) - if max_sharpe >= best_sharpe: - exceed += 1 - - p_value = (exceed + 1) / (len(distribution) + 1) + lengths = {int(entry["returns"].size) for entry in valid} + if len(lengths) != 1: + raise ValueError( + "reality-check candidates must share one explicitly aligned return calendar" + ) + aligned_length = lengths.pop() + if aligned_length < 5: + return None + calendars = [] + for entry in valid: + equity_df = (entry.get("metrics") or {}).get("equity_df") + if isinstance(equity_df, pd.DataFrame) and "timestamp" in equity_df: + calendars.append(equity_df["timestamp"].astype(str).to_numpy()) + if calendars and len(calendars) != len(valid): + raise ValueError("reality-check candidates have incomplete timestamp calendars") + if calendars and any( + not np.array_equal(calendars[0], calendar) for calendar in calendars[1:] + ): + raise ValueError("reality-check candidate timestamps are not aligned") + candidate_matrix = np.column_stack( + [np.asarray(entry["returns"], dtype=float) for entry in valid] + ) + candidate_names = [ + _format_param_label(entry.get("params") or {}) for entry in valid + ] + correction = centered_max_statistic_test( + candidate_matrix, + benchmark_returns=np.zeros(aligned_length, dtype=float), + candidate_names=candidate_names, + seed=seed, + num_bootstrap=max(1, n_bootstrap), + method=method_lower, # type: ignore[arg-type] + block_length=block_len, + ) return { - "p_value": float(p_value), - "distribution": distribution, + "p_value": correction["p_value"], + "distribution": correction["distribution"], "best_sharpe": float(best_sharpe), + "observed_max_statistic": correction["observed_statistic"], + "best_candidate": correction["best_candidate"], + "test": correction["test"], + "null": correction["null"], + "null_centered": True, + "synchronous_resampling": True, + "benchmark": "zero_return", "method": method_lower, - "block_length": block_len, + "block_length": correction["block_length"], "num_models": len(valid), - "num_bootstrap": len(distribution), + "num_bootstrap": correction["num_bootstrap"], } diff --git a/tests/test_audit_lab.py b/tests/test_audit_lab.py new file mode 100644 index 00000000..23e5191c --- /dev/null +++ b/tests/test_audit_lab.py @@ -0,0 +1,58 @@ +import hashlib +import json +from pathlib import Path + +from microalpha.audit_lab import run_audit_lab + + +def _hashes(root: Path) -> dict[str, str]: + return { + path.name: hashlib.sha256(path.read_bytes()).hexdigest() + for path in sorted(root.iterdir()) + if path.is_file() + } + + +def test_audit_lab_is_byte_identical_across_clean_paths(tmp_path: Path): + first = tmp_path / "first" + second = tmp_path / "another-location" + result_a = run_audit_lab(first) + result_b = run_audit_lab(second) + + assert result_a["receipt_sha256"] == result_b["receipt_sha256"] + assert _hashes(first) == _hashes(second) + + +def test_audit_lab_detects_all_four_known_failure_modes(tmp_path: Path): + result = run_audit_lab(tmp_path) + payload = result["results"] + + assert payload["leakage"]["unavailable_rows_blocked"] == 756 + assert payload["leakage"]["inflation_removed"] > 10.0 + assert payload["execution"]["inflation_removed"] > 10.0 + assert payload["costs"]["gross_sharpe"] > payload["costs"]["net_sharpe"] + assert payload["costs"]["reconciliation_error"] == 0.0 + assert payload["selection"]["noise_family_p_value"] >= 0.05 + assert payload["selection"]["planted_control_p_value"] <= 0.01 + + +def test_receipt_hashes_every_canonical_artifact(tmp_path: Path): + result = run_audit_lab(tmp_path) + receipt = json.loads((tmp_path / "receipt.json").read_text(encoding="utf-8")) + + assert len(result["receipt_sha256"]) == 64 + assert set(receipt["artifacts"]) == { + "audit_results.json", + "comparison.csv", + "audit_lab.svg", + "data_lineage.svg", + } + for name, expected in receipt["artifacts"].items(): + assert hashlib.sha256((tmp_path / name).read_bytes()).hexdigest() == expected + assert receipt["generator"]["version"] == "0.2.0" + for name, expected in receipt["generator"]["source_sha256"].items(): + source = Path("src/microalpha") / name + assert hashlib.sha256(source.read_bytes()).hexdigest() == expected + serialized = (tmp_path / "receipt.json").read_text(encoding="utf-8") + assert "/Users/" not in serialized + assert "/Volumes/" not in serialized diff --git a/tests/test_cli_audit_lab.py b/tests/test_cli_audit_lab.py new file mode 100644 index 00000000..d881c3d6 --- /dev/null +++ b/tests/test_cli_audit_lab.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +import json +import os +import subprocess +import sys +from pathlib import Path + + +def _environment() -> dict[str, str]: + env = dict(os.environ) + env["PYTHONPATH"] = "src" + return env + + +def test_audit_demo_cli_writes_receipt_and_json_stdout(tmp_path: Path) -> None: + output = tmp_path / "evidence" + completed = subprocess.run( + [ + sys.executable, + "-m", + "microalpha.cli", + "audit-demo", + "--out", + str(output), + ], + check=True, + capture_output=True, + text=True, + env=_environment(), + ) + payload = json.loads(completed.stdout) + + assert payload["receipt_sha256"] + assert (output / "receipt.json").is_file() + assert payload["results"]["claim_boundary"].endswith("not alpha or market evidence") + + +def test_cli_version_is_available() -> None: + completed = subprocess.run( + [sys.executable, "-m", "microalpha.cli", "--version"], + check=True, + capture_output=True, + text=True, + env=_environment(), + ) + assert completed.stdout.startswith("cli.py ") diff --git a/tests/test_multiple_testing.py b/tests/test_multiple_testing.py new file mode 100644 index 00000000..25a4f844 --- /dev/null +++ b/tests/test_multiple_testing.py @@ -0,0 +1,64 @@ +import numpy as np +import pytest + +from microalpha.multiple_testing import centered_max_statistic_test +from microalpha.walkforward import bootstrap_reality_check + + +def test_noise_family_is_not_promoted_after_max_statistic_correction(): + rng = np.random.default_rng(20260715) + candidates = rng.normal(0.0, 0.01, size=(756, 128)) + result = centered_max_statistic_test( + candidates, + benchmark_returns=np.zeros(756), + seed=91, + num_bootstrap=999, + block_length=8, + candidate_names=[f"noise_{idx:03d}" for idx in range(128)], + ) + + assert result["null_centered"] is True + assert result["synchronous_resampling"] is True + assert float(result["p_value"]) >= 0.05 + + +def test_planted_positive_control_survives_correction(): + rng = np.random.default_rng(7) + candidates = rng.normal(0.0, 0.01, size=(756, 32)) + candidates[:, 0] += 0.002 + result = centered_max_statistic_test( + candidates, + benchmark_returns=np.zeros(756), + seed=11, + num_bootstrap=999, + block_length=8, + candidate_names=["planted_control", *[f"noise_{idx:02d}" for idx in range(31)]], + ) + + assert result["best_candidate"] == "planted_control" + assert float(result["p_value"]) <= 0.01 + + +def test_candidate_and_benchmark_must_align(): + with pytest.raises(ValueError, match="must align"): + centered_max_statistic_test( + np.zeros((10, 2)), + benchmark_returns=np.zeros(9), + ) + + +def test_walkforward_reality_check_rejects_tail_alignment(): + results = [ + { + "returns": np.zeros(10), + "metrics": {"sharpe_ratio": 0.0}, + "params": {"model": "a"}, + }, + { + "returns": np.zeros(9), + "metrics": {"sharpe_ratio": 0.0}, + "params": {"model": "b"}, + }, + ] + with pytest.raises(ValueError, match="aligned return calendar"): + bootstrap_reality_check(results, seed=7) diff --git a/tests/test_order_flow_diagnostics.py b/tests/test_order_flow_diagnostics.py index 0ab5a043..65d3b2e3 100644 --- a/tests/test_order_flow_diagnostics.py +++ b/tests/test_order_flow_diagnostics.py @@ -19,8 +19,18 @@ def _write_prices(tmp_path, symbol, dates, prices): def test_order_flow_diagnostics_populated(tmp_path): - _write_prices(tmp_path, "AAA", ["2020-01-31", "2020-02-03"], [50.0, 52.0]) - _write_prices(tmp_path, "BBB", ["2020-01-31", "2020-02-03"], [60.0, 61.0]) + _write_prices( + tmp_path, + "AAA", + ["2020-01-31", "2020-02-03", "2020-02-04"], + [50.0, 52.0, 52.5], + ) + _write_prices( + tmp_path, + "BBB", + ["2020-01-31", "2020-02-03", "2020-02-04"], + [60.0, 61.0, 61.5], + ) universe_path = tmp_path / "universe.csv" pd.DataFrame( @@ -64,7 +74,7 @@ def test_order_flow_diagnostics_populated(tmp_path): payload = order_flow.payload() assert payload["entries"] - entry = payload["entries"][0] + entry = max(payload["entries"], key=lambda item: item["orders_created_count"]) assert entry["selected_long"] >= 1 assert entry["target_weights_nonzero_count"] >= 1 assert entry["orders_created_count"] >= 1 diff --git a/tests/test_point_in_time.py b/tests/test_point_in_time.py new file mode 100644 index 00000000..fc4b5990 --- /dev/null +++ b/tests/test_point_in_time.py @@ -0,0 +1,41 @@ +import numpy as np +import pytest + +from microalpha.point_in_time import PointInTimeViolation, require_point_in_time + + +def test_point_in_time_gate_reports_exact_rows(): + with pytest.raises(PointInTimeViolation) as captured: + require_point_in_time( + np.array([1.0, 2.0, 3.0]), + np.array([1.0, 4.0, 5.0]), + row_ids=["safe", "late-b", "late-c"], + ) + assert captured.value.count == 2 + assert captured.value.row_ids == ("late-b", "late-c") + + +def test_point_in_time_gate_accepts_available_rows(): + require_point_in_time( + np.array([1.0, 2.0, 3.0]), + np.array([0.0, 2.0, 2.5]), + row_ids=["a", "b", "c"], + ) + + +@pytest.mark.parametrize( + ("decision_at", "available_at"), + [ + (np.array([1.0]), np.array([np.nan])), + ( + np.array(["2026-01-02"], dtype="datetime64[D]"), + np.array(["NaT"], dtype="datetime64[D]"), + ), + ], +) +def test_point_in_time_gate_fails_closed_on_missing_timestamps( + decision_at, available_at +): + with pytest.raises(PointInTimeViolation) as captured: + require_point_in_time(decision_at, available_at, row_ids=["missing"]) + assert captured.value.row_ids == ("missing",) diff --git a/tests/test_reporting_spa.py b/tests/test_reporting_spa.py index 58e641aa..07700a3c 100644 --- a/tests/test_reporting_spa.py +++ b/tests/test_reporting_spa.py @@ -67,7 +67,24 @@ def test_compute_spa_null_case_identical_strategies() -> None: summary = compute_spa(obs, avg_block=5, num_bootstrap=200, seed=1) assert summary.status == "ok" assert summary.p_value is not None - assert summary.p_value >= 0.8 + assert 0.0 <= summary.p_value <= 1.0 + assert summary.candidate_stats[0]["t_stat"] == summary.candidate_stats[1]["t_stat"] + + +def test_compute_spa_does_not_promote_all_negative_candidates() -> None: + rng = np.random.default_rng(321) + obs = pd.DataFrame( + { + "less_bad": rng.normal(-0.001, 0.01, size=400), + "worse": rng.normal(-0.003, 0.01, size=400), + } + ) + summary = compute_spa(obs, avg_block=8, num_bootstrap=499, seed=9) + + assert summary.status == "ok" + assert summary.observed_stat == 0.0 + assert summary.p_value is not None + assert summary.p_value >= 0.95 def test_compute_spa_dominant_strategy() -> None: diff --git a/tests/test_tplus1_execution.py b/tests/test_tplus1_execution.py index 48ebde4d..3f6024e9 100644 --- a/tests/test_tplus1_execution.py +++ b/tests/test_tplus1_execution.py @@ -2,8 +2,14 @@ from microalpha.broker import SimulatedBroker from microalpha.engine import Engine -from microalpha.events import MarketEvent, SignalEvent -from microalpha.execution import Executor +from microalpha.events import MarketEvent, OrderEvent, SignalEvent +from microalpha.execution import ( + TWAP, + VWAP, + Executor, + ImplementationShortfall, + LOBExecution, +) from microalpha.portfolio import Portfolio @@ -25,6 +31,37 @@ def get_latest_price(self, symbol, timestamp): return None if event is None else event.price +class SymbolAwareStubData(StubData): + def get_future_timestamps(self, start_timestamp, n, symbol=None): + futures = [ + event.timestamp + for event in self.events + if event.timestamp > start_timestamp + and (symbol is None or event.symbol == symbol) + ] + return sorted(futures)[:n] + + +class ClockGuardedData(StubData): + """Fails if an executor reads a price before its event is streamed.""" + + def __init__(self, events): + super().__init__(events) + self.current_timestamp = None + self.price_reads = [] + + def stream(self): + for event in self.events: + self.current_timestamp = event.timestamp + yield event + + def get_latest_price(self, symbol, timestamp): + self.price_reads.append((self.current_timestamp, timestamp)) + if self.current_timestamp is None or timestamp > self.current_timestamp: + raise AssertionError("future market price was read before its event") + return super().get_latest_price(symbol, timestamp) + + class SingleTradeStrategy: def __init__(self): self.triggered = False @@ -62,3 +99,90 @@ def test_orders_fill_no_earlier_than_next_tick(): assert portfolio.fill_timestamps assert all(ts >= 2 for ts in portfolio.fill_timestamps) + + +def test_future_fill_is_materialized_only_when_engine_reaches_timestamp(): + events = [ + MarketEvent(1, "SPY", 100.0, 1.0), + MarketEvent(2, "SPY", 101.0, 1.0), + MarketEvent(3, "SPY", 102.0, 1.0), + ] + data = ClockGuardedData(events) + portfolio = LoggingPortfolio(data, 100000.0) + + class StateObservingStrategy(SingleTradeStrategy): + def __init__(self): + super().__init__() + self.observed = [] + + def on_market(self, event): + position = portfolio.positions.get("SPY") + self.observed.append( + { + "timestamp": event.timestamp, + "qty": 0 if position is None else position.qty, + "cash": portfolio.cash, + } + ) + return super().on_market(event) + + strategy = StateObservingStrategy() + engine = Engine( + data, + strategy, + portfolio, + SimulatedBroker(Executor(data)), + rng=np.random.default_rng(11), + ) + engine.run() + + assert strategy.observed[0] == {"timestamp": 1, "qty": 0, "cash": 100000.0} + assert strategy.observed[1]["timestamp"] == 2 + assert strategy.observed[1]["qty"] > 0 + assert strategy.observed[1]["cash"] < 100000.0 + assert portfolio.fill_timestamps == [2] + assert all(read_ts <= engine_ts for engine_ts, read_ts in data.price_reads) + + +def test_all_safe_planners_fail_closed_without_a_future_tick(): + data = StubData([MarketEvent(1, "SPY", 100.0, 1.0)]) + order = OrderEvent(1, "SPY", 10, "BUY") + planners = [ + Executor(data), + TWAP(data, slices=2), + VWAP(data, slices=2), + ImplementationShortfall(data, slices=2), + ] + for planner in planners: + assert planner.plan(order, 1) == [] + assert planner.last_reject_reason == "no_future_market_event" + assert planner.execute(order, 1) is None + assert planner.last_reject_reason == "no_future_market_event" + + class BookThatMustNotBeTouched: + def submit(self, _order): + raise AssertionError("terminal t+1 order reached the book") + + lob = LOBExecution(data, book=BookThatMustNotBeTouched(), lob_tplus1=True) + assert lob.plan(order, 1) == [] + assert lob.last_reject_reason == "no_future_market_event" + + +def test_async_multiasset_order_uses_next_event_for_same_symbol(): + events = [ + MarketEvent(1, "AAA", 100.0, 1.0), + MarketEvent(2, "BBB", 50.0, 1.0), + MarketEvent(3, "AAA", 101.0, 1.0), + ] + data = SymbolAwareStubData(events) + strategy = SingleTradeStrategy() + portfolio = LoggingPortfolio(data, 100000.0) + engine = Engine( + data, + strategy, + portfolio, + SimulatedBroker(Executor(data)), + rng=np.random.default_rng(11), + ) + engine.run() + assert portfolio.fill_timestamps == [3]