Files
Adriano Dal Pastro 1afb1014c9 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>
2026-06-21 07:05:04 +00:00

76 lines
4.5 KiB
Python

"""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)