Files
PythagorasGoal/scripts/research/blind/agents/agent_40_skewgate.py
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

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)