research: 3-year stake-sizing campaign + violent-move studies (nothing changes signals)

Walk-forward "second opinion" stake multiplier (meta-labelling on 65 decision-time
features, 3yr Oanda books @ $0.65) FAILED the pre-registered rule on M5 (clean null)
and M15 (lift inside the label-shuffle null, explained by BUY-vs-SELL). Nothing that
changes numbers ships; the 12-month M5 book re-run is byte-identical to the pinned one.

Behaviour-neutral plumbing:
- position_sizing.build_features (65 features; multiplier path returns 1.0, no artifact)
- SignalReport.penalty_tags / penalty_total / momentum_fading (record-keeping)
- TradeSetup.stake_multiplier = None (reserved)
- gpu_backtest --export-features (default export stays the pinned 63 columns; extras
  forced to the tail even when the first trade lacks an indicator)

Research tools (backtest/research): book_report, burst_map, burst_precursors,
open_trade_bursts, calendar_impact, burst_indicator_map, sizing_model, watch_rule_eval.
Findings recorded in backtest/research/sizing_campaign_2026-09.md: M5 stretch measures
are positive in both halves; every open-trade protection against violent bars loses on
both TFs; high-impact USD releases cause 20-31% of violent bars but trades held through
them do better; violent bars come when price is already at an extreme, not from calm.

Module 29 tests (13). Suite: 435 passed.

Claude-Session: https://claude.ai/code/session_01RWV2yLt3YXHHGVYHSQU2EE
This commit is contained in:
Harwinder Sekhon
2026-09-05 21:19:24 -04:00
parent bed8b549ac
commit 6bf58fd9d4
16 changed files with 4076 additions and 3 deletions
+17
View File
@@ -70,6 +70,10 @@ class TradeSetup:
# platform defaults; 0.0 means trailing is genuinely disabled.
trail_arm_r: Optional[float] = None
trail_dist_r: Optional[float] = None
# Post-decision stake multiplier from position_sizing (advisory
# overlay). None = unsized — every gate/exit path ignores it, and
# the backtester's R accounting is unaffected unless it opts in.
stake_multiplier: Optional[float] = None
@dataclass
@@ -128,6 +132,13 @@ class SignalReport:
market_regime: str = ""
volatility_regime: str = ""
exhaustion_count: int = 0
# Record-keeping only (no gate reads these): the penalty tags that
# fired in _compute_final_signal (tag -> RAW pre-decay amount), the
# decayed total actually subtracted from confidence, and whether the
# momentum-fading half of the exhaustion block was true.
penalty_tags: Dict[str, float] = field(default_factory=dict)
penalty_total: float = 0.0
momentum_fading: bool = False
class SignalAggregator:
@@ -1552,6 +1563,12 @@ class SignalAggregator:
if total_penalty >= MAX_PENALTY:
break
# Record-keeping for position_sizing / signal-log forensics.
# Purely additive — nothing below reads these fields.
report.penalty_tags = {name: float(amount) for name, amount in penalties}
report.penalty_total = float(total_penalty)
report.momentum_fading = bool(momentum_fading)
confidence -= total_penalty
confidence = max(0.0, min(95.0, confidence))