Files
PythagorasGoal/scripts/research/stops_lab.py
T
Adriano Dal Pastro 856a02fcc5 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) <noreply@anthropic.com>
2026-06-23 13:09:43 +00:00

102 lines
4.5 KiB
Python

"""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*0.4: on=True
e[i]=1.0 if on else lvl
return e
def expo_trailstop(r, trig=0.06, reentry="newhigh"):
"""Stop DURO trailing (uscita TOTALE) — test EQUO: trigger/re-entry sul MERCATO (NAV sempre-
investito di riferimento, non sull'equity stoppata che si congela). Esci se DD del NAV > 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 d<trig*0.5: on=True
e[i]=1.0 if on else 0.0
return e
def expo_monthly(r, trig=0.05):
e=np.ones(len(r)); idx=r.index; cur=None; mret=1.0; stopped=False
for i in range(len(r)):
ym=(idx[i].year,idx[i].month)
if ym!=cur: cur=ym; mret=1.0; stopped=False
if stopped: e[i]=0.0; continue
mret*=(1+r.values[i])
if mret-1 < -trig: stopped=True
return e
def expo_volstop(r, win=30, pctl=0.90):
rv=pd.Series(r.values,index=r.index); v=rv.rolling(win,min_periods=15).std()*ANN
thr=v.expanding(min_periods=60).quantile(pctl).shift(1)
e=np.where((v.shift(1)>thr).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()