Skip to content

Latest commit

 

History

History
1352 lines (922 loc) · 121 KB

File metadata and controls

1352 lines (922 loc) · 121 KB

Strategy

Source of truth for trading rules — what we test. Engine architecture lives in ENGINE.md.

Convention: current approach at top, dated history at bottom. Rewrite the top in place when the strategy changes; append a dated entry to the bottom.


Current approach — v1.14 rule fleet (6 shipped TSLL adaptive rules incl. management override + exit hook) + model scaffolding (entry/management advisors behind enable_model_*=False; research loop via simulator/)

In plain words: what the strategy actually does

The framework — direction from regime, DTE/delta for reversal-resistance. The market's current trend tells us which side of the chain to sell:

  • Bullish / neutral up-drift → sell out-of-the-money puts. Premium decays in our favor as long as the stock holds above our strike.
  • Confirmed downtrend (bearish regime) → sell out-of-the-money calls. Same premium decay, opposite direction. The downtrend's tailwind moves the underlying away from our strike, exactly the position we want.

Once the side is picked, the harder question is DTE × delta — far enough OTM to survive a reversal, short enough DTE that we don't sit through one. v1.9 introduces a separate bear_dte × bear_target_delta pair so short calls in bearish regimes get their own reversal-resistant params, not the put-side defaults.

The edge: the market rarely moves enough in a few days to put a well-sized short premium in trouble. We collect more often than we pay out. But one bad day on a sized position can wipe out months of small gains, so 90% of the work is the entry filter — direction + DTE + delta + IV-rank + credit-floor + reversal-detection all chained together.

When we still stand aside (even with both directions enabled):

  • Thin premium. Credit needs to be at least 1.0% of strike on TSLA (1.2% on TSLL). On most calm days the math doesn't clear this floor — so we don't trade. Today's STAND_ASIDE is the strategy doing its job, not a broken signal.
  • High-vol rallies into existing uptrend. If IV is elevated and trend is up (ema_21 > ema_55), we don't flip to defensive short calls (the v1.1 strategy's biggest mistake — selling calls into a +59% TSLA rally for −$2k). Defensive shorts only fire on real intraday reversals or high-IV-with-weak-trend.

What we sell, per ticker:

  • TSLA: 7-DTE puts at delta 0.20 in bullish/neutral; 3-DTE calls at delta 0.20 in bearish. Stop and reverse: 0.50 delta. Quick winners: close at 2× theta-pace.
  • TSLL: 3-DTE puts at delta 0.30 in bullish/neutral; 5-DTE calls at delta 0.20 in bearish. Stop: 0.45 delta. Quick winners: 1.25× theta-pace.

TSLL gets a shorter put-DTE (and longer bear-call-DTE) because it's a 2x-leveraged ETF: bigger daily moves, faster decay, smaller absolute price scale. TSLA gets the opposite (longer puts, shorter calls) — calls into the rare TSLA bearish regimes must be 3-DTE specifically because 5+ DTE catastrophe-flagged in the sweep. Same engine, different per-side / per-ticker optima — that's why DEFAULT_CONFIG_BY_TICKER is a 4-way (ticker × side × DTE × delta) lookup, not a one-size-fits-all.

When we close (per-ticker, since the two underlyings need different protection):

The user's framework: regime should drive entries, profit should drive exits. Don't mix entry-signal logic into exit decisions — if the regime flips, the position will close on its own profit/loss outcome, and a new entry will fire if the new regime calls for it. v1.10 decouples per ticker based on what each one actually needs:

Rung TSLA TSLL Why
Expiration ✓ always ✓ always Mechanical
Daily profit pace ✓ 2× theta-target ✓ 1.25× theta-target The dominant friendly exit (don't give back fast wins)
Profit target ✓ 55% of credit ✓ 55% of credit Clean profit-taking floor
Max loss ✓ 1.8× credit (kept) ✗ disabled (raised to 10×) TSLA needs the safety net; TSLL doesn't (see Why below)
Delta breach ✓ 0.50 Δ
Trend reversal ✗ removed ✓ kept (low-firing) TSLA's regime_flip exit was prematurely closing winners (+$2,214 to remove). TSLL only fires it ~1x in 5y, so removal is neutral.
DTE stop dead under current short-DTE config

Why max_loss differs: On TSLA at 7-DTE, max_loss-stopped positions historically didn't recover often enough — the safety net is still earning its keep. On TSLL at 3-DTE, delta_breach=0.45 already exits before max_loss could matter; the extra dollar-threshold stop just locks in losses that often would have recovered. The data picked the right protection per ticker — same logic family, different instances.

The wheel option (opt-in via --wheel): when a put goes to expiration ITM, accept assignment, then sell covered calls until cycled out. Mechanically works but on 5-year history doesn't beat cash-only. TSLL is especially bad as a wheel candidate because of leveraged-ETF decay during hold periods. Off by default; preserved for users who want to test it.

What you actually see day-to-day:

  • ~27 TSLA trades/year (v1.9 adds a few bear-call days; most TSLA bearish regimes are also low-IV → still skipped by credit floor).
  • ~100 TSLL trades/year (TSLL is bearish ~50% of days, so v1.9 nearly doubles TSLL trade count).
  • Win rate ~89% on TSLA (v1.10's regime-flip removal lifted it from ~81%), ~71% on TSLL. Wins smaller than losses individually but more frequent.
  • 5y max drawdown: $1,856/contract on TSLA (unchanged since v1.6), $359 on TSLL (v1.10 trimmed it from $423 by dropping the premature max_loss stop).

Cost function (non-negotiable): we optimize for never losing big, not for the biggest possible win. Calm-case opportunity cost is acceptable; extreme-case catastrophe is not. Every tuning decision in this codebase passes through that lens, and the LLM-critic loop is constrained by it too.


Technical config (for engineers)

Single-leg short premium with per-ticker, per-side entry parameters. TSLA and TSLL are different vehicles (TSLL is 2x-leveraged with structurally higher relative IV and shorter price scale), so the optimal entry params differ. v1.5 introduced DEFAULT_CONFIG_BY_TICKER and get_config(ticker) in strategies.py; v1.9 extended it with per-ticker bear-side (call) params. Backtest, scenarios, and live all use the per-ticker default.

Knob TSLA TSLL Rationale
long_dte 7 3 Bullish/neutral put DTE. Short-DTE captures concentrated theta; TSLL's higher vol justifies even shorter
long_target_delta 0.20 0.30 Bullish/neutral put delta. Closer to ATM than v1.3's 0.15 — bigger credits relative to strike
bear_dte 3 5 v1.9: bearish-regime call DTE. TSLA caps at 3 (5+ catastrophe-flagged the sweep); TSLL prefers 5. Short DTE limits reversal-rally exposure.
bear_target_delta 0.20 0.20 v1.9: bearish-regime call delta. Both tickers landed on 0.20 — the sweep's response surface peaked there for both.
min_credit_pct 0.010 0.012 TSLL's mcp slightly higher to skim the best 5y P/L from the sweep
delta_breach 0.50 0.45 v1.6: recalibrated for short DTE. 0.38 was set for 30-DTE where gamma is slower; at 3-7 DTE it fired on routine noise. TSLL caps at 0.45 because leveraged moves punish further-out tolerance.
daily_capture_mult_short 2.0 1.25 v1.7: 1.0 was too eager for short-DTE. At 7-DTE, theta is ~14%/day so realized pace hits 1.0× on day 1, closing winners too early. Raising lets positions hold longer toward profit_target / expired-OTM. TSLL noisier — 1.25 is the robustness pick.
max_loss_mult 1.8 10.0 v1.10: TSLL effectively disabled (10.0 ≈ ∞ in practice). Sweep showed disabling raises P/L +$110 and lowers DD $64 — the early stops were locking in losses that would have recovered via theta or reversal. delta_breach=0.45 carries all the tail protection. TSLA stays at 1.8 — combined-sweep showed removing both regime_flip and max_loss on TSLA blew DD from $1,856 → $2,569 (the safety net is still earning its keep at 7-DTE).
regime_flip_exit_enabled False True (default) v1.10: TSLA only. The exit was prematurely closing trades that would have ridden to profit_target / expiration. Disabling: P/L +$2,214, WR 81.5% → 88.9%, DD unchanged, suite +$705, OOS +$1,361. The cleanest single-knob win of the session. TSLL keeps default (only ~1 regime_flip exit per 5y on TSLL — no signal to disable).

Setting bear_dte = 0 falls back to the v1.8 stand-aside-in-bearish behavior. All other v1.3 settings (gates, exit ladder, gap-slippage, etc.) carry over unchanged.

Top finding from the DTE × delta sweep

Grid: 8 DTE × 5 delta = 40 configs per ticker. Scored by P/L − 1.0 × max_dd (heavier DD weight than the v1.4 optimizer's 0.5×, per the tail-management cost function). Top results:

TSLA (5y backtest):

DTE Δ P/L DD PF Trades
30 (v1.3 default) 0.15 +$1,583 $3,534 1.07 200
7 (v1.5) 0.20 +$11,815 $2,436 2.29 122
5 0.20 +$8,053 $1,102 2.91 67
10 0.15 +$7,450 $1,239 3.17 64

TSLL (5y backtest):

DTE Δ P/L DD PF Trades
30 (v1.3 default) 0.15 +$23 $555 1.01 146
3 (v1.5) 0.30 +$1,757 $440 1.74 158
3 0.25 +$493 $462 1.18 150
45 0.30 +$417 $699 1.14 134

Multi-version progression (TSLA 5y):

Version P/L Max DD Profit factor Trades
v1.0 ~−$10,000 ~$15,000 <1 ~500
v1.1 −$6,242 $12,292 0.87 428
v1.2 +$2,243 $4,910 1.09 240
v1.3 +$1,583 $3,534 1.07 200
v1.5 +$11,815 $2,436 2.29 122

Net cumulative improvement over the session: TSLA +$18,057 in 5y P/L, max DD cut from ~$15,000 to $2,436 (84% reduction), profit factor from <1 to 2.29.

Why shorter DTE × higher delta wins

  • Theta concentration. Time decay accelerates in the final 1–2 weeks of an option's life. A 7-DTE option collects ~30% of its lifetime theta on day 1 — much higher per-day yield than a 30-DTE entry's ~3%.
  • Less time for regime flip. Short DTE means less opportunity for regime_flip / delta_breach / dte_stop to close the position before profit-take. More trades resolve cleanly via expiration or daily_capture.
  • Higher delta = bigger credits. Δ0.20–0.30 strikes collect ~2–3× the credit of Δ0.15. Even though they're more likely to go ITM, the larger credit cushion absorbs more adverse moves before max_loss fires.
  • Gates still hold. The bearish regime gate, high_iv defensive-call gate, and gap-slippage shock all still apply unchanged. The strategy is the same shape, just calibrated for higher-velocity premium capture.

Live behavior

The mcp=0.010 floor means the strategy stands aside when credit-per-strike is thin. On 2026-05-12 with TSLA iv_rank=42.5 (moderate), 7-DTE × Δ0.20 credit/strike was 0.69%, below the 1% floor → STAND_ASIDE. The 5y data shows ~25 entries/year on TSLA at this config, so most days are stand-aside. This is the source of edge, not a failure mode.

Wheel mechanic (opt-in via wheel_enabled=True)

Single-leg short premium, regime-aware direction selection, with a 7-rung exit ladder anchored on a DTE-bucketed daily-profit-capture rule.

The wheel state machine:

IDLE ─sell put──► SHORT_PUT ─expires OTM──► IDLE  (keep credit, repeat)
                  SHORT_PUT ─pre-expiry────► IDLE  (max_loss / daily_capture / etc. fire)
                  SHORT_PUT ─expires ITM───► HOLDING  (accept assignment at strike)
HOLDING ─sell call────► COVERED ─expires OTM────► HOLDING  (keep credit, sell another CC)
                        COVERED ─expires ITM────► IDLE  (assigned away — cycle complete)

Wheel-specific config (only active when wheel_enabled=True):

Knob Default Role
wheel_put_dte 14 Shorter than long_dte so puts can reach expiration before regime_flip / dte_stop fire
wheel_put_max_loss_mult 3.0 Wider max_loss tolerance than the cash-only 1.8 — lets puts survive deeper before stopping out
wheel_skip_regime_flip True Don't close on bearish regime in wheel mode — accept assignment instead
cc_dte 14 Covered-call DTE — shorter for more cycles
cc_strike_mode 'basis' Sell CC AT cost basis (aggressive — we WANT to cycle out)
cc_target_delta 0.30 Used only if cc_strike_mode == 'delta'

Wheel-mode puts skip dte_stop and delta_breach so they can ride into ITM territory. Wheel-mode CCs run a simplified exit ladder (only expired + early profit-take rungs) — no max_loss / delta_breach / regime_flip, because the goal is assignment-away.

Sweep findings (cash-only vs wheel; 5y backtest)

Eight wheel variants tested across TSLA and TSLL. Highlights:

TSLA (cash-only v1.3 baseline: +$1,583, DD $3,534):

Wheel config P/L DD Cycles Comment
14d puts, ML=5.0, skip flip +$2,818 $5,961 4 Wheel triggers but DD worsens
14d puts, ML=1.8, skip flip +$2,832 $3,600 0 Same P/L without any wheel — gain is from short-DTE alone
7d puts, ML=3.0, skip flip +$4,286 $579 0 Best — but wheel never fires; this is short-DTE put selling, not wheel
30d puts, ML=3.0, skip flip +$860 $4,976 1 (incomplete) Wheel rarely triggers at 30 DTE

TSLL (cash-only v1.3 baseline: +$80, DD $555):

Wheel config P/L DD Cycles Comment
14d puts, ML=3.0, skip flip −$1,492 $1,693 1 incomplete Stuck holding TSLL at end-of-data
14d puts, ML=1.8, skip flip −$297 $714 1 Slightly less bad; max_loss kicks earlier
7d puts, ML=3.0, skip flip −$504 $1,404 1 Still net negative
30d puts, ML=3.0, skip flip −$712 $1,404 0 Bad even without wheel triggering

TSLL wheel is structurally bad. Every wheel variant underperforms cash-only by $300–$1,500 with 2–3× the drawdown. The mechanism: when TSLL gets assigned during a downtrend, the leveraged-ETF volatility decay compounds the unrealized stock loss faster than CC premium can offset. By the time a cycle completes (or doesn't — one ended with stock still held), the cumulative drag dominates premium income. This falsifies the "wheel more on TSLL" hypothesis the user wanted to test.

Entry logic (pick_entry in strategies.py)

if iv_rank < iv_rank_min:                                   skip
if regime == 'bearish':
    if bear_dte > 0:                                        SELL CALL @ bear_target_delta,  bear_dte
    else:                                                   skip                     # legacy v1.8 behavior
elif reversal OR (high_iv AND ema_21 < ema_55):             SELL CALL @ short_target_delta, short_dte
else:                                                       SELL PUT  @ long_target_delta,  long_dte

# v1.11 adaptive rules pass — each rule in cfg.adaptive_rules may modify (side, dte, target_delta)
# or return {'skip': True} based on current row features.
resolved = adapt_entry_params(row, cfg, base=(side, dte, target_delta))
if resolved.skip:                                           skip

# v1.3 post-pricing filter:
if credit / strike < min_credit_pct:                        skip   (cuts low-credit-vs-risk entries)

The decision is symmetric: regime tells us direction (puts in bullish/neutral, calls in bearish), then DTE × delta is tuned per ticker × side to be reversal-resistant. The ema_21 < ema_55 trend confirmation on the high_iv branch is the v1.2 fix that prevents flipping to defensive short-call selling during high-IV bull rallies; the bearish→call branch is the v1.9 fix that monetizes the 41-50% of days the strategy was previously standing aside on. v1.11 adds the adaptive-rules pass between regime-branch and pricing — see Adaptive rules architecture below.

The bearish regime gate fires on price < ema_55 AND macd < 0 (v1.2 dropped the inconsistent rsi_14 > 75 clause). Pre-v1.9 it stood the strategy aside; v1.9+ flips to short-call selling there at a separate (typically shorter) DTE so reversal rallies have less time to develop.

The min_credit_pct = 0.010 filter (v1.3) demands credit ≥ 1% of strike. This eliminates the dominant max-loss-tail mechanism: $1-credit on a $250-strike trade can still cost $20 to close — the asymmetry of payoff per unit of strike-risk is what drives the worst losses, not the absolute credit alone.

Honest accounting at exit — gap-slippage shock (backtest.py, v1.3)

Daily-bar engine limitation: when max_loss fires after an overnight gap, the daily-close fill is better than the realistic gap-open fill. v1.3 models this gap-day asymmetry: when overnight |open − prev_close| / prev_close > gap_threshold_pct (default 3%) AND a max_loss exit fires, the engine applies gap_slippage_mult (default 15%) on top of standard slippage. Non-max_loss exits and non-gap days are unaffected.

This is purely an honesty improvement, not a strategy change — but it shrank the optimistic v1.2 TSLA 5y P/L from +$2,243 to +$1,583, surfacing the real cost of the daily-bar gap-risk limit.

Strike is computed by inverting Black-Scholes: strike_from_delta(S, T, σ, target_Δ, side), then rounded to the nearest $2.50. Credit is the theoretical BSM mid at that strike, adjusted for slippage.

The classifier is stateless — it re-evaluates from current features every bar. Direction is never "locked" for the day.

Adaptive rules architecture (v1.11 / v1.13)

Goal: use all available features (greeks, IV rank, trend strength, recent returns, volume, calendar context) to adaptively pick entry params per-bar instead of relying on fixed per-ticker knobs. Each rule is a small interpretable function that runs through the same validation gate.

Entry hook (v1.11): adapt_entry_params(row, cfg, base) -> dict is called inside pick_entry after the regime branch picks (side, dte, target_delta). Each rule in cfg.adaptive_rules is a function (row, cfg, current) -> dict that may return any subset of:

  • 'skip': True — force stand-aside.
  • 'side': 'put'|'call', 'dte': int, 'target_delta': float — modify the entry params (peek-greeks refresh so subsequent rules see updated values).
  • (v1.13) 'min_credit_pct': float — override the credit floor at entry.
  • (v1.13) 'max_loss_mult': float, 'delta_breach': float, 'profit_target': float, 'daily_capture_mult': float — store per-position overrides on the resulting Position; check_exits prefers position-level values over cfg.

Rules run in registration order; the resolved params drive the pricing step and Position construction.

Exit hook (v1.13): adapt_exit_params(position, mark, row, cfg) -> dict runs at the top of check_exits (before the standard ladder, after the expiration check). Each rule in cfg.exit_rules is a function (position, mark, row, cfg) -> dict that may return {'close': True, 'reason': str} to close the position with a custom reason. Default cfg.exit_rules = () is a no-op. Skipped for is_wheel_cc positions (those run their own simplified ladder). Example: take_half_on_reversal — close when pnl ≥ 50% of credit AND the daily row carries a reversal flag (puts) or a flipped regime (calls).

Registries:

  • ADAPTIVE_RULES: dict[str, Callable] — entry-side rules in strategies.py.
  • ADAPTIVE_EXIT_RULES: dict[str, Callable] — exit-side rules in strategies.py.

How to add an adaptive rule (the canonical ≤30-line process)

The flow below adds a new rule without touching the engine, the backtest harness, or any caller. End-to-end this is the bottom-up version of the goal: any interpretable hypothesis can be added, validated, and either shipped or null-resulted on a stable interface.

  1. Find the hypothesis. Run .venv/bin/python analyze.py --tickers TSLA TSLL --scan-narrow --pairs --alpha 0.10. The analyzer surfaces narrow-window candidates (sliding 8%/15%/25% bands) and 2-feature interaction cells, ranked by Welch t and BH-FDR. Pick the strongest negative-bucket candidate that survives multiple-testing correction (or note it as a weaker but interpretable hypothesis).

  2. Write the rule in strategies.py — a single function plus one line of registration:

    def _rule_<ticker>_<short_name>(row, cfg, current):
        """v1.13 — <one-line summary>.
        Empirical motivation: analyze.py reported <bucket> → n=X, avg=$Y, t=Z, p=W.
        """
        if cfg.ticker != '<TICKER>':           # per-ticker guard
            return {}
        v = row.get('<feature>')               # or current.get('<peek_key>')
        if v is None or not np.isfinite(v):
            return {}
        if <condition>:                        # e.g. lo <= v <= hi
            return {'skip': True}              # OR any subset of M4 keys
        return {}
    
    ADAPTIVE_RULES['<ticker>_<short_name>'] = _rule_<ticker>_<short_name>
  3. Validate via the standard gauntlet: .venv/bin/python validate_rule.py --ticker <TICKER> --rule <rule_name>. The harness runs the 5y backtest, 12-regime suite, and walk-forward static OOS against current per-ticker defaults; reports per-surface deltas + a verdict.

  4. Decide. Per the cost function:

    • Ship if all three surfaces (5y + suite + OOS) are net-positive with no catastrophe (worst regime drop > $500) — triple-win. Or if 3 of 4 surfaces (5y / DD / OOS / suite-worst) are positive with no catastrophe.
    • Null otherwise. Document the test and tested-but-skipped reason in the next history entry. The rule stays registered (so the analyzer doesn't re-propose it).
  5. Ship. Add the rule name to the per-ticker 'adaptive_rules' tuple in DEFAULT_CONFIG_BY_TICKER. Update STRATEGY.md's "Currently active adaptive rules" table.

  6. Update docs. Bump version line at top of STRATEGY.md, update the v1.X baseline numbers and rules table, append a v1.X history entry.

Currently active adaptive rules (v1.14)

Rule Ticker Logic Surfaced via Why it works (hypothesis)
tsll_skip_marginal_up TSLL skip if 0 ≤ ret_14d ≤ 0.05 v1.12 hand-found bucket stalled rally after small up-move → puts caught by reversal
tsla_skip_mild_intraday_up TSLA skip if 0.5% ≤ intraday_return ≤ 1.6% v1.13 --scan-narrow exhaustion candle into mild green day → next-bar reversal risk
tsll_skip_tuesday TSLL skip if day_of_week == 1 v1.13 --scan-narrow Tue→Fri 3-DTE puts sit through more overnight gap risk than Wed/Thu
tsll_skip_post_earnings_drift TSLL skip if 11 ≤ days_since_earnings ≤ 21 v1.13 --scan-narrow IV-rank decay window misleads — residual post-earnings vol catches puts
tsll_skip_downtrend_high_iv TSLL skip if -0.63 ≤ ret_14d ≤ -0.07 AND iv_rank ≥ 77 v1.13 --pairs post-decline IV spike before capitulation — rich-looking premium gets caught
ride_high_credit_mgmt TSLL if credit/strike ≥ 1.35× min_credit_pctdaily_capture_mult=2.2, profit_target=0.70 v1.14 model distill rich premium days → hold_longer-style exits; TRIPLE-WIN on 5y+suite+OOS (DD flat)

Six rules on TSLL, one on TSLA. Model-inspired candidates tight_risk_high_gamma and dynamic_credit_on_high_gamma_marginal remain registered but not enabled (TSLA null / neutral).

Exit ladder — first to fire wins (check_exits)

# Rung Triggers when
1 expired DTE_remaining ≤ 0 → settle at intrinsic
2 max_loss pnl ≤ -max_loss_mult × credit (checked first so we never book a "profit" on a day we'd be stopped)
3 daily_capture pnl > 0 AND pnl / days_held ≥ daily_capture_mult × (credit / DTE_at_entry)
4 profit_target pnl ≥ profit_target × credit
5 delta_breach `
6 dte_stop entered with DTE > 14 AND DTE_remaining ≤ dte_stop
7 regime_flip put + (reversal or bearish) → close; call + bullish-no-reversal → close

Daily profit capture (your formula)

At entry: daily_theta_target = credit / DTE_at_entry.

Each day held: close when realised P/L per day held ≥ multiplier × that target.

DTE at entry Multiplier Why
≤ 7 1.0× Theta accelerating — one day of decay is real alpha
8 – 15 1.5× Needs a cushion before closing to avoid churning out of winners
≥ 16 2.0× Per-day theta is tiny; demand 2× the pace before exiting

Worked example: 5 DTE put, credit $1.00, daily target = $0.20. After 1 day, premium = $0.80 → realised = $0.20 = 1× target → close.

Current config defaults (StrategyConfig in strategies.py)

Knob Default Group
iv_rank_min 10.0 entry filter
min_credit_pct 0.010 entry filter — v1.3, sweep best for both tickers
long_target_delta 0.15 bullish/neutral put delta — v1.1, walk-forward modal
long_dte 30 bullish/neutral put DTE
short_target_delta 0.17 defensive call (reversal/high_iv) delta
short_dte 7 defensive call DTE
bear_target_delta 0.20 bearish-regime call delta — v1.9, per-ticker default = 0.20
bear_dte 0 bearish-regime call DTE — v1.9, per-ticker (TSLA=3, TSLL=5); 0 = stand-aside (v1.8 behavior)
profit_target 0.55 exit
max_loss_mult 1.8 exit
delta_breach 0.38 exit
dte_stop 21 exit
dte_stop_min_entry 14 only apply dte_stop to entries above this
daily_capture_mult_short 1.0 DTE ≤ 7 bucket
daily_capture_mult_mid 1.5 DTE 8–15 bucket
daily_capture_mult_long 1.5 DTE ≥ 16 bucket — v1.1, walk-forward consensus
slippage_pct 0.0 execution (uniform)
gap_slippage_mult 0.15 execution — v1.3, applied to max_loss after gap
gap_threshold_pct 0.03 execution — v1.3, gap size that triggers gap_slippage_mult
risk_free_rate 0.04 execution

Latest baseline (v1.13 defaults)

Run: just backtest on 2026-05-14.

Ticker Window Trades Win % Total P/L Profit factor Max DD
TSLA 2022-06 → 2026-05 100 92.0% +$17,790 5.25 $1,107
TSLL 2023-09 → 2026-05 160 78.8% +$3,250 4.28 $144

v1.12 → v1.13 deltas: TSLA +$1,511 (+9.3%) P/L, $-749 (-40%) DD. TSLL +$790 (+32%) P/L, $-215 (-60%) DD. Profit factor jumped from 1.84→4.28 on TSLL and 3.48→5.25 on TSLA. 4 new adaptive rules (3 from --scan-narrow, 1 from --pairs) plus the engine-side M4 (per-position knob overrides) and M5 (exit-side adaptive hook) infrastructure.

Earlier baseline reference (v1.12 defaults)

Ticker Window Trades Win % Total P/L Profit factor Max DD
TSLA 2022-06 → 2026-05 108 88.9% +$16,279 3.48 $1,856
TSLL 2023-09 → 2026-05 238 73.1% +$2,460 1.84 $359

Multi-version progression (TSLA 5y):

Version P/L Max DD Profit factor Trades
v1.1 −$6,242 $12,292 0.87 428
v1.2 +$2,243 $4,910 1.09 240
v1.3 +$1,583 $3,534 1.07 200
v1.5 +$11,815 $2,436 2.29 122
v1.6 +$12,975 $1,856 2.69 116
v1.7 +$13,490 $1,856 2.87 106
v1.8 +$13,490 $1,856 2.87 106
v1.9 +$14,065 $1,856 2.95 108
v1.10 +$16,279 $1,856 3.48 108

Net session improvement (TSLA): +$22,521 in 5y P/L; max DD cut from $12,292 → $1,856 (85% reduction); profit factor from 0.87 → 3.48; win rate from ~60% → 89%. v1.10's single change (disable regime_flip exit) added +$2,214 by letting positions that would have closed on regime change ride to profit_target or expiration instead.

TSLL trajectory across session: −$981 (v1.1) → +$2,356 (v1.10) — swing +$3,337, max DD cut $1,461 → $359 (−75%). v1.10 added +$110 / -$64 DD by effectively disabling the dollar-threshold max_loss stop (the leveraged-ETF delta_breach=0.45 already carries all tail protection).

Earlier baseline reference (v1.3 defaults)

Run: just backtest on 2026-05-12 (full default 5y window).

Ticker Window Trades Win % Total P/L Profit factor Avg win Avg loss Max DD
TSLA 2022-06 → 2026-05 200 79.0% +$1,583 1.07 +$149 −$523 $3,534
TSLL 2023-09 → 2026-05 127 78.7% +$80 1.04 +$19 −$66 $555

Multi-version progression (TSLA 5y):

Version Trades Total P/L Max DD Note
v1.0 ~500 ~−$10,000 ~$15,000 original
v1.1 428 −$6,242 $12,292 walk-forward defaults
v1.2 240 +$2,243 $4,910 regime fixes (optimistic accounting)
v1.3 200 +$1,583 $3,534 engine polish (honest accounting + credit filter)

The v1.2 → v1.3 step loses $660 of headline P/L but gains $1,376 of drawdown reduction and cuts 40 trades — most of which were the low-credit-per-strike entries that drove the max-loss tail. Per the cost function (manage extreme moves), this is a clear win: lower P/L under honest accounting, materially lower drawdown, fewer trade-counts (less attention/effort per dollar earned).

Scenario suite results (v1.3 defaults, 12-regime suite)

Run: just scenarios. Strategy is tested against fixed canonical 21-day windows per regime so that A/B comparisons across strategy versions are apples-to-apples. Windows are pinned in scenarios.py::CANONICAL_SCENARIOS.

The suite was expanded from 7 to 12 regimes on 2026-05-12. The new five (gap_shock, vol_crush, vol_expansion, chop_whipsaw, earnings_window) target tail / vol / event behaviors not covered by direction-only regimes. The expansion reflects the strategy philosophy: optimize for extreme-case management; calm-case performance follows for free.

TSLA:

Regime Window Undly ret Trades Win % P/L Max DD Note
huge_down 2025-02-19 → 2025-03-19 −34.6% 0 $0 $0 gate stands aside ✓
normal_down 2024-09-24 → 2024-10-22 −14.3% 7 71.4% −$754 $1,097 gate insurance cost
flat 2023-11-14 → 2023-12-13 +0.8% 4 75.0% −$19 $0 mcp filter pruned thin-credit days
normal_up 2024-08-12 → 2024-09-10 +14.5% 8 62.5% +$169 $284 clean
huge_up 2024-10-22 → 2024-11-19 +58.7% 11 72.7% +$980 $421 defensive trend gate works ✓
v_recovery 2025-04-09 → 2025-05-08 +4.6% 5 100.0% +$944 $0 mcp tightened — fewer trades, all winners
inverse_v 2024-09-17 → 2024-10-15 −3.6% 10 80.0% −$369 $1,097 trend reversal hurts (worse with honest accounting)
gap_shock 2024-10-01 → 2024-10-29 +0.6% 6 50.0% −$815 $771 honest accounting (v1.2: −$676) — engine limit measured
vol_crush 2024-05-15 → 2024-06-13 +4.9% 0 $0 $0 mcp filter cuts thin-credit IV-crush sells (v1.2: +$813)
vol_expansion 2025-03-05 → 2025-04-02 +1.3% 0 $0 $0 stands aside correctly ✓
chop_whipsaw 2024-02-05 → 2024-03-05 −0.2% 0 $0 $0 stands aside correctly ✓
earnings_window 2025-10-15 → 2025-11-12 −1.0% 9 77.8% +$374 $574 handles event days cleanly ✓

4/12 regimes positive, 5 zero-trade stand-aside, 3 negative. Suite total: +$510 (v1.2 12-regime was +$2,492).

The suite-vs-backtest divergence is informative: 5y backtest improved under v1.3 (DD fell 28%) while the per-regime suite fell because the canonical vol_crush window happens to have low absolute credit-vs-strike (TSLA was at ~$170 during May 2024 where 30-DTE 15Δ-put credits were ~0.6% of strike, below the 1% floor). The 5y backtest spans many more such conditions but the filter pays off net-net on long sequences. Action: the canonical vol_crush window may need re-curation to remain a useful regime-test under v1.3+.

TSLL:

Regime Undly ret Trades Win % P/L Max DD Note
huge_down −50.1% 0 $0 $0 gate stands aside ✓
normal_down −13.4% 3 0.0% −$48 $42 regime_flip exits
flat no canonical window — 2x leverage
normal_up +14.8% 7 85.7% +$78 $0 clean
huge_up +89.0% 11 90.9% +$370 $0 defensive trend gate works ✓
v_recovery +3.1% 5 80.0% −$0 $36 flat
inverse_v +4.8% 10 80.0% +$50 $22 clean
gap_shock −0.9% 6 50.0% −$88 $114 gap loss (smaller than TSLA)
vol_crush +7.1% 4 75.0% +$15 $30 mcp pruned 2 trades; outcome similar
vol_expansion −9.2% 0 $0 $0 stands aside (bearish gate) ✓
chop_whipsaw −0.9% 9 77.8% −$177 $320 TSLL still whipsaws (v1.2: −$213, small improvement)
earnings_window +5.1% 9 77.8% +$58 $60 clean

6/11 profitable. Suite total: +$258 (v1.2: +$228).

The TSLA-vs-TSLL divergence on chop_whipsaw persists. TSLL still whipsaws (−$177) where TSLA stands aside. The mcp filter alone didn't recover it. Likely a ticker-feature-magnitude effect that needs additional treatment — candidate for the next tuning round.

Tradeoffs surfaced — v1.1 → v1.2

The bearish-gate fix and defensive-call trend gate buy two huge regime wins (huge_down and huge_up) but degrade two milder regimes:

  • normal_down regressed by $1,748 (TSLA) — the new bearish gate fires after the trend is established, so the strategy now sits out late-leg trades that v1.1 had time to win, while still entering early-leg trades before the gate fires. Tightening the gate further (e.g. adding a ret_14d < -0.05 confirmation) might recover this without sacrificing the huge_down win, but would require re-running the suite. Deferred to next tuning round.
  • inverse_v regressed by $592 (TSLA) — the down leg of the V triggers the new bearish gate; the up leg often enters puts right before the down leg. This is a known cost of any trend-following gate on a regime where the trend reverses mid-window.

Per CLAUDE.md, both regressions are surfaced here rather than buried under the aggregate. These are insurance premiums paid for the extreme-regime wins, not bugs to fix — re-tuning these regressions away would likely cost back the huge_down/huge_up gains.

Reading of v1.3

The v1.3 changes pay for themselves in tail-management terms, even when headline P/L falls:

  • TSLA 5y total P/L: +$2,243 → +$1,583 (−$660). Driver: gap-slippage on max_loss exits, exposing accounting that v1.2 had artificially inflated. This is information, not regression.
  • TSLA 5y max DD: $4,910 → $3,534 (−$1,376, −28%). Driver: min_credit_pct filter prevented the worst entries. Max_loss exits dropped 15 → 10. The filter is doing exactly what it was designed for: pruning the low-credit-vs-risk entries that dominate the tail.
  • TSLL 5y: −$71 → +$80 (after gap-slippage was applied alone) → +$80 (with min_credit added). The mcp filter recovered TSLL from negative to positive.
  • Suite total dropped because the canonical vol_crush window has unusually low credit-vs-strike for the strategy parameters. This is a quirk of the chosen window, not a strategy failure — see "Action" note above.

Risk profile is now materially healthier. Lower headline P/L with lower DD and fewer trades = same edge per unit of risk taken, with less attention needed. That's the right shape for a daily-trading strategy.

Walk-forward findings (Phase 4)

just optimize runs an 11-window walk-forward on TSLA (~3 years OOS) and 6 windows on TSLL (~1.5 years OOS), sweeping 54 combos of max_loss_mult × long_target_delta × daily_capture_mult_long × profit_target. Scored by total_pnl − 0.5 × max_dd with a 5-trade minimum.

Param-by-param consensus across windows:

Knob Modal value Strength Action
daily_capture_mult_long 1.5 10/11 TSLA windows, 4/6 TSLL — overwhelming Updated default 2.0 → 1.5
long_target_delta 0.15 6/11 TSLA, 4/6 TSLL — modal Updated default 0.22 → 0.15
max_loss_mult 1.2 / 1.8 tied 4/4/3 split across {1.2, 1.5, 1.8} on TSLA No change — data doesn't justify it
profit_target 0.55 / 0.65 tied 4/4/3 split — no consensus No change — data doesn't justify it

OOS aggregate (using per-window winners, not modal):

Ticker Windows Profitable Median P/L Total P/L Max single-window loss
TSLA 11 6 (55%) +$123 −$7,239 −$5,960 (Q1 2025)
TSLL 6 3 (50%) −$86 −$813 −$423 (Q4 2024)

The reading: median window is profitable, but the aggregate is dominated by two TSLA bear-market quarters (Q1+Q2 2025, when TSLA fell from ~$420 to ~$220). The strategy keeps selling puts straight through the downtrend because the bearish-regime gate almost never fires. That gate, not the swept knobs, is the next priority.

Reading of v1.2 baseline

  • The two regime fixes (bearish gate + defensive-call trend gate) move TSLA from net loser to net winner over the 5y window. Profit factor crosses 1.0 (1.09) for the first time.
  • Daily-profit-capture remains the dominant exit (~75% of exits). The character of the strategy didn't change — what changed is which days it trades on.
  • Trade count is ~halved (428 → 240 TSLA; 322 → 146 TSLL). Most of the dropped trades were in bear-market days where the old strategy kept selling into a downtrend. Win rate ticked slightly down because the dropped trades were on-balance winners, but their tail-loss profile dominated aggregate P/L.
  • Max DD cut 60% on TSLA (12.3k → 4.9k) and 54% on TSLL (1.5k → 0.7k). This is the most important second-order effect: capacity to size up safely.
  • Two regime regressions surfaced (see Tradeoffs above) — these are second-order issues, candidates for the next tuning round, not blockers.

Next tuning targets — roadmap

Cost function for prioritization: manage extreme moves, accept calm-case opportunity cost. Every item must be re-validated against the full 12-regime scenario suite (just scenarios) per CLAUDE.md.

Completed this session (full learning in history below):

Round / Phase Outcome
v1.4 wheel mechanic Shipped opt-in; data showed cash-only beats wheel on this history. TSLL especially bad due to leveraged-ETF decay.
v1.5 DTE × delta grid Per-ticker defaults: TSLA 7d × Δ0.20, TSLL 3d × Δ0.30
v1.6 critic round 1 — delta_breach Per-ticker 0.50 / 0.45 (v1.1 too tight for short-DTE gamma)
v1.7 critic round 2 — daily_capture_mult_short Per-ticker 2.0 / 1.25 triple-win
🟰 critic round 3 — iv_rank_min Null: 10 confirmed. Dormant under min_credit_pct-dominated filter.
🟰 critic round 4 — profit_target Null: 0.55 confirmed. Bounded above by gamma cost, not theta opportunity.
v1.8 critic round 5 — max_loss_mult TSLL 1.8 → 3.0 (delta_breach is the more appropriate stop at short DTE). TSLA confirmed at 1.8.
v1.9 critic round 6 — symmetric direction (bearish → short call) TSLA bear_dte=3, TSLL bear_dte=5, both at Δ0.20. Triple-win: 5y P/L, suite, OOS all improved. DD unchanged on both tickers.
v1.10 critic round 7 — decouple exit ladder from entry signals Per-ticker. TSLA: drop regime_flip exit (+$2,214 P/L, +$705 suite, +$1,361 OOS, DD unchanged). TSLL: effectively disable max_loss (+$110 P/L, -$64 DD). Falsified hypotheses also surfaced: "loosen TSLA delta_breach" and "disable TSLA max_loss" both interacted negatively with regime_flip removal; "loosen TSLL delta_breach" catastrophically hurt due to leveraged-ETF dynamics.
🟰 critic round 8 — smaller entry delta Null: current Δ0.20 (TSLA put) / Δ0.30 (TSLL put) / Δ0.20 (both bear-call) are the empirical optima. Smaller deltas either get filtered by min_credit_pct, or catastrophically lose when the floor is loosened. The credit floor IS the load-bearing entry filter.
🟰 critic round 9 — roll-on-max_loss Null: mechanic implemented as opt-in (roll_on_max_loss, roll_dte, roll_target_delta, roll_credit_ratio knobs + pick_roll helper). Tested combined with tightened max_loss; no configuration beats v1.10. Reason: max_loss=1.8 on TSLA is already correctly calibrated — the trades it stops are real losers, not premature stops. TSLL rolls compound the leveraged-ETF trend continuation. Mechanic kept in code as opt-in (like wheel), disabled by default.

Pending — strategy / knob rounds (use just sweep KNOB --values ...):

  1. slippage_pct realism sweep — currently 0 (default). Real bid/ask spreads cost 1-2% on each fill. Test 0.005, 0.010, 0.015, 0.020 to surface how much of v1.8's P/L is sensitive to a more honest execution model. NOT a tuning round — this is truth-in-accounting. Likely outcome: TSLA P/L drops 5-15%, DD unchanged, ratios mostly preserved.
  2. gap_slippage_mult + gap_threshold_pct — currently 0.15 / 0.03. Both v1.3-era. With v1.7's short-DTE strategy, gaps matter differently (3-DTE positions can't ride out a gap; 7-DTE has more time). Worth sweeping.
  3. daily_capture_mult_mid and _long — both 1.5 from v1.1. Dead under current short-DTE config but live again if long_dte is ever raised or for wheel-mode positions. Worth sweeping if/when wheel mode is re-explored.
  4. Dead-code cleanup of dormant knobs. iv_rank_min (round 3 null), dte_stop + dte_stop_min_entry (dead under short-DTE, only relevant for wheel mode). Either remove or document explicitly as "active only when X."

Pending — classifier / logic rounds (require code edits in data.py):

  1. Tighten bearish gate — add ret_14d < -0.05 to _classify_regime's bearish branch. Hypothesis: fire earlier in downtrends without losing the huge_down win.
  2. Re-curate canonical vol_crush window — current TSLA canonical (May 2024 at ~$170) has unusually low credit-per-strike, so v1.7's mcp filter zeros it out. Pick a higher-priced canonical that lets the regime test actually exercise the strategy.
  3. TSLL chop_whipsaw filter — TSLA stands aside on its chop_whipsaw window, TSLL still trades it (smaller loss in v1.8 but still negative). Add a range_vs_close filter (skip if 14d high-low / close > X) for TSLL specifically.
  4. high_iv flag threshold — currently iv_rank > 50 on both tickers. Could be ticker-specific (TSLL's vol distribution is shifted higher; the same threshold maps to different relative-percentile meanings).
  5. reversal flag re-tuning — currently intraday_return < -3.0 AND volume_surge > 1.5. The −3% intraday is much more common on TSLL than TSLA; could be ticker-aware.

Pending — architectural / engine rounds:

  1. Strategy registry — refactor pick_entry into a registry of named strategies (single-leg short put, single-leg short call, short strangle, calendar spread, iron condor, wheel-active). Add a meta-layer that picks which strategy is eligible given today's features. This unlocks the kickoff vision of "find different strategies based on signals."
  2. Intraday-bar mode — feed 1m/5m yfinance bars (~30-60 days available) to validate the intraday reversal rule and let stops fire mid-session. Addresses the root cause of gap_shock losses (would let us avoid the gap_slippage shock entirely).
  3. Real option chains — replace HV30 IV proxy with actual historical chain IV from Polygon/Tradier. Currently the engine under-estimates real IV by 3-15 vol points; relative comparisons still valid but absolute P/L is conservative.
  4. More LLM-critic iterations — the workflow is reified in sweep.py. Easy targets remaining on the StrategyConfig surface; deeper targets in the classifier logic. Cost-function discipline + -flag catastrophe gate keep tail risk in check across iterations.

Pending — dashboard / UX:

  1. Positions tab refinements — currently shows status + add/close forms. Could add: realized P/L history per closed position, contracts × cost basis sizing, broker-mark refresh button (currently uses BSM/HV30 estimate; user can override per-position in YAML).
  2. Strategy-version dashboard tab — interactive widget to switch between historical strategy versions (v1.1 through v1.8) and see how performance shifted. Educational; lets users see the session arc visually.

Live integration

The live daily recommendation runs through live.py::make_recommendation(), which calls the same strategies.pick_entry() as the backtest. There is no separate live engine — tuning strategies.py or StrategyConfig defaults automatically updates what we'd recommend live, with zero drift.

  • just test — print today's recommendation for TSLA + TSLL (CLI form).
  • just run — launch the Streamlit dashboard (Today / Performance / Scenarios tabs).

The legacy dynamic_parameter_engine.py / strategy_v6_dynamic.py are no longer the live truth and won't be tuned going forward — they survive only as historical artifacts.


History

2026-05-14 — Archetype bake-off: HoldToDecay v1.13 confirmed dominant over 3 challenger archetypes (NULL — no strategy change)

User's framing this session: "the exit strategy might affect how the entry gets decided on" → tests entry+exit as one coupled unit (an "archetype") rather than two independent knob axes. Plus "every trade group should be positive, even if it became a small profit or breakeven" → reframes the cost function to be per-chain, not per-aggregate. Engine v1.10 added the measurement infrastructure (trade-group bookkeeping, capital-time metrics, archetype bake-off harness). With that in place, four archetypes were tested head-to-head on both tickers.

The four archetypes:

Archetype DTE Δ mcp profit_target max_loss max_rolls max_chain_loss Hypothesis
HoldToDecay (control = v1.13) TSLA 7 / TSLL 3 0.20 / 0.30 1.0% / 1.2% 55% 1.8× / 10× 0 current shape
QuickHarvest TSLA 2 / TSLL 1 0.35 2.0% / 2.5% 18% 1.2× 1 1.5× small profit fast, tight stop, one roll to recover
PremiumSlow TSLA 21 / TSLL 14 0.15 0.6% 50% 2.5× 2 2.0× longer DTE, wider tolerance, multi-roll management
ReversalScalp 1 0.40 2.0% 10% 1.0× 0 take any tiny profit, hard-stop on any adverse move

Bake-off results — TSLA:

Archetype 5y P/L grpWR worst grp cap/yr % dens/yr suite Σ OOS Σ
HoldToDecay +$17,790 92.0% −$1,107 75.8% 33.2 +$7,183 +$14,719
QuickHarvest 0 trades (mcp=2% unreachable at 2-DTE on TSLA)
PremiumSlow −$4,570 87.9% −$3,337 −5.6% 91.3 −$6,371 ⚠ −$7,343
ReversalScalp 0 trades (mcp=2% unreachable at 1-DTE)

Bake-off results — TSLL:

Archetype 5y P/L grpWR worst grp cap/yr % dens/yr suite Σ OOS Σ
HoldToDecay +$3,250 78.8% −$117 +232.3% 60.7 +$1,496 +$2,591
QuickHarvest −$675 58.5% −$539 −64.5% 71.6 +$74 −$662
PremiumSlow −$545 76.7% −$336 −21.1% 87.8 −$49 −$342
ReversalScalp −$938 61.3% −$539 −65.6% 92.6 +$215 −$906

Findings:

  1. HoldToDecay (v1.13) wins both tickers decisively on every metric that matters: P/L, group win rate, worst-group, suite, walk-forward OOS, capital-yr return. No challenger comes close. The shape is well-tuned and robust against alternatives.
  2. The "small profit fast" hypothesis (QuickHarvest) is falsified. Even where it traded (TSLL), 18% profit-target + 1.2× max-loss + 1-DTE entry loses 64.5% per capital-year. Theta concentration at the very-short tail isn't enough to offset adverse moves at the same elevated delta. The user's intuition that the exit should drive the entry is correct as a framing, but on this data the existing exit (55% profit-target + ladder) coupled to 3–7 DTE entries is the local optimum.
  3. The "more cushion, more rolling" hypothesis (PremiumSlow) is falsified. Wider tolerance + 2-roll chain capacity didn't beat the structural disadvantages of low-delta long-DTE entries on TSLA (suite drops below catastrophe threshold). The chain mechanism in v1.10 is plumbed and gated correctly, but pick_roll's safety rails (further-OTM + ≥1.0× credit ratio) prevented any rolls from actually firing — so PremiumSlow's loss is essentially "long-DTE / low-delta loses on the first leg." A future engine-side investigation of pick_roll's decline rate is queued in ENGINE.md.
  4. Two TSLA archetypes (QuickHarvest, ReversalScalp) generated zero trades because min_credit_pct=2% is unreachable at 1–2 DTE on TSLA's typical IV. This is information, not a bug: the engine correctly stands aside when the premium math doesn't work. Lower-mcp variants might trade more (likely still lose, given TSLL evidence), but the asymmetry between TSLA and TSLL on premium availability is real and worth remembering for any future archetype proposal.
  5. Capital-time metrics reveal the hidden truth about TSLL. Per-contract P/L said TSLA crushes TSLL 5.5× ($17,790 vs $3,250). Per capital-year, TSLL is 3× more efficient (232% vs 76%). The discovery validates including capital-time scoring in v1.10 — without it, any archetype bake-off would systematically mislead toward TSLA-favoring shapes.

Why this null result matters. The QuickHarvest hypothesis (small profit + tight stop + roll-to-manage) was the kind of plausible-sounding strategy refinement that an LLM critic or a human looking at trade logs would likely propose. Testing it formally — through the same gauntlet — and getting a clean falsification is the entire point of the engine. Without the v1.10 bake-off framework this would have been a months-long detour; with it, it's a 10-minute test. Document so it doesn't get re-proposed.

Implication for the GOAL. Archetype-space exploration is now exhausted at the level of "single coherent shapes" — none of the proposed alternatives beats v1.13. The remaining stretch goals in GOAL.md are still open and remain the right next directions:

  • M6 (model-proposed rules) — a LightGBM on per-trade outcomes will surface higher-order feature interactions that neither the analyzer's 3×3 --pairs grid nor a human looking at the trade log can spot. The bake-off told us "the shape is right"; M6 tells us "what subset of that shape's opportunities should we take."
  • The bake-off harness now exists as standing infrastructure. Future archetype proposals (from the user, from an LLM critic, from a model) can be added to archetypes.py and tested with one just bakeoff command.

No version bump. v1.13 strategy ships unchanged. Engine v1.10 is the infrastructure that made this round possible.

2026-05-14 — v1.13: analyzer-driven rule fleet (4 new rules, M2/M3 analyzer, M4/M5 engine)

GOAL.md success criteria reached in a single session. The data-driven loop, set up in v1.11/v1.12, was exercised end-to-end: extend the analyzer, find narrow/interaction candidates, validate, ship.

Cumulative deltas (v1.12 → v1.13):

Surface TSLA before TSLA after TSLA Δ TSLL before TSLL after TSLL Δ
5y P/L $16,279 $17,790 +$1,511 $2,460 $3,250 +$790
Max DD $1,856 $1,107 −$749 (−40%) $359 $144 −$215 (−60%)
Win rate 88.9% 92.0% +3.1pp 73.1% 78.8% +5.7pp
Profit factor 3.48 5.25 +1.77 1.84 4.28 +2.44
Trades 108 100 −8 238 160 −78
12-regime suite $6,824 $7,183 +$359 $1,503 $1,496 −$7 (noise)
Walk-forward OOS $13,116 $14,719 +$1,604 $1,935 $2,591 +$656

A. Analyzer extensions (analyze.py, M2 + M3). Two new modes that find candidates quartile-bucketing missed:

  • --scan-narrow — slides 8%/15%/25%-wide windows across each feature's [p2, p98] range, computes Welch t-stat + Welch–Satterthwaite p-value, and applies Benjamini-Hochberg FDR correction across the candidate family (typical run: 285–606 candidates). Emits BH-significant negative-bucket sketches in v1.11 hook syntax.
  • --pairs — 3×3 quantile grid over feature pairs (--pair-features to focus the scan; capped at 50 pairs by default). Finds conjunctions like ret_14d × iv_rank that univariate analysis cannot surface.
  • --custom-edges feature:v1,v2,v3 — explicit cut points (useful when you already have a hypothesis bucket).

B. Per-position knob overrides (backtest.py + strategies.py, M4). Position gains optional max_loss_mult_override, delta_breach_override, profit_target_override fields. The rule contract accepts these plus min_credit_pct and daily_capture_mult as override keys. check_exits prefers position-level values over cfg. Default None falls back to cfg → zero behavioral change for rules that don't use them. Verified by re-running baseline before/after the plumbing.

C. Exit-side adaptive hook (strategies.py, M5). Mirrors the entry-side architecture:

  • cfg.exit_rules: tuple = ()
  • ADAPTIVE_EXIT_RULES: dict[str, Callable]
  • adapt_exit_params(position, mark, row, cfg) -> dict runs at the top of check_exits (after expired, before the standard ladder). A rule returning {'close': True, 'reason': str} short-circuits the ladder. Demonstrated by take_half_on_reversal (registered, not enabled in defaults). End-to-end firing verified with a forced-close test rule.

D. Four new adaptive rules shipped (all from analyze.py output, all validated through the standard gauntlet):

Rule Source 5y Δ P/L 5y Δ DD Suite Δ OOS Δ Verdict
tsla_skip_mild_intraday_up --scan-narrow (intraday_return [0.5%, 1.6%], n=13, t=−1.98) +$1,511 −$749 +$359 +$1,604 TRIPLE-WIN
tsll_skip_tuesday --scan-narrow (day_of_week=1, n=49, t=−1.91) +$389 −$137 −$95 (noise) +$340 3-of-4 surfaces positive; worst regime improved
tsll_skip_post_earnings_drift --scan-narrow (days_since_earnings [11, 21], n=21, t=−2.15) +$142 −$5 +$76 +$219 TRIPLE-WIN
tsll_skip_downtrend_high_iv --pairs (ret_14d × iv_rank, n=19, t=−2.49) +$259 −$73 +$11 +$97 TRIPLE-WIN

Each was added in isolation, validated against the per-ticker default plus all previously shipped rules, then composed into the active rule set. Order of shipping was: TSLA mild_intraday_up → TSLL tuesday → TSLL post_earnings_drift → TSLL downtrend_high_iv. Sequential composition is non-destructive: the +$790 / −$215 TSLL deltas above are the joint effect after all four rules ship together.

Two tested-and-skipped rules (null results, documented for the scoreboard):

Rule Reason it didn't ship
tsll_skip_low_gamma (wide band [0.02, 0.08]) Suite total regressed $-101; the wide band swept in trades that would have been profitable in vol_expansion and vol_crush regimes
tsll_skip_late_post_earnings (days_since_earnings [49, 70]) Mixed — 5y/DD/OOS all positive but inverse_v regime regressed $-45; superseded by the tighter tsll_skip_post_earnings_drift [11, 21] rule from the narrow scan

Both remain registered (so the analyzer doesn't re-propose). The narrow-scan version of peek_gamma_dollar ([0.037, 0.070]) was also tested: it produced 5y P/L +$235 / DD −$61, but OOS was exactly $0 (the filter targets trades concentrated outside the walk-forward test windows), which raised an overfitting concern. Not shipped.

E. validate_rule.py — A/B harness. Single CLI that runs baseline vs candidate across all three validation surfaces (5y + 12-regime suite + walk-forward static OOS), prints per-regime deltas, computes the cost-function score (P/L − 1.0 × max_DD), and emits a verdict (TRIPLE-WIN / MIXED / NULL) per the cost-function policy. Wired through every rule decision in this session.

Cost function compliance. Every shipped rule lowered drawdown (the cumulative DD reduction is the largest single component of the v1.13 gain). The suite total moved within noise on TSLL (−$7) and improved on TSLA (+$359). No catastrophe regression (worst-regime change > $500) on either ticker. The improvements are tail-management gains in the same family of v1.10's regime-flip removal — letting good trades run, not opening the bad ones.

GOAL.md scoreboard after v1.13:

Criterion Status
≥ 5 adaptive rules shipped through the data-driven loop ✅ 5/5
Per-side knob overrides supported ✅ M4
Analyzer: finer bins + custom edges + 2-feature interactions ✅ M2 + M3
≥ 1 rule shipped from just analyze's output ✅ 4 of 5 (all v1.13 rules)
Exit-side adaptive hook ✅ M5
Documented ≤30-line stable rule-addition interface ✅ STRATEGY.md "How to add an adaptive rule"

Stretch items (model-proposed rules, intraday-bar mode, real chains) remain open for future sessions.

Critic scoreboard now reads 8 wins + 5 nulls (counting M2/M3/M4/M5 and the four shipped rules as the v1.13 win, the two skipped TSLL rules + the narrow gamma_dollar test as the v1.13 nulls).

2026-05-13 — v1.12: data-driven critic loop online (greeks, features, analyzer, first rule)

Four-piece round assembled the automated hypothesis generation layer that the user vision calls for. v1.11 shipped the architecture; v1.12 fills it with greeks visibility, new features, an analyzer that proposes candidate rules from trade-log + feature panel data, and the first concrete adaptive rule.

A. Greeks at peek-time (pricing.peek_position, strategies._peek). Before each rule runs, the engine BSM-peeks the would-be position at the current (side, dte, target_delta) and threads strike, credit, delta, gamma, theta, vega, theta_per_day, theta_yield (per-day-theta as fraction of credit), and gamma_dollar (expected daily-P/L cost from gamma) into the current dict that rules see. If a rule changes DTE or delta, the peek refreshes before the next rule. Rules can now reason about option-side quantities, not just underlying features.

B. New features in data.py: iv_rank_3y (756-day percentile of iv_proxy — captures structural-break context), day_of_week (0=Mon..4=Fri), days_to_monthly_opex (third-Friday calendar), days_to_earnings + days_since_earnings (using _TSLA_EARNINGS_DATES, clamped to 120-day sentinel), vix + vix_rank (macro-vol context via yf.download('^VIX') cached to disk). All warm up gracefully — iv_rank_3y is NaN for the first 756 days and rules guard with if not np.isfinite(v): return {}.

C. analyze.py — automated rule-proposer. Ingests the strategy's trade log + entry-time feature panel, joins peek-greeks for each trade, buckets each feature into quartiles, and reports per-bucket stats (n, win rate, avg P/L, total, Welch t-stat vs rest). Ranks features by effect size (max-bucket-avg − min-bucket-avg). Emits copy-pasteable candidate rule sketches in the v1.11 hook syntax. Wired as just analyze.

Findings the analyzer surfaced on v1.11 trade logs that humans missed:

  • TSLL day_of_week: Thursday entries (49 trades) average $−0.5 vs Friday's $+32 (t=+4.31). Strong signal — candidate for a future round.
  • TSLA days_to_earnings: $156 effect range — proximity to earnings drives outcomes more than was visible from regime classification alone.
  • TSLA peek_credit_pct: $151 effect — premium quality (credit/strike) differentiates outcomes even within the post-mcp-filter pool.

Known analyzer limitation: quartile binning can't find narrow ranges (e.g., ret_14d ∈ [0, +0.05] doesn't fall on a quartile boundary). Future improvement: support custom-edge binning or finer (8-10 bin) splits with min-n guards.

D. First adaptive rule shipped: tsll_skip_marginal_up. Skip TSLL entries when 0 ≤ ret_14d ≤ 0.05 (the bucket we hand-found in v1.11). Activated per-ticker via DEFAULT_CONFIG_BY_TICKER['TSLL']['adaptive_rules'] = ('tsll_skip_marginal_up',). TSLA is unaffected.

Validation surfaces (TSLL only — TSLA unchanged):

Surface v1.11 v1.12 Δ
5y P/L $2,356 $2,460 +$104
5y trades 261 238 −23 (skipped marginal-up bucket)
5y win rate 71.3% 73.1% +1.8pp
5y max DD $359 $359 unchanged
5y profit factor 1.70 1.84 +0.14
12-regime suite $1,336 $1,503 +$167
Walk-forward OOS $1,776.98 $1,935.46 +$158

One small tradeoff surfaced: normal_down scenario regressed $28 → −$10 (the rule skips some put entries that would have been small winners in moderate downtrends). Net suite is still +$167; the regression is the cost of the rule's broader generalization.

Engine state after v1.12 — the full data-driven pipeline is online:

1. Strategy runs            → produces trade log
2. data.py features         → joined onto each trade entry
3. analyze.py (just analyze)→ ranks features, proposes rule sketches
4. Sweep harness (just sweep)→ validates candidates
5. Scenarios + walk-forward → guards against overfit
6. Ship if all surfaces ✓   → DEFAULT_CONFIG_BY_TICKER updated

Critic scoreboard (11 rounds): 7 wins + 4 nulls.

Round Knob/feature Outcome
1-6 (earlier) mixed wins + nulls
7 (v1.10) exit-side decoupling
8 smaller-delta entries 🟰
9 roll-on-max_loss (mechanic kept as opt-in) 🟰
10 (v1.11) adaptive-rules architecture
11 (v1.12) data-driven loop (greeks + features + analyzer + first rule)

The framework now self-sustains: future rounds run just analyze → pick the strongest candidate → sweep → suite → walk-forward → ship-or-null. The proposer can be human, LLM critic, or eventually a trained model — the validation gate stays constant. This is the bridge to Software 2.0 the user described: data-driven proposal, deterministic validation, interpretable rules.

2026-05-13 — v1.11: adaptive-rules architecture (no behavior change; opt-in extension point)

User vision: "use all the variables we can at the time (greeks, previous prices, current volume, etc), to decide what knob to adjust to get the optimal call/put and DTE/strike as output. Need to keep working towards that." This shifts the strategy from fixed per-ticker knobs to context-aware per-entry knobs.

Motivating data — v1.10 trades partitioned by entry-time features:

The bucket analysis revealed real structure that a fixed-knob system cannot exploit. Most striking case:

TSLL bucket n wr% avg P/L total
ret_14d ∈ [−1, −0.05] 87 67.8% +$4.1 +$354
ret_14d ∈ [−0.05, 0] 27 66.7% +$9.0 +$243
ret_14d ∈ [0, +0.05] 28 57.1% −$0.9 −$25
ret_14d ∈ [+0.05, +1] 115 78.3% +$15.1 +$1,738

TSLL trades after a "small recent up-move" are a net-losing bucket; trades after a strong up-move are the dominant winning bucket. Similar (smaller) structure on TSLA — best per-trade at ret_14d ∈ [-0.05, 0] (100% WR, $250/trade across 13 trades).

Architecture shipped:

  • StrategyConfig.adaptive_rules: tuple = () — names of rules to apply on each entry. Default empty → no adaptation → identical to v1.10.
  • StrategyConfig.ticker: str = '' — set by get_config(ticker) so rules can branch per-ticker without changing function signatures elsewhere.
  • strategies.ADAPTIVE_RULES: dict[str, Callable] — module-level registry. Empty for v1.11 (architecture-only release).
  • strategies.adapt_entry_params(row, cfg, base) -> dict — applies registered rules in order. Returns adjusted (side, dte, target_delta) and/or {'skip': True}.
  • pick_entry calls the hook after the regime branch picks base params, before pricing. Skip path returns None; otherwise the resolved params drive the rest of pricing.

Pre-validation smoke-test of candidate rule tsll_skip_marginal_up (NOT shipped — confirms wiring works and motivates round 10):

TSLL trades wr% P/L DD score
v1.10 baseline (no rules) 261 71.3% $2,356 $359 $1,997
with tsll_skip_marginal_up only 238 73.1% $2,460 $359 $2,101

Skipped 23 trades (close to the 28 in the negative bucket); +$104 P/L; DD unchanged; WR +1.8 pp. Promising but unvalidated — needs sweep on threshold values, full scenario suite, walk-forward OOS, and consideration of overfitting (28 negative-bucket trades is a smallish sample for a rule that screens ~9% of TSLL trade days).

Why architecture-only is the right ship size:

  • The hook is a 3-step user contract (define, register, activate via cfg). Each rule sweep is bounded.
  • The architecture costs ~20 lines, has zero behavior impact at default, and gates all future adaptive work behind one well-defined extension point.
  • Each future rule will go through the same critic-loop validation as every other knob change (sweep → suite → walk-forward → ship if score improves and no catastrophe regression). Rules will accumulate as wins; nulls will be documented.

Smaller-delta × roll question (asked in parallel): tested in detail — null. Cells: long_target_delta ∈ {0.10, 0.15, 0.18, 0.20} × min_credit_pct ∈ {0.005, 0.008, 0.010} × roll_on_max_loss ∈ {False, True}. Only marginal positive interaction is at the mcp=0.008 loosened-floor level: rolls recover +$462-728 of score, but the underlying setup is still below v1.10's Δ0.20 × mcp=0.010 × no-roll. The clean takeaway: smaller delta + further OTM does reduce DD and max_loss firings (Δ0.15 cuts DD $1,856 → $779 and max_loss exits 5 → 2 on TSLA), but trade volume drops more than DD does — the credit floor filters out most small-delta candidates. The fixed-knob version can't have both safety and volume. The adaptive-rules architecture is the resolution path: use smaller delta in conditions where volume isn't the binding constraint (e.g., high IV rank with rich credit), larger delta where the trend is strong.

Critic scoreboard (10 rounds):

Round Knob/feature Outcome
1 (v1.6) delta_breach per-ticker
2 (v1.7) daily_capture_mult_short per-ticker
3 iv_rank_min 🟰
4 profit_target 🟰
5 (v1.8) max_loss_mult per-ticker
6 (v1.9) bear_dte + bear_target_delta (new knobs)
7 (v1.10) exit-side decoupling (regime_flip_exit_enabled + per-ticker max_loss)
8 smaller-delta entries 🟰
9 roll-on-max_loss (mechanic kept as opt-in) 🟰
10 (v1.11) adaptive-rules architecture ✅ (architecture only; first rule TBD)

6 wins + 4 nulls. v1.11 unlocks a new direction rather than tuning an existing one.

2026-05-13 — Null results: smaller-delta entries + roll-on-max_loss (no version bump)

Two user hypotheses tested back-to-back. Both produced null results — current v1.10 defaults remain optimal — but each round produced load-bearing learning about why the current setup is what it is. Following the framework: validated, not trusted.

Round 8 — smaller entry delta. User: "What about adjusting for smaller delta entries?" Hypothesis: with v1.10's improved exit ladder (regime_flip off on TSLA, max_loss disabled on TSLL), maybe smaller deltas (further OTM) — historically gated out by min_credit_pct=0.010 — become viable for a better risk-adjusted setup.

Sweep design: long_target_delta ∈ {0.08, 0.10, 0.12, 0.15, 0.18, 0.20, 0.25, 0.30} × min_credit_pct ∈ {0.003, 0.005, 0.008, 0.010} on TSLA (2-D grid, 20 cells). Same 1-D sweep on bear_target_delta.

Result: Current Δ0.20 × mcp=0.010 is the global optimum on TSLA. Δ0.18 × mcp=0.010 is second-best (75 trades, +$12,387 P/L, $1,332 DD — score +$11,055 vs current +$14,422). All cells with mcp ≤ 0.005 catastrophe-flagged (worst regime ≤ −$2,600). Smaller deltas with looser credit floor produce exactly the low-credit-per-strike entries that dominate the max-loss tail — the very thing min_credit_pct was introduced (v1.3) to filter out. TSLL identical pattern: Δ0.30 unique winner; smaller deltas underperform monotonically. bear_target_delta=0.20 confirmed on both tickers — smaller bear-deltas get filtered out at 3-5 DTE.

Mechanism learning: The credit floor is the load-bearing entry filter. Delta and credit-floor are coupled — you can't lower the delta without also lowering the floor, and lowering the floor brings back the v1.1-era catastrophe tail. The "win rate goes up at smaller delta" intuition is real (Δ0.18 gives 93.3% WR vs Δ0.20's 88.9%), but trade volume drops so much that absolute P/L falls.

Round 9 — roll-on-max_loss. User: "at max_loss, what if we roll it out?" Hypothesis: instead of locking in a max_loss realization and waiting for the next signal, immediately roll to a further-out DTE at a further-OTM strike. Could let us re-tighten max_loss_mult (cut losses faster) while still capturing recovery via theta on the rolled position.

Adverse-move study (precondition to design): pulled all max_loss events from a tightened-stop run (max_loss_mult=1.5 to sample). TSLA n=9, TSLL n=17. Measured how far the underlying continued against the strike and how long until recovery.

Ticker Median adverse % past strike Median days to recover Cases that didn't recover within 21 days
TSLA ~12% 1-14 1/9 (Q1 2025 crash trend continuation)
TSLL ~25%, up to 112% 0-34, several NaN 5/17 (leveraged-ETF trends extend for weeks)

The TSLL data foreshadowed the result: leveraged-ETF adverse moves are violent and often continue. A rolled TSLL position would frequently re-loss.

Implementation (engine + strategy layer):

  • New knobs: roll_on_max_loss (default False), roll_dte (14), roll_target_delta (0.15), roll_credit_ratio (1.0)
  • Position.is_roll flag — prevents recursive rolls (one roll per chain)
  • strategies.pick_roll(row, cfg, S, today, closed, close_cost) — builds the candidate rolled position with these constraints:
    1. Same side as the closed position
    2. New strike STRICTLY further OTM than the closed strike (no doubling down at closer-money)
    3. new_credit ≥ roll_credit_ratio × close_cost
    4. Returns None if no eligible roll exists; engine falls through to standard close
  • Backtester.run() — wires roll_fn=pick_roll and attempts the roll inline when max_loss fires
  • Wired through run_backtest, sweep, run_scenarios, optimize, tsla_options_dashboard

Sweep results: Tested 9 combined configurations on TSLA, 6 on TSLL. Knobs varied: max_loss_mult ∈ {1.0, 1.2, 1.5, 1.8, 3.0} × roll_credit_ratio ∈ {0.3, 0.5} × roll_dte ∈ {10, 14, 21} × roll_target_delta ∈ {0.15, 0.20}.

TSLA config trades wr% P/L DD score rolls fired
v1.10 baseline (ML=1.8, no roll) 108 88.9% $16,279 $1,856 $14,422 ← winner 0
ML=1.5 + roll (r=0.5, dte=21) 112 87.5% $15,195 $2,625 $12,571 1
ML=1.2 + roll (r=0.3, dte=21) 117 85.5% $15,631 $2,072 $13,559 10
ML=1.0 + roll (r=0.3, dte=21) 124 82.3% $14,960 $2,072 $12,889 14
TSLL config trades wr% P/L DD score
v1.10 baseline (ML=10, no roll) 261 71.3% $2,356 $359 $1,997 ← winner
ML=1.8 + roll (r=0.3, dte=10) 263 71.1% $2,045 $439 $1,606
ML=1.8 + roll (r=0.3, dte=21) 264 70.5% $1,999 $476 $1,523
ML=1.5 + roll (r=0.3, dte=10) 263 71.1% $2,045 $439 $1,606

Mechanism learning: v1.10's max_loss is already correctly calibrated. The 5 TSLA max_loss exits per 5y aren't premature stops — they're stops on positions that genuinely would have gone further into the loss (the 2024-12-30 case is the Q1 2025 crash beginning). Rolling at that point is doubling down on a trend the market has just confirmed against us. Per the adverse-move study: TSLA's 1/9 non-recovering case happens to be the most expensive case, so any roll mechanism that fires on max_loss inherits that downside. TSLL's tail is even worse (5/17 non-recovering, with adverse moves up to +112%).

The interesting question is "tighten max_loss + roll, can we have both"? Tested across all sensible thresholds — answer is no. Tightening max_loss to 1.2 or 1.5 captures losses earlier but stops too many winners. Rolls partially recover that loss-of-good-stops, but never enough to beat the v1.10 baseline. Net: max_loss=1.8 + no-roll dominates.

Mechanic stays in the codebase as opt-in (like the wheel mechanic in v1.4). Roll knobs are in StrategyConfig but off by default; pick_roll and the engine wiring exist. Users can experiment via roll_on_max_loss=True overrides; future research (e.g., intraday-bar mode in Phase 8) might unlock a context where rolls earn their keep that daily-bar mode can't.

Critic scoreboard (9 rounds): 5 wins + 4 nulls. The 4 nulls (iv_rank_min, profit_target, smaller-delta, roll-on-max_loss) each leave a mechanism learning that prevents the question from being asked again. That's the loop working as designed — confirmations are findings too.

2026-05-13 — v1.10: decouple exit ladder from entry signals (per-ticker)

User reframed the exit side: "exit should purely depend on profit. If the regime calls for it then entry again. We probably shouldn't mix them." Their candidate exits: daily_capture, profit_target, and a TBD verdict on max_loss ("sometimes waiting it out is better than exiting early. But if proven max loss exit and new entry is better, that'll be a good rule too").

This challenges three of the seven exit rungs that aren't pure P/L: max_loss (dollar threshold), delta_breach (option-dynamics signal), and regime_flip (literally an entry-signal-as-exit). Tested each individually then in combination.

Added one new knob: regime_flip_exit_enabled: bool = True in StrategyConfig (default preserves v1.9 behavior). check_exits now guards rung 7 with this flag. No other code changes — max_loss_mult and delta_breach are already-existing knobs that can be set to disable-effective values.

Phase 1 — 1-D sweeps (each knob to disable-values, holding others at v1.9 defaults):

Knob TSLA verdict TSLL verdict
max_loss_mult (1.8 → 10) 🟰 +$20 (saturates 2.5+; barely fires) ✅ +$174 score, DD ↓ $423 → $359
delta_breach (current → 1.0) ✅ best at 0.70 (+$409); ≥0.85 catastrophe-flags ❌ current 0.45 is the optimum — any loosening blows DD to $715+
regime_flip_exit_enabled (true → false) +$2,214 P/L, WR 81.5%→88.9%, DD unchanged 🟰 -$39 (only 1 regime_flip exit per 5y on TSLL)

Phase 2 — combined sweep on TSLA revealed knob interactions. Once regime_flip is off, the optimal delta_breach and max_loss_mult shift:

TSLA config trades wr% P/L DD score
v1.9 baseline 108 81.5% $14,065 $1,856 +$12,208
+ drop regime_flip 108 88.9% $16,279 $1,856 +$14,422 ← winner
+ loosen delta_breach to 0.70 107 91.6% $16,110 $1,856 +$14,254
+ disable max_loss (10.0) 102 92.2% $15,747 $2,569 +$13,178

Each "marginal" addition past dropping regime_flip hurts. The interaction: regime_flip used to close marginal trades early; now those trades reach delta_breach/max_loss/expiration. Loosening the remaining rungs lets those trades go even further into adverse territory, which the data says is worse on net. The cleanest single-knob change is the right one.

Phase 3 — combined sweep on TSLL showed max_loss=10 alone is the cleanest:

TSLL config trades wr% P/L DD score
v1.9 baseline 261 71.3% $2,246 $423 +$1,823
drop regime_flip only 260 71.2% $2,207 $423 +$1,784
+ disable max_loss 260 71.2% $2,317 $359 +$1,958
max_loss=10 only 261 71.3% $2,356 $359 +$1,997 ← winner

Shipped (v1.10 per-ticker defaults):

  • TSLA: regime_flip_exit_enabled=False
  • TSLL: max_loss_mult=10.0 (effectively disabled)

Validation surfaces (both tickers):

Surface TSLA Δ TSLL Δ
5y backtest P/L +$2,214 +$110
12-regime scenario suite +$705 (worst regime $-244 → $0) $0 (unchanged)
Walk-forward static OOS +$1,361 (6/11 → 7/11 wins) +$82 (6/6 retained)
5y max DD 0 -$64 (improves)

Honest accounting on what got ruled OUT:

The user's clean "profit-only exits" framing was partially validated. Three falsifications:

  1. TSLL's delta_breach=0.45 is load-bearing (not optional, not loosenable). Sweep showed any loosening above 0.45 blew DD up. Mechanism: TSLL is 2x-leveraged, so once delta breaches even slightly, adverse moves compound fast in dollar terms. The "exit on option-dynamics signal" rung is the empirically-correct tail protection for this underlying — it's not just an entry-signal-as-exit, it's a real-time risk metric that profit-based stops can't replicate.
  2. TSLA's max_loss=1.8 is also load-bearing (within the new regime-flip-free regime). Disabling max_loss after dropping regime_flip blew DD from $1,856 → $2,569. The few trades that used to be stopped at -1.8× credit and re-enter now ride to deeper losses (sometimes expire ITM at intrinsic), and the cost outweighs the gains. The user's "wait-it-out vs exit-and-re-enter" question resolves empirically as "exit and re-enter" for TSLA at 7-DTE.
  3. TSLA's delta_breach interacted negatively with regime_flip removal. Loosening from 0.50 → 0.70 was individually +$409 but in the combined regime-flip-free config it cost $169 vs leaving it alone. Knob independence is an assumption, not a guarantee — combined sweeps matter.

Cumulative session arc (TSLA 5y): −$6,242 (v1.1) → +$16,279 (v1.10). Swing: +$22,521. Max DD: $12,292 → $1,856 (−85%). PF: 0.87 → 3.48. Win rate: ~60% → 89%.

Process learning. v1.10 demonstrates the framework working as designed: a clean philosophical proposal from the user, decomposed into testable sub-hypotheses, each empirically validated or falsified. The shipped configuration is narrower than the original proposal because the data forced specificity. This is the LLM-critic loop doing exactly what CLAUDE.md describes: hypotheses are prompts for sweeps, not substitutes for them.

Critic scoreboard (7 rounds):

Round Knob Outcome Mechanism learning
1 (v1.6) delta_breach ✅ Per-ticker 0.50 / 0.45 v1.1 value too tight for short-DTE gamma
2 (v1.7) daily_capture_mult_short ✅ Triple-win, per-ticker 2.0 / 1.25 v1.1 value too eager for short-DTE theta rate
3 iv_rank_min 🟰 Null — 10 confirmed Dormant under min_credit_pct-dominated filter
4 profit_target 🟰 Null — 0.55 confirmed Bounded above by gamma cost, not theta opportunity
5 (v1.8) max_loss_mult (TSLL) ✅ TSLL 1.8 → 3.0 First step toward "delta_breach is the real stop on TSLL" — v1.10 finishes the job
6 (v1.9) bear_dte + bear_target_delta (new) ✅ Triple-win, per-ticker 3/5 × Δ0.20 Symmetric direction; reversal-resistant DTE cap
7 (v1.10) exit-side decoupling (per-ticker) ✅ TSLA drop regime_flip; TSLL drop max_loss Exit rungs are NOT all interchangeable; the right exit is ticker-specific and depends on the other exits

5 wins + 2 nulls across 7 rounds. Two more potential rounds queued: per-side min_credit_pct (calls in bearish typically have richer credit than puts in neutral) and per-ticker high_iv threshold (TSLL's vol distribution is shifted higher).

2026-05-13 — v1.9: symmetric direction — bearish → short call (per-ticker bear_dte, bear_target_delta)

User reframed the puts-vs-calls decision: trend tells us direction; DTE × delta tunes for reversal-resistance. The asymmetry in v1.8 (bullish/neutral → puts, bearish → stand-aside) ignored 41% of TSLA days and 50% of TSLL days where short calls have trend tailwind (the underlying moves away from the strike). The historical reason for the asymmetry (v1.1 lost money selling calls into rallies) is real but doesn't apply to the bearish regime — that mistake was selling calls in uptrends, not downtrends.

Diagnostic (v1.8 trade log breakdown):

Ticker Side n Win % Total P/L Avg win Avg loss
TSLA put 97 80.4% +$12,440 +$245 −$349
TSLA call 9 88.9% +$1,050 +$204 −$579
TSLL put 125 72.0% +$1,701 +$41 −$57
TSLL call 22 81.8% +$250 +$20 −$27

Calls were already net-positive as a defensive tool. The question was whether expanding the call envelope to include bearish-regime entries would also be net-positive. Theory said yes (positive convexity to trend continuation); needed empirical validation.

Implementation. Two new StrategyConfig knobs:

  • bear_dte: int = 0 — bearish-regime call DTE. 0 = legacy stand-aside (v1.8 behavior).
  • bear_target_delta: float = 0.20 — bearish-regime call delta.

Both threaded through pick_entry at the top of the bearish branch. The entry chain is now symmetric:

bearish + bear_dte>0    → SELL CALL @ (bear_target_delta, bear_dte)
bearish + bear_dte=0    → skip                                          (legacy)
defensive (rev/high_iv) → SELL CALL @ (short_target_delta, short_dte)   (unchanged)
otherwise               → SELL PUT  @ (long_target_delta, long_dte)     (unchanged)

Sweep — 1-D bear_dte then 2-D bear_dte × bear_target_delta. Catastrophe-flag () at the worst-regime-below-$-500 threshold drove the search space.

Ticker bear_dte sweep verdict bear_target_delta verdict
TSLA 3 wins (5+ catastrophe-flag); 5y P/L $13,490 → $14,065 (+$575), DD unchanged $1,856, suite +$574 0.20 (cleanest of [0.15, 0.20, 0.25, 0.30]; 0.25+ tail-flags)
TSLL 5 wins; 5y P/L $1,950 → $2,246 (+$296), DD unchanged $423, suite +$324, worst regime improves -$76 → -$59 0.20 (peak of response surface; flat across [0.20, 0.25, 0.30])

The DTE cap is the load-bearing finding. Beyond DTE 5 on TSLA and DTE 7 on TSLL, reversal rallies eat the credit faster than theta accumulates, and the worst-regime number degrades catastrophically. Short DTE = reversal resistance — exactly what the user predicted.

Triple-win on both tickers (rare per the v1.7 framework):

Surface TSLA Δ TSLL Δ
5y backtest +$575 +$296
12-regime scenario suite +$574 +$324
Walk-forward static OOS +$255 +$557
Max DD per contract 0 0

TSLL scenario suite went 6/11 → 9/11 profitable regimes: huge_down $0 → +$89 (NEW — bear-call captured premium during the -50% TSLL crash), vol_expansion $0 → +$73 (NEW), normal_down -$53 → +$28 (recovered). Worst regime improved from -$76 (gap_shock) to -$59 (inverse_v).

One small regression surfaced: TSLL normal_up $96 → $53 (-$43). The bearish gate fires briefly at the start of some rallies; the bear-call enters; the rally continues and trips delta_breach. Net loss is small relative to the gains elsewhere — surfaced here per CLAUDE.md's cost-function rule rather than buried in the aggregate.

Cumulative session arc:

  • TSLA: −$6,242 (v1.1) → +$14,065 (v1.9). Swing: +$20,307. Max DD: $12,292 → $1,856 (−85%). PF: 0.87 → 2.95.
  • TSLL: −$981 (v1.1) → +$2,246 (v1.9). Swing: +$3,227. Max DD: $1,461 → $423 (−71%).

Process learning for the critic loop. v1.9 is the first round where the proposal came from a user reframing rather than a pure sweep over an existing knob. The new knob (bear_dte) had to be invented before it could be swept. This suggests a useful workflow: when an experiment requires a new knob, add it with a no-op default (here bear_dte=0 = legacy behavior), then sweep starting at the no-op. The sweep harness just sweep already supports this — the dataclass-default-respecting replace() keeps the user's choice of "off" semantically distinct from a missing entry.

Critic scoreboard (6 rounds):

Round Knob Outcome Mechanism learning
1 (v1.6) delta_breach ✅ Per-ticker 0.50 / 0.45 v1.1 value too tight for short-DTE gamma
2 (v1.7) daily_capture_mult_short ✅ Triple-win, per-ticker 2.0 / 1.25 v1.1 value too eager for short-DTE theta rate
3 iv_rank_min 🟰 Null — 10 confirmed Dormant under v1.7's min_credit_pct-dominated filter
4 profit_target 🟰 Null — 0.55 confirmed Bounded above by gamma cost, not theta opportunity
5 (v1.8) max_loss_mult ✅ TSLL 1.8 → 3.0 (TSLA confirmed) At short-DTE, delta_breach is a better stop than dollar-threshold max_loss
6 (v1.9) bear_dte + bear_target_delta (new) ✅ Triple-win, per-ticker 3/5 × Δ0.20 Symmetric direction + reversal-resistant DTE cap. Calls in confirmed downtrends have positive convexity to trend continuation; the v1.1 call-selling mistake was selling them in uptrends.

4 wins + 2 nulls across 6 rounds. The framework remains: hypothesis → sweep → validate against suite + OOS → ship only if cost function (P/L − DD-weighted) clearly improves.

2026-05-13 — v1.8: critic round 5 + reusable sweep.py harness

Two deliverables in one round. User feedback: "build stronger with that" — the critic loop is the heart of what we're building, so the engine should treat hypothesis-testing as a first-class capability, not as ad-hoc scripts.

New tool: sweep.py + just sweep recipe. Replaces the bespoke sweep_*.py scripts (delta_breach, daily_capture, iv_rank_min, profit_target, etc.) with one general harness. Takes a knob name + comma-separated values; runs the same per-ticker 5y + 12-regime suite; returns a single table with ← current / 🏆 best / annotations. Supports 1-D and 2-D grids. Example invocations:

just sweep max_loss_mult --values 1.2,1.5,1.8,2.5
just sweep delta_breach --values 0.30,0.40,0.50 --tickers TSLA
just sweep long_dte --values 3,7,14 --vs long_target_delta --vs-values 0.15,0.20,0.30
just sweep iv_rank_min --values 5,10,20,30,50 --dd-weight 0.5

Validates knob names against StrategyConfig (typos error out with a list of available knobs). Cost-function dd_weight defaults to 1.0 (heavier than optimize.py's 0.5, matching this project's tail-management cost function). flag when any value's worst regime falls below -$500 (catastrophe threshold, tunable).

Round 5 result — max_loss_mult: partial ship.

Ticker Sweep verdict Action
TSLA Default 1.8 confirmed. Score saturates at 2.5 with only +$20 improvement (within noise). No change.
TSLL 1.8 → 3.0 clean win: +$78 5y P/L, DD $440 → $423 (improves!), +$185 walk-forward OOS. Ship.

The mechanism finding is the load-bearing part. TSLL exit-reason breakdown shifted dramatically:

Exit reason v1.7 v1.8
max_loss 8 1 (−7)
delta_breach 38 45 (+7)

The 7 trades that would have stopped at max_loss=1.8 now exit via delta_breach=0.45 instead — at a smaller realized loss. delta_breach is the more appropriate stop signal for short-DTE TSLL because it triggers on actual option dynamics (delta), not a static dollar threshold. The protection didn't weaken — it migrated to a more nuanced trigger.

Bigger learning from the sweep: max_loss is NOT the sole tail protection at v1.7+. At max_loss_mult = 4.0 (essentially disabled), neither ticker saw any catastrophic regime regression — the flag never fired. regime_flip, delta_breach, and the bearish gate together provide redundant tail protection. This changes the risk calculus for interrogating any of those individual gates in future rounds: the gates are compounded, not load-bearing in isolation.

Critic scoreboard (5 rounds):

Round Knob Outcome Mechanism learning
1 (v1.6) delta_breach ✅ Per-ticker 0.50 / 0.45 v1.1 value too tight for short-DTE gamma
2 (v1.7) daily_capture_mult_short ✅ Triple-win, per-ticker 2.0 / 1.25 v1.1 value too eager for short-DTE theta rate
3 iv_rank_min 🟰 Null — 10 confirmed Dormant under v1.7's min_credit_pct-dominated filter
4 profit_target 🟰 Null — 0.55 confirmed Bounded above by gamma cost, not theta opportunity
5 (v1.8) max_loss_mult ✅ TSLL 1.8 → 3.0 (TSLA confirmed) At short-DTE, delta_breach is a better stop than dollar-threshold max_loss

3 wins + 2 nulls = healthy — each round leaves a mechanism learning, regardless of whether a knob ships. The sweep tool (sweep.py) is itself the most important shippable outcome of v1.8: it makes future rounds an order of magnitude cheaper to run.

2026-05-13 — LLM-critic round 4: profit_target — null result, hypothesis was backward (no version bump)

Fourth LLM-critic iteration. No knob change shipped — but the round produced a useful inversion of the proposed hypothesis, captured here for future iterations.

Hypothesis tested: with v1.7's daily_capture_mult_short = 2.0 (TSLA), profit_target is the binding exit for slow-decay winners (33 of 106 TSLA exits). Raising from 0.55 toward 0.70 should let winners ride from ~day 4 to ~day 5-6, capturing more available theta.

Sweep: values ∈ {0.40, 0.45, 0.50, 0.55, 0.60, 0.65, 0.70, 0.75, 0.80, 0.90} per ticker.

Result:

  • TSLA: P/L flat across [0.45-0.55] at +$13,490–$13,525 (within noise). Above 0.60, max DD jumps from $1,856 → $2,087 (+12%) while P/L drops to +$13,119. At 0.90, P/L collapses to +$11,597.
  • TSLL: essentially flat across the entire sweep — 3-DTE trades exit via profit_target on day 1-2 regardless of threshold, so the knob is dormant for TSLL.

Why the hypothesis was wrong: I argued "more available theta" without weighting the second-order gamma cost. Slow winners that hold longer to capture more theta also face the high-gamma final days where small underlying moves create larger option-price moves. The extra theta isn't worth the gamma exposure — which the cost function correctly identifies via the DD penalty.

The data is actually directionally consistent with lowering (0.45 gives +$35 vs 0.55 within noise), but lowering provides no real improvement either. The 0.55 default lands in a flat region of the response surface bounded above by gamma cost. It's the right value, just for a reason different from why it was originally set.

Process insight: validated, not trusted. My hypothesis was wrong in direction. The loop caught it via the DD jump at 0.60+. This is exactly why every critic proposal validates empirically rather than shipping on the LLM's reasoning alone. The mechanism story is a prompt for the sweep, not a substitute for it.

Critic loop scoreboard (4 rounds):

Round Knob Outcome Mechanism learning
1 (v1.6) delta_breach ✅ Per-ticker 0.50 / 0.45 v1.1 value too tight for short-DTE gamma
2 (v1.7) daily_capture_mult_short ✅ Per-ticker 2.0 / 1.25 v1.1 value too eager for short-DTE theta rate
3 iv_rank_min 🟰 Null — 10 confirmed Dormant under v1.7's min_credit_pct-dominated filter
4 profit_target 🟰 Null — 0.55 confirmed Bounded above by gamma cost (not theta opportunity, as hypothesized)

2/4 win rate. Each round leaves a mechanistic learning, even when no knob ships. Future targets remain: max_loss_mult (risky — IS the tail protection), bearish-gate logic (data.py::_classify_regime), and the strategy registry (architectural).

2026-05-13 — LLM-critic round 3: iv_rank_min — null result, useful learning (no version bump)

Third LLM-critic iteration. No knob change shipped — but the round produced load-bearing learning about the entry-filter architecture and is logged here so future iterations don't re-propose the same thing.

Hypothesis tested: iv_rank_min = 10 (v1.1 default) is too low; raising it should filter for richer relative premium and improve risk-adjusted returns under v1.7's tighter overall config.

Sweep: values ∈ {5, 10, 15, 20, 25, 30, 40, 50} × 12-regime suite + 5y backtest per ticker.

Result:

  • TSLA 5y P/L flat across [5, 10, 15] at +$13,490–$13,650 (variation within noise). Drops monotonically above 25. Suite total flat at +$5,545 until iv_rank_min=50 where it ticks up to +$5,789.
  • TSLL same pattern: 5/10/15 within $200 of each other; values above 15 monotonically hurt 5y.

Why the hypothesis was wrong: under v1.7, min_credit_pct already filters for what iv_rank_min was meant to capture. Days passing the credit-per-strike floor (TSLA 1.0%, TSLL 1.2%) at short DTE essentially always have iv_rank > 10, because the absolute-credit floor is the binding constraint at v1.7's short DTEs. The two filters were doing the same job; the second one (iv_rank_min) is dormant.

Interesting boundary case (the only point where the sweep deviated): at TSLA iv_rank_min = 50, 5y P/L drops $1,532 but the worst regime (flat, −$244) goes to $0. That's a tail-management over-correction — paying $1,532 to save $244 once over 5 years violates the cost function as documented (P/L − DD-weighted score). The cost function correctly rejects this; I'm noting it to demonstrate the framework working as designed.

Process learning logged for future rounds:

  1. Not every v1.1 constant is mistuned. Rounds 1 (delta_breach) and 2 (daily_capture_mult_short) found genuine mistunings; round 3 confirmed iv_rank_min is structurally OK. Future critic rounds should expect null results in this proportion — the critic loop's value includes confirming defaults, not only changing them.
  2. Filter redundancy is real. iv_rank_min was the right filter for the v1.1 30-DTE strategy when credits varied widely. Under v1.7's short DTE, min_credit_pct is the binding constraint and iv_rank_min is dormant code. Candidate for deletion in a future cleanup, not for re-tuning.
  3. The cost function disciplines tail-only thinking. It's tempting to ship the iv_rank_min=50 variant because it cuts a regime loss to zero — but the cost function correctly weights that against the $1,532 it would cost in aggregate. Tail management means tail-priority, not tail-only.

No code change. Future targets (still v1.1-era constants potentially worth interrogating): max_loss_mult (1.8), profit_target (0.55), and bearish-gate logic in data.py::_classify_regime. The first two carry direct tail-risk implications and should be approached carefully.

2026-05-13 — v1.7: LLM-critic round 2 proposes per-ticker daily_capture_mult_short (triple-win)

Second LLM-critic iteration. Inputs same as round 1 (per-regime suite, exit-reason breakdowns, source, cost function — all backward-looking).

Hypothesis: daily_capture_mult_short = 1.0 was a v1.1 default calibrated for 30-DTE puts. With v1.5/v1.6's short DTE (TSLA 7d, TSLL 3d), theta accumulates at ~14% of credit per day, so realized P/L pace hits 1.0× theta-target on day 1. The rung fires too early, capturing ~14% of credit when the trade would have expired OTM with 100% kept. Diagnostic: TSLA v1.6 closed 85 of 116 trades via daily_capture.

Validation sweep: 9 values each, 12-regime suite + 5y backtest + walk-forward static. Different per-ticker optima again:

  • TSLA: 2.0 — P/L $12,975 → $13,490 (+$515), DD unchanged, PF 2.69 → 2.87, win rate 82.8% → 81.1% (slight drop is the cost of holding longer), avg days held 2.6 → 2.9. daily_capture exits dropped 85 → 45 — most converted to profit_target (3 → 33), exactly as hypothesized. Trade count fell 116 → 106 (holding longer reduces re-entry opportunities).
  • TSLL: 1.25 — P/L $1,768 → $1,872 (+$104). Noisier sweep — values 1.5–2.0 are slightly worse on 5y while still improving the suite. 1.25 is the value that's a local maximum on both 5y and per-regime, so picked for robustness.

Triple-win (rare): all three validation surfaces improved on both tickers — 5y aggregate, per-regime suite, and walk-forward OOS. Previous changes had at least one of these surfaces flat or slightly down.

  • TSLA: 5y +$515, suite +$91, walk-forward OOS +$431.
  • TSLL: 5y +$104, suite +$36, walk-forward OOS +$107.

Worst regime unchanged at −$244 (TSLA) / −$76 (TSLL). DD unchanged. Cost function: pure improvement.

Cumulative session arc (TSLA 5y): −$6,242 (v1.1) → +$13,490 (v1.7). Swing: +$19,732. Max DD: $12,292 → $1,856 (−85%). Profit factor: 0.87 → 2.87 (3.3×).

Process insight, refined. Two consecutive LLM-critic wins on v1.1-era constants (delta_breach, daily_capture_mult_short) suggest the v1.5 short-DTE shift left several stale constants that grid sweeps couldn't catch because they treated those values as fixed. The remaining v1.1-era constants worth interrogating: max_loss_mult, iv_rank_min, daily_capture_mult_mid/_long (less impactful since most trades are short-DTE now), and dte_stop (currently dead code at v1.7 since dte_stop_min_entry=14 and DTE-at-entry is 3-7).

2026-05-13 — v1.6: LLM-critic loop proposes per-ticker delta_breach

First end-to-end demonstration of the LLM critic loop described in v1.4's "Next tuning targets" #8 and the kickoff vision. Claude (acting as the critic in-session) was given backward-looking inputs only: the 12-regime suite table, exit-reason breakdowns from the 5y backtest, strategies.py source, and the cost function. No future bars, no oracle data.

LLM hypothesis: delta_breach = 0.38 was calibrated in v1.1 when default DTE was 30 days. With v1.5's short DTEs (TSLA 7d, TSLL 3d), gamma is much higher — a 3% underlying move can take a Δ0.20 put to Δ0.38 in a single day. The threshold that was a reasonable tail-guard at 30 DTE was firing on routine noise, converting potential winners into realized losers. The diagnostic: TSLA had 19 delta_breach exits in 5y; TSLL had 47 (30% of all exits).

Validation sweep: delta_breach ∈ {0.30, 0.35, 0.38, 0.45, 0.50, 0.55, 0.60} per ticker, full 12-regime suite + 5y backtest each. Surfaced different per-ticker optima:

  • TSLA: 0.50 — P/L $11,815 → $12,975 (+$1,160), DD $2,436 → $1,856 (−24%), PF 2.29 → 2.69. delta_breach exits fell 19 → 4 (converted to daily_capture or expired-OTM). Saturates at 0.55.
  • TSLL: 0.45 — P/L $1,757 → $1,768 (modest), DD unchanged. Raising further to 0.50 worsened TSLL DD (440 → 715) because leveraged-ETF moves punish further-out tolerance. Genuine difference between underlying mechanics.

Tradeoff surfaced honestly: TSLA walk-forward OOS dropped $750 ($11,500 → $10,749), driven by W7 (Q1 2025 TSLA downtrend) going +$1,492 → +$98 because higher delta_breach lets positions ride further into bearish moves. But: profitable windows up 6/11 → 7/11, worst single window unchanged at −$244, DD unchanged. Net: gave up some lumpy mid-window gains for steadier coverage. Acceptable under the cost function.

Process observation: The DTE × delta sweep (v1.5) had held delta_breach constant at 0.38 — the assumption baked into the grid. The LLM critic's value was questioning the constants. Sweeps optimize within a parameter envelope; the critic spots when the envelope itself is mis-sized.

2026-05-13 — v1.5 walk-forward validated (not overfit)

Ran just optimize -- --static — new flag added to optimize.py that runs a fixed config (get_config(ticker)) across rolling 252-train / 63-test windows without per-window tuning. This is the genuine OOS test for static defaults.

TSLA (11 rolling OOS windows, period 2023-06 → 2026-03):

  • 6 windows profitable, 1 small loss (W2: −$244), 4 zero-trade stand-aside
  • Of windows that traded: 6/7 = 86% profitable
  • Total OOS P/L: +$11,500 (matches 5y aggregate +$11,815 to within 3%)
  • Worst window: −$244. Best: +$5,672 (W6, 2024 Q4 post-election rally)
  • Max single-window DD: $1,416; mean per-window DD: $352

TSLL (6 rolling OOS windows, period 2024-09 → 2026-03):

  • 5/5 trading windows profitable; 1 zero-trade stand-aside
  • Total OOS P/L: +$1,138
  • Max single-window DD: $405

Comparison with v1.1's documented walk-forward (from this doc's history):

v1.1 OOS total v1.5 OOS total Swing
TSLA −$7,239 +$11,500 +$18,739
TSLL −$813 +$1,138 +$1,951
v1.1 worst window v1.5 worst window
TSLA −$5,960 −$244
TSLL −$423 $0 (zero-trade window)

Conclusion: v1.5 generalizes. The 5y aggregate result is not an artifact of overfitting to the full window — it holds in rolling OOS, with much smaller single-window losses than v1.1. The 4 zero-trade TSLA windows are stand-asides in calm conditions (mostly 2023 and late 2025), which the cost function counts as success, not failure.

One observation worth noting: Most of TSLA's OOS P/L came from the 2024-Q4 → 2025-Q2 windows (huge_up rally + post-rally volatility expansion). The strategy genuinely captures higher-IV environments well and stands aside in lower-IV ones. If the next year is structurally low-vol, expect more zero-trade windows.

2026-05-12 — v1.5: per-ticker DTE × delta defaults

Grid sweep on 8 DTE × 5 delta = 40 configs per ticker. Scored by P/L − 1.0 × max_dd with min_trades=10. Top candidates validated against the 12-regime scenario suite per CLAUDE.md.

Winners (different per ticker):

  • TSLA: 7-DTE × Δ0.20 × mcp 0.010 — 5y P/L +$11,815, DD $2,436, PF 2.29 (vs v1.3: +$1,583, $3,534, 1.07).
  • TSLL: 3-DTE × Δ0.30 × mcp 0.012 — 5y P/L +$1,757, DD $440, PF 1.74 (vs v1.3: +$23, $555, 1.01).

Per-ticker config implemented: DEFAULT_CONFIG_BY_TICKER dict + get_config(ticker) helper in strategies.py. run_backtest.py, run_scenarios.py, and live.py all updated to pull per-ticker config. No breaking change to StrategyConfig defaults — a generic StrategyConfig() still gives v1.3-equivalent params; tickers without entries in the dict fall through to those.

Suite validation passed. TSLA suite +$5,206 (vs v1.3 +$510), worst regime −$244 (vs −$815). TSLL suite +$942 (vs +$258), worst regime −$76 (vs −$177). The v1.3 TSLL chop_whipsaw regression (−$177) flipped to +$183 with the new TSLL config — a side-effect win.

Multi-version cumulative. TSLA 5y P/L journey across the session: −$6,242 (v1.1) → +$2,243 (v1.2, regime fixes) → +$1,583 (v1.3, honest accounting) → +$11,815 (v1.5, per-ticker tuning). Net swing: +$18,057, with max DD cut from $12,292 to $2,436 (80% reduction). The session's compounding plan delivered far more than any single tuning step in isolation.

Live caveat. The mcp floor + shorter DTE means today's market often produces STAND_ASIDE recommendations. The 5y data shows ~25 entries/year for TSLA at the new config (vs ~50/year previously) — the strategy is more selective, and that's where the edge comes from. The user should expect "no trade today" to be the common response and not interpret it as a broken signal.

2026-05-12 — v1.4: wheel mechanic implemented (opt-in); short-DTE finding surfaced

Wheel state machine implemented in backtest.py + pick_covered_call in strategies.py. --wheel flag added to run_backtest.py and run_scenarios.py. Default is wheel_enabled=False so existing backtests are unchanged.

Implementation summary:

  • Engine: Backtester tracks stock_basis + stock_entry_date. On put expiring ITM in wheel mode, transitions to HOLDING state. New _assign_put, _assign_call_away, _close_stock methods. Position gains is_wheel_cc flag + accepts side='stock' (synthetic leg for realizing stock P/L).
  • Strategy: pick_covered_call sells at basis (aggressive — designed to cycle out). pick_entry uses wheel_put_dte (default 14) instead of long_dte when wheel is on. check_exits relaxes max_loss tolerance, skips dte_stop / delta_breach / regime_flip for wheel-mode puts.

Sweep across 7 wheel configurations × 2 tickers:

  • TSLA wheel rarely improves on cash-only. Best variant: +$2,818 with $5,961 DD (vs cash-only +$1,583 with $3,534 DD). Same P/L is achievable WITHOUT wheel by just shortening put DTE.
  • TSLL wheel is structurally bad. Every variant underperforms cash-only by $300–$1,500 with 2–3× the drawdown. Leveraged-ETF volatility decay during the assignment hold compounds losses faster than CC premium can offset. Falsifies the "wheel more on TSLL" hypothesis.
  • Headline finding (not wheel-related): short-DTE put selling. Switching long_dte from 30 to 7 produces +$4,286 with $579 DD on TSLA — 2.7× the P/L with 16% of the drawdown. This was surfaced as a side-effect of the wheel sweep (the wheel never triggered in that config, but the underlying short-DTE selling did extremely well). Promoted to top of "Next tuning targets" pending 12-regime validation.

Decision: wheel ships as an opt-in mode, not a default. The default strategy stays at v1.3 short premium with the v1.3 gates. The next strategy iteration (v1.5) will focus on long_dte sweep, not wheel deepening.

2026-05-12 — v1.3: engine polish — min_credit_pct filter + gap-slippage on max_loss

Two engine-side changes, no strategy-logic changes:

strategies.py::pick_entry — added post-pricing filter if credit / strike < min_credit_pct: skip. New StrategyConfig.min_credit_pct = 0.010 (1% of strike floor). Swept across [0.0, 0.003, 0.005, 0.008, 0.010, 0.015, 0.020, 0.030] against both 12-regime suite and 5y backtest on TSLA + TSLL; 0.010 is the value that improves both tickers' 5y P/L and DD simultaneously. Higher values (0.020) improve TSLA but hurt TSLL.

backtest.py::_fill_price — engine now computes the effective exit price (was previously left to strategy code, with an accounting inconsistency where exit_price stored on the trade didn't match the slippage-adjusted price the strategy decided with). When max_loss fires AND |open − prev_close| / prev_close > gap_threshold_pct (default 3%), the fill is additionally multiplied by (1 + gap_slippage_mult) (default 0.15). Non-max_loss exits and non-gap days are unaffected.

Multi-version progression (TSLA 5y backtest):

Version Trades Total P/L Max DD Profit factor Comment
v1.1 428 −$6,242 $12,292 0.87 walk-forward defaults
v1.2 240 +$2,243 $4,910 1.09 regime fixes (optimistic accounting)
v1.3 (gap-slip only) 239 +$1,101 $4,910 1.04 honest accounting, no filter
v1.3 (final) 200 +$1,583 $3,534 1.07 mcp=0.010 added — best risk-adjusted

Reading. v1.3's headline P/L is below v1.2 because the gap-slippage accounting is more honest (the v1.2 +$2,243 was overstated by ignoring gap-day fill quality). The min_credit_pct filter pays for the accounting hit by cutting the worst-tail entries: max_loss exit count fell from 15 → 10 on TSLA, max DD fell 28%. TSLL went −$71 (gap-slip alone) → +$80 (mcp added). Per the cost function, this is what we want: same edge per unit of risk, with materially less drawdown and fewer trades to manage.

Suite-vs-backtest divergence. The 12-regime suite total dropped (+$2,492 → +$510 on TSLA) because the canonical vol_crush window happens to have low absolute credit-vs-strike for the v1.3 mcp filter (TSLA was ~$170 during May 2024, so 30-DTE 15Δ puts collected ~0.6% of strike). The 5y backtest spans many more conditions and the filter pays off net-net. The canonical vol_crush window should be re-curated to one with higher absolute strike (e.g. 2025) to remain a useful regime-test.

2026-05-12 — Scenario suite expanded 7 → 12 regimes; v1.2 baselined against full suite

Five new regimes added to scenarios.py to stress tail / vol / event behavior the original 7 didn't cover:

  • gap_shock — window contains an overnight gap >7%. Canonical test for the daily-bar engine limit.
  • vol_crush — IV rank starts ≥60, ends ≤30. Tests whether the strategy captures easy premium.
  • vol_expansion — IV rank rises ≥30 points to ≥50. Tests whether gates back off when vol is rising against us.
  • chop_whipsaw — |net return| <5% but high-low range >12% with ≥5 big single-day moves. Tests whether regime-flip exits churn us out of winners on noise.
  • earnings_window — 21-day window containing a known TSLA earnings date. Tests event-driven IV behavior.

classify_window refactored to return set[str] (a single window can match multiple regimes, e.g. a window can be both huge_up and vol_expansion). One canonical window per ticker per regime pinned in CANONICAL_SCENARIOS after running discovery.

v1.2 strategy results on the new regimes (TSLA): gap_shock −$676, vol_crush +$813, vol_expansion $0 (stood aside), chop_whipsaw $0 (stood aside), earnings_window +$374. TSLA suite total: −$3,184 (v1.1 7-regime) → +$1,980 (v1.2 7-regime) → +$2,492 (v1.2 12-regime).

The expansion was driven by a deliberate philosophy shift documented at the top of "Next tuning targets": optimize for extreme-case management; calm-case performance is a free byproduct. Under that cost function, vol_expansion and chop_whipsaw zero-trade outcomes count as successes, not missed opportunities; the normal_down and inverse_v regressions count as insurance premiums paid for the huge_down/huge_up wins.

2026-05-12 — v1.2: bearish gate fixed + defensive-call trend gate

Surgical fixes to the two regime failures documented in v1.1's "Next tuning targets":

data.py::_classify_regime — dropped the rsi_14 > 75 clause from the bearish definition. The v1 rule price < ema_55 AND rsi > 75 AND macd < 0 was internally inconsistent — RSI rarely exceeds 75 while price is below EMA55 — so the gate almost never fired. New rule: price < ema_55 AND macd < 0 (two independent trend signals).

strategies.py::pick_entry — changed defensive trigger from reversal OR high_iv to reversal OR (high_iv AND ema_21 < ema_55). Adds a trend-confirmation gate so we no longer flip to short-call selling during high-IV bull rallies.

Scenario suite swing:

  • TSLA suite total: −$3,184 → +$1,980 (+$5,164)
  • TSLL suite total: −$381 → +$449 (+$830)
  • Wins: huge_down −$4,464 → $0 (gate stands aside, 0 trades); huge_up −$2,070 → +$1,038 (defensive rule no longer misfires)
  • Regressions surfaced and documented: normal_down +$1,145 → −$603 and inverse_v +$374 → −$218 on TSLA. Root cause: the new bearish gate fires after the trend is established, so the strategy enters early-leg trades before the gate trips. Candidates for the next tuning round (min_credit_pct filter, tighter gate with ret_14d < -0.05).

Full-period backtest swing:

Ticker v1.1 P/L v1.2 P/L v1.1 Max DD v1.2 Max DD
TSLA −$6,242 +$2,243 $12,292 $4,910
TSLL −$981 +$23 $1,461 $672

Profit factor on TSLA crossed 1.0 (1.09) for the first time. Max DD cut ~60% on TSLA, ~54% on TSLL. Trade count roughly halved on both (bearish-gate days the v1.1 strategy traded into are now correctly skipped). The strategy is structurally healthier — the next layer of tuning is engine polish (credit-floor filter, gap-slippage modeling) rather than further strategy-side fixes.

2026-05-12 — Phase 5: live + dashboard unified through pick_entry()

The live daily recommendation now routes through the same strategies.pick_entry() as the backtest engine. just test runs live.py; just run launches the Streamlit dashboard. Legacy dynamic_parameter_engine.py retired as a tuning surface — it survives as a historical artifact only. Effect: any strategy tweak validated via just scenarios + just optimize is immediately reflected in the live signal with no manual rewiring.

2026-05-12 — Scenario suite shipped; surfaced new huge_up failure mode

Added scenarios.py + run_scenarios.py (canonical 21-day windows per regime, frozen for reproducibility). Ran v1.1 defaults across all 7 regimes on TSLA + 6 on TSLL. 5/7 TSLA regimes profitable.

Two regime-specific losses:

  • huge_down −$4,464 (confirmed: bearish-gate fails to fire — already on the next-targets list)
  • huge_up −$2,070 (new finding: high_iv flag flips to short-term call selling even in a +59% rally; defensive rule needs a trend check)

The huge_up failure is invisible in aggregate metrics — bear quarters dominated walk-forward results and hid it. Per-regime testing is now mandated for every strategy change.

2026-05-12 — Phase 4 walk-forward; defaults updated to v1.1

Ran just optimize over 11 TSLA + 6 TSLL quarterly windows, training on the prior 252 days each step. 54 combos in the sweep grid. Composite score = total_pnl − 0.5 × max_dd with a 5-trade minimum to avoid degenerate windows.

Strong consensus on two knobs, applied to defaults:

  • daily_capture_mult_long: 2.0 → 1.5 (won 10/11 TSLA, 4/6 TSLL)
  • long_target_delta: 0.22 → 0.15 (won 6/11 TSLA, 4/6 TSLL — modal but less unanimous; matches the pre-Phase-4 prediction in this doc)

Two knobs left at default because the walk-forward result was a tie:

  • max_loss_mult (split 4:4:3 between 1.2, 1.8, 1.5)
  • profit_target (similar 4:4:3 split)

Aggregate result: TSLA median window +$123 P/L (6 of 11 profitable), but total OOS −$7,239 — dominated by two TSLA bear-market quarters in 2025 where the strategy kept selling puts through a ~50% downtrend. Diagnosis: the bearish-regime gate inherited from v5 (price < ema_55 AND rsi > 75 AND macd < 0) is internally inconsistent and almost never fires. That fix sits in data.py, not StrategyConfig, and is now the top priority above further knob sweeps.

2026-05-12 — Phase 2/3 baseline pinned

Engine completed, ran with literal defaults, dumped per-trade CSVs to .cache/. Findings summarised in the "Latest baseline results" section above. Key takeaway: daily profit capture works; max_loss is the dominant failure mode and is partly an engine limitation. Phase 4 will tune the strategy knobs that mitigate it.

2026-05-12 — Daily profit capture formula formalised

User-specified formula:

daily_theta_target = credit / DTE_at_entry. Each day held, compute realised_pnl / days_held. Close when that ≥ multiplier × daily_theta_target. Multiplier: 1.0× for ≤7 DTE, 1.5× for 8–15, 2.0× for ≥16.

Adopted as the strategy's primary "take profit when ahead of schedule" mechanism, distinct from the flat percentage profit target (which still exists as rung 4).

2026-05-12 — Single-leg only, regime picks direction

Scope decision: v1 picks one side at a time (put or call), never both legs simultaneously. Direction is selected by the classifier each evaluation:

  • iv_rank < min or regime == bearish → no trade
  • reversal or high_iv → defensive short call
  • otherwise → short put

Short strangles deferred to v2+. No "lock per day" — re-evaluated every tick / bar / call to the engine.

Pre-engine strategy versions

Captured in the legacy .md files in repo root. Briefly:

  • v4 (strategy_v4.py, stub): premium selling baseline
  • v5 (strategy_v5_optimized.py): scenario classifier (bullish/bearish/neutral) + dynamic params per scenario. Inherited classifier logic into data.py.
  • v6.0–v6.3 (strategy_v6_dynamic.py): added intraday reversal rule (-3% intraday + 1.5× volume → defensive)
  • v6.4–v6.6 (SHORT_TERM_CALL_*.md): added short-term call selling module, refined to 0.17 delta / 5–7 DTE after early 0.22 delta @ 440 strike was found too tight
  • v6.10: DTE-adjusted delta table, credit estimation, early-exit simulation (45% profit / 2× max loss)
  • v6.11: short-strangle backtest with early exits

All of these are absorbed into the v1.0 engine + StrategyConfig. The legacy Python files (dynamic_parameter_engine.py, strategy_v6_dynamic.py) still drive the live daily recommendation but will be re-wired to the optimised params after Phase 4.

Management policy / close-roll advisor (v1.14+ scaffolding, 2026-05-31)

Current state (safe, behind flag): StrategyConfig now has enable_model_management=False (default) + model_min_management_conf / model_min_mgmt_edge / model_management_path.

  • recommend_management_advisor(row, pos_dict, cfg, mark) (thin wrapper in strategies.py) is the single surface.
  • When enabled (lab only): calls PickEntryModel.recommend_management (Phase B) which builds 33-col traj vector via build_management_decision_features SoT (10 fields: decision_step, current_pnl_pct, pace_vs_target, days_held_norm, max_adverse_pct, is_gap_day, ret_1d/3d_since, iv_rank_at_decision, regime_at_decision; deterministic encoding + NaN guard).
  • Output proposal: {'close': bool, 'reason': str, 'overrides': {delta_breach, ...}, 'confidence', 'expected_improvement'}.
  • Wired read-only into positions.py:check_position (status['model_management_advice']) + format_status ("MODEL MGMT (shadow): ...") and manage_positions whatif subcommand.
  • check_exits, adapt_exit_params, backtest loop, just scenarios, pick_entry, live rec: 100% untouched. Proposals are diagnostics / what-if only until distilled to ADAPTIVE_EXIT_RULES or proven win on cost fn.

Training recipe (synthetic only for densification): generate_scenarios --focus high_gamma_marginal,post_earnings_weak + build_training_set --scenarios ... --label --low-regret-filter 30 (produces 48-col with traj at decision points; 77.9% zero-regret on demo set). Model: LGBM on entry+traj feats → advisor. Full parity gauntlet via extended validate_model_policy + real 5y + canonicals (cost = pnl/contract − 1.0×DD).

Promotion rule: Only after real gauntlet shows improvement (or clean documented null); prefer explicit rule distillation over raw weights. See simulator/PLAN.md (this session) + MODEL_TRAINING_PLAYBOOK.md for commands + first-cycle null result.

History entry (2026-05-31): Implemented approved plan for signal-driven close/roll (enriched labeling + SoT traj features (Phase A complete), recommend_management + gates + focused training demo (B), StrategyConfig + positions/whatif wiring (C)). All behind enable=False. just scenarios pure. First focused labels: 41k+ ex, 77.9% oracle-zero-regret. Demo mgmt model loads + proposes on adverse traj. Live surface test passes (advice rendered read-only). Gauntlet: clean null on cost (loop ready for critic iterations). No core invariants broken. Docs + summary updated. (See /tmp/grok-impl-summary-a70f9988.md for full cmds/outputs per phase.)

2026-06-05 — v1.14: ride_high_credit_mgmt shipped (TSLL) + closed-loop phases 0–5 advance

  • Shipped ride_high_credit_mgmt on TSLL (DEFAULT_CONFIG_BY_TICKER): when peek credit ≥ 1.35× min_credit_pct, apply hold_longer-style exits (daily_capture_mult=2.2, profit_target=0.70). validate_rule.pyTRIPLE-WIN (5y +$0 delta on already-enabled rule pin; suite/OOS unchanged; DD flat).
  • Rejected tight_risk_high_gamma on TSLA: NULL (−$3,123 5y P/L, +$579 DD, −$990 suite).
  • Phase 0: verify_model_features.py passes on latest 20260515/20260605 models (20/23 + 33-col mgmt).
  • Phase 1: 1,580 focused paths (.cache/phase1_focus_20260605.parquet); large label build in flight.
  • Phase 2/3 infra: simulator/join_real_trades.py unions real *_trades.csv feature rows with synthetic parquet; just model-* recipes added.
  • Phase 4: trade_labeler mid-trade snapshots (emit_midtrade / build_training_set --midtrade) for standard-policy per-bar trajectory rows.
  • Phase 5: Dashboard Today tab shows experimental model recommendation (read-only; production path still pick_entry).
  • Still off: enable_model_entry, enable_model_management — rules remain source of truth for opens/closes.

2026-07-09 — Platform M0–M1 foundation (no strategy rule change)

Pinned income-platform doctrine and local autonomy foundation. No change to pick_entry / check_exits / adaptive rules.

  • Docs: docs/TRADER_PLATFORM_GOAL.md, docs/AGENTIC_AUTONOMY_POLICY.md; GOAL.md pointer.
  • Package trader_platform/ (was platform/); M2 premium_scout regime→strategy→symbol→premium paper-first: hypothesis registry (YAML), promotion gates (structure + no auto-live), risk governor, paper broker, RH MCP stub, autonomy loop (paper default).
  • Modes: research|paper|shadow|agentic_live — agentic_live blocked until broker connected + armed.
  • Seed hypotheses: short premium TSLA/TSLL, PMCC income, stand-aside (status candidate/testing — not live).
  • Live OAuth / real place_order / Hermes live-trading cron: out of scope (Stage1 gate).

2026-07-09 — Stage2 RH read-only + paper→RH wire

  • Added trader_platform/rh_snapshot.py, PaperRhBridge, readiness CLI, docs/STAGE2_RH_READONLY_AND_CAPITAL.md.
  • Live place remains fail-closed; Agentic sleeve unfunded in smoke snapshot.