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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@ data → universe → factors(特征) → alpha(合成/预测) → portfolio(+ri
- 财务字段**一次 fetch + 一次 as-of 对齐**;合成仍 EqualWeightAlpha 等权;`drop_missing` 要求全因子齐备(披露);demo+财务因子仍可读报错;重名因子报错。
- 报告:active factor list / per-factor coverage+IC+分位 / combo score 诊断 / 财务 coverage 按字段(TRADED vs diagnostic);`output.baseline_report_name` 分离 phase3 报告。
- 真实结果:多因子 annual **−9.05%**(单因子 −10.19%);momentum_20 IC 0.0083(跨 run 一致)/ roe 0.0006 / netprofit_yoy 0.0001 / combo −0.0038;财务 ann_date 覆盖 100%;PIT SW-L1 98.53%。回归不破:phase2 rerun −10.19%/0.0083 不变,demo 0.96/0.84 不变。
- ✅ 当前质量门:`pytest -p no:cacheprovider` **249 passed**;`ruff` clean;`validate-config`(demo + `example_tushare.yaml` + `phase2_real_baseline.yaml` + `phase3_real_multifactor.yaml`)OK;`run-phase0`(demo)OK。
- 🔧 **Phase 3-2 walk-forward IC 加权 alpha**(`p3-ic-weighted-alpha` 分支,代劳待验收):`alpha/ic_weight.py::RollingICWeightAlpha`(`alpha.model: ic_weighted`);EqualWeightAlpha 仍默认+回归基线;不调参、非收益声明。
- Lookahead 边界测试锁定:(factor[t], fwd_h[t]) 仅**已实现**(`t+h <= d` 交易日序)进权重;扰动未实现 fwd 权重不变;fwd 只进 `alpha.fit`,factors 层绝不接触。
- rolling(默认 60d/min20)/expanding;历史不足→该日退回等权(计数披露);权重 L1 归一化、保留符号。报告新增 **Alpha model** 必含小节(模型/超参/覆盖率/每期权重表/fallback/非调参声明)。
- `config/phase3_real_ic_weighted.yaml` 与 phase3_real_multifactor 唯一差异 alpha.model(直接可比)。
- 真实结果:annual **−3.57%**(等权 −9.05%),训练覆盖 201/221(20 fallback 全在窗口攒满前)。⚠️ 优于等权非业绩声明——单年窗口+权重逐期翻号即小样本不稳定,照实披露。回归不破:等权 rerun −9.05%/0.0083 不变,demo 0.96/0.84 不变。
- ✅ 当前质量门:`pytest -p no:cacheprovider` **269 passed**;`ruff` clean;`validate-config`(demo + `example_tushare.yaml` + `phase2_real_baseline.yaml` + `phase3_real_multifactor.yaml` + `phase3_real_ic_weighted.yaml`)OK;`run-phase0`(demo)OK。
- ⚠️ 剩余(已显式披露):日线 only、demo 路径非真数据。
- 路线图下一步:alpha 加权合成(IC 加权/回归)/ 分钟级(architecture.html §11)。
- 路线图下一步:样本外/IC 稳定性检验 / 分钟级(architecture.html §11)。
1 change: 1 addition & 0 deletions BIAS_AUDIT.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- `momentum_20[t] = close[t] / close[t-window] - 1`,严格只用 t 及之前的收盘价(`groupby(symbol).shift(window)`)。
- 事件顺序固定:在 t 收盘计算因子,t 收盘后调仓,从 t+1 持有。回测用**下一持有期**的收益结算,绝不使用因子已经看见的当日收益。
- forward returns 只在 `analytics/` 计算,因子层永远拿不到未来收益(INV-001)。
- **alpha 层 walk-forward 权重训练(P3-2,`alpha.model: ic_weighted`)**:alpha 层是**唯一**允许看 forward returns 的层,且只用于拟合因子权重——训练严格 walk-forward:对每个打分日 d,(factor[t], fwd_h[t]) 对只有在**已实现**(按交易日序 `t + h <= d`,h 日 forward return 在 t+h 才实现)时才进入 d 的权重;扰动任何未实现的 forward return 不改变 d 的权重(扰动测试锁定)。窗口 rolling(默认,保守)或 expanding;历史不足(任一因子有效已实现 IC < min_periods)→ 该日**退回等权**并计数披露;权重 L1 归一化、保留符号。固定配方、不调参,非收益声明。factors 层边界不变:forward returns 由 pipeline 在 alpha 边界计算、只传 `alpha.fit`,因子计算在其之前完成且永不接触。

## PIT 成分股

Expand Down
11 changes: 9 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,13 @@ data → universe → factors(特征) → alpha(合成/预测) → portfolio(+ri
- 报告增强:active factor list / per-factor coverage+IC+分位 / **combo score** 诊断 / 财务 coverage **按字段**披露(TRADED vs diagnostic 角色标注);`output.baseline_report_name` 使 phase3 报告独立于 phase2。
- **真实结果**(SSE50 2023-07~2024-06,~14min):多因子 annual **−9.05%**(单因子 −10.19%);per-factor IC: momentum_20 0.0083(与 phase2 run 完全一致,跨 run 一致性实证)/ roe 0.0006 / netprofit_yoy 0.0001;combo IC −0.0038;财务两字段 ann_date 覆盖 **100%**;PIT SW-L1 98.53%。财务因子在该小截面短窗口 IC≈0,照实披露——这是 plumbing 验证。
- **回归不破**:phase2 单因子真实 rerun annual −10.19% / IC 0.0083 不变;demo ic 0.96/annual 0.84 不变。
- ✅ 质量门:`pytest` **249 passed**(P0=97 / P1=78 / P2-1=20 / P2-2=22 / P2-3=14 / P2-4=8 / P3-1=10);`ruff` clean;`validate-config`(demo + `example_tushare.yaml` + `phase2_real_baseline.yaml` + `phase3_real_multifactor.yaml`)+ `run-phase0`(demo)均 OK。
- 🔧 **Phase 3-2 walk-forward IC 加权 alpha**(`p3-ic-weighted-alpha` 分支,代劳待验收):新增 `alpha/ic_weight.py::RollingICWeightAlpha`(`alpha.model: ic_weighted`);EqualWeightAlpha 仍默认 + 回归基线。**不调参、非收益声明**。
- **Lookahead 边界(测试锁定)**:训练严格 walk-forward——(factor[t], fwd_h[t]) 只有**已实现**(交易日序 `t+h <= d`)才进日 d 的权重;**扰动未实现 forward returns 权重不变**(扰动测试)+ `t+h` 切片精确边界测试。forward returns 由 pipeline 在 alpha 边界计算、只传 `alpha.fit`,factors 层照旧绝不接触(不变量 #1)。
- rolling(默认保守,窗口=60 交易日,min_periods=20)/ expanding 可配;历史不足 → 该日**退回等权**(与 EqualWeightAlpha 逐 bit 一致)并计数披露;权重 L1 归一化、保留符号(负 IC 因子负权重)。
- 报告新增 **Alpha model** 必含小节:active model / 超参 / 训练覆盖率 + fallback 次数 / 每调仓日生效权重表(fallback 行标注)/ 非调参声明 + 等权基线对比指引。
- `config/phase3_real_ic_weighted.yaml`:与 phase3_real_multifactor **唯一差异是 alpha.model**(universe/window/因子/中性化全同,直接可比)。
- **真实结果**(SSE50 2023-07~2024-06,~14min):annual **−3.57%**(等权 −9.05% / 单因子 −10.19%),maxDD −12.93%,训练覆盖 **201/221**(20 个 fallback 全在窗口攒满前,90.95%)。⚠️ 优于等权**不是**业绩声明——单年窗口 + 权重逐期翻号(如 momentum_20 从 −0.58 到 +0.36)正是小样本不稳定的体现,照实披露。
- **回归不破**:phase3 等权真实 rerun annual −9.05% / IC 0.0083 不变;demo equal_weight ic 0.96/annual 0.84 不变(测试锁定)。
- ✅ 质量门:`pytest` **269 passed**(P0=97 / P1=78 / P2-1=22 / P2-2=22 / P2-3=14 / P2-4=8 / P3-1=10 / P3-2=18);`ruff` clean;`validate-config`(demo + `example_tushare.yaml` + `phase2_real_baseline.yaml` + `phase3_real_multifactor.yaml` + `phase3_real_ic_weighted.yaml`)+ `run-phase0`(demo)均 OK。
- ⚠️ 剩余(已显式披露):日线 only、demo 路径非真数据。
- 路线图下一步:alpha 加权合成(IC 加权/回归)/ 分钟级(architecture.html §11)。
- 路线图下一步:样本外/IC 稳定性检验 / 分钟级(architecture.html §11)。
40 changes: 40 additions & 0 deletions RUNBOOK.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,46 @@ What P3-1 changes (locked by tests):
labelled TRADED factor vs diagnostic-only. Standard analytics stays
report-only (alphalens cross-checks the primary factor).

## Phase 3-2 — walk-forward IC-weighted alpha

P3-2 adds the first LEARNED factor combination while keeping the lookahead
boundary auditable: `alpha.model: ic_weighted` weights each factor by its mean
cross-sectional rank IC over a trailing window of **realized** observations
only. `EqualWeightAlpha` stays the default and the regression baseline.
Documented by `config/phase3_real_ic_weighted.yaml` (identical universe /
window / factors / neutralization to `phase3_real_multifactor.yaml` — the ONLY
change is the alpha model, so the two runs are directly comparable).

```bash
# validate (no network)
... -m qt.cli validate-config --config config/phase3_real_ic_weighted.yaml
# run the ic-weighted real baseline (network + token; heavy, ~10-30 min)
... -m qt.cli run-phase2-baseline --config config/phase3_real_ic_weighted.yaml
```

Lookahead boundary (locked by tests):

- The factor layer NEVER sees forward returns (invariant #1 unchanged); the
pipeline computes them at the alpha boundary and hands them ONLY to
``alpha.fit``.
- Training is **walk-forward**: at each date d, a (factor[t], fwd_h[t]) pair is
admissible only once REALIZED — ``t + h <= d`` in trading-day positions (the
h-day forward return of factor date t realizes at t+h). Perturbing any
not-yet-realized forward return cannot change d's weights (perturbation test).
- Window modes: ``rolling`` (trailing `window` trading days, the conservative
default) or ``expanding`` (all realized history).
- **Fallback**: if any factor has fewer than ``min_periods`` valid realized ICs
in the window (or the ICs are degenerate), that date falls back to the EQUAL
WEIGHT mean — identical to `EqualWeightAlpha` — and is counted + disclosed in
the report.
- Weights are L1-normalized and sign-preserving (a negative-IC factor gets a
negative weight). Fixed recipe, no parameter search — NOT a tuned-performance
claim.

The baseline report gains an **Alpha model** section: active model,
hyper-params, training coverage (fallback count), and the effective weights at
every settled rebalance date (fallback rows flagged).

## Quality gate

```bash
Expand Down
31 changes: 25 additions & 6 deletions TEST_REPORT.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# TEST_REPORT — Phase 0 + Phase 1 + Phase 2 + Phase 3-1 (bias-boundary → execution realism → PIT industry → standard analytics → multi-factor)
# TEST_REPORT — Phase 0 + Phase 1 + Phase 2 + Phase 3 (bias-boundary → execution realism → PIT industry → standard analytics → multi-factor → walk-forward IC alpha)

## Commands

Expand All @@ -11,19 +11,20 @@ Run from the repo root with the project python (env `quant_mf`):
/home/shaofl/Development/env_tools/envs/quant_mf/bin/python -m qt.cli validate-config --config config/example_tushare.yaml
/home/shaofl/Development/env_tools/envs/quant_mf/bin/python -m qt.cli validate-config --config config/phase2_real_baseline.yaml
/home/shaofl/Development/env_tools/envs/quant_mf/bin/python -m qt.cli validate-config --config config/phase3_real_multifactor.yaml
/home/shaofl/Development/env_tools/envs/quant_mf/bin/python -m qt.cli validate-config --config config/phase3_real_ic_weighted.yaml
/home/shaofl/Development/env_tools/envs/quant_mf/bin/python -m qt.cli run-phase0 --config config/example.yaml
```

## Results

| Gate | Command | Result |
|---|---|---|
| Unit + integration | `pytest -q` | **249 passed, 0 failed** |
| Unit + integration | `pytest -q` | **269 passed, 0 failed** |
| Lint | `ruff check .` | **All checks passed** |
| Config validation | `validate-config` (demo + `example_tushare.yaml` + `phase2_real_baseline.yaml` + `phase3_real_multifactor.yaml`) | exit `0`, prints `OK` |
| Config validation | `validate-config` (demo + `example_tushare.yaml` + `phase2_real_baseline.yaml` + `phase3_real_multifactor.yaml` + `phase3_real_ic_weighted.yaml`) | exit `0`, prints `OK` |
| End-to-end run | `run-phase0` (demo) | exit `0`, writes `artifacts/reports/phase0_summary.md` |

Counts below are the actual per-file `pytest` numbers (sum = 249).
Counts below are the actual per-file `pytest` numbers (sum = 269).

## Per-file breakdown — Phase 0 core (97)

Expand Down Expand Up @@ -73,7 +74,7 @@ Counts below are the actual per-file `pytest` numbers (sum = 249).

| Test file | Tests | Feature |
|---|---|---|
| `test_phase2_baseline.py` | 20 | collectors, demo/real guard, report-field contract, no-secret-leak, settled-vs-candidate dates, loaded-vs-in-window membership, list_date + PIT-industry coverage, standard-analytics cross-check, P3-1 factor-list/per-field-role/per-factor-table/report-name |
| `test_phase2_baseline.py` | 22 | collectors, demo/real guard, report-field contract, no-secret-leak, settled-vs-candidate dates, loaded-vs-in-window membership, list_date + PIT-industry coverage, standard-analytics cross-check, P3-1 factor-list/per-field-role/per-factor-table/report-name, P3-2 alpha-model disclosure (equal-weight line + ic-weighted weights/fallback/no-claim) |

## Per-file breakdown — Phase 2-2 execution realism (22)

Expand Down Expand Up @@ -101,7 +102,14 @@ Counts below are the actual per-file `pytest` numbers (sum = 249).
| Test file | Tests | Red-line / feature |
|---|---|---|
| `test_multifactor_pipeline.py` | 10 | all enabled factors built (order / disabled / duplicate / none), financials fetched ONCE for all fields + as-of both columns + input immutability, demo+financial readable error, e2e demo multi-factor panel + per-factor/combo analytics + primary==first, report factor list + combo + no secret, single-factor legacy shape |
| **Total (P0 + P1 + P2-1..P2-4 + P3-1)** | **249** | |

## Per-file breakdown — Phase 3-2 walk-forward IC alpha (18)

| Test file | Tests | Red-line / feature |
|---|---|---|
| `test_ic_weight_alpha.py` | 12 | **lookahead red-line**: perturbing unrealized forward returns cannot change weights; exact `t + h <= d` realization cutoff (min_periods boundary); insufficient-history equal-weight fallback (== EqualWeightAlpha row mean); single-factor degeneration to ±1; L1 normalization + sign preservation; degenerate-IC fallback; rolling-vs-expanding window; fit requires forward_returns; dated-cross-section contract; input immutability; weights/fallback log |
| `test_ic_alpha_pipeline.py` | 6 | alpha dispatch by config (equal_weight / ic_weighted + params / unknown = ConfigError), equal-weight default keeps exact demo numbers (ic 0.96 / annual 0.84) + report line, ic_weighted demo e2e (summary, weights log, early-fallback→late-trained, L1 rows, report disclosure, no secret), ic-weights differ from equal weight on diverging-IC synthetic data |
| **Total (P0 + P1 + P2-1..P2-4 + P3-1 + P3-2)** | **269** | |

## Real-data validation (manual, not in CI — TEST-002 keeps the suite network-free)

Expand Down Expand Up @@ -163,6 +171,17 @@ Counts below are the actual per-file `pytest` numbers (sum = 249).
diagnostics, and per-field ann_date coverage labelled TRADED vs
diagnostic-only. `output.baseline_report_name` keeps the phase3 report file
separate from the phase2 one.
- **P3-2 walk-forward IC alpha (locked by tests):** `alpha.model: ic_weighted`
weights factors by mean realized rank IC over a trailing window — a
(factor[t], fwd_h[t]) pair enters date d's weights only once REALIZED
(`t + h <= d`, trading days); a perturbation test proves unrealized forward
returns cannot change any date's weights. Forward returns are computed at the
alpha boundary and handed ONLY to `alpha.fit` (the factor layer never sees
them, invariant #1). Insufficient realized history (< min_periods valid ICs
for any factor) falls back per-date to the EQUAL-WEIGHT mean — bitwise the
EqualWeightAlpha combination — and is counted in the report. Weights are
L1-normalized, sign-preserving. `EqualWeightAlpha` remains the default; its
demo numbers (ic 0.96 / annual 0.84) are locked unchanged.
- A duplicate test-function name across two files was found and renamed during P2-2
(it had been silently shadowing one test in the full-suite run). A second, harmless
duplicate (`test_enrich_does_not_mutate_input` in two files) was verified NOT to drop
Expand Down
4 changes: 4 additions & 0 deletions alpha/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
class AlphaModel(ABC):
"""Abstract multi-factor combination / prediction model."""

# pipeline hint: True when fit() needs historical forward returns (the
# pipeline then computes and passes them to fit — and ONLY to fit).
requires_forward_returns: bool = False

@abstractmethod
def fit(
self,
Expand Down
Loading