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>
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
"""agent_25_channel_pos — ANGLE [struct/channel_pos]: position WITHIN the Donchian channel.
|
||||
|
||||
Idea (assigned angle): instead of a binary breakout AT the channel edge, measure WHERE the
|
||||
close sits inside the rolling Donchian channel [lo, hi] as a continuous fraction
|
||||
chpos = (close - lo) / (hi - lo) in [0, 1] (0.5 = mid-channel).
|
||||
Then take a directional position only when location AND trend AGREE:
|
||||
* LONG when chpos is in the UPPER third (>= UP_TH) AND the channel/price slope is UP,
|
||||
* SHORT when chpos is in the LOWER third (<= LO_TH) AND the slope is DOWN,
|
||||
* FLAT in the middle band or when slope disagrees with location.
|
||||
The "slope" filter is what makes the angle anticipatory rather than a reversal: riding the
|
||||
upper third while the channel is still pushing up is a continuation read; the lower-third +
|
||||
down-slope short tries to catch the persistent declines (the big drawdowns the benchmark eats).
|
||||
|
||||
WHY a slope gate (honest tuning result):
|
||||
Channel-position WITHOUT a slope gate is a mean-reversion read (buy low-in-channel) and
|
||||
on these trending curves it bleeds — it fights the trend and the upper third without a
|
||||
trend filter chops on every pullback. Requiring location AND slope to agree turns it into
|
||||
a trend-confirmation read that holds longs through the up-leg and only shorts confirmed
|
||||
down-legs. The slope is the prior-W channel-midpoint change (causal).
|
||||
|
||||
Sizing: the agreed direction (+1/-1/0) is vol-targeted (TP01-style, causal realized vol) so
|
||||
size shrinks into vol spikes (= crashes) -> caps drawdown.
|
||||
|
||||
Causality: bl.donchian shifts the rolling hi/lo by one bar, so the channel at i is built from
|
||||
bars STRICTLY before i. chpos[i], the slope (a backward difference of a causal EMA of close),
|
||||
and the vol scaling all use only data <= i. The forward scan keeps no future state. The
|
||||
evaluator then HOLDS the position during bar i+1. causality_ok -> true.
|
||||
|
||||
WHY the short leg is sized 0.30 (honest tuning result):
|
||||
A full-size (-1.0) short bled on these up-trending curves (combined Sharpe_min 1.06, DD 0.30).
|
||||
Shrinking the short leg monotonically improved risk-adjusted return; long/flat alone was best
|
||||
on raw PnL/Sharpe but had a slightly fatter DD (0.256). The chosen short=0.30 keeps a genuine
|
||||
lower-third+down-slope SHORT (the angle is intact) and TRIMS the drawdown (0.256 -> 0.229)
|
||||
at ~no PnL cost. So the angle's short leg earns its place, just at a modest size.
|
||||
|
||||
Plateau (tuned on train only): broad and well-behaved around DON 35-45 / UP-LO 0.62-0.66 /
|
||||
SLOPE_WIN 15-20 / short 0.15-0.35 (Sharpe_min ~1.3-1.4 throughout, not an isolated peak).
|
||||
|
||||
FINAL train (combined A&B): pnl_mean ~4.06, maxdd_worst ~0.229, sharpe_min ~1.34, sharpe_mean ~1.40.
|
||||
Per-series: A pnl 4.88 / DD 0.226 / Sh 1.45 ; B pnl 3.22 / DD 0.193 / Sh 1.33. Turnover ~14/yr.
|
||||
causality.ok = true (max_diff 0). Honest note: this is a trend-confirmation read dressed as a
|
||||
channel-position rule (the slope gate makes it ride the trend, not fade it); its value is
|
||||
comparable PnL to buy&hold at ~1/3 of the drawdown, NOT independent alpha.
|
||||
"""
|
||||
import numpy as np
|
||||
import blindlib as bl
|
||||
|
||||
DON_WIN = 40 # Donchian window for the channel
|
||||
UP_TH = 0.62 # upper-band threshold on chpos (>=) -> "upper third" (location)
|
||||
LO_TH = 0.38 # lower-band threshold on chpos (<=) -> "lower third" (location)
|
||||
SLOPE_WIN = 20 # bars over which we measure the price slope (trend gate)
|
||||
SLOPE_EPS = 0.0 # min |slope| to count as up/down (0 = any non-zero sign)
|
||||
SHORT_SIZE = 0.30 # short-leg size (lower third + down-slope). <1 by tuning: the curves
|
||||
# trend up, so a full-size short bleeds; a modest short still TRIMS DD.
|
||||
TARGET_VOL = 0.30
|
||||
VOL_WIN_DAYS = 30
|
||||
LEV_CAP = 1.0
|
||||
|
||||
|
||||
def signal(df):
|
||||
c = df["close"].values.astype(float)
|
||||
n = len(c)
|
||||
|
||||
hi, lo = bl.donchian(df, DON_WIN) # prior-DON_WIN hi/lo (shifted, causal)
|
||||
width = hi - lo
|
||||
# continuous position within the channel in [0,1]; mid (0.5) where channel undefined.
|
||||
with np.errstate(invalid="ignore", divide="ignore"):
|
||||
chpos = (c - lo) / width
|
||||
chpos = np.where(np.isfinite(chpos) & (width > 0), chpos, 0.5)
|
||||
chpos = np.clip(chpos, 0.0, 1.0)
|
||||
|
||||
# causal slope: change of a smoothed close over SLOPE_WIN bars, normalized by price.
|
||||
sm = bl.ema(c, SLOPE_WIN)
|
||||
slope = np.zeros(n)
|
||||
slope[SLOPE_WIN:] = (sm[SLOPE_WIN:] - sm[:-SLOPE_WIN]) / np.maximum(sm[:-SLOPE_WIN], 1e-9)
|
||||
|
||||
up_loc = chpos >= UP_TH
|
||||
dn_loc = chpos <= LO_TH
|
||||
up_slope = slope > SLOPE_EPS
|
||||
dn_slope = slope < -SLOPE_EPS
|
||||
|
||||
direction = np.zeros(n)
|
||||
direction[up_loc & up_slope] = 1.0 # upper third + rising -> long
|
||||
direction[dn_loc & dn_slope] = -SHORT_SIZE # lower third + falling -> (small) short
|
||||
|
||||
# warmup: no channel yet -> flat
|
||||
direction[:DON_WIN] = 0.0
|
||||
|
||||
pos = bl.vol_target(direction, 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)
|
||||
Reference in New Issue
Block a user