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>
91 lines
3.7 KiB
Python
91 lines
3.7 KiB
Python
"""agent_47_trail_mom — momentum entry with ACTIVE TRAILING-STOP position management.
|
|
|
|
Angle [family=mix, slug=trail_mom]:
|
|
* Enter LONG/SHORT on multi-horizon momentum (the "trend is your friend" entry).
|
|
* Then actively MANAGE the position with a trailing stop measured in ATR units from
|
|
the best favourable price seen since the trade opened:
|
|
- adverse excursion (price pulls back toward the trail) -> REDUCE exposure,
|
|
- follow-through (new favourable extreme) -> ADD exposure back, up to full size.
|
|
* Vol-target the whole thing so DD stays bounded.
|
|
|
|
CAUSAL: every value at bar i uses only rows 0..i. The trailing state machine is a pure
|
|
forward loop (no future peek). The evaluator shifts the position, so position[i] is the
|
|
weight held during bar i+1 — decided from data up to close[i].
|
|
"""
|
|
import numpy as np
|
|
import blindlib as bl
|
|
|
|
|
|
def _mom_dir(c):
|
|
"""Multi-horizon momentum direction in [-1,1] (causal). Equal-weight 20/50/100."""
|
|
d = np.zeros(len(c))
|
|
for w, wt in ((20, 0.34), (50, 0.33), (100, 0.33)):
|
|
m = c / bl.sma(c, w) - 1.0
|
|
d += wt * np.tanh(8.0 * m)
|
|
return np.clip(d, -1.0, 1.0)
|
|
|
|
|
|
def signal(df):
|
|
c = df["close"].values.astype(float)
|
|
h = df["high"].values.astype(float)
|
|
l = df["low"].values.astype(float)
|
|
n = len(c)
|
|
|
|
direction = _mom_dir(c) # desired sign + conviction
|
|
a = bl.atr(df, 14) # causal ATR (vol unit for trail)
|
|
a = np.where(np.isfinite(a) & (a > 0), a, np.nan)
|
|
|
|
# ---- trailing-stop state machine (pure causal forward loop) -------------
|
|
TRAIL_K = 4.0 # trail distance in ATR from the favourable extreme
|
|
REDUCE_K = 0.8 # adverse excursion (ATR) at which we start shrinking
|
|
sized = np.zeros(n) # managed exposure scalar in [0,1]
|
|
cur_sign = 0.0
|
|
best = np.nan # best favourable price since entry (max if long, min if short)
|
|
expo = 0.0 # current exposure fraction in [0,1]
|
|
|
|
for i in range(n):
|
|
d = direction[i]
|
|
sgn = np.sign(d) if abs(d) > 0.20 else 0.0 # dead-zone: avoid chop flip
|
|
ai = a[i]
|
|
if not np.isfinite(ai):
|
|
sized[i] = 0.0
|
|
continue
|
|
|
|
# entry / flip: reset trailing state, start at conviction-scaled exposure
|
|
if sgn != 0.0 and sgn != cur_sign:
|
|
cur_sign = sgn
|
|
best = c[i]
|
|
expo = min(1.0, abs(d))
|
|
elif sgn == 0.0:
|
|
cur_sign = 0.0
|
|
expo = 0.0
|
|
best = np.nan
|
|
|
|
if cur_sign != 0.0 and np.isfinite(best):
|
|
# update favourable extreme
|
|
if cur_sign > 0:
|
|
best = max(best, h[i])
|
|
adverse = (best - c[i]) / ai # how far pulled back (ATR units)
|
|
else:
|
|
best = min(best, l[i])
|
|
adverse = (c[i] - best) / ai
|
|
# trailing management:
|
|
if adverse >= TRAIL_K:
|
|
expo = 0.0 # stopped out
|
|
elif adverse >= REDUCE_K:
|
|
# linearly reduce between REDUCE_K and TRAIL_K
|
|
frac = 1.0 - (adverse - REDUCE_K) / (TRAIL_K - REDUCE_K)
|
|
target = min(1.0, abs(d)) * max(0.0, frac)
|
|
expo = min(expo, target) # reduce only on adverse
|
|
else:
|
|
# follow-through region -> add back toward full conviction
|
|
target = min(1.0, abs(d))
|
|
expo = expo + 0.34 * (target - expo) # ease back up
|
|
sized[i] = cur_sign * expo
|
|
else:
|
|
sized[i] = 0.0
|
|
|
|
# ---- vol-target the managed directional series --------------------------
|
|
pos = bl.vol_target(sized, df, target_vol=0.20, vol_win_days=30, leverage_cap=1.0)
|
|
return np.clip(pos, -1.0, 1.0)
|