14 · Dynamic Exit Rules — Walk-Forward Backtest
Generated 2026-05-15. Raw results: /tmp/exit-rules-results.json.
Source: scripts/backtest-exit-rules.mjs, helpers: src/lib/exit-rules.ts.
Verdict (honest)
NO exit rule combination beats the validation threshold of
ΔSharpe_ann ≥ +0.20 AND max-DD reduction ≥ 30 % AND bootstrap
p-value < 0.10 vs the T+90 fixed-hold baseline. The largest Sharpe
improvement observed is ΔSR = +0.034 (time+insider+vol) — an order of
magnitude below threshold and statistically indistinguishable from noise
(p = 0.71).
STRATEGY_PROOF.exitRules was NOT updated. The live recommendations
remain a pure entry signal with no documented exit playbook beyond T+90.
Setup
- Universe: 16 005 BUY-direction
BacktestResultrows with fullpriceAtTrade → price90dcoverage (2021-12 → 2026-04). - Walk-forward: 24-month train / 12-month test, rolling. 2 folds (2023-12, 2024-12). σ priors computed on TRAIN only.
- Bucket σ: per (role × size) on TRAIN-window 90-day returns; fallback
cascade
role::size → role → __overall. - Price path: reconstructed from {30, 60, 90, 160} day snapshots in
BacktestResult. Sparse but no look-ahead. Linear interpolation for insider-sell event prices (rule 4). - DD calc: trades binned into 16 chronological bins, averaged within bin, compounded — a realistic-portfolio proxy. Bin-level NAV.
- Bootstrap: 1000 iter, seed=42, two-sided pooled-resample p-value.
Rule definitions
| # | Rule | Math |
|---|---|---|
| 1 | Stop-loss | exit at first t > 0 where (P_t − P_0)/P_0 ≤ −k·σ_bucket, k=2 |
| 2 | Profit target | partial 50 % at +1σ, remainder at +2σ (else T+90 close) |
| 3 | Time-stop | exit at T+30 if ` |
| 4 | Insider sell | exit at first t ≤ 90 with any new Cession* on company.id |
| 5 | Vol regime | exit if 30-day rolling annualized realized vol > 2·σ_ann (entry bucket) |
Per-rule walk-forward results (vs baseline)
Baseline: SR_ann = −0.012, mean = 0.83 %, win = 46.9 %, max-DD = −17.89 %, hold = 90 d, n = 7 032.
| Rule | SR_ann | ΔSR | Mean % | Win % | Max-DD % | DD red. % | Avg hold | p |
|---|---|---|---|---|---|---|---|---|
| stop | −0.036 | −0.024 | 0.35 | 46.7 | −22.21 | −24.1 | 91.1 d | 0.41 |
| profit | −0.050 | −0.038 | 0.31 | 47.4 | −16.83 | 5.9 | 89 d | 0.29 |
| time | −0.012 | 0.000 | 0.62 | 45.9 | −16.60 | 7.2 | 68.8 d | 0.69 |
| insider | −0.016 | −0.004 | 0.57 | 47.2 | −15.08 | 15.7 | 69.1 d | 0.64 |
| vol | +0.018 | +0.030 | 1.60 | 46.8 | −18.59 | −3.9 | 91.4 d | 0.26 |
Observations:
- Vol regime is the only single rule with positive ΔSharpe AND positive Δmean (+0.77 pp). p = 0.26 — not significant. Holds barely change (avg 91.4 d) → the rule fires on very few trades but those it exits are unusually bad.
- Time-stop cuts holding period to 68.8 d (free capital) with ΔSR = 0 — neutral on quality, marginally faster turnover. Plausible capital-efficiency play but not a return enhancer.
- Stop-loss underperforms baseline — the −2σ band crystallizes losses that mostly mean-revert by T+90. Confirms a well-known small-cap insider-buy pattern.
- Profit target suffers worst Sharpe drop (−0.038). +1σ is hit frequently then the partial leg locks in modest gains while the full +2σ is rarely reached → caps upside without trimming risk.
- Insider-sell reduces DD by 15.7 % but at the cost of ΔSR = −0.004 (insiders' SELL signal on FR mid-caps is noisy in this window).
Best combination
time+insider+vol:
- SR_ann = 0.022, ΔSR = +0.034
- Mean = 1.03 %, ΔMean = +0.20 pp
- Win = 45.9 %, ΔWin = −1.0 pp
- Max-DD = −10.29 %, DD reduction = +42.5 % vs baseline ✓
- Avg hold = 55.1 d (39 % faster capital recycling)
- p = 0.71 — not statistically significant ✗
This combination DOES clear the DD-reduction gate (≥30 %) but fails BOTH the ΔSharpe gate (+0.034 < +0.20) and significance (p = 0.71 ≫ 0.10). DD reduction is partly mechanical: shorter holding period → smaller bin returns → smaller compounded drawdowns. We refuse to claim it as alpha.
Sensitivity: stop-loss multiplier
| k (σ mult.) | Mean % | SR_ann | Win % | Max-DD % | Avg hold |
|---|---|---|---|---|---|
| 1.5 | −0.29 | −0.068 | 45.7 | −26.69 | 92.3 d |
| 2.0 | 0.35 | −0.036 | 46.7 | −22.21 | 91.1 d |
| 2.5 | 0.62 | −0.023 | 46.8 | −19.51 | 90.5 d |
| 3.0 | 0.69 | −0.019 | 46.8 | −18.99 | 90.1 d |
Monotone in k: tighter stop, worse outcome. The −2σ band is below the optimum (the optimum is "no stop at all" on this universe). This argues against any tight stop on insider-buy small/mid-cap signals.
Open questions / next steps
- Sparse price path: rules can only fire at 4 checkpoints (30, 60, 90,
160 d). A daily price store would let stop-loss fire intraday and is
the single largest possible improvement. Hydrating a
PriceHistorymodel from Yahoo daily closes is on the roadmap. - Bucket σ at entry-time only: we use TRAIN-window σ_90d; a more sophisticated overlay would use trailing realized vol per company.
- Regime conditioning: rules may work in bear regimes only.
Conditional walk-forward by
RecoSnapshot.regimeis a logical next pass. - Profit-target asymmetry: a one-sided variant (full @ +2σ only, no partial) would avoid clipping the tail. Worth a follow-up.
- Insider-sell stratified: filter cessions to ≥1 % of market-cap and to dirigeant (vs board) roles — the 6 810-event index is dominated by small administrative cessions which dilute the signal.
Reproducibility
node --env-file=.env.local scripts/backtest-exit-rules.mjs
Seed = 42 throughout. Output: /tmp/exit-rules-results.json.
Files
src/lib/exit-rules.ts— 5 pure helpers +earliestExitcombiner.scripts/backtest-exit-rules.mjs— walk-forward harness, JS port of the TS kernels (kept logically identical).