Skip to content

feat(runtime): P2-2 execution realism — direction-aware fills + min_listing_days - #3

Merged
StackOverFlow11 merged 2 commits into
mainfrom
p2-execution-realism
Jun 9, 2026
Merged

feat(runtime): P2-2 execution realism — direction-aware fills + min_listing_days#3
StackOverFlow11 merged 2 commits into
mainfrom
p2-execution-realism

Conversation

@StackOverFlow11

@StackOverFlow11 StackOverFlow11 commented Jun 9, 2026

Copy link
Copy Markdown
Owner

Highlights

  • P2-2 implements direction-aware execution feasibility (up-limit blocks buys, down-limit blocks sells, suspended/missing blocks both; blocked trades carry forward, executed-only turnover, no leverage) + min_listing_days (enforced on the real path via stock_basic.list_date; demo no-op).
  • The report now shows ACHIEVED holdings (the actual post-feasibility book from BacktestDriver.holdings_log()), not the constructor's desired target — a blocked sell shows the carried name, a blocked buy is absent.
  • Verification: pytest 205 passed, ruff clean, 3× validate-config OK (demo / example_tushare / phase2_real_baseline), run-phase0 (demo) OK.
  • Real baseline (recorded in the handoff): SSE50 re-run ≈ 11.7 min, report regenerated from final code.

Summary

Phase 2-2: execution / tradability realism. Splits SELECTION eligibility (what the strategy wants) from EXECUTION feasibility (what the market lets you trade), adds direction-aware limit/suspension fills, and enforces universe.min_listing_days. No new factor, no parameter tuning, no live/QMT/vnpy. Demo/P0/P1 numbers are unchanged.

What changed

Execution feasibility (runtime/fills.py, new):

  • simulate_fills(current, target, can_buy, can_sell) — cash-coherent sell-then-buy fill model. Sells execute first (freeing cash); buys are funded from available cash and scaled down proportionally if blocked sells starved them, so the book never sums to > 1 (no leverage). Blocked trades carry forward; turnover/cost count only executed trades. Returns FillResult (achieved + blocked_buys/blocked_sells/cash_constrained/carried).
  • feasibility_from_cross derives (can_buy, can_sell) from the panel flags — independent of the selection toggles (feasibility is market physics, not policy): at-up-limit blocks buys, at-down-limit blocks sells, suspended/missing-close blocks both.

Wiring:

  • SimExecution.rebalance_to routes through simulate_fills (optional can_buy/can_sell; None → all feasible → achieved == target, so the demo/offline path is unchanged). settle() returns the invested book's return only; the driver owns the idle-cash return (BT-007), so SimExecution no longer takes a cash_return (dead after the split).
  • BacktestDriver._step reads feasibility from the rebalance date's cross-section (no lookahead), settles over the achieved book, adds idle * cash_return, and records feasibility_log() + holdings_log() (the ACHIEVED book per settled rebalance).

min_listing_days (was a no-op):

  • apply_tradable_filters enforces it as a buy/selection filter — names younger than min_listing_days as of each date are excluded (boundary age == min allowed); a missing list_date is a disclosed data gap (kept, never silently dropped). Real path enriches list_date from stock_basic; demo stays a disclosed no-op.

Reporting / disclosure:

  • phase2 report gains an Execution feasibility section (blocked buys/sells, carried, executed turnover, invested fraction); Holdings per period now reports the driver's ACHIEVED book (holdings_log()), not the constructor's desired target; list_date coverage (e.g. 67/68) disclosed. BIAS_AUDIT.md / RUNBOOK.md / TEST_REPORT.md / CLAUDE.md updated.

Review loop (closed)

A code-reviewer pass returned APPROVE (0 CRITICAL/HIGH); its 1 MEDIUM (SimExecution.cash_return dead code) was removed and LOWs addressed. Two human review rounds then flagged: (1) HIGH — Holdings per period reported the desired target instead of the achieved book (broke auditability once a trade is blocked) → fixed (driver records & reports the achieved book; regression test on a down-limit blocked sell); (2) MEDIUM — list_date coverage not disclosed for the run → added; (3) doc-consistency text in downgrades/RUNBOOK → corrected. All fixed and verified before this PR.

Quality gates (this round, env quant_mf)

Gate Command Result
Unit + integration pytest -p no:cacheprovider 205 passed, 0 failed
Lint ruff check . All checks passed
Config validation validate-config × example.yaml / example_tushare.yaml / phase2_real_baseline.yaml all OK
End-to-end (demo) run-phase0 --config config/example.yaml OK (ic_mean=0.96, P0/P1 unchanged)
Real re-run (tushare, not CI) run-phase2-baseline --config config/phase2_real_baseline.yaml OK — 68 names / 11 settled rebalances, 701s ≈ 11.7 min; report regenerated from final code

Hygiene: no tushare token, conflict markers, or artifacts//*.parquet tracked.

Real-data validation (SSE50, manual, not in CI)

  • list_date enriched 67/68 known (1 disclosed data gap, kept).
  • Execution feasibility section is all-zeros: SSE50 blue-chips do not hit daily limits, so the machinery runs and honestly reports nothing to block — numbers unchanged vs P2-1 (P2-2 is behaviour-neutral on this universe). Blocking behaviour is proven by synthetic driver tests (test_driver_feasibility.py), not the real run.

Test plan

  • test_fills.py (10): cash-coherent fills (no leverage), direction semantics, executed-only turnover, demo equivalence.
  • test_driver_feasibility.py (6): end-to-end down-limit carries / up-limit blocks buy / suspended no-trade / holdings == achieved (not desired) / feasibility log == nav index.
  • test_min_listing_days.py (6): boundaries (age <, ==, >), missing list_date kept, no-op cases.
  • phase2 report: achieved-holdings + list_date coverage rendered (test_phase2_baseline.py).
  • Whole suite network-free (TEST-002); demo/P0/P1 numbers unchanged.

…sting_days

P2-2 closes execution/tradability realism gaps. It splits SELECTION eligibility
(what the strategy wants) from EXECUTION feasibility (what the market lets you
trade) and stops the backtest from silently forcing impossible trades. No new
factor, no parameter tuning.

Execution feasibility (new):
- runtime/fills.py: simulate_fills(current, target, can_buy, can_sell) — a
  cash-coherent sell-then-buy fill model. Sells execute first (freeing cash),
  buys are funded from available cash and scaled down proportionally if blocked
  sells starved them, so the book never sums to > 1 (no leverage). Blocked trades
  carry forward; turnover counts only executed trades. feasibility_from_cross
  derives (can_buy, can_sell) from the panel flags: at_up_limit blocks buys,
  at_down_limit blocks sells, suspended/missing-close blocks both — independent
  of the selection toggles (feasibility is market physics, not policy).
- SimExecution.rebalance_to routes through simulate_fills (optional can_buy/
  can_sell; None -> all feasible -> achieved == target, so demo/P0/P1 numbers are
  unchanged). settle() returns the invested book's return only; the driver owns
  the idle-cash return (BT-007), so SimExecution no longer takes a cash_return
  (it was dead after this split).
- BacktestDriver._step reads feasibility from the rebalance date's cross-section
  (no lookahead), settles over the achieved book, adds idle*cash_return, and
  records a per-rebalance feasibility_log() (blocked buys/sells, carried,
  executed turnover, invested fraction) aligned 1:1 with the NAV index.

min_listing_days (was a no-op):
- apply_tradable_filters enforces it as a buy/selection filter: names younger
  than min_listing_days as of each date are excluded (boundary age==min allowed);
  a missing list_date is a disclosed data gap (kept, never silently dropped).
- Real path enriches list_date from tushare stock_basic; demo stays a disclosed
  no-op. enrich_listing + TushareCovariatesFeed.listing_dates + pipeline wiring.

Reporting/disclosure: phase2 report gains an Execution feasibility section;
BIAS_AUDIT/RUNBOOK/TEST_REPORT/CLAUDE updated; _collect_downgrades is now
direction-aware and path-aware for listing.

Tests (204 passed): test_fills (10), test_driver_feasibility (5),
test_min_listing_days (6). Also fixed a duplicate test-function name across two
files that had been silently shadowing one test in the full-suite run.

Real validation (SSE50, ~11 min, not in CI): the feasibility machinery runs and
honestly reports zero blocks on blue-chips (they don't hit limits); list_date
enriched 67/68; numbers unchanged vs P2-1 (P2-2 is behaviour-neutral there).
… list_date coverage

Review fixes for the P2-2 auditable report (report/doc correctness, not strategy):

1. HIGH — Holdings per period reported the constructor's DESIRED target
   (reconstruct_holdings -> constructor.build), so once a real trade is blocked the
   report would show the wanted book, not the held one — breaking P2-2 auditability.
   BacktestDriver now records the ACHIEVED book per settled rebalance
   (holdings_log()); the phase2 report uses it. A blocked sell shows the carried
   name, a blocked buy is absent. reconstruct_holdings removed. Regression test
   drives a down-limit blocked sell and asserts the reported holdings are the
   carried name (achieved), never the desired target, and equal the execution book.

2. MEDIUM — the real run logged "list_date 67/68 known" but the report only stated
   the generic missing-list_date policy. list_date coverage (known / missing this
   run) is now in Phase2Result and the report, with a test.

3. Doc consistency — the holdings downgrade text, module docstring, and RUNBOOK
   still described holdings as a constructor.build reconstruction; updated to the
   driver's achieved (post-feasibility) book. RUNBOOK downgrade section updated so
   direction-aware limits and enforced min_listing_days are no longer listed as
   deferred.

Gates: 205 passed, ruff clean. Real re-run (SSE50, ~11.7 min) regenerated the
report from final code: ACHIEVED-holdings note, list_date 67/68 (1 disclosed gap),
no stale constructor.build text; numbers unchanged (no blocks on blue-chips).
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