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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@

## Unreleased

### Public validation: signal-date availability

- The generic and full AkShare validation scripts now select portfolios and
predict scores using signal-date availability. Previously they also required
next-day availability, so a known future halt or missing row changed today's
holdings. Historical training labels and trailing IC estimates still require
observations on both dates.
- New validation metadata records `availability_protocol`. Reports made with
v0.4.1 or earlier need revalidation; changing the selection mask can change
weights, turnover, and net performance. This is a causality fix, not alpha
evidence or a halt-aware execution model.

### Markowitz optimiser: DPP form, reliability, and caching

- The risk term is now `sum_squares(F.T @ w)` with `Sigma = F F^T` instead of
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,9 @@ Archived public-data snapshot (2026-07-29, before v0.4.1):

**The numbers below predate the v0.4.1 mask-first and halt-safe return fixes.**
They remain historical records and have not been revalidated with the corrected
protocol. Re-run with v0.4.1 or later before drawing conclusions. The detailed
protocol. The [October 17 audit](docs/audits/public-revalidation-20261017/README.md)
also found next-day availability entering portfolio selection through v0.4.1;
revalidation requires that correction too. The detailed
dashboard includes the protocol note, cost-sensitivity charts, and reproduction
commands.

Expand Down
4 changes: 3 additions & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@ Demo 跑通后,可以继续[查看 benchmark](docs/benchmark_board.md)或
## 归档公开验证(2026-07-29)

**以下数字生成于 v0.4.1 修复前,尚未用修正后的 mask-first 与停牌收益口径重新验证。**
保留原始报告便于审计;不能把这些数字当作当前版本的验证结果。请使用 v0.4.1 或更新版本重跑。
保留原始报告便于审计;不能把这些数字当作当前版本的验证结果。
[10 月 17 日审计](docs/audits/public-revalidation-20261017/README.md)还发现 v0.4.1 及之前的
公开验证用次日可用性决定当天选股,重跑也需要包含该修复。

归档结果采用 AkShare 的当前沪深 300 公开数据,在日频 213 因子近似流程中显式计入换手和交易成本。缓冲因子组合在 7 bps 有效成本假设下得到 22.20% 年化收益、0.919 Sharpe;同区间等权基线为 17.75% 和 0.882。完整口径、成本敏感性图和复现命令见[验证看板](docs/validation_dashboard.md)。

Expand Down
121 changes: 121 additions & 0 deletions docs/audits/public-revalidation-20261017/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# Signal-Date Causality Audit and Public-Data Blocker — 2026-10-17

**The public validation scripts used tomorrow's data availability to decide
today's holdings.** This audit fixes that lookahead in both validation paths.
The planned AkShare revalidation did not reach a backtest: both predeclared
single-symbol probes failed with upstream disconnects. No new public-market
performance result is claimed.

This bundle distinguishes synthetic engineering evidence from the public-data
failure. Neither supports investment or deployable-alpha claims. The original
proprietary paper experiment was not run or evaluated here.

## Scope and provenance

- Base main: `ccdcf16a7c0768dbb31fa22f91ae02b692f0fd9b`.
- Corrected code: `3443987fcabdf1c46d198bfa7a878bd4e938a0a0`.
- Scope was recorded before data requests in [plan.md](plan.md).
- Exact commands: [commands.txt](commands.txt).
- Environment: [environment.json](environment.json),
[pip-freeze.txt](pip-freeze.txt). CPU: Apple M5, 10 logical CPUs, 32 GiB RAM;
macOS 26.6.2 arm64, Python 3.12.14, PyTorch 2.14.1, AkShare 1.19.1. CPU runs;
CUDA unavailable, MPS available but unused.
- Provider probes used the base plus the candidate patch; the fix was committed
before final verification. Evidence scripts were untracked during the runs.
- Bundle integrity: [checksums.txt](checksums.txt). Traceback repository paths
are redacted to `<repo>` and trailing whitespace is stripped; error messages
and test failures remain intact.

## The causality issue

`_valid_forward_mask` correctly requires observations at `t` and `t+1` to build
a historical training label. Both `run_validation` and `run_full_pipeline` also
used it for portfolio eligibility and out-of-sample inference. Consequently, a
stock absent tomorrow was excluded today even though it was observed today.

The correction uses signal-date availability after the limit mask and factor
warm-up checks for selection and inference. Historical training labels and
trailing IC estimates retain their separate forward-label mask. New report
metadata records `availability_protocol`. No change to the proprietary paper
results is asserted; this finding concerns the public Python validation paths.

## Synthetic regression evidence

Two tests were run with the base script sources and failed because the stock's
weight was zero immediately before its future halt. The same tests pass with
the correction. [old-code-regressions.txt](old-code-regressions.txt) preserves
both assertion failures. Additional tests verify that MLP inference still scores
an observed pre-halt row without training on missing labels, and that trailing
IC ignores unavailable labels while scoring today's universe.

The [same-input comparison](same-input-comparison.json), generated by
[compare_signal_masks.py](compare_signal_masks.py), runs both script versions
on one fixed synthetic panel: 90 dates, 12 stocks, seed 3, with a zero-filled
halt at row 31 for stock 0. Its field-byte fingerprint is in the JSON.

| Equal-weight position | Before correction | After correction |
|---|---:|---:|
| Row 30, immediately before the halt | 0 | 1/12 |
| Row 31, the observed halt | 0 | 0 |

The desired property is that tomorrow's missing observation does not determine
today's portfolio. The same-input example changes net returns and turnover in
both directions across the simple strategies; improvement in return is not the
acceptance criterion. The generated numbers use 5 bps costs plus 2 bps slippage,
daily cadence, and 100 moving-block bootstrap samples with 20-day blocks. They
are synthetic engineering diagnostics, not predictive evidence.

Verification at the corrected code commit:

- Full suite: **143 passed**, [pytest.txt](pytest.txt).
- Ruff: passed, [ruff.txt](ruff.txt).
- `mlquant demo`: completed on CPU using synthetic data; 200 held-out dates at
5 bps costs. Its artifacts stay under `artifacts/small/`; no performance
claim is drawn from them.
- The generated corrected synthetic report passed the existing report audit:
[report-audit.json](report-audit.json). This quality check is not a profitability
test.
- `git diff --check`: passed.

## Public-data failure, kept separately

The predeclared first step was one AkShare request each for `600519` and
`000001`, forward-adjusted (`qfq`), daily, 2021-01-01 through 2025-01-01,
`retries=1`. [provider_probe.py](provider_probe.py) preserves the probe source;
[provider-probe.json](provider-probe.json) records timestamps and wall times,
and [provider-probe.txt](provider-probe.txt) preserves loader warnings and errors.

| Symbol | Start, Asia/Shanghai | Outcome |
|---|---|---|
| `600519` | 2026-10-17 11:16:12 | Remote end closed the connection; no data returned |
| `000001` | 2026-10-17 11:16:14 | Remote end closed the connection; no data returned |

The loader surfaced `ValueError: No data returned`, following
`RemoteDisconnected('Remote end closed connection without response')` warnings.
The cause beyond the observed disconnect is unknown; no HTTP 429 is asserted.
No panel was generated, so there is no public-data fingerprint, coverage audit,
portfolio, or new Sharpe estimate. Cost assumptions are not applicable to these
download-only probes. The planned expanded run's 7 bps assumption was not used.
No alternative provider or synthetic fallback was relabeled as public data.

## What remains unvalidated

- July 2026 A-share reports predate the mask-first/halt-safe fixes and this
availability correction. They are preserved with explicit protocol notes.
- Their normalized input panel is unavailable in this workspace. A fresh
provider download cannot isolate code changes from provider revisions,
constituent changes, or environment differences. The predeclared 20-name
diagnostic would not be the July 300-name reproduction.
- The engine still simplifies execution across halts and limits. A missing-day
forced exit or an infeasible trade is not modeled as a real exchange fill;
this correction removes selection lookahead, not execution risk.
- All-factor joint-mask coverage, MLP portfolio coverage, and equal evaluation
windows need measurement on real input before interpreting comparative returns.
- Public data remains subject to adjustments, survivorship bias, ST/IPO limit
metadata gaps, revisions, and access failures. A tradability proxy does not
replace licensed point-in-time data.

The next data run should use the fixed scope in [plan.md](plan.md), preserve
normalized-input hashes, coverage, warnings and cost assumptions, and apply
the corrected code. The [validation dashboard](../../validation_dashboard.md)
continues to show archived evidence while that rerun is blocked by data access.
14 changes: 14 additions & 0 deletions docs/audits/public-revalidation-20261017/checksums.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
add6f432fb0c4c1aeb362589835ca95f569d580770cd9ad4c739776f5ec5cf56 README.md
176b19059aca9b51e6f66120d1a15eeb36f560ca57e701d7088e93f003fb007e commands.txt
6b9d8ef1b33afe1dd2bccc1414e5af8f05d1ac02a7d878ef7e330d61a9dd686a compare_signal_masks.py
eb8aa9ae8580c0ae7e2a2122d553daf5f9a5108e3f311b92dea4c1fa045f51f5 environment.json
72bc96838ebef7d2f8313a97f8102e928dc8d2825a3c6eb53bfe48b94a0b6f86 old-code-regressions.txt
94a93d9356993469a3998a826f4071135a077b3551160f18bb940a6a2923102a pip-freeze.txt
50303cc1338ca2e1aa8f5f81c76f23b44851d732506558b7e39e6ca96e558c8e plan.md
8ae9b46dd96cece8f0554e24f8a5f50b4e178f2b79bae984a54233c7ceb25051 provider-probe.json
204a0c0e4d4309441a326134f2b4ba7736b31450ac2dffd8da47d61b8a6ff28f provider-probe.txt
0f603a2ddbb5c85d57444971425b29c39d39f8844752094c7f9ee9d396d15af7 provider_probe.py
765605818743ce3b523153f5fd32b74a3a1188f805bff25debee1ada8042abcf pytest.txt
f555ab552d0ccd7e557dc81d9c34f6797d26fb4fa8d8b5361e0c17ce51dd3be6 report-audit.json
82b3e6a6c090a57601d22943bd23fca9218d1031dbe5a7b754092f9a156b4f18 ruff.txt
7fad8c060e1f2a28eb864b163ec90c9af1ff63dbc10f4ce87701bb1f9fe54f98 same-input-comparison.json
19 changes: 19 additions & 0 deletions docs/audits/public-revalidation-20261017/commands.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Install:
/Users/duyimin/.cache/codex-runtimes/codex-primary-runtime/dependencies/python/bin/python3 -m venv .venv
.venv/bin/python -m pip install -e '.[dev]'
.venv/bin/python -m pip freeze

Provider probe (source saved in provider_probe.py; original execution used the same code on stdin):
.venv/bin/python -u docs/audits/public-revalidation-20261017/provider_probe.py

Old-code regressions (public validation script sources temporarily loaded from base ccdcf16; corrected sources restored before validation):
.venv/bin/python -m pytest tests/test_public_data_validation.py::test_signal_weights_do_not_use_tomorrows_availability tests/test_akshare_csi300_full_pipeline.py::test_full_pipeline_separates_signal_and_forward_label_masks -q

Corrected verification at 3443987:
OMP_NUM_THREADS=1 OPENBLAS_NUM_THREADS=1 .venv/bin/python -m pytest
.venv/bin/python -m ruff check src tests scripts/public_data_validation.py scripts/akshare_csi300_full_pipeline.py
OMP_NUM_THREADS=1 OPENBLAS_NUM_THREADS=1 .venv/bin/mlquant demo
OMP_NUM_THREADS=1 OPENBLAS_NUM_THREADS=1 .venv/bin/python docs/audits/public-revalidation-20261017/compare_signal_masks.py
.venv/bin/python scripts/audit_validation_report.py artifacts/revalidation-20261017/corrected/summary.json --output-md artifacts/revalidation-20261017/report-audit.md --output-json artifacts/revalidation-20261017/report-audit.json

git diff --check
95 changes: 95 additions & 0 deletions docs/audits/public-revalidation-20261017/compare_signal_masks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
"""Offline same-input comparison; synthetic engineering evidence only.

Run from the repository root:
.venv/bin/python docs/audits/public-revalidation-20261017/compare_signal_masks.py
"""
from dataclasses import replace
import hashlib
import json
from pathlib import Path
import subprocess
import sys
import types

import torch

ROOT = Path(__file__).resolve().parents[3]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / 'src'))
from mlquant.data.synthetic import SyntheticConfig, make_synthetic_panel
import scripts.public_data_validation as corrected

BASE = 'ccdcf16a7c0768dbb31fa22f91ae02b692f0fd9b'
OUT = ROOT / 'artifacts/revalidation-20261017'
OUT.mkdir(parents=True, exist_ok=True)
torch.set_num_threads(1)
torch.set_num_interop_threads(1)
source = subprocess.check_output(
['git', 'show', f'{BASE}:scripts/public_data_validation.py'], cwd=ROOT, text=True,
)
old = types.ModuleType('archived_validation')
old.__file__ = str(ROOT / 'scripts/public_data_validation.py')
sys.modules[old.__name__] = old
exec(compile(source, old.__file__, 'exec'), old.__dict__)

# A stock is observed today but is halted tomorrow. Zero-filled data on the
# halt day mirrors loader behavior; both versions receive this identical panel.
panel = make_synthetic_panel(SyntheticConfig(n_dates=90, n_stocks=12, halt_prob=0, seed=3))
mask = panel.mask.clone()
mask[31, 0] = False
fields = {name: getattr(panel, name).masked_fill(~mask, 0)
for name in ('open', 'high', 'low', 'close', 'volume', 'vwap', 'amount')}
panel = replace(panel, mask=mask, **fields)
panel.assert_consistent()
raw_mask = panel.mask.clone()
hash_state = hashlib.sha256()
for name in ('open', 'high', 'low', 'close', 'volume', 'vwap', 'amount', 'mask'):
hash_state.update(name.encode())
hash_state.update(getattr(panel, name).numpy().tobytes())

result = {
'category': 'synthetic engineering comparison; no predictive or investment evidence',
'base_commit': BASE,
'corrected_commit': subprocess.check_output(['git', 'rev-parse', 'HEAD'], cwd=ROOT, text=True).strip(),
'synthetic_config': {'n_dates':90, 'n_stocks':12, 'seed':3, 'halt_prob':0},
'perturbation': 'zero-filled halt at date index 31, stock index 0',
'panel_hash_method': 'SHA-256 over field names and C-order tensor bytes for open/high/low/close/volume/vwap/amount/mask',
'panel_sha256': hash_state.hexdigest(),
'threads': {'torch':1,'interop':1},
'costs_bps':5, 'slippage_bps':2,
'versions': {},
}
for label, module in (('old', old), ('corrected', corrected)):
module.load_validation_panel = lambda cfg: panel
captured = {}
summarise = module._summarise_strategy

def capture(name, weights, returns, _captured=captured, _summarise=summarise, **kwargs):
_captured[name] = weights.copy()
return _summarise(name, weights, returns, **kwargs)

module._summarise_strategy = capture
cfg = module.ValidationConfig(
source='synthetic', preset='us-large-100', tickers=(), start='2021-01-01',
end='2022-01-01', max_tickers=12, device='cpu', costs_bps=5, slippage_bps=2,
cost_grid_bps=(0,7,15,30), bootstrap_samples=100, bootstrap_block_size=20,
train_window=40, test_window=20, step=20, top_quantile=0.25, seed=3,
epochs=1, batch_size=128, hidden=16,
models=('equal_weight','momentum_20','alpha101_mean'),
output_dir=OUT / label, synthetic_dates=90, synthetic_stocks=12,
command=(sys.executable, str(Path(__file__).relative_to(ROOT))),
)
rows = module.run_validation(cfg)
result['versions'][label] = {
'equal_weight_before_halt': float(captured['equal_weight'][30,0]),
'equal_weight_on_halt': float(captured['equal_weight'][31,0]),
'results': rows,
}
module._summarise_strategy = summarise
assert torch.equal(panel.mask,raw_mask)
assert result['versions']['old']['equal_weight_before_halt'] == 0
assert result['versions']['corrected']['equal_weight_before_halt'] > 0
assert result['versions']['corrected']['equal_weight_on_halt'] == 0
(OUT / 'same-input-comparison.json').write_text(json.dumps(result, indent=2) + '\n')
print(json.dumps({version:{key:row[key] for key in ('equal_weight_before_halt','equal_weight_on_halt')}
for version,row in result['versions'].items()},indent=2))
38 changes: 38 additions & 0 deletions docs/audits/public-revalidation-20261017/environment.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"base_commit": "ccdcf16a7c0768dbb31fa22f91ae02b692f0fd9b",
"working_tree_dirty": true,
"python": "3.12.14 (main, Aug 25 2026, 13:50:33) [Clang 22.1.3 ]",
"pytorch": "2.14.1",
"akshare": "1.19.1",
"platform": "macOS-26.6.2-arm64-arm-64bit",
"machine": "arm64",
"cpu": "Apple M5",
"logical_cpus": 10,
"memory_bytes": 34359738368,
"device": "cpu",
"cuda_available": false,
"mps_available": true,
"torch_threads_default": 4,
"torch_interop_threads_default": 10,
"data_source": "AkShare public A-share OHLCV",
"cost_assumptions": "5 bps costs + 2 bps slippage for planned run; not applicable to provider probe",
"limitations": [
"Public provider responses may change or fail.",
"No normalized July panel is available for exact comparison.",
"Candidate patch is uncommitted at probe time; base commit and patch are preserved."
],
"corrected_code_commit": "3443987fcabdf1c46d198bfa7a878bd4e938a0a0",
"probe_working_tree": "base commit plus candidate patch; fixed code committed afterwards",
"verification_working_tree": "corrected code commit; untracked evidence scripts only",
"verification_threads": {
"OMP_NUM_THREADS": 1,
"OPENBLAS_NUM_THREADS": 1,
"same_input_torch": 1,
"same_input_interop": 1,
"pytest_torch": null,
"pytest_interop": null
},
"pytest_threads_note": "OMP/BLAS environment fixed; test suite sets or changes PyTorch pools in individual tests; no test timing comparison claimed",
"demo_costs_bps": 5,
"same_input_effective_costs_bps": 7
}
Loading
Loading