Files
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

92 lines
4.9 KiB
Python

"""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)