diff --git a/scripts/analysis/fade_diag_by_regime.py b/scripts/analysis/fade_diag_by_regime.py new file mode 100644 index 0000000..7a33680 --- /dev/null +++ b/scripts/analysis/fade_diag_by_regime.py @@ -0,0 +1,66 @@ +import sys; sys.path.insert(0,".") +import numpy as np, pandas as pd +from scripts.analysis.regime_lab import load_features +from scripts.analysis.explore_lab import atr +import importlib +FEE=0.001; LEV=3 + +def load_strat(mod): + m=importlib.import_module(mod) + return next(v() for k,v in vars(m).items() if isinstance(v,type) and hasattr(v,'generate_signals') and getattr(v,'__module__','')==m.__name__) + +STR={"MR01":("scripts.strategies.MR01_bollinger_fade",dict(bb_window=50,k=2.5,sl_atr=2.0,max_bars=24,trend_max=3.0)), + "MR02":("scripts.strategies.MR02_donchian_fade",dict(n=20,sl_atr=2.0,max_bars=24,trend_max=3.0)), + "MR07":("scripts.strategies.MR07_return_reversal",dict(n=50,k=3.5,tp_atr=2.0,sl_atr=1.5,max_bars=24,trend_max=3.0))} + +def replay(df, sigs): + h=df['high'].values; l=df['low'].values; c=df['close'].values + out=[]; last=-1 + for s in sigs: + i=s.idx + if i<=last: continue + d=s.direction; tp=s.metadata['tp']; sl=s.metadata['sl']; mb=s.metadata['max_bars'] + j=min(i+mb,len(c)-1); exit_p=c[j] + for t in range(i+1,j+1): + if d==1: + if l[t]<=sl: exit_p=sl;j=t;break + if h[t]>=tp: exit_p=tp;j=t;break + else: + if h[t]>=sl: exit_p=sl;j=t;break + if l[t]<=tp: exit_p=tp;j=t;break + ret=(exit_p-c[i])/c[i]*d*LEV-FEE*LEV + out.append((i,ret)); last=j + return out + +# raccogli tutti i trade con il loro dvol_pct e hurst all'ingresso +rows=[] +for asset in ("BTC","ETH"): + df=load_features(asset,"1h"); ts=pd.to_datetime(df['timestamp'],unit='ms',utc=True) + for code,(mod,par) in STR.items(): + s=load_strat(mod); sigs=s.generate_signals(df,ts,**par) + for i,ret in replay(df,sigs): + rows.append(dict(asset=asset,code=code,year=ts.iloc[i].year,ret=ret, + dvol_pct=df['dvol_pct'].iloc[i], hurst=df['hurst'].iloc[i], + dvol=df['dvol'].iloc[i])) +R=pd.DataFrame(rows).dropna(subset=['dvol_pct']) +print(f"trade totali (con DVOL, 2021+): {len(R)}") + +print("\n=== PnL medio per trade per TERZILE DVOL (bassa/media/alta vol) ===") +R['dvbin']=pd.cut(R['dvol_pct'],[0,.33,.66,1.0],labels=['LOW-vol','MID','HIGH-vol']) +g=R.groupby('dvbin',observed=True)['ret'] +print(f" {'regime':<10}{'n':>6}{'ret_medio%':>12}{'win%':>8}{'somma%':>10}") +for b in ['LOW-vol','MID','HIGH-vol']: + x=R[R.dvbin==b]['ret'] + print(f" {b:<10}{len(x):>6}{x.mean()*100:>12.3f}{(x>0).mean()*100:>8.1f}{x.sum()*100:>10.0f}") + +print("\n=== dentro LOW-vol: split per HURST (anti-persistente vs trending) ===") +LV=R[R.dvbin=='LOW-vol'].copy() +LV['hbin']=pd.cut(LV['hurst'],[0,.45,.55,1.0],labels=['hurst<.45 (anti-pers)','.45-.55','>.55 (trend)']) +for b in ['hurst<.45 (anti-pers)','.45-.55','>.55 (trend)']: + x=LV[LV.hbin==b]['ret'] + if len(x): print(f" {b:<24}{len(x):>6} ret_medio {x.mean()*100:>+7.3f}% win {(x>0).mean()*100:>5.1f}% somma {x.sum()*100:>+6.0f}%") + +print("\n=== per anno: PnL fade in LOW-vol vs resto ===") +for y in range(2021,2027): + lo=R[(R.year==y)&(R.dvbin=='LOW-vol')]['ret']; hi=R[(R.year==y)&(R.dvbin!='LOW-vol')]['ret'] + print(f" {y}: LOW-vol somma {lo.sum()*100:>+6.0f}% (n{len(lo)}) | MID/HIGH somma {hi.sum()*100:>+6.0f}% (n{len(hi)})") diff --git a/scripts/analysis/fade_loss_by_regime.py b/scripts/analysis/fade_loss_by_regime.py new file mode 100644 index 0000000..e1fa839 --- /dev/null +++ b/scripts/analysis/fade_loss_by_regime.py @@ -0,0 +1,50 @@ +import sys; sys.path.insert(0,".") +import numpy as np, pandas as pd +from scripts.analysis.regime_lab import load_features +import importlib +FEE=0.001; LEV=3 +def load_strat(mod): + m=importlib.import_module(mod) + return next(v() for k,v in vars(m).items() if isinstance(v,type) and hasattr(v,'generate_signals') and getattr(v,'__module__','')==m.__name__) +STR={"MR01":("scripts.strategies.MR01_bollinger_fade",dict(bb_window=50,k=2.5,sl_atr=2.0,max_bars=24,trend_max=3.0)), + "MR02":("scripts.strategies.MR02_donchian_fade",dict(n=20,sl_atr=2.0,max_bars=24,trend_max=3.0)), + "MR07":("scripts.strategies.MR07_return_reversal",dict(n=50,k=3.5,tp_atr=2.0,sl_atr=1.5,max_bars=24,trend_max=3.0))} +def replay(df,sigs): + h=df['high'].values;l=df['low'].values;c=df['close'].values;out=[];last=-1 + for s in sigs: + i=s.idx + if i<=last: continue + d=s.direction;tp=s.metadata['tp'];sl=s.metadata['sl'];mb=s.metadata['max_bars'];j=min(i+mb,len(c)-1);exit_p=c[j];reason='time' + for t in range(i+1,j+1): + if d==1: + if l[t]<=sl: exit_p=sl;j=t;reason='sl';break + if h[t]>=tp: exit_p=tp;j=t;reason='tp';break + else: + if h[t]>=sl: exit_p=sl;j=t;reason='sl';break + if l[t]<=tp: exit_p=tp;j=t;reason='tp';break + out.append((i,(exit_p-c[i])/c[i]*d*LEV-FEE*LEV,reason));last=j + return out +rows=[] +for asset in ("BTC","ETH"): + df=load_features(asset,"1h");ts=pd.to_datetime(df['timestamp'],unit='ms',utc=True) + for code,(mod,par) in STR.items(): + s=load_strat(mod) + for i,ret,reason in replay(df,s.generate_signals(df,ts,**par)): + rows.append(dict(ret=ret,reason=reason,dvol_pct=df['dvol_pct'].iloc[i],hurst=df['hurst'].iloc[i], + vratio=df['vratio'].iloc[i],higuchi=df['higuchi'].iloc[i])) +R=pd.DataFrame(rows).dropna(subset=['dvol_pct','hurst']) +L=R[R.ret<0] # solo i trade in perdita +print(f"trade {len(R)} | in perdita {len(L)} ({len(L)/len(R)*100:.0f}%) | somma perdite {L.ret.sum()*100:.0f}% | media perdita {L.ret.mean()*100:.2f}%") +print("\n=== somma PERDITE per regime (dove si concentra il danno) ===") +R['dvbin']=pd.cut(R.dvol_pct,[0,.33,.66,1],labels=['LOWvol','MID','HIGHvol']) +R['hbin']=pd.cut(R.hurst,[0,.45,.55,1],labels=['anti<.45','.45-.55','trend>.55']) +piv=R[R.ret<0].pivot_table(index='dvbin',columns='hbin',values='ret',aggfunc='sum',observed=True)*100 +print((piv.round(0)).to_string()) +print("\n (numeri = somma % delle perdite per cella; piu negativo = piu danno)") +print("\n=== quota di SL (stop) per regime ===") +slr=R.groupby(['dvbin','hbin'],observed=True).apply(lambda x:(x.reason=='sl').mean()*100, include_groups=False) +print(slr.round(0).to_string()) +# worst tail +print(f"\n=== peggiori 1% trade: dove? ===") +W=R.nsmallest(max(10,len(R)//100),'ret') +print(f" worst {len(W)} trade: dvol_pct medio {W.dvol_pct.mean():.2f}, hurst medio {W.hurst.mean():.2f}, quota hurst>.55 {(W.hurst>.55).mean()*100:.0f}%, quota dvol<.33 {(W.dvol_pct<.33).mean()*100:.0f}%") diff --git a/scripts/analysis/fade_lossguard_workflow.js b/scripts/analysis/fade_lossguard_workflow.js new file mode 100644 index 0000000..e529743 --- /dev/null +++ b/scripts/analysis/fade_lossguard_workflow.js @@ -0,0 +1,120 @@ +export const meta = { + name: 'fade-lossguard', + description: 'Sistema anti-perdite per le fade in regime trending/low-vol: test meccanismi su MR01/02/07', + phases: [ + { title: 'Test', detail: 'agenti: ogni meccanismo di filtro applicato alle fade reali (BTC+ETH)' }, + { title: 'Synth', detail: 'classifica + miglior loss-guard, gate: riduce DD senza uccidere edge' }, + ], +} + +const API = ` +=== Harness (gia pronto) === +import sys; sys.path.insert(0,'.') +import numpy as np, pandas as pd, importlib +from scripts.analysis.regime_lab import load_features, report +from scripts.analysis.explore_lab import robust, atr + +def load_strat(mod): + m=importlib.import_module(mod) + return next(v() for k,v in vars(m).items() if isinstance(v,type) and hasattr(v,'generate_signals') and getattr(v,'__module__','')==m.__name__) +FADES={'MR01':('scripts.strategies.MR01_bollinger_fade',dict(bb_window=50,k=2.5,sl_atr=2.0,max_bars=24,trend_max=3.0)), + 'MR02':('scripts.strategies.MR02_donchian_fade',dict(n=20,sl_atr=2.0,max_bars=24,trend_max=3.0)), + 'MR07':('scripts.strategies.MR07_return_reversal',dict(n=50,k=3.5,tp_atr=2.0,sl_atr=1.5,max_bars=24,trend_max=3.0))} + +# colonne regime_lab (causali): dvol, dvol_pct, vrp, funding_z, dvol_chg, hurst, higuchi, vratio, frac_up/dn +# ADX (se ti serve) calcolalo causale da OHLC; efficiency-ratio Kaufman = |c[i]-c[i-n]| / sum|diff| su [i-n,i]. + +# PATTERN: genera i segnali fade, poi APPLICA IL TUO FILTRO scartando le entries in regime sfavorevole, +# confronta BASELINE vs FILTRATA su ogni fade x asset: +def entries_from(strat, df, par, keep=lambda df,i: True): + ts=pd.to_datetime(df['timestamp'],unit='ms',utc=True) + out=[] + for s in strat.generate_signals(df,ts,**par): + if keep(df, s.idx): # keep=False -> filtro scarta (loss-guard) + out.append({'i':s.idx,'d':s.direction,'tp':s.metadata['tp'],'sl':s.metadata['sl'],'max_bars':s.metadata['max_bars']}) + return out +# per ogni (fade,asset): res_base=report(.., entries_from(..., keep=tutto)); res_filt=report(.., col tuo keep) +# confronta: Sharpe OOS, DD full/oos, ret, #trade (quanti scartati), e robust(). Aggrega sulle 6 combo. +` + +const CONTEXT = ` +PROBLEMA: le fade (MR01 Bollinger, MR02 Donchian, MR07 return-reversal) sono mean-reversion 1h con +filtro trend EMA200 (trend_max=3.0). DIAGNOSI EMPIRICA (3022 trade, 2021+): le PERDITE e gli STOP +si concentrano nel regime PERSISTENTE/TRENDING, NON nella bassa vol: +- somma perdite per cella (Hurst x DVOL): la cella peggiore e' hurst>0.55 (-2695% in low-vol, + dominante in ogni terzile vol). I peggiori 1% trade hanno hurst medio 0.61 (77% con hurst>0.55). +- tasso STOP-LOSS: 43% quando hurst>0.55 vs 21% quando hurst<0.45 (anti-persistente). 2x. +- net: le celle restano positive (i winner battono), quindi filtrare toglie anche winner -> il + loss-guard e' utile SOLO se riduce DD/coda SENZA uccidere l'edge netto. +RICERCA ESTERNA (confermata): (a) Hurst regime filter: MR solo H<0.45, in 0.45-0.55 ridurre size, +evitare H>0.55. (b) ADX: MR profit factor 1.62 con ADX<20 vs -0.74 con ADX>30 (switch di regime piu' +importante). (c) ATR/vol-EXPANSION ratio>1.5 disabilita MR -> ha prevenuto il 72% delle perdite +maggiori. (d) time-stop: se non rientra in ~15 barre e' un trend, esci. + +OBIETTIVO: trovare il MIGLIOR meccanismo (o combo) che, applicato alle fade reali, RIDUCE DD/coda/ +stop-rate MANTENENDO l'edge netto OOS. Metodologia: causale no-look-ahead (le colonne regime_lab +sono causali; i filtri usano solo dati <= i), netto fee (report() fa OOS+sweep). LEZIONE FR01: un +filtro che riduce le perdite ma anche i winner spesso NON migliora -> il gate vero e' DD giu' a +parita' (o quasi) di Sharpe/ret, idealmente Sharpe SU e DD GIU'. + +` + API + +const SCHEMA = { + type: 'object', + properties: { + meccanismo: { type: 'string' }, + descrizione: { type: 'string' }, + base_oos_sharpe: { type: 'number' }, filt_oos_sharpe: { type: 'number' }, + base_dd_full: { type: 'number' }, filt_dd_full: { type: 'number' }, + base_oos_ret: { type: 'number' }, filt_oos_ret: { type: 'number' }, + trade_scartati_pct: { type: 'number' }, + riduce_perdite: { type: 'boolean', description: 'riduce DD/coda/stop-rate' }, + preserva_edge: { type: 'boolean', description: 'edge netto OOS preservato (Sharpe non crolla, robust resta)' }, + buon_lossguard: { type: 'boolean', description: 'riduce perdite SENZA uccidere edge -> candidato' }, + verdetto: { type: 'string', description: 'numeri base vs filtrato aggregati sulle 6 combo fade x asset' }, + }, + required: ['meccanismo', 'buon_lossguard', 'riduce_perdite', 'preserva_edge', 'verdetto'], +} + +const MECHS = [ + ['Hurst-skip H>0.55', 'scarta le fade quando rolling-hurst(window=100) >= 0.55 (regime persistente). Test anche soglia 0.50 e 0.60, riporta la migliore.'], + ['Hurst-size transition', 'NON scartare ma RIDURRE: tieni tutte le entries ma pesa size 1.0 se hurst<0.45, 0.5 se 0.45-0.55, 0.25 se >0.55. (Per testare la riduzione size col report attuale: approssima scartando il 50%/75% delle entries nelle bin alte in modo deterministico per indice, oppure confronta solo le bin.)'], + ['ADX-skip', 'calcola ADX(14) causale; scarta le fade quando ADX>25 (trend). Test soglie 20/25/30.'], + ['vol-expansion vratio', 'scarta le fade quando vratio (vol breve/lunga, colonna regime_lab) > 1.5 (vol in espansione = breakout, non range). Test 1.3/1.5/1.8. (la ricerca dice -72% perdite maggiori)'], + ['efficiency-ratio Kaufman', 'ER = |c[i]-c[i-n]|/sum(|diff|) su finestra n=20; scarta quando ER>0.5 (moto efficiente/trending). Test 0.4/0.5/0.6.'], + ['time-stop piu corto', 'riduci max_bars da 24 a 12 o 15 (esci prima se non rientra = probabile trend). Confronta DD/edge.'], + ['Hurst + vol-expansion combo', 'scarta se hurst>0.55 OPPURE vratio>1.5. Verifica se la combo riduce piu DD del singolo senza perdere piu edge.'], + ['Hurst + ADX combo', 'scarta se hurst>0.55 E ADX>25 (doppia conferma di trend) -> piu selettivo, scarta meno winner.'], + ['vol-target sizing', 'scala la size per 1/realized_vol (target vol costante): approssima tenendo solo le entries in vol moderata, riporta effetto su DD/coda.'], + ['DVOL-rising skip', 'scarta le fade quando dvol_chg>0 forte (DVOL in salita = stress/espansione vol imminente). Test soglie su dvol_chg.'], +] +const ASSETS_NOTE = 'Applica a tutte e 3 le fade (MR01,MR02,MR07) su BTC E ETH (6 combo), aggrega base vs filtrato.' + +phase('Test') +// ogni meccanismo = 1 agente che testa su tutte le 6 combo; piu' 4 agenti che esplorano combo/parametri fini +const tasks = MECHS.map(([nm, desc]) => () => agent( + CONTEXT + `\n\nMECCANISMO DA TESTARE: ${nm}\n${desc}\n\n${ASSETS_NOTE}\n` + + `Scrivi uno script in /tmp (cd /opt/docker/PythagorasGoal && uv run python /tmp/.py), confronta ` + + `BASELINE (fade senza filtro) vs FILTRATA su ogni combo, AGGREGA (media o somma equity) e riporta. ` + + `Il filtro deve essere CAUSALE. Decidi buon_lossguard=true SOLO se riduce DD/coda/stop-rate MANTENENDO ` + + `l'edge netto OOS (Sharpe non crolla, ret OOS resta ampiamente positivo). Cita i numeri base vs filtrato.`, + { label: `mech:${nm.slice(0, 18)}`, phase: 'Test', schema: SCHEMA })) + +const results = (await parallel(tasks)).filter(Boolean) + +phase('Synth') +const good = results.filter(r => r.buon_lossguard) +const synthesis = await agent( + CONTEXT + + `\n\nRisultati di ${results.length} meccanismi testati:\n${JSON.stringify(results, null, 1)}\n\n` + + `SINTESI FINALE (italiano) per il decisore: + 1) Esiste un loss-guard che riduce le perdite/DD delle fade in regime trending SENZA uccidere l'edge? + 2) Tabella: meccanismo | base vs filtrato (OOS Sharpe, DD, ret, %trade scartati) | buon_lossguard? + 3) Il MIGLIORE (e l'eventuale combo) con i numeri. Quanto DD/coda si risparmia e a che costo di ret. + 4) Coerenza con la ricerca esterna (Hurst<0.45 / ADX / vol-expansion / time-stop). + 5) Raccomandazione: quale filtro applicare alle fade live, con che soglia, e il caveat (serve feed + DVOL/regime live? il filtro va validato a livello PORT06 = riduce il DD del portafoglio?). + Onesta: se nessuno migliora davvero (riduce solo ret), dillo. Cita NUMERI reali.`, + { label: 'synth-lossguard', phase: 'Synth' }) + +return { results, good, synthesis }