1afb1014c9
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>
101 lines
5.1 KiB
Python
101 lines
5.1 KiB
Python
"""Agent 40 — Return-skew regime gate on a trend signal (family=stat, slug=skewgate).
|
|
|
|
THE ANGLE (assigned): avoid fat-tail-DOWN regimes. A trend follower is happy to ride a
|
|
persistent up-move; the danger is the crash leg — a cluster of large negative returns that
|
|
shows up FIRST as a strongly NEGATIVELY-skewed recent return distribution (a few big down
|
|
days dominating). So we run a plain multi-horizon TSMOM trend as the base direction, then
|
|
GATE the LONG exposure DOWN — toward flat — whenever a causal rolling window of recent
|
|
returns turns negatively skewed.
|
|
|
|
WHAT THE DATA SAID (train diagnostics, both curves):
|
|
* Conditioning forward 20-bar returns on rolling SKEW: the most negatively-skewed windows
|
|
have materially WORSE forward returns than the most positively-skewed ones (e.g. Series B,
|
|
40-bar skew: bottom-quartile fwd ~0.00 vs top-quartile ~+0.08). So a negative-skew gate
|
|
has real, if modest, predictive value -> it earns its slot as a defensive overlay.
|
|
* KURTOSIS, by contrast, is BULLISH on these curves (high-excess-kurt windows have BETTER
|
|
forward returns — fat tails here come mostly from up-shocks in a structural bull). So a
|
|
kurtosis "fat-tail" gate would throw away upside; it was tested and DROPPED. The gate is
|
|
SKEW-ONLY. (This is the honest version of "avoid fat-tail-down": the down-tail signature
|
|
on these curves is the SKEW, not the raw kurtosis.)
|
|
|
|
Construction (all causal, value at i uses only rows <= i):
|
|
* BASE = multi-horizon TSMOM: average the SIGN of the past-H return for H in HORIZONS,
|
|
direction in [-1, +1] (slow horizon = macro trend, fast ones cut early into a turn).
|
|
Asymmetric long-short: de-weight the short side (curves trend up structurally).
|
|
* GATE = rolling SKEW_WIN skewness of returns. A smooth multiplier on the LONG side only:
|
|
1.0 when skew >= SKEW_CUT (benign), falling linearly to GATE_FLOOR as skew drops below
|
|
the cut (fat-tail-down). Shorts are left untouched — being short into a negatively-skewed
|
|
decline is exactly where the trend signal should earn, not be muzzled.
|
|
* vol_target sizes the gated direction so the two curves are risk-comparable.
|
|
|
|
CAUSAL: rolling skew uses a trailing window (pandas .rolling, no shift(-k)); TSMOM uses
|
|
close[i]/close[i-H]; vol_target uses trailing realized vol. Verified by causality_ok
|
|
(max_diff 0.0).
|
|
|
|
TUNING (split='train' only, combined A&B). Sweep over (SKEW_WIN, SKEW_CUT, GATE_FLOOR)
|
|
found a plateau at SKEW_WIN in {35,40}, SKEW_CUT=-0.3, GATE_FLOOR=0: the gate lifts
|
|
sharpe_min from 1.37 (ungated base) to ~1.46 and pnl_mean from 3.22 to ~3.32. The chosen
|
|
cell (40, -0.3, 0.0) is interior on every axis. FINAL train combined:
|
|
pnl_mean ~3.32, maxdd_worst ~0.21, sharpe_min ~1.46.
|
|
|
|
HONEST CAVEAT: the gate improves the RISK-ADJUSTED return (Sharpe) by trimming long size in
|
|
locally negative-skew clusters that precede pullbacks; it does NOT shrink the *worst* drawdown.
|
|
Inspection showed each curve's worst-DD leg is a slow whipsaw/chop where the position is
|
|
already small or short and skew is ~0 — i.e. NOT a fat-tail-down crash. So the angle's
|
|
defensive value here is Sharpe, not maxdd. A negative result on the maxdd front, reported
|
|
honestly.
|
|
"""
|
|
import numpy as np
|
|
import pandas as pd
|
|
import blindlib as bl
|
|
|
|
# --- trend base (multi-horizon TSMOM) ---
|
|
HORIZONS = (45, 130, 240) # ~1.5 / 4.5 / 8 months of daily bars
|
|
SHORT_W = 0.25 # de-weight short side (curves trend up)
|
|
TARGET_VOL = 0.30
|
|
VOL_WIN_DAYS = 45
|
|
LEV_CAP = 1.5
|
|
|
|
# --- negative-skew (fat-tail-down) gate on the LONG side ---
|
|
SKEW_WIN = 40 # window for rolling return skew
|
|
SKEW_CUT = -0.3 # skew >= this = benign (gate 1.0); below = bite
|
|
GATE_FLOOR = 0.0 # min long multiplier when skew is deeply negative
|
|
|
|
|
|
def _tsmom_sign(c: np.ndarray, h: int) -> np.ndarray:
|
|
"""Sign of the past-h-bar return, causal. 0 for i < h."""
|
|
out = np.zeros(len(c))
|
|
if h < len(c):
|
|
out[h:] = np.sign(c[h:] / c[:-h] - 1.0)
|
|
return out
|
|
|
|
|
|
def _neg_skew_gate(r: np.ndarray) -> np.ndarray:
|
|
"""Causal multiplier in [GATE_FLOOR, 1] for the LONG side. 1.0 when rolling skew is at
|
|
or above SKEW_CUT; falls linearly to GATE_FLOOR as skew drops below the cut."""
|
|
sk = pd.Series(r).rolling(SKEW_WIN, min_periods=SKEW_WIN).skew().values
|
|
sk = np.nan_to_num(sk, nan=0.0)
|
|
skew_bad = np.clip((SKEW_CUT - sk) / abs(SKEW_CUT), 0.0, 1.0) # 0 benign -> 1 deeply neg
|
|
gate = 1.0 - (1.0 - GATE_FLOOR) * skew_bad
|
|
return gate
|
|
|
|
|
|
def signal(df):
|
|
c = df["close"].values.astype(float)
|
|
r = bl.simple_returns(c)
|
|
|
|
# base trend direction (multi-horizon TSMOM, asymmetric long-short)
|
|
sig = np.zeros(len(c))
|
|
for h in HORIZONS:
|
|
sig += _tsmom_sign(c, h)
|
|
sig /= len(HORIZONS)
|
|
raw = np.where(sig >= 0.0, sig, sig * SHORT_W)
|
|
|
|
# negative-skew gate: shrink LONG risk only, leave shorts at full size
|
|
gate = _neg_skew_gate(r)
|
|
gated = np.where(raw > 0.0, raw * gate, raw)
|
|
|
|
pos = bl.vol_target(gated, df, target_vol=TARGET_VOL,
|
|
vol_win_days=VOL_WIN_DAYS, leverage_cap=LEV_CAP)
|
|
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
|