From 856a02fcc59efd8558af6c1f1e51f0036405cdfe Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Tue, 23 Jun 2026 13:09:43 +0000 Subject: [PATCH] research(stops): SL classici vs soft-guard -> il soft-guard vince (lo SL duro whippa nel grind) Goal 'prova anche SL'. Test equo (trigger/re-entry sul NAV mercato). soft-guard -4% Sh 1.38/DD 5.8% resta il migliore; trail-stop -6% valido ma inferiore (1.34/6.6%); -4% whipsaw (1.07, inMkt 42%); stop mensile/vol inutili. Per un DD da grind, de-risk parziale > uscita totale. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/diary/2026-06-23-tail-hedge-lab.md | 17 ++++ scripts/research/stops_lab.py | 101 ++++++++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 scripts/research/stops_lab.py diff --git a/docs/diary/2026-06-23-tail-hedge-lab.md b/docs/diary/2026-06-23-tail-hedge-lab.md index 761657f..b1c4e18 100644 --- a/docs/diary/2026-06-23-tail-hedge-lab.md +++ b/docs/diary/2026-06-23-tail-hedge-lab.md @@ -47,3 +47,20 @@ Vol-target NON aiuta (il 2022 non e' uno spike di vol). - Trade: -2.1pp CAGR per dimezzare il MaxDD e azzerare quasi il 2022. Sensato se la priorita' e' il DD. - Parametri (-4% trigger) semplici; il meccanismo (non la soglia esatta) e' la sostanza. Script: tail_hedge_lab.py. + +## Aggiornamento — protezioni CLASSICHE (stop-loss), goal "prova anche SL" (`stops_lab.py`) +Confronto equo (trigger/re-entry sul NAV di mercato, non sull'equity congelata): +| protezione | Sharpe | MaxDD | 2022 | CAGR | in-mkt | +|---|---|---|---|---|---| +| baseline | 1.48 | 8.4% | -4.4% | 11.3% | 100% | +| **soft-guard -4% (0.4x)** | **1.38** | **5.8%** | **-1.8%** | 9.2% | 100% | +| trail-stop -4% (uscita tot.) | 1.07 | 7.5% | +0.0% | 6.6% | 42% | +| trail-stop -6% (re:newhigh) | 1.34 | 6.6% | -2.1% | 9.0% | 72% | +| trail-stop -8% | 1.41 | 8.3% | -4.2% | 10.0% | 87% | +| stop mensile -5% | 1.48 | 8.4% | -4.4% | 11.3% | 100% (mai scatta) | +| vol-stop (>90pctl) | 1.48 | 8.4% | -4.4% | 10.4% | 100% | + +VERDETTO: lo SL classico funziona solo a -6% (e resta inferiore al soft-guard); a -4% fa WHIPSAW +(Sh 1.48->1.07, fuori mercato 58%) perche' l'uscita TOTALE viene choppata nel grind. Il soft-guard +alla stessa soglia non whippa (de-risk parziale 0.4x). Stop mensile/vol inutili (bersaglio sbagliato). +Conferma: per un DD da grind, de-risk PARZIALE > stop-loss duro. Soft-guard -4% confermato come scelta. diff --git a/scripts/research/stops_lab.py b/scripts/research/stops_lab.py new file mode 100644 index 0000000..152ca12 --- /dev/null +++ b/scripts/research/stops_lab.py @@ -0,0 +1,101 @@ +"""STOPS LAB — protezioni CLASSICHE (stop-loss) sul combo vs la guardia-DD morbida. + +Confronta su Sharpe/MaxDD/2022/CAGR/NumTrades/time-in-market: + - baseline + - guardia-DD morbida (de-risk a 0.4x, gia' scelta) + - TRAILING STOP duro (uscita TOTALE) a -4/-6/-8% dal picco, re-entry su nuovo massimo + - TRAILING STOP con re-entry su RECUPERO (DD < meta' soglia) + - STOP MENSILE (flat per il resto del mese se perdita mensile > X%) + - VOL STOP (de-risk se vol realizzata 30g > 90 pctl espandente) +Tesi da verificare: lo stop DURO taglia il DD ma fa whipsaw nel grind 2022 (Sharpe/CAGR peggiori, +piu' trade). Dati: combo_daily (cache). sqrt(252). +""" +import sys +from pathlib import Path +import numpy as np, pandas as pd +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)); sys.path.insert(0, str(ROOT / "scripts" / "research")) +from combo_yearly_report import combo_daily +ANN = np.sqrt(252.0) + + +def _sh(r): r = np.asarray(pd.Series(r).dropna(), float); return float(np.mean(r)/np.std(r)*ANN) if len(r)>5 and np.std(r)>0 else 0.0 +def _dd(r): eq=np.cumprod(1+np.asarray(r,float)); pk=np.maximum.accumulate(eq); return float(np.max((pk-eq)/pk)) if len(eq) else 0.0 +def _yr(r,y): return float(np.prod(1+r[r.index.year==y].values)-1) if (r.index.year==y).any() else 0.0 +def _cagr(r): r=r.dropna(); return (np.prod(1+r.values))**(252/len(r))-1 +def _ntr(expo): return int((np.abs(np.diff(expo, prepend=expo[0]))>1e-9).sum()) # cambi di esposizione = trade + + +def expo_softguard(r, trig=0.04, lvl=0.4): + rv=r.values; eq=np.cumprod(1+rv); pk=np.maximum.accumulate(eq); e=np.ones(len(rv)); on=True + for i in range(1,len(rv)): + dd=(pk[i-1]-eq[i-1])/pk[i-1] if pk[i-1]>0 else 0 + if dd>trig: on=False + if dd trig; + re-entry 'newhigh' = NAV torna al picco; 'recover' = DD del NAV < trig/2.""" + rv=r.values; n=len(rv) + nav=np.cumprod(1+rv); pk=np.maximum.accumulate(nav); dd=(pk-nav)/pk + e=np.ones(n); on=True + for i in range(1,n): + d=dd[i-1] + if on and d>trig: + on=False + elif not on: + if reentry=="newhigh" and nav[i-1]>=pk[i-1]-1e-12: on=True + elif reentry=="recover" and dthr).values, 0.4, 1.0); e=np.nan_to_num(e,nan=1.0) + return e + + +def show(name, r, expo): + g=pd.Series(expo*r.values,index=r.index) + tim=float((expo>1e-9).mean())*100 + print(f" {name:30} Sh {_sh(g):>5.2f} MaxDD {_dd(g.values)*100:>4.1f}% 2022 {_yr(g,2022)*100:>+5.1f}% " + f"CAGR {_cagr(g)*100:>+5.1f}% trades {_ntr(expo):>4} inMkt {tim:>3.0f}%") + + +def main(): + print("="*104); print(" STOPS LAB — protezioni classiche (SL) vs guardia-DD morbida (combo TP01+GTAA, 2019-26)"); print("="*104) + r=combo_daily() + print(f"\n {'(esposizione media applicata ai rendimenti del combo)':<30}") + show("baseline (nessuna)", r, np.ones(len(r))) + print(" --- guardia-DD MORBIDA (de-risk a 0.4x) ---") + show("soft-guard -4%", r, expo_softguard(r,0.04)) + print(" --- STOP-LOSS DURO (uscita totale, trailing dal picco) ---") + for t in (0.04,0.06,0.08): + show(f"trail-stop -{t*100:.0f}% (re:newhigh)", r, expo_trailstop(r,t,"newhigh")) + show("trail-stop -6% (re:recover)", r, expo_trailstop(r,0.06,"recover")) + print(" --- altri classici ---") + show("stop mensile -5%", r, expo_monthly(r,0.05)) + show("vol-stop (30g, >90pctl)", r, expo_volstop(r)) + print("\n NB: lo stop DURO che taglia molto il DD di solito paga in Sharpe/CAGR e in n.trade (whipsaw") + print(" nel grind). Confronta col soft-guard: stessa protezione, meno whipsaw?") + + +if __name__ == "__main__": + main()