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
+[](https://github.com/MateoBodon/microalpha/actions/workflows/ci.yml)
[](https://mateobodon.github.io/microalpha/)
+[](pyproject.toml)
+[](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.
+
-| 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
+
+
+
+- **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.
-
-
-
-| 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.
+
+
+
+## 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 @@
+
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 @@
+
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.
+
-## 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.
+
----
+## 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
+
+
+
+| 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.
+
+
+
+## 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 = [
+ '",
+ ]
+ )
+ 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 = [
+ '",
+ ]
+ )
+ 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]