chore(analysis): dedup engine gate PORT06 + drift monitor giornaliero + impact bfill

- _port06_gate_common.py: build_trades_variant/equity_from_trades/port_metrics/dd
  fattorizzati dai 3 gate exit16/trendmax/dip01 (-214 righe duplicate). Nessun
  copy-drift trovato; versione promossa = trendmax (superset con hurst_mask).
  Output dei 3 gate verificato BYTE-IDENTICO prima/dopo. dip_trades resta nel suo
  script (sibling deliberato long-only/orig_gap, non una copia).
- drift_monitor.py: rolling-return per famiglia vs distribuzione storica propria
  (warn sotto p5; oggi: FADE 120g al p2). In crontab host giornaliero 07:15 UTC
  con report Telegram. Osservabilita', non filtro di trading.
- daily_equity_bfill_impact.py: bug bfill _daily_equity QUANTIFICATO -> non
  materiale (OOS invariato per costruzione, FULL DD 3.46->3.67 col fix, nessun
  verdetto gate a rischio). Lasciato documentato in TODO, niente fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-06-11 20:04:23 +00:00
parent 51baed22e4
commit 8a2b065dd7
7 changed files with 483 additions and 222 deletions
+4 -100
View File
@@ -29,116 +29,20 @@ from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
import pandas as pd
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
from src.data.downloader import load_data
from src.strategies.fade_base import hurst_skip_mask
from scripts.analysis.strategy_research import atr
from scripts.analysis.risk_management import strats_for, FEE_RT, LEV, POS, INIT
from scripts.analysis.combine_portfolio import (
_norm, IDX, port_returns, metrics, SPLIT, OOS_DATE,
from scripts.analysis.risk_management import strats_for
from scripts.analysis.combine_portfolio import OOS_DATE
from scripts.analysis._port06_gate_common import (
build_trades_variant, equity_from_trades, port_metrics, dd as _dd,
)
from scripts.portfolios._defs import PORTFOLIOS
from src.portfolio import weighting as W
BUFFER = 0.5 # EXIT-16 close-confirm (come in produzione)
HURST_MAX = 0.55 # loss-guard live
TREND_MAX = 3.0
EMA_LONG = 200
def build_trades_variant(ents, df, mode, trend_max, hurst_mask=None,
buffer=BUFFER, lev=LEV, fee_rt=FEE_RT, ema_long=EMA_LONG):
"""Engine di exit16_port06_impact.build_trades_variant + skip Hurst opzionale.
mode="orig" : SL intrabar al livello (SL prima del TP) == canonico.
mode="exit16" : SL intrabar OFF; TP intrabar al livello (priorita' nel bar);
SL solo se il CLOSE sfonda sl0 -/+ buffer*ATR14[j], fill a close[j].
trend_max : None = filtro OFF (live attuale); 3.0 = candidato.
hurst_mask : bool[i]=True -> salta l'ingresso (loss-guard live).
"""
h, l, c = df["high"].values, df["low"].values, df["close"].values
n = len(c)
a = atr(df, 14)
el = pd.Series(c).ewm(span=ema_long, adjust=False).mean().values
fee = fee_rt * lev
out = []
last = -1
for e in ents:
i, d = e["i"], e["d"]
if i <= last or i + 1 >= n:
continue
if hurst_mask is not None and hurst_mask[i]:
continue
if trend_max is not None and a[i] and abs(c[i] - el[i]) / a[i] > trend_max:
continue
entry = c[i]
tp, sl0, mb = e["tp"], e["sl"], e["max_bars"]
exit_p = c[min(i + mb, n - 1)]
j = min(i + mb, n - 1)
for k in range(1, mb + 1):
j = i + k
if j >= n:
exit_p = c[n - 1]
break
if mode == "orig":
hs = (d == 1 and l[j] <= sl0) or (d == -1 and h[j] >= sl0)
ht = (d == 1 and h[j] >= tp) or (d == -1 and l[j] <= tp)
if hs:
exit_p = sl0
break
if ht:
exit_p = tp
break
if k == mb:
exit_p = c[j]
else: # exit16
ht = (d == 1 and h[j] >= tp) or (d == -1 and l[j] <= tp)
if ht:
exit_p = tp
break
aj = a[j] if np.isfinite(a[j]) else 0.0
confirm = (d == 1 and c[j] < sl0 - buffer * aj) or \
(d == -1 and c[j] > sl0 + buffer * aj)
if confirm:
exit_p = c[j]
break
if k == mb:
exit_p = c[j]
ret = (exit_p - entry) / entry * d * lev - fee
out.append((i, j, ret))
last = j
return out
def equity_from_trades(df, trades):
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
n = len(df)
eq = np.full(n, INIT, dtype=float)
cap = INIT
for i, j, ret in sorted(trades, key=lambda t: t[1]):
cap = max(cap + cap * POS * ret, 10.0)
eq[j:] = cap
s = pd.Series(eq, index=ts).resample("1D").last().reindex(IDX).ffill().bfill()
return _norm(s)
def port_metrics(members: dict[str, pd.Series], p):
ids = p.sleeve_ids
dr = pd.DataFrame({i: members[i].pct_change().fillna(0.0) for i in ids})
w = W.weight_vector(p.weighting, ids, dr, weights=p.weights,
caps=p.caps, clusters=p.clusters, lookback=p.vol_lookback)
drp = port_returns({i: members[i] for i in ids}, w)
return metrics(drp), metrics(drp, lo=SPLIT)
def _dd(s):
pk = s.cummax()
return float(((pk - s) / pk).max() * 100)
def main():