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:
@@ -24,106 +24,23 @@ 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 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 (
|
||||
fade_daily_equity, _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 promossa: close-confirm con buffer 0.5 ATR
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- engine replay
|
||||
def build_trades_variant(ents, df, mode, buffer=BUFFER,
|
||||
lev=LEV, fee_rt=FEE_RT, trend_max=3.0, ema_long=200):
|
||||
"""Replica ESATTA di risk_management.build_trades, cambiando SOLO il ramo SL.
|
||||
|
||||
mode="orig" : SL intrabar al livello (SL prima del TP) == canonico.
|
||||
mode="exit16" : SL intrabar DISATTIVATO; close-confirm sul close[j]:
|
||||
long esci a close[j] se close[j] < sl0 - buffer*atr14[j]
|
||||
short esci a close[j] se close[j] > sl0 + buffer*atr14[j]
|
||||
TP intrabar al livello e max_bars al close INVARIATI.
|
||||
"""
|
||||
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 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: no SL intrabar; TP intrabar; poi close-confirm SL al close[j]
|
||||
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 fade_equity_variant(asset, fn, params, mode):
|
||||
"""Stesso flusso di combine_portfolio.fade_daily_equity ma con build_trades_variant."""
|
||||
df = load_data(asset, "1h")
|
||||
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
||||
trades = build_trades_variant(fn(df, **params), df, mode=mode, trend_max=3.0)
|
||||
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)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- pesi PORT06
|
||||
def port_metrics(members: dict[str, pd.Series], weights: dict[str, float]):
|
||||
dr = port_returns(members, weights)
|
||||
return metrics(dr), metrics(dr, lo=SPLIT)
|
||||
return equity_from_trades(df, trades)
|
||||
|
||||
|
||||
def main():
|
||||
@@ -174,17 +91,9 @@ def main():
|
||||
for sid in fade_ids:
|
||||
members_e16[sid] = eq_e16[sid] # sostituisco SOLO le 6 colonne fade
|
||||
|
||||
ids = p.sleeve_ids
|
||||
# pesi cap canonici (gli stessi che usa Portfolio.backtest)
|
||||
dr_base = pd.DataFrame({i: members_base[i].pct_change().fillna(0.0) for i in ids})
|
||||
w_base = W.weight_vector(p.weighting, ids, dr_base, weights=p.weights,
|
||||
caps=p.caps, clusters=p.clusters, lookback=p.vol_lookback)
|
||||
dr_e16 = pd.DataFrame({i: members_e16[i].pct_change().fillna(0.0) for i in ids})
|
||||
w_e16 = W.weight_vector(p.weighting, ids, dr_e16, weights=p.weights,
|
||||
caps=p.caps, clusters=p.clusters, lookback=p.vol_lookback)
|
||||
|
||||
f_b, o_b = port_metrics({i: members_base[i] for i in ids}, w_base)
|
||||
f_e, o_e = port_metrics({i: members_e16[i] for i in ids}, w_e16)
|
||||
# pesi cap canonici (gli stessi che usa Portfolio.backtest) dentro port_metrics
|
||||
f_b, o_b = port_metrics(members_base, p)
|
||||
f_e, o_e = port_metrics(members_e16, p)
|
||||
|
||||
print("\n" + "=" * 96)
|
||||
print(f" [3] PORT06 — pesi={p.weighting} caps={p.caps} | OOS da {OOS_DATE} | leva3x interna fade, pos0.15")
|
||||
@@ -207,8 +116,6 @@ def main():
|
||||
f"{'orig DD%':>10s}{'e16 DD%':>10s}")
|
||||
for sid in fade_ids:
|
||||
ro = eq_orig[sid]; re = eq_e16[sid]
|
||||
def _dd(s):
|
||||
pk = s.cummax(); return float(((pk - s) / pk).max() * 100)
|
||||
rro = (ro.iloc[-1] / ro.iloc[0] - 1) * 100
|
||||
rre = (re.iloc[-1] / re.iloc[0] - 1) * 100
|
||||
print(f" {sid:<10s}{rro:>12.1f}{rre:>14.1f}{rre-rro:>+10.1f}"
|
||||
|
||||
Reference in New Issue
Block a user