Execution-Fidelity Fix Spec — unify backtest & live construction
Authored 2026-05-29 (II) AFK. Operationalizes the session's #1 finding (RESEARCH.md top banner): the live execution path (
execution/daily.py) and the backtest (backtest/engine.py) have diverged into two different strategies. This spec is the concrete plan to unify them. It is the #1 priority before any real-money deployment.
The problem (verified this session)
The two paths share only signal + selection (252/5 momentum + smoothness, top-50 by rank + hysteresis). Everything else diverges:
| component | backtest (validated) | live (daily.py, active) |
|---|---|---|
| Weighting | HRP (keystone) | equal-weight 1/N |
| Sizing | half-Kelly (keystone) | none |
| Vol control | symmetric vol-target (12%) | one-sided vol-delever only |
| Drawdown control | 20% kill-switch (keystone) | none |
| Sector cap | 35% (binds ~20% of rebals) | none |
| Cadence | monthly (first-of-month) | weekly (every Tuesday) |
| Emergency exits | none | rank>200 / zero-vol |
Root cause: the construction/risk overlays are implemented twice — inlined in
engine.py (backtest) and partially in portfolio/constructor.py:construct_portfolio
(used only by the inactive pipeline.compute_targets) — and the active live path
(daily.py:compute_diff) reimplements a third, stripped version (equal-weight).
Three implementations → guaranteed drift.
The fix (recommended)
Extract ONE shared construction function and route BOTH the backtest engine and the live executor through it.
# portfolio/construct.py (new or extend constructor.py)
def build_target_weights(
selected: list[str], # the top-N names (post-selection/hysteresis)
returns_window: ndarray, # per-name returns for HRP + Kelly + vol-target
equity_history: list[float], # for the kill-switch
sector_map, config,
) -> dict[str, float]:
# 1. kill-switch (drawdown) -> {} (cash) if triggered
# 2. HRP weights on selected (compute_hrp_weights)
# 3. half-Kelly portfolio scalar (compute_kelly_weights -> scale HRP)
# 4. position + sector limits
# 5. symmetric vol-target (vol_target_scalar, max_leverage)
# return final target weights
- engine.py: replace its inlined block (lines ~590–680: Kelly scalar, limits,
vol-target) with a call to
build_target_weights. Verify the backtest is byte-identical after (final equity 3,419,610.237847) — the inlined logic and the function must match exactly. This is the safe first step (refactor, no behavior change). - daily.py: replace
compute_diff's equal-weight target_weights withbuild_target_weights(selected_names, ...). The selection/hysteresis stays; only the sizing changes. Add the kill-switch + sector-cap (now inside the shared fn). Decide cadence: monthly stock-selection (gate the alpha rebalance to the monthly turn) to match validation, keeping the daily vol-delever as an additional intra-month safety overlay. - Lock it with a test:
build_target_weightsoutput for a fixed input must equal the engine's pre-refactor weights AND be what daily.py applies (one source of truth, no drift).
Sequencing / caveats
- Refactor engine first (byte-identical), THEN route daily.py through it — so the live change is "adopt the already-validated construction", not new code.
- The equal-vs-HRP and weekly-vs-monthly are DELIBERATE tradeoffs (RESEARCH entries): equal-weight outperformed HRP in 2017+ (1.24 vs 0.995) but is CPCV-REJECTED/crisis-fragile; weekly is benign-now but whipsaw-prone in crises. Unifying to the validated (HRP, monthly) gives up recent return for crisis robustness — make the call on purpose.
- Re-baseline after: once live = validated, the backtest numbers finally describe the live strategy; quote net walk-forward ~0.80.
- Then the 30-day live paper-trade A/B (the only true OOS test) is meaningful — currently it's testing a strategy the backtest never validated.
Why this is #1
Every CPCV/walk-forward/keystone result in this repo describes the HRP+Kelly+vol-target+kill-switch+monthly strategy. The live trades a bare equal-weight top-N book with NO crisis controls. Recent paper-trading success is not evidence of safety — the crisis protections are all absent. Unify before real money.