research(blind): 52 agenti ciechi su curve anonime BTC/ETH — orchestratore valuta PnL/maxDD, niente di nuovo regge
Flotta di 52 subagenti "esperti di segnali" su storico BTC/ETH ANONIMIZZATO (Series A/B rebased a 100, calendario sintetico, split 70/30) — non sanno cosa siano. Ognuno scrive un signal(df)->position causale (script o ML), tunato solo sul train. Orchestratore valuta su PnL e maxDD nel test held-out. Harness cieco leak-free (riusabile): - make_blind.py: export anonimo + overlay; blindlib.py: evaluator con shift della posizione + GUARDIA DI CAUSALITA' online (squalifica ogni look-ahead, ML incluso); blind_eval.py CLI; score_all.py giudice OOS; verify_top.py (corr-al-trend, fee-stress, jackknife). - 52/52 passano la guardia (zero leak su tutta la flotta). Esito OOS (benchmark buy&hold: -7% PnL, 68% DD): - top = macd (+21%, DD 11%, Sh 0.84), accel, vol_of_vol, regime_switch, rf, obv — tutti trend/vol-regime. Sharpe OOS ~0.84 decade dal train ~1.4. Mean-rev e ML in fondo. - 3 scettici indipendenti: REFUTED. regime-luck (top-5 bar = 67-102% del PnL); trend-redundancy (HAC alpha t=+0.9..+1.5, nessuno >1.96 — TSMOM travestito); overfit (accel/vov knife-edge). Verdetto: ri-conferma CIECA e indipendente del soffitto direzionale ~1.3. macd = classe-TP01, forward-monitor non deploy. Diario 2026-06-21-blind-signal-fleet.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
"""agent_22_dd_derisk — ANGLE: drawdown-state de-risking overlay (family=vol, slug=dd_derisk).
|
||||
|
||||
Idea (assigned angle):
|
||||
Ride the up-trend, but CUT exposure as the asset's running drawdown deepens, and
|
||||
RE-RISK as it recovers back toward the peak. On these two structurally up-trending
|
||||
curves every large decline begins as a drawdown below the running peak; trimming
|
||||
exposure while the curve bleeds below its high mechanically pulls the book light
|
||||
through the worst legs and re-arms it once the high is reclaimed.
|
||||
|
||||
Construction (all causal / online):
|
||||
* dd[i] = close[i] / running_peak(close[0..i]) - 1 in (-1, 0] -> the LIVE drawdown.
|
||||
* |dd| is lightly EWMA-smoothed (span DD_SMOOTH) so the re-risk on the snap-back is
|
||||
not whipsawed by single-bar wicks; the smoother is causal (ewm, adjust=False).
|
||||
* A smooth de-risk multiplier maps the (smoothed) drawdown to a [W_FLOOR, 1] scale:
|
||||
scale = clip( 1 - (|dd_smooth| / DD_REF) ** P , W_FLOOR, 1 )
|
||||
Shallow dd -> ~full size; as |dd| approaches DD_REF the scale is bled to W_FLOOR.
|
||||
W_FLOOR>0 keeps a small core position through the deep regime (re-arms instantly on
|
||||
recovery) rather than fully exiting and missing the V-bottom.
|
||||
* This dd-scaled LONG is then vol-targeted (inverse realized vol, slow VOL_WIN_D
|
||||
window). A crash is also a vol spike, so inverse-vol sizing de-risks the same legs
|
||||
from the other side — the two de-risk mechanisms stack. Long/flat only: both curves
|
||||
are sharply V-bottomed, so shorting the recoveries is whipsaw; a de-risk goes toward
|
||||
a light long, never short.
|
||||
|
||||
Why no explicit trend filter: tested, it HURTS the risk-adjusted result here. The
|
||||
drawdown overlay already does the de-risking a trend gate would do, but smoothly and
|
||||
without the gate's whipsaw round-trips at the V-bottoms. Pure dd-derisk + slow
|
||||
inverse-vol gives the better Sharpe.
|
||||
|
||||
CAUSAL: running peak (left-to-right accumulate), drawdown, the EWMA smoother and the
|
||||
realized-vol window at i all use rows <= i only. The evaluator shifts the position (held
|
||||
during bar i+1). No future rows, no centered window, no global fit -> causality_ok=true
|
||||
(verified: max_diff 0.0).
|
||||
|
||||
Tuning (split='train' only, A & B equal weight; buy&hold ref: A Sh0.89/DD0.77,
|
||||
B Sh1.16/DD0.79). The de-risk SHAPE (DD_REF / P / W_FLOOR / DD_SMOOTH) sets the Sharpe;
|
||||
TARGET_VOL is a clean DD/PnL dial (Sharpe flat ~1.10-1.14 across 0.25..0.50). Chosen cell
|
||||
is interior on every axis with a flat plateau (Sharpe 1.08..1.15, DD 0.19..0.24):
|
||||
DD_REF=0.20 P=1.0 W_FLOOR=0.20 DD_SMOOTH=4 VOL_WIN_D=120 TARGET_VOL=0.40
|
||||
-> train combined: pnl_mean ~1.63, maxdd_worst ~0.22, sharpe_min ~1.14.
|
||||
Honest read: this is a DEFENSIVE long-only book, not alpha. Its value is the DRAWDOWN —
|
||||
~0.22 vs ~0.77-0.79 buy&hold (a ~3.5x cut) at comparable risk-adjusted PnL. Because it
|
||||
never shorts, its Sharpe ceiling (~1.1-1.2) is set by the absence of a direction call: it
|
||||
can avoid sizing into the big declines but cannot profit from them. That is the inherent
|
||||
limit of the de-risk-overlay angle on these curves.
|
||||
"""
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import blindlib as bl
|
||||
|
||||
DD_REF = 0.20 # drawdown (fraction) at which the de-risk multiplier hits the floor
|
||||
P = 1.0 # de-risk curvature (linear here; >1 keeps near-full on shallow dips)
|
||||
W_FLOOR = 0.20 # minimum exposure scale in the deep regime (keeps a re-armable core)
|
||||
DD_SMOOTH = 4 # EWMA span on |drawdown| -> de-whipsaw the re-risk on snap-backs
|
||||
VOL_WIN_D = 120 # slow trailing realized-vol horizon (days); stable, low turnover
|
||||
TARGET_VOL = 0.40 # DD/PnL dial; Sharpe flat across 0.25..0.50 -> picked for PnL/DD balance
|
||||
LEV_CAP = 1.0 # long-only, never lever past fully invested -> preserves the DD cut
|
||||
|
||||
|
||||
def _drawdown_scale(c: np.ndarray) -> np.ndarray:
|
||||
"""Causal de-risk multiplier in [W_FLOOR, 1] driven by the live drawdown."""
|
||||
peak = np.maximum.accumulate(c) # running peak over rows <= i (causal)
|
||||
dd = c / peak - 1.0 # (-1, 0]
|
||||
ad = np.abs(dd)
|
||||
ad = pd.Series(ad).ewm(span=DD_SMOOTH, adjust=False).mean().values # causal smoother
|
||||
depth = ad / DD_REF
|
||||
return np.clip(1.0 - depth ** P, W_FLOOR, 1.0)
|
||||
|
||||
|
||||
def signal(df):
|
||||
c = df["close"].values.astype(float)
|
||||
scale = _drawdown_scale(c) # long/flat de-risk exposure in [W_FLOOR, 1]
|
||||
pos = bl.vol_target(scale, df, target_vol=TARGET_VOL,
|
||||
vol_win_days=VOL_WIN_D, leverage_cap=LEV_CAP)
|
||||
return np.clip(np.nan_to_num(pos, nan=0.0), 0.0, LEV_CAP)
|
||||
Reference in New Issue
Block a user