项目文件夹

文件
2026-07-13 13:26:28 +08:00

238 行
8.6 KiB
Markdown

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# Notebook Test Suite
Every notebook in this repository is tested via [Papermill](https://papermill.readthedocs.io/) parameter injection. The same code path always runs — only the data scale differs between production and test.
---
## Quick Start
```bash
# Run all environments (ml4t, gpu, py312, benchmark, neo4j)
./scripts/run_all_tests.sh
# Run one environment
./scripts/run_all_tests.sh ml4t
# Rerun everything (ignore already-passed)
./scripts/run_all_tests.sh --force
# Run a specific notebook via pytest
docker compose run --rm ml4t pytest tests/test_chapter_notebooks.py -v -k "01_timegan"
# Run locally (with uv)
uv run pytest tests/test_chapter_notebooks.py -v -k "11_ml_pipeline"
```
---
## How It Works
### Papermill Parameter Injection
Every notebook has a `# %% tags=["parameters"]` cell with **production defaults** — the values readers see in the book:
```python
# %% tags=["parameters"]
MAX_SYMBOLS = 0 # 0 = all symbols (production)
N_EPOCHS = 500
START_DATE = "2006-01-01"
```
During testing, Papermill creates an **injected cell** after the tagged cell that overrides selected values:
```python
# Injected by Papermill
MAX_SYMBOLS = 15 # Reduced for fast execution
N_EPOCHS = 2
```
The notebook code sees only the final (overridden) values. **There are no `if TEST:` branches** — the same code path always runs, just with less data or fewer iterations.
### Output Isolation
Tests set `ML4T_OUTPUT_DIR` to a temp directory. All notebook writes (`get_output_dir()`, `get_case_study_dir()`) redirect there, so production artifacts (trained models, backtest results) are never overwritten.
### Seeded Fixtures
Downstream notebooks (Ch11+) need upstream pipeline outputs (features, labels, model predictions). Rather than running the full pipeline during every test, `tests/fixtures/seed_results.py` creates minimal but schema-correct fixtures:
- Registry databases (`run_log/registry.db`) with realistic training_runs, prediction_sets, and backtest entries
- Feature parquets (350 rows × 15 symbols)
- Label parquets (5 label variants per case study)
- Prediction parquets (200 rows per model)
Fixtures are deterministic and only written if the file doesn't already exist — real upstream results take priority.
---
## Test Data
### Architecture
Tests require two things: **raw market data** (what loaders read) and **pipeline intermediates** (what downstream notebooks consume). Both live in a private repo that CI pulls automatically.
```
~/ml4t/test-data/ # Local clone of ml4t/third-edition-test-data (~2 GB private GitHub repo)
├── data/ # Pre-subsampled raw data (553 MB)
│ ├── etfs/ # 15 most liquid ETFs (full date range)
│ ├── crypto/ # 5 largest perps
│ ├── futures/ # 8 CME products
│ ├── fx/ # 8 major pairs
│ ├── equities/ # 50 US stocks, 3 NASDAQ-100 (minute bars)
│ │ ├── microstructure/ # Synthetic ITCH/LOB/MBO for Ch03
│ │ └── firm_characteristics/ # 200 most-observed per month
│ ├── factors/ # Fama-French + AQR
│ └── manifest.json # Symbol counts per dataset
└── intermediates/ # Pre-computed pipeline outputs (301 MB)
├── etfs/ # registry.db, features, labels, predictions
├── crypto_perps_funding/
├── ... # All 9 case studies
└── _metadata.json # Generation timestamp and parameters
```
**Key design decisions:**
- **Same schema, fewer symbols**: Loaders work without code changes. Full date ranges are preserved so cross-validation folds remain valid.
- **Pre-computed intermediates**: Running all 9 case study pipelines from scratch takes ~25 minutes. Pre-computing once and shipping the results lets CI focus on testing individual notebooks.
- **Fixtures as safety net**: If intermediates are missing or a new notebook needs data that wasn't pre-computed, `seed_results.py` generates minimal placeholders at test time.
### Running Tests Locally
**With the test-data repo** (full test coverage):
```bash
# Clone test data once
git clone git@github.com:ml4t/third-edition-test-data.git ~/ml4t/test-data
# Point tests at it
export ML4T_DATA_PATH=~/ml4t/test-data/data
export ML4T_OUTPUT_DIR=/tmp/ml4t-test-output
# Seed intermediates
mkdir -p $ML4T_OUTPUT_DIR
cp -r ~/ml4t/test-data/intermediates/* $ML4T_OUTPUT_DIR/
# Run tests
uv run pytest tests/test_chapter_notebooks.py -v -k "11_ml_pipeline"
```
**Without the test-data repo** (limited coverage):
```bash
# Point at your production data
export ML4T_DATA_PATH=/path/to/your/data
export ML4T_OUTPUT_DIR=/tmp/ml4t-test-output
# Fixtures will be auto-generated for missing intermediates
uv run pytest tests/test_chapter_notebooks.py -v -k "01_ols_inference"
```
### Regenerating Test Data
If the data schema or pipeline logic changes, regenerate the test data:
```bash
# 1. Subsample raw data
uv run python tests/create_test_data.py --output ~/ml4t/test-data
# 2. Generate pipeline intermediates (runs all 9 case studies, ~25 min)
ML4T_DATA_PATH=~/ml4t/test-data/data \
ML4T_OUTPUT_DIR=~/ml4t/test-data/intermediates \
uv run python tests/_internal/generate_intermediates.py
# 3. Commit and push to test-data repo
cd ~/ml4t/test-data && git add -A && git commit -m "regenerate test data"
```
---
## Environments
Each notebook is assigned to exactly one Docker environment via `docker_env` in `overrides.yaml`.
| Environment | Docker Service | Notebooks | What It Provides |
|-------------|---------------|-----------|-----------------|
| `ml4t` | `ml4t` | ~410 | CPU, all Python packages |
| `gpu` | `ml4t-gpu` | ~31 | NVIDIA GPU (PyTorch CUDA) |
| `py312` | `py312` | ~10 | gensim, signatory, esig, pfhedge, tfcausalimpact (Python 3.12) |
| `benchmark` | `benchmark` + database services | 2 | TimescaleDB, ClickHouse, QuestDB, InfluxDB |
| `neo4j` | `ml4t` + Neo4j service | 7 | Neo4j graph database |
Notebooks default to `ml4t` unless tagged otherwise. Multi-environment tags (e.g., `docker_env: ml4t+neo4j+gpu`) require all listed services.
The test runner (`scripts/run_all_tests.sh`) iterates over environments, running only notebooks tagged for each one.
---
## Override Format
Per-notebook configuration lives in `tests/overrides.yaml`:
```yaml
05_synthetic_data/01_timegan:
docker_env: gpu # Runs only in GPU environment
timeout: 600 # Max seconds before test is killed
parameters: # Papermill parameter overrides
TRAIN_STEPS: 100
BATCH_SIZE: 32
case_studies/etfs/07_gbm:
timeout: 300
parameters:
MAX_SYMBOLS: 15
START_DATE: "2020-01-01"
26_mlops_governance/05b_feast_live:
skip: true # Never runs
skip_reason: "Requires Feast feature server"
```
**Override fields:**
| Field | Default | Purpose |
|-------|---------|---------|
| `timeout` | 300 | Max seconds per notebook |
| `docker_env` | `ml4t` | Which Docker environment to use |
| `skip` | false | Skip this notebook entirely |
| `skip_reason` | — | Reason displayed in test output |
| `parameters` | {} | Papermill parameter overrides |
---
## Files
| File | Purpose |
|------|---------|
| `test_chapter_notebooks.py` | Parametrized tests for Ch01-Ch27 teaching notebooks |
| `test_case_studies.py` | Parametrized tests for all 9 case study pipelines |
| `test_backtest_schedule.py` | Backtest-specific integration tests |
| `conftest.py` | Session fixtures: data dirs, output seeding, config patching |
| `pm_helpers.py` | Papermill execution, override loading, Docker env detection |
| `overrides.yaml` | Per-notebook parameter overrides, timeouts, skip/env tags |
| `fixtures/seed_results.py` | Registry DB and parquet fixture generation |
| `_internal/` | Scripts for generating test data and intermediates (not run during tests) |
---
## CI Pipeline
GitHub Actions runs tests on every push and PR:
1. **Checkout** code + test data repo (via deploy key)
2. **Seed** intermediates into `ML4T_OUTPUT_DIR`
3. **Run** pytest inside Docker containers (one job per environment)
4. **Upload** JUnit XML results as artifacts
See `.github/workflows/test.yml` for the full configuration.
---
## Adding a New Notebook
1. Add a `# %% tags=["parameters"]` cell with production defaults
2. Add an entry in `overrides.yaml` with timeout and parameter overrides
3. If GPU-required, add `docker_env: gpu`
4. Run: `./scripts/run_all_tests.sh ml4t` (or the relevant environment)
5. If the notebook depends on upstream pipeline outputs, ensure `seed_results.py` generates the necessary fixtures