Skip to content

fix: audit-driven hardening — backtest correctness, live risk, API, performance - #84

Open
sijie-Z wants to merge 285 commits into
developfrom
feature/system-hardening
Open

sijie-Z wants to merge 285 commits into
developfrom
feature/system-hardening

Conversation

@sijie-Z

@sijie-Z sijie-Z commented Oct 19, 2026 •

Copy link
Copy Markdown
Owner

A multi-agent audit of backtest accounting, live trading, risk, the API and hot paths. Highlights:

  • Backtest weights now drift between rebalances (the engine silently rebalanced daily); benchmark is per-date; Sortino uses downside deviation; drawdown starts at inception; covariance keeps suspended names; RiskParity is a real convex model; capacity executes on the next close.
  • Live/OMS: cycle lock against duplicate orders, T+1 rollover, IOC rejection removes the resting remainder, market orders get a reference price, quotes are validated, QMT fill callbacks work.
  • Risk: reducing sells pass position/loss/drawdown/kill checks; daily P&L is daily; profits no longer trip the loss breaker; Greeks compute on add and hedge in shares; stress paths are real shocks.
  • API: sensitive endpoints are loopback-only without a token, WebSocket checks the token, several always-500 endpoints fixed, heavy handlers moved off the event loop.
  • Performance: neutralize 5.67s→0.15s per factor, breakout 8.6s→0.09s, Barra fit 9.2s→0.24s, SQLite writes 12ms→0.13ms.

Verification: pytest tests 1732 passed / 3 skipped, ruff check . clean.


2026-10-24 Update: research discipline, data PIT, cost realism, live accounting

  • Research discipline: walk-forward purge/embargo; per-fold factor selection on train-window ICs with BH-FDR; sweep and factor screening report Deflated Sharpe + BH-FDR; formal holdout window with API/CLI guards.
  • Data PIT: removed the Baostock publish-date fallback (look-ahead); Tushare per-date index membership now applied in production (buys gated, index exits remain sellable); factor/signal cross-sections respect membership; ST history derived from Baostock isST and Tushare namechange; data-cache schemas bumped.
  • Cost realism: main backtest path passes share volume and raw close so the impact model actually runs; stamp tax follows the statutory date schedule; cost-aware optimizer (linear cost + quadratic turnover penalty + ADV participation cap); covariance is PSD-certified (psd_wrap), removing the silent equal-weight fallbacks seen when the estimation window is shorter than the universe.
  • Live accounting: QMT lazy reconnect with broker order/trade resync; daily EOD books-vs-broker reconciliation persisted to the store, with an independent expected side derived from a startup baseline plus the trade ledger; asynchronous broker fills are synced back into the store.
  • Tooling: tools/real_data_research.py reproduces the cached-panel research end to end (factors -> walk-forward -> net-of-cost report).
  • Real-data evidence (cached Baostock panel, 101 names, 2018-2025): a low-turnover factor shows net-of-cost OOS Sharpe ~0.67 (sector-neutral ~0.41, sector+amount-neutral ~0.29), with a capacity threshold around CNY 200M. DSR p ~0.33 — a candidate, not a validated alpha. Details in docs/research/REAL_DATA_EVIDENCE_2026-10-24.md.

Targeted test suites are green; CI will run the full matrix.

2026-10-25 Update 3: decision memo, orthogonal screen, full-suite verification

  • Full test suite by directory: 3,862 passed. Two failures were found and fixed: a layer-boundary violation (the walk-forward factor selector now lives in the CORE research layer so tools do not import strategy) and a calendar-dependent T+1 restart test (exchange clock pinned to a fixed trading day).
  • Deployment decision memo committed at docs/research/DEPLOYMENT_DECISION_2026-10-25.md: no real-money deployment yet. Reconsider only after 6 months of forward paper tracking, a broad PIT universe with delisted/ST names, an orthogonal signal with alpha t >= 2 and DSR p < 0.1, and completed reconciliation/kill-switch drills.
  • Orthogonal factor pre-screen (5 pre-registered candidates: lottery, idiosyncratic vol, Amihud illiquidity, intraday overnight, abnormal volume): 0/5 passed; at portfolio level they correlate 0.94-0.98 with the existing long basket.
  • Portfolio construction comparison: equal-weight top-N, signal-proportional and risk-parity variants all fail the turnover/drawdown criteria; MVO stays.
  • Volatility targeting: six pre-registered variants; 1/5 passed, information ratio unchanged - mechanical de-leveraging, not recommended.
  • Production tooling: tools/daily_signal.py now enforces the 30% sector cap inside the optimizer; target portfolio and the 2020-2025 rolling paper ledger were regenerated under that constraint.

2026-10-25 Update 4: wide pool, true 2026 holdout, forward paper

  • Wide pool fetched via a working Sina endpoint: 300 listed names (106 Shanghai / 99 Shenzhen main board / 92 ChiNext), 2018-01-02 to 2026-10-23, with amount/turnover; cross-checked against Baostock (return correlation >= 0.9994, turnover 1.000).
  • Wide-pool replication: the low-turnover direction survives (Sharpe 0.805 vs same-pool equal-weight 0.493), but the edge is mostly lower volatility - the information ratio vs the equal-weight pool is slightly negative, and the pool has no delisted names (survivorship disclosed). EP adds no incremental evidence on the wide pool (EP+turnover 0.674 < turnover-only 0.805).
  • True 2026 holdout (2026 never used for any selection; production signal + paper ledger, frozen parameters): turnover-only -4.89% vs benchmark -11.63% (+6.73pp), Sharpe -0.57 vs -0.83, drawdown -17.5% vs -22.2%; EP+turnover -8.49%. Defensive tilt persisted out of sample, absolute return still negative.
  • A pre-registered trend-filter overlay is a null result (Sharpe 0.683 -> 0.527 in 2020-2025; -0.57 -> -1.14 in 2026) and was not adopted.
  • paper_track now resolves the raw-price basis automatically (Sina adjusted panels use amount/volume; Baostock raw panels keep close, byte-identical legacy output).
  • Forward paper is wired for two frozen targets (EP+turnover and turnover-only, as of 2026-10-23, execution 2026-10-26) with a weekday post-close update task; first run may need a one-time tool approval in the desktop app.

Update 5 — external-review round (2026-10-25)

Metric conventions. Re-audited Sharpe/DSR/IR on all saved OOS series. The platform's custom Sharpe (CAGR-3%)/vol is 0.03–0.05 below the standard annualized Sharpe sqrt(252)*mean(daily excess)/std(daily excess); DSR already used the standard convention internally, so its p-values are unchanged. Reports now lead with the standard Sharpe.

Benchmark matrix (same pool). OOS 2020-02-11..2025-12-31: strategy 15.5% CAGR vs daily-EW 15.3%, market-cap-weighted 8.6%, constant-share buy&hold 13.2%. The edge over mcap/buy&hold comes from lower volatility (defensive tilt); versus same-pool daily EW the net edge is close to zero.

Cost stress (frozen strategy, same code path). impact 10bps baseline vs punitive fixed slippage 30/50bps: standard Sharpe 0.474 / 0.441 / 0.415; cumulative excess vs daily EW +2.4% / -2.4% / -6.0%. Absolute returns are not cost-fragile (monthly, turnover-capped), but the net advantage over EW does not survive conservative costs — an explicit negative result against the review's acceptance criterion.

Freeze. Strategies frozen in docs/research/FREEZE_MANIFEST_2026-10-25.md (sha256 of targets/code/config) with evaluation and termination rules; tag forward-freeze-2026-10-23. Historical data defect ledger (15 items) in docs/research/DATA_DEFECT_LEDGER_2026-10-25.md.

New return source, pre-registered: ETF momentum rotation. Pre-registration written before any backtest (docs/research/PREREG_ETF_ROTATION_2026-10-25.md); 15 liquid A-share ETFs, 12-1 momentum, top-3, inverse-vol, cash defensive leg, monthly rebalance. Results (docs/research/ETF_ROTATION_RESULTS_2026-10-25.md):

  • OOS 2020–2025: net Sharpe 0.733, CAGR 15.4%, beats HS300 (0.153), same-pool EW (0.509), 60/40 and cash; survives 30/50bps fixed costs (+3.3/+2.4pp vs EW); sensitivity grid over 18 configs is a coherent positive plateau, DSR p=0.55 (not significant), excess concentrated in 2020.
  • 2026 true holdout (193 sessions): -14.6% vs HS300 -9.8% and EW -8.9%, MDD -30.7% vs -14.5% — H2 fails, so per the pre-registered rule the strategy is frozen, not deployed, no re-tuning.

Data fixes found during the pilot (all documented): Sina ETF prices carry unadjusted share conversions (159928 4:1, 2021) and missing distributions (510500, 2022); switched total returns to official Eastmoney NAV growth. Eastmoney timestamps are Beijing midnight — parsing as UTC introduced a one-day lookahead (lag-correlation check: 0.978 at lag+1 before, 0.98 at lag 0 after the fix). The square-root impact rule explodes on zero/stale ADV (a -15x daily return appeared in placebo runs) — switched to causal 60-day median ADV.

Net position after this round: the stock strategies stay in forward paper (frozen); the ETF direction is frozen; no real money. The 90-day plan is in docs/research/DEPLOYMENT_DECISION_2026-10-25.md §8.

Addendum — PIT valuation replication. Wide-pool EP was rebuilt from Baostock daily peTTM/pbMRQ (published-report basis, 99.97% coverage; rank correlation with the Baidu panel 0.98/0.996). Rerunning variants A/B unchanged: EP Rank IC 0.0196 → 0.0204, Sharpe 0.497 → 0.479 and 0.674 → 0.644. The EP-adds-nothing conclusion is not an artifact of the valuation source (report section 22; defect ledger #3 closed).

Addendum 2 — PIT ST replication (material). The 101-pool ST mask was previously static/all-False. Rebuilt from Baostock daily isST: 22 of 101 names had ST episodes (7,859 asset-days; ranges match public history). Under a production-faithful ST policy the frozen strategy's standard Sharpe falls from 0.474 to 0.375 (CAGR 9.27% → 7.71%), and its cumulative excess versus the investable equal-weight benchmark flips from +2.4% to -11.2%; the ST-free EW benchmark itself is stronger (10.9% CAGR, Sharpe 0.453) because ST names were net losers. Since the production pipeline already filters ST, the earlier 0.49 overstated the deployable strategy. Deployment decision is unchanged (no real money); the evidence bar is lower. Report section 23, ledger #2.

Addendum 3 — live target ST contamination fixed (freeze v2). Auditing the frozen 2026-10-23 forward book found 5 of 40 holdings were ST as of that date (002168, 002650, 300419, 603189, 603825): daily_signal.py had no ST filter and the wide panel carries no is_st column. Fix: --exclude-codes-file drops a PIT exclusion list before construction and records it in provenance/diagnostics (unit-tested); forward_update.py moved into tools/ and now snapshots the current Sina ST list at month end, failing closed when the snapshot is unavailable. Both 2026-10-23 targets were rebuilt with the PIT ST list (v1 files preserved, not deleted); the freeze manifest is upgraded to v2 with new hashes and tag forward-freeze-2026-10-25. The forward ledger had not started, so v2 is a starting point, not a post-hoc edit of tracked records. Report section 25.


Update 6 — live-execution safety and forward v3 (2026-10-26)

Live/QMT safeguards. Stale cached snapshots are no longer re-certified as fresh; QMT ticks require a valid recent timetag interpreted as Asia/Shanghai. Working orders suppress duplicate intents, including kill-switch de-risking, and order generation uses an authoritative order-book check rather than a stale cache. QMT fills are deduplicated by traded_id across callbacks and resyncs, with quantity-weighted VWAP; cumulative order snapshots and incremental trades are never added. Submission-outcome-unknown orders stay pending and are recovered by a durable local intent in strategy_name, including the resync/exception race. Position/account query failures now fail closed instead of looking like flat positions or usable cash.

Backtest correctness. Invalid ADV/volatility uses per-asset fixed-slippage fallback instead of a CNY-1 denominator; impact ADV is now the causal 60-session median shifted one day. Buy funding includes transaction costs, and cash/NAV/fee reconciliation debits the same cost exactly once. Iceberg no longer emits zero-share slices.

Forward integrity (v3). The complete 2026-10-23 PIT ST snapshot contains 13 names, not only the five excluded by v2. Active targets were rebuilt (33 turnover-only and 32 EP+turnover holdings); v1/v2 are preserved under superseded/. Empty/partial/malformed ST inputs fail closed. A zero-fill initial rebalance cannot publish an all-cash ledger, zero-CNY buys no longer create phantom positions, target schedules must remain an unchanged hash prefix, and execution follows the exchange calendar. A stale daily cache now fails with an explicit "provider has not published the current close" error.

Verification. 3,942 passed / 23 skipped across all 24 test directories (batched with bounded BLAS threads); Ruff and git diff --check pass. A scratch-only first-execution dry-run succeeded for both v3 books (33 and 32 buys, no blocked buys). Recomputed deployable baseline after the cost fixes: standard Sharpe 0.387, CAGR 7.89%, cumulative excess vs investable EW -9.6%; the no-real-money decision is unchanged. No real forward NAV is claimed in this update.

Addendum — first real forward session recorded. On 2026-10-26 the official close-based ledgers started successfully after the provider published the daily bars. Turnover-only: 32 filled positions, NAV 1,998,328.40 (-0.0836%), costs 1,671.60 CNY. EP+turnover: 31 filled positions, NAV 1,998,386.68 (-0.0807%), costs 1,613.32 CNY. 600094 was correctly blocked as limit-up and remains cash; stale assets 0 and all target hashes verified. This first session is close-entry/cost accounting only, so no holding-period benchmark conclusion is drawn. A separate tools/forward_realtime.py now provides indicative intraday target monitoring (3 unit tests) without altering the official close ledger.

Addendum — realtime now uses the actual book. Rolling summaries now persist end-of-day positions, reference prices and cash. tools/forward_realtime.py marks those official positions to live quotes (basis=official_book) and only falls back to target weights for legacy ledgers. Latest indicative values: turnover 1,998,650.51 (+0.0161% vs the official close NAV) and EP+turnover 2,003,818.78 (+0.2718%); these remain intraday references, not official NAV. Book/NAV reconciliation and valuation tests pass (66 tests in the affected modules).

Addendum — forward data reliability review. Weekend/holiday runs now require the latest expected exchange session (not merely accept any stale cache). Per-asset refresh failures cannot be hidden by one fresh row: failures affecting the current target/book, or any failure before a month-end signal, abort the update. Earlier missing month-ends are detected and catch-up uses only the prior target book, never future weights. Stock and merged parquet writes are atomic. The daily read-back validator now supports later sessions and pending targets instead of hard-coding inception day. Affected tools suite: 149 passed.

Addendum — executable QMT safety drills and runbook. tools/qmt_safety_drill.py now runs seven production-code fault-injection scenarios: stale ticks/timezones, fill dedup/VWAP, partial-cancellation cumulative snapshots, submission-timeout recovery races, kill-switch delayed sells, fail-closed account/order books, and fee-aware cash funding. All seven pass, plus 566 tests across trading/live/tools. A new docs/QMT_SAFETY_DRILL.md gives the exact pre-open, intraday, close, and read-back commands. xtquant/miniQMT is not installed here, so the report explicitly sets real_miniqmt_certified=false; tomorrow's documented default is real-data forward paper, not real-money orders.

Addendum — forward day two, after a real data-integrity repair. The first 2026-10-27 refresh produced current rows for only 106/300 names; non-empty stale responses were being accepted as fresh, materially overstating NAV. Per-name retries plus a saved, spot-confirmed fallback (with adjustment-scaled OHLC and turnover recomputation) brought 299/300 names current; 002717 is confirmed suspended. The mixed-date outputs were archived and recomputed, with first-day rows unchanged and an independent first-book recomputation matching exactly. Corrected results: turnover NAV 1,952,009.91 (-2.40% since inception) vs equal-weight benchmark -2.91%; EP+turnover NAV 1,956,791.96 (-2.16%) vs -2.91%. Zero stale target assets. The updater now retries/rejects stale responses and fails on watched-asset or widespread refresh failures.

@coderabbitai

coderabbitai Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 66837b2e-bbbc-4595-8a1d-852bce61ee45

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

The py3.11 CI job sat in 'Run tests' for 25+ minutes while py3.12 finished
in six. A per-test timeout turns a silent hang into a named failure with the
hung thread's stack so the root cause can be fixed; the CI job timeout stays
as the backstop.
Security (P0): authorize on the ASGI scope path with root_path normalization
instead of request.url.path. Starlette 0.40's BadHost parsing turned
`Host: x/..` into `/../api/config`, which skipped the token/loopback gate
while the router still matched `/api/config`; the raw-ASGI regression harness
shows 200-with-data before and 403/401 after for every bad-Host variant,
root_path deployment, encoded path, docs, static and the WS handshake. A new
outermost RequestGuard validates Host (optional QUANT_ALLOWED_HOSTS), caps
bodies at 10 MB, rate-limits compute endpoints per client, and runs before
metrics/auth/CORS/routing. Token comparison is timing-safe; a latent 422
serialization 500 is fixed.

Security (P1/P2): sweep/parallel-sweep/research windows are bounded;
/api/core/store/* and audit scope by X-Tenant-Id with cross-tenant reads
requiring loopback + explicit all_tenants; delete_position requires a tenant;
HTML reports escape data and script JSON; retention archives neutralize CSV
formulas; config-version redaction is pattern-based; WS accepts only the
bearer subprotocol (query token removed, breaking); QMT passwords are rejected
in request bodies; Tushare forces HTTPS; python-multipart and lightgbm are
bumped to fixed versions.

Corporate actions: providers expose a canonical split/cash-dividend feed
(synthetic deterministic, Tushare dividend, Baostock query_dividend_data),
DataPipeline exposes it, and the paper broker/ExecutionEngine/LiveEngine/
LiveRunner/dual-track PaperBroker apply actions once per (asset, ex_date) on
the ex-date (split quantity/cost, dividend cash, T+1, persisted markers,
stock-lot reseed); real brokers never apply locally. The web engine, CLI
trade, daemon and ValidatedProvider are wired, and every path reports
corporate_actions_applied.

Testing: pytest-timeout (600s/test) turns hangs into stack dumps. All test
directories pass individually (3,282 passed, 3 skipped); a single-process
full run hit host memory pressure (180 MB free from other sessions), so CI
runs the authoritative full suite.
- fastapi 0.128 -> 0.157.0, starlette 0.40 -> 1.9.0 (BadHost
  GHSA-86qp-5c8j-p5mr, StaticFiles UNC and form-parse DoS fixed upstream).
  The P0 middleware fix remains defense-in-depth. API/core/utils/cli suites
  pass on the new stack locally.
- axios ^1.7 -> ^1.20.0 in the frontend; production build passes. echarts 6
  is a major chart-API migration and stays scheduled separately.
- mypy fixes surfaced by the new import graph: EventStore buffer Optional
  annotation, queue/handler-name annotations, bucket boundaries float,
  metrics dict value type, audit record Any, and an explicit EXCHANGE_TZ
  re-export on the core.store shim.
The test activated the process-wide RiskMonitor, whose persistence wrote a
KILL switch into data/trading.db; every later live run restored it and
blocked orders. Inject a fresh monitor backed by a temp store and route the
module's default resolution to it. The existing polluted row is left for the
operator to deactivate (no deletion without explicit approval).
…ls POST-only

RequestGuard now validates any browser Origin against the request Host
(scheme/host/port, default-port normalized) or QUANT_ALLOWED_ORIGINS before
routing/auth; an unlisted Origin gets 403 (WS close 1008), duplicate or
opaque origins are rejected, and requests without Origin (curl/server-to-
server) are unaffected. This closes the loopback-mode CSRF hole where a
malicious page could POST to 127.0.0.1.

/ws-quotes/{start,stop} and /l2/{start,stop} change server state, so they are
POST-only now; GET returns 405 and performs nothing (the /ws-quotes/{code}
catch-all needed an explicit GET stub). Frontend wrappers updated and the
production build passes. QUANT_ALLOWED_ORIGINS documented in .env.example,
including the Vite dev cross-origin flow and TLS-proxy scheme caveat.

Verified: tests/test_api 328 passed; ruff clean; frontend build passes.
Update the endpoint tables and Round 11 handover; QUANT_ALLOWED_ORIGINS is
documented in .env.example.
…d uv.lock

Verification found the security bumps were reversed: requirements pinned
python-multipart 0.0.32 / lightgbm 4.6.0 but pyproject still had 0.0.6 /
4.5.0, and CI's `pip install -e .` (without --no-deps) downgraded them.
pyproject now matches, CI installs both editable packages with --no-deps,
and uv.lock is regenerated (starlette 1.9.0, urllib3 2.8.0,
python-multipart 0.0.32). Add floors for certifi>=2026.10.19 (compromised-CA
advisory) and urllib3>=2.8.0.
- One-sided date overrides are merged with the config before the 10-year
  span check (end_date=2100-12-31 alone used to expand the default 2021
  start to 29,218 days); GET analysis/monitor endpoints join the per-client
  rate limit.
- The token-less loopback bypass now also requires a loopback Host name
  (localhost/127.0.0.1/::1/testserver or an explicit allowlist entry), so a
  DNS-rebound page presenting Host=attacker.example from 127.0.0.1 gets 403
  with no handler execution. Missing Host is rejected when
  QUANT_ALLOWED_HOSTS is configured.
- Large request bodies (>64 KB) on protected routes are authenticated before
  buffering; unauthenticated clients no longer make the server read 10 MB.
- /api/core/events filters audit.* payloads (use the tenant-filtered
  /api/core/audit); Prometheus method labels are allowlisted; QMT `server` is
  operator config (QMT_SERVER) and rejected in request bodies; webhook failure
  logs strip URL credentials; config redaction covers list-valued secrets;
  responses carry nosniff/DENY/referrer-policy headers.

Verified: tests/test_api 341 passed; utils/risk/core/monitoring 773 passed;
ruff clean.
The OMS kept every order and filled blotter entry for the process lifetime
(the audit accepted 1,000 unthrottled orders) and /api/oms/* had no admission
control. Completed orders are now pruned to 90% of MAX_ORDERS once the book
exceeds 10k, active orders are never evicted, the blotter is trimmed on the
same rule, and /api/oms/order, /api/oms/fill and /api/trading/order join the
per-client token bucket.
- /api/core/store/stats reports db_name instead of the absolute path.
- /api/portfolio/import is capped at 10,000 rows (a 100k-row CSV was
  accepted under the 10 MB body cap).
- L2 GET endpoints no longer lazily start the provider (a drive-by <img>
  could start threads); they return 503 pointing at POST /api/l2/start.
- WebSocket admission reserves its slot before the first await, so
  concurrent handshakes can no longer overshoot the 100-client cap.
- Config redaction detects secret query parameters (token/api_key/etc.) and
  redacts them inside lists; HTML responses carry a CSP that allows only
  self plus the Google Fonts origins used by index.html.
Red-team follow-up:
- The token-less WebSocket branch now requires a loopback Host (or an
  explicit QUANT_ALLOWED_HOSTS entry), matching the HTTP guard; a
  DNS-rebound page from 127.0.0.1 presenting attacker.example is closed 1008
  before accept (regression tests for both cases).
- Portfolio import requires exactly {code, hold_vol} per row; disjoint keys
  used to become 40k pandas columns and an 8M-cell frame.
- /api/execution/smart-route and /api/trading/run-once join the per-client
  token bucket.
- OMS refuses new orders at the active-order cap (instead of growing
  unboundedly), caps fills per order at 1,000, and the API bounds ticker
  (32), strategy (64) and quantity (100M).
- The audit-event filter normalizes topic names (audit./AUDIT/whitespace) and
  drops every audit-shaped topic from /api/core/events.
- Webhook failure logs keep only scheme+host(+port): Slack/Discord tokens
  live in the path and were still logged; an invalid port no longer raises.
- Swagger/ReDoc are exempt from the SPA CSP (they load cdn.jsdelivr.net);
  /ws-quotes reads no longer lazily start the provider.

Verified: tests/test_api 349, execution+risk 450, ruff clean.
…ocale

The em dash in the xgboost comment made pip's locale decoder fail on
Windows (UnicodeDecodeError); CI's UTF-8 environment was unaffected.
- ECharts 5.6 -> 6.1 (XSS advisory GHSA-fgmj-fm8m-jvvx); production build
  passes and the terminal renders four canvases with no console errors.
- The WebSocket URL prefers 127.0.0.1 when the page is opened via localhost
  (browsers resolve localhost to ::1 first, and WebSockets do not fall back to
  IPv4 like HTTP does). Origin validation treats localhost and 127.0.0.1/::1
  as the same loopback origin only when both ends are loopback, so a
  DNS-rebound attacker host is still rejected.
- Research-context builds are single-flight per key: correlation + ic-decay
  (or a reload during the first build) share one analytics slot instead of
  exhausting both and returning 429.
- The UI retries 429 GETs up to four times with exponential backoff, so the
  first 40-60 s context build no longer leaves a panel blank until reload.
- Loopback UI clients with a loopback Host are exempt from the GET research
  bucket (remote clients and rebound hosts are not).
Document that the default provider is synthetic (not live quotes), the three
real-time paths (engine event stream, market/live-portfolio/WS quotes, real
research providers with incremental updates), the token/origin requirements,
and the measured Baostock/network caveat. Counts: 3,342 cases.
- purge_gap delays each test window by N trading days after train end
- embargo removes N trading days after prior folds' test windows from training
- both default to 0, preserving the historical fold layout exactly
- fold results expose train_days (training days after embargo removal)
…andoned

Dev 2019-2022 Sharpe 1.10, OOS 2023-2025 0.275 vs CB equal-weight
0.382; 2026 holdout -5.13% vs -4.34%; 30/50bps excess flips negative.
Per pre-registration the direction is dropped without retuning. Grid
enumeration is 18 configs (prereg text said 12; corrected), DSR N=18,
nothing significant.
- deduplicate QMT fills, use execution VWAP, and recover unknown submissions
- reject stale quotes and unknown account/order books; suppress duplicate intents
- make impact ADV causal with safe fallbacks and fee-aware cash funding
- rebuild forward targets with the complete PIT ST list (v3)
- reject zero-fill inception, preserve target hash prefixes and exchange sessions
- add regression coverage across execution, backtest, tools, and live trading
Uses Sina latest/previous-close quotes to show intraday target returns
and indicative NAV without writing or altering the official close-based
forward ledgers. Includes continuous snapshot mode and unit tests.
Turnover: 32 positions, NAV 1998328.40, -0.0836%, costs 1671.60 CNY.
EP+turnover: 31 positions, NAV 1998386.68, -0.0807%, costs 1613.32 CNY.
600094 was limit-up and correctly blocked; no stale assets or target hash changes.
Persist end-of-day positions, reference prices and cash in each rolling
summary. The realtime monitor now marks the official book to live quotes
and only falls back to target weights for legacy ledgers. Includes
book/NAV reconciliation and valuation tests.
- require the latest expected exchange session on weekdays and weekends
- fail when refreshed assets in the current target/book are stale, and on
  any month-end refresh failure
- catch up earlier missing month-ends using only the prior target book
- write stock and merged parquet files atomically
Cover stale ticks, fill dedup/VWAP, partial-cancellation cumulative
snapshots, submission timeout recovery, kill-switch delayed sells,
fail-closed account queries, and fee-aware funding. All seven drills
pass; real miniQMT certification remains explicitly false.
A single provider error no longer ends the trading-day monitor or
publishes stale data as fresh. Errors are logged and the next interval
is attempted; three consecutive failures stop the run. Includes live
three-snapshot verification and regression tests.
… two

Retry and reject non-empty stale responses instead of treating them as
fresh. The first run covered only 106/300 names and materially overstated
NAV. After per-name repair plus spot-confirmed, adjustment-scaled rows,
299/300 names are current and 002717 is confirmed suspended. Independent
first-book recomputation matches exactly: turnover NAV 1952009.91 and
EP+turnover NAV 1956791.96, with zero stale target assets.
…ebalance

The October month-end (2026-10-30) is the first real rolling rebalance.
Two rounds of adversarial review found one blocker and several major
gaps; this commit closes them:

- Blocker: the dated ST snapshot was only captured after the data gates.
  If Friday's run exits 2 on a gate, no st_codes_20261030.txt exists and
  Monday's overdue catch-up refuses to use the current ST list, so the
  November rebalance would stall permanently. The snapshot is now taken
  at the very start of a month-end session, before any gate, and an
  exception there cannot abort the refresh.
- A zeroed or degraded spot feed could have reclassified every failure
  as suspended and nullified the month-end gates. Suspension
  confirmation now requires a market-wide frame (>=100 codes), >=50% of
  codes trading, exactly zero volume across every row of the code with
  no missing values, and a bounded reclassification count; anything
  doubtful keeps the failure.
- The any-failure month-end gate now also applies to overdue catch-up
  runs that can actually be generated (dated PIT snapshot present).
- Target files are trusted only if they parse, match their filename
  date, and carry non-empty weights; corrupt targets are regenerated
  and ignored by the overdue scan. daily_signal writes targets
  atomically so a killed process cannot strand a truncated book.
- The ledger rebuilds even when target generation is blocked.
- Spot fetches get a 90s wall-clock deadline, at most two attempts, and
  a ten-minute in-process failure memo so nested retries cannot multiply
  full-market paginations; one successful pagination is shared per run.
- ST reuse re-validates file shape and uses the same floor family as the
  writer; the ST list gets a plausibility floor.

Strategy inputs, costs, constraints and ledger directories are
unchanged. tools tests: 176 passed. A --skip-refetch rebuild on the
production path reproduced both ledgers byte-identically, and the
independent readback still verifies target identities with zero stale
assets.
The account holder must verify identity, sign the quant agreements,
download the broker's QMT installer and log in with their own
credentials; everything after that (xtquant wiring, config, read-only
connectivity checks, simulation-account certification drills, report
and runbook updates) is handled by the operator. Passwords never leave
the client and are never shared. Includes a fallback path: for a
monthly strategy the system can emit an order list for manual entry,
which needs no broker API access at all.
The pre-registered window only means something if the running code and
targets cannot change unnoticed. The guard recomputes sha256[:16] for
the frozen code, the inception targets and the PIT ST list, pins every
forward target the first time it is seen, and keeps a row-count plus
prefix hash per ledger so past NAV rows cannot be edited after the
fact. Any drift exits non-zero for a scheduled task to alarm on.

All eight registered artifacts match the freeze manifest today; drift
detection was verified by deliberately corrupting a pinned hash.
An independent overfitting audit showed the guard covered only 5 of
the modules that actually determine targets and NAV. It now pins 13
code/data files (optimizers, covariance, constraints, factor
processing/evaluation, metrics, exchange calendar, industry map), the
two inception targets and the PIT ST list, hashes with LF
normalization so git autocrlf cannot cause cross-machine false
alarms, and refuses to rebaseline its state once drift is detected so
alarms persist until a person resolves them.

The audit also verified the main path is clean: injecting future rows
leaves the frozen 2026-10-23 targets bit-identical, prev-weight
chaining rejects future books, valuation ffill is causal, and both
ledgers reproduce to within 4e-7 CNY. Remaining blockers and majors
(valuation vintage refresh before the November month-end, a pinned
evaluation spec, 2027 calendar, benchmark ST exclusion, ledger
rebuild comparison) are recorded with owners and deadlines in
FREEZE_MANIFEST section 8.
Both strategies beat the equal-weight universe today: turnover
+1.736% (cum -0.705%) and EP+turnover +1.831% (cum -0.369%) versus
benchmark +1.614% (cum -1.338%). The 002717 suspension reclassification
ran in production for the first time: the stale daily bar was
confirmed untraded by spot volume and did not block the ledger.

The freeze guard reported the two ledgers' first three lines as
rewritten; git diff shows strategy NAV, returns, drawdown, positions
and costs byte-identical, with only the 2026-10-27 benchmark daily
return moving by 2.2e-7 after the suspended name left the benchmark
cross-section. Reviewed, recorded in the manifest, and the guard
baseline re-accepted with --accept (the flag exists precisely so a
reviewed adjustment is explicit rather than silent).
The overfitting audit found the valuation store had not advanced past
2026-10-24: the vendor samples PE(TTM)/PB per stock on its own irregular
dates, and no scheduled task refreshed them, so EP inputs were being
ffilled from an ever-staler snapshot and would reach a ~37-day vintage
at the November month-end, outside the p90 = 18 days seen in research.

tools/fetch_valuation.py appends only vendor-dated rows (never later
than today), writes per asset atomically, rebuilds the merged panel
restricted to the price universe, reports per-asset vintage, and exits
non-zero when more than 5% of assets fail or the post-refresh median
vintage exceeds 21 days. The daily task now runs it between the freeze
guard and the forward update; on a month-end session a failed refresh
skips target generation instead of producing a stale EP book.

Factor definitions, parameters, constraints and the frozen inception
targets are unchanged: this restores the data supply the research
relied on, and is recorded as v3.2 in the freeze manifest. The tool is
added to the freeze guard; 17 artifacts verify. Four regression tests
cover append-only semantics, causality, idempotence, failure thresholds
and universe filtering.
A broad down day (universe equal-weight -2.44%) with both books
holding up: turnover -1.331% (cum -2.03%) and EP+turnover -0.898%
(cum -1.26%) versus benchmark cum -3.74%. Cumulative excess now
+1.72pp and +2.48pp. Zero stale assets, target identities verified,
freeze guard green, refetch had one expected suspended name (002717)
reclassified by spot volume.

The 17:15 scheduled session had again been terminated mid-command by
the environment, so the ledger was produced by the manual rerun of the
same pipeline (valuation refresh then forward_update) at 19:03; the
new 19:40 fallback task exists for exactly this case going forward.
Recomputes the user's securities account from Sina quotes and reconciles
to the broker app to the cent (value 2351.80, pnl -558.30 on
2026-10-29), tracks bucket weights, and alerts on face-value delisting
proximity for the distressed single stock (now 1.15), single-day moves
>=5%, single position >20% and tech-bucket weight >85%. Daily snapshot
task scheduled for weekday close.
…day five

Both strategies generated their 2026-10-30 target from the PIT ST
snapshot captured earlier that day (197 codes, zero ST in either book)
with weights summing to 1.0, a 5% single-name cap and only 1.8%
one-way turnover versus the October book: turnover 35 names (+2),
EP+turnover 33 (+1). Execution is pending at Monday's close per the
frozen schedule.

Day five stayed defensive in a falling market (universe -2.87%):
turnover -1.03% to 1,939,198.11 (cum -3.04%) and EP+turnover -1.11%
to 1,952,848.17 (cum -2.36%), lifting cumulative excess to +3.47pp
and +4.15pp. Zero stale assets, both target identities verified, and
the freeze guard passes with all four targets pinned.
Both books rolled into their 2026-10-30 targets at today's close:
turnover now holds 35 names and EP+turnover 33, with rebalance costs
of 212.91 and 205.84 CNY for the 1.8% one-way turnover. Six sessions
and two rebalances are on record with zero pending targets and zero
stale assets.

On a strong up day (universe +2.41%) the defensive October book
lagged: turnover +1.70% to 1,972,179.44 (cum -1.39%) and EP+turnover
+1.39% to 1,979,890.41 (cum -1.01%), leaving cumulative excess at
+2.87pp and +3.25pp versus the benchmark's -4.26%. Freeze guard green
with all four targets pinned.
Closes audit item B3: the 3/6-month evaluation previously had no
executable definition of its metrics and conflicting thresholds across
documents. EVALUATION_SPEC_2026-11.md now pins the evaluation dates,
the window start, the Sharpe formula (primary rf=0, secondary rf=3%),
the benchmarks (B1 ST-excluded investable EW rebuilt from Baostock PIT
isST; B2 the ledger column), the decision rule (6-month: Sharpe >= 0.2
and positive excess vs both B1 and B2; 3-month diagnostic only),
confidence intervals and the multiplicity treatment.

tools/forward_evaluation.py is the only allowed report generator: it
reads only the append-only nav.csv ledgers, never summary.json, and
writes a hashed report. Both files are added to the freeze guard.
End-to-end validated today on the six sessions so far (B1 rebuild
succeeded; excess figures match the ledger columns).
The 17:15 scheduled attempt was again refused by the mixed-date gate
(55 of 300 names unpublished at 16:53); the 17:45 conditional retry
found a complete panel and produced the ledger with a single expected
failure (the suspended 002717). Both new November books rose on a
down day (universe -0.87%): turnover +0.99% to 1,991,749.98 (cum
-0.41%) and EP+turnover +1.18% to 2,003,300.78 (cum +0.17%), lifting
cumulative excess to +4.68pp and +5.26pp. Seven sessions, two
rebalances, zero stale assets, target identities verified.
Both November books crossed into positive cumulative territory even as
a +2.45% rally day favored the market: turnover +0.73% to 2,006,329.71
(cum +0.32%) and EP+turnover +0.42% to 2,011,672.89 (cum +0.58%)
versus the benchmark's -2.76%, leaving cumulative excess at +3.08pp
and +3.34pp. Eight sessions, two rebalances, zero stale assets; the
guard verifies all 19 frozen artifacts.
The 17:04 attempt was refused by the mixed-date gate (22 names still
unpublished); the 17:46 conditional retry completed the ledger. On
another strong up day (universe +2.26%) the books again rose less but
kept building cumulative lead: turnover +1.50% to 2,036,456.22 (cum
+1.82%) and EP+turnover +1.32% to 2,038,319.42 (cum +1.92%) versus
benchmark cum -0.56%, leaving excess at +2.38pp and +2.47pp. Nine
sessions, two rebalances, zero stale assets.
A risk-off Friday: the universe fell 1.69% (benchmark cum -2.24%) and
both books held up far better, turnover -0.38% to 2,028,664.33 (cum
+1.43%) and EP+turnover -0.37% to 2,030,738.74 (cum +1.54%), keeping
cumulative excess at +3.67pp and +3.77pp. Ten sessions, two
rebalances, zero stale assets, target identities verified.

The 16:54 and 17:51 attempts were refused by the mixed-date gate (27
then 7 names unpublished); the 18:42 conditional retry completed the
ledger. A post-close spot snapshot had shown +0.17%/+0.15%
indicatively - another reminder that only official daily bars are
authoritative.
The first weekly overfit-watch run reported a clean week (guard green,
nine commits touching no frozen path, four target hashes matching,
ledgers consistent with their summaries) and flagged two pre-week
commits, 0ffcf61 and 692a8d1, that had edited the section 6 hash line
in place during the 10-26/27 defect-fix days. Recorded as the same
class already disclosed under m1: both code versions were superseded
by v3.1 and are now hash-pinned; no version bump or interruption is
needed, and this note closes the item.
First clean first-attempt run in a week: one expected failure (suspended
002717), no gate rejection. The universe was flat-to-down (benchmark
cum -2.11%) while both books drifted slightly lower but kept their
lead: turnover -0.01% to 2,028,475.64 (cum +1.42%) and EP+turnover
-0.23% to 2,025,997.74 (cum +1.30%), leaving cumulative excess at
+3.53pp and +3.41pp. Eleven sessions, two rebalances, zero stale
assets.
The 16:54 attempt was refused by the mixed-date gate (59 names
unpublished); the 17:42 conditional retry completed the ledger. The
market rallied 1.62% and the books again lagged but stayed positive:
turnover +0.32% to 2,034,976.58 (cum +1.75%) and EP+turnover +0.04%
to 2,026,756.48 (cum +1.34%) versus benchmark cum -0.52%. The
cumulative excess narrowed accordingly to +2.27pp and +1.86pp -
the expected give-back on a strong up day. Twelve sessions, two
rebalances, zero stale assets.
The scheduled daily and evening-fallback sessions did not complete
during the day; the fallback task fired when the app was reopened at
22:14 and produced the ledger through its normal flow (valuation
refresh, refetch, rebuild). A manual duplicate launch was detected and
stopped to avoid double-fetching Sina.

Another strong up day (benchmark +1.09%) with the books lagging again
but positive: turnover +0.28% to 2,040,743.23 (cum +2.04%) and
EP+turnover +0.09% to 2,028,588.86 (cum +1.43%). Thirteen sessions,
two rebalances, zero stale assets, guard green.
The 16:55 attempt was refused by the mixed-date gate (30 names
unpublished) and the 17:20 scheduled session died with an API 405
before doing any work; the 19:07 manual rerun produced the ledger
with a single expected failure (suspended 002717).

A down day (benchmark -1.45%) where the books again lost less:
turnover -1.35% to 2,013,172.48 (cum +0.66%) and EP+turnover -1.02%
to 2,007,879.61 (cum +0.39%) versus benchmark cum -0.90%, excess
+1.56pp and +1.29pp. Fourteen sessions, two rebalances, zero stale
assets, guard green.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant