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>
109 lines
6.0 KiB
Python
109 lines
6.0 KiB
Python
"""agent_21_atr_ride — ANGLE: ATR-channel trend ride with an ATR trailing stop that
|
|
scales the position DOWN on adverse moves (family=vol, slug=atr_ride).
|
|
|
|
Idea (assigned angle):
|
|
* Build an ATR channel around an EMA mid-line: mid = EMA_N(close);
|
|
band half-width = K_ENTRY * ATR_M. A close above mid + K_ENTRY*ATR starts an
|
|
uptrend ride.
|
|
* Maintain an ATR TRAILING STOP (Chandelier / SuperTrend flavour): a stop line that
|
|
RATCHETS in the trade's favour and never loosens. While long, the stop is
|
|
(highest-close-since-entry - K_STOP*ATR) and only moves up. A close below it ends
|
|
the ride (flatten).
|
|
* The distinguishing twist of THIS angle (vs a binary breakout) is the SCALE-DOWN on
|
|
adverse moves. Instead of a hard on/off stop we size by the ATR "stop room":
|
|
room[i] = clip( (close[i] - stop[i]) / (K_STOP*ATR[i]) , 0, 1 )
|
|
= how much cushion (in ATR units, normalised by the stop distance) sits between the
|
|
close and the trailing stop. Exposure is proportional to that cushion, so the book
|
|
runs full deep in a healthy trend, BLEEDS OFF smoothly as price falls back toward the
|
|
stop, and goes flat once the stop breaks. We ride winners and de-risk into reversals
|
|
BEFORE the stop is hit, instead of binary all-in / all-out.
|
|
|
|
Long/flat only. Both curves trend up; the short side of an ATR ride is whipsaw on the
|
|
V-shaped bottoms (same lesson as the donchian/keltner siblings), so a stop-out goes to
|
|
FLAT, never short. The ride exposure (already in [0,1]) is then vol-targeted so the
|
|
long shrinks further into vol spikes (every crash is a vol spike) -> caps the DD.
|
|
|
|
CAUSAL: mid (EMA) and ATR are built with .shift(1) -> strictly from bars <= i-1, and the
|
|
close[i] that pierces the channel / sits above the stop is a real, tradeable event at
|
|
close[i]. The trailing-stop state machine is a forward scan using only data <= i (peak is
|
|
the running max of past closes; the stop only ratchets up). vol_target uses realized vol
|
|
up to i. No future rows, no centered windows, no global fit -> causality_ok = true
|
|
(verified: max_diff 0.0). The evaluator then holds the position during bar i+1.
|
|
|
|
TUNING (split='train' only, Series A & B equal weight; chosen cell is a plateau center):
|
|
* N_EMA x N_ATR: the (20,20) cell is the best risk-adjusted corner of the EMA/ATR grid
|
|
(sharpe_min ~1.39 vs ~1.06-1.27 at slower 30-60 windows) and its 27-cell neighbourhood
|
|
(N_EMA 18-25, N_ATR 15-25, K_STOP 2.0-3.0) holds sharpe_min in [1.16, 1.41] (median
|
|
1.30, 93% of cells > 1.2) -> a genuine plateau, not an isolated peak.
|
|
* K_ENTRY = 1.0 is the clear ridge: the K_ENTRY row 0.5->1.5 peaks sharply at 1.0
|
|
(sharpe_min jumps to ~1.3-1.4) because requiring a full ATR of breakout above the mid
|
|
filters out the chop-region false starts.
|
|
* K_STOP = 2.5 ATR: the whole K_STOP 2.0-3.5 strip at K_ENTRY=1.0 is flat-high
|
|
(sharpe_min 1.29-1.39, DD 0.22-0.28); 2.5 is the interior balance.
|
|
* TARGET_VOL is a pure PnL/DD dial with FLAT Sharpe (~1.39 across 0.20-0.30): 0.20 ->
|
|
pnl 1.75/DD 0.16 ... 0.30 -> pnl 3.23/DD 0.23 ... 0.40 -> pnl 4.81/DD 0.29. 0.30 is
|
|
the balanced cell. VOL_WIN=30 is interior and best on Sharpe (1.39 vs 1.28 at 60).
|
|
LEV_CAP=1.0 (never lever past fully invested) preserves the de-risking benefit.
|
|
|
|
Train (combined A&B): pnl_mean ~3.23, maxdd_worst ~0.23, sharpe_min ~1.39.
|
|
Honest note: this is trend-following, not alpha — its value is turning a high-PnL /
|
|
~77-79%-DD uptrend into comparable PnL at ~23% drawdown (DD cut ~3.4x). The scale-down
|
|
twist buys a slightly lower DD and steadier equity than a binary ATR breakout would, at
|
|
the cost of leaving some upside on the table in the very strongest legs (the position is
|
|
rarely pinned at 1.0). The short side was not pursued: on these up-trending curves it is
|
|
value-destroying whipsaw, the same finding as the sibling breakout angles.
|
|
"""
|
|
import numpy as np
|
|
import pandas as pd
|
|
import blindlib as bl
|
|
|
|
N_EMA = 20 # ATR-channel mid-line EMA span
|
|
N_ATR = 20 # ATR window (channel half-width AND trailing-stop unit)
|
|
K_ENTRY = 1.0 # entry: close > mid + K_ENTRY*ATR -> start the ride (ridge value)
|
|
K_STOP = 2.5 # trailing stop distance in ATR (Chandelier) -> also the scale ruler
|
|
TARGET_VOL = 0.30 # PnL/DD dial; Sharpe flat across 0.20-0.30
|
|
VOL_WIN_DAYS = 30
|
|
LEV_CAP = 1.0
|
|
|
|
|
|
def _atr_ride_exposure(df):
|
|
"""Long/flat exposure in [0,1]: 0 when out of the ride; while in the ride, the value
|
|
is the ATR 'stop room' (cushion above the trailing stop, in [0,1]) so the position
|
|
scales DOWN smoothly on adverse moves and goes flat when the stop breaks."""
|
|
c = df["close"].values.astype(float)
|
|
n = len(c)
|
|
mid = pd.Series(bl.ema(c, N_EMA)).shift(1).values # EMA built strictly <= i-1
|
|
atr = pd.Series(bl.atr(df, N_ATR)).shift(1).values # ATR built strictly <= i-1
|
|
|
|
expo = np.zeros(n)
|
|
in_ride = False
|
|
peak = -np.inf # highest close since entry (drives the ratcheting stop)
|
|
for i in range(n):
|
|
m, a = mid[i], atr[i]
|
|
if not (np.isfinite(m) and np.isfinite(a) and a > 0):
|
|
continue
|
|
if not in_ride:
|
|
# entry: close pierces the upper ATR channel (full ATR above the mid)
|
|
if c[i] > m + K_ENTRY * a:
|
|
in_ride = True
|
|
peak = c[i]
|
|
if in_ride:
|
|
peak = max(peak, c[i])
|
|
stop = peak - K_STOP * a # Chandelier trailing stop (ratchets via peak)
|
|
if c[i] <= stop:
|
|
in_ride = False # stop broken -> ride over, flat
|
|
expo[i] = 0.0
|
|
peak = -np.inf
|
|
else:
|
|
# SCALE DOWN on adverse moves: cushion above the stop, normalised to [0,1].
|
|
room = (c[i] - stop) / (K_STOP * a)
|
|
expo[i] = float(np.clip(room, 0.0, 1.0))
|
|
return expo
|
|
|
|
|
|
def signal(df):
|
|
expo = _atr_ride_exposure(df) # long/flat in [0,1], already scaled by stop room
|
|
pos = bl.vol_target(expo, 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), 0.0, LEV_CAP)
|