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

146 lines
8.4 KiB
Python

"""Agent 28 — Williams %R momentum/reversion HYBRID, trend-gated (family=osc, slug=willr).
The angle (assigned): Williams %R momentum/reversion hybrid with a trend gate. Williams %R
is the inverse of the Stochastic %K: %R = -100 * (HH - close) / (HH - LL) over a trailing
window, ranging -100 (close at the window LOW = oversold) .. 0 (close at the window HIGH =
overbought). It measures where the close sits in its own rolling high/low channel, so it is
self-normalizing and sweeps the FULL -100..0 range even inside a bull (measured on train:
%R<-80 ~14% of bars, %R>-20 ~26% of bars). That dual occupancy is what makes a HYBRID
(reversion on one leg + momentum on the other) genuinely playable here.
Reading the train curves first (both A and B, split='train'): they trend UP very hard
(A 100->792, B 100->2400). A pure symmetric reversion ("short every %R>-20") would just
short the bull and bleed; a pure momentum rule rides crashes. The HYBRID + trend gate
resolves this by using %R DIFFERENTLY on each side of a long trend filter:
REVERSION LEG (in an UPTREND, close above a long SMA):
%R dipping into oversold (< OS, e.g. -80) is a BUY-THE-DIP setup. To ANTICIPATE the
bounce instead of knife-catching a still-falling close, we require %R to TURN BACK UP
(cross up through a short signal line = SMA of %R, the standard stochastic-style
trigger). We then HOLD the long (hysteresis) until %R recovers past EXIT, then flat.
This is the reversion half of the hybrid.
MOMENTUM LEG (in an UPTREND): once %R pushes into and STAYS overbought (> OB, e.g. -20),
in a hard bull that is NOT a fade signal — overbought persists and the trend runs. So
instead of shorting it (textbook reversion) we take a SMALLER continuation LONG
(MOM_W). This is the momentum half of the hybrid: %R>-20 in an uptrend = "trend is
strong, stay with it", the opposite trade to what reversion alone would do. This is
the key difference from the pure-reversion stochastic/RSI agents.
DOWNTREND (close below the long SMA): the symmetry returns and %R is read as reversion
again — %R overbought (> OB) with a cross DOWN through its signal line is a reversion
SHORT (rips fade). %R oversold we stand flat (don't knife-catch long under a
downtrend). The short side is down-weighted (SHORT_W) because the drift is up; on
train it is marginal (see HONEST NOTE).
So the gate does three jobs: (1) picks the reversion side (dip-long in up, rip-short in
down), (2) flips the overbought reading from "fade" to "ride" inside the bull (the hybrid),
(3) suppresses the side that fights the drift. Sizing is smooth (deeper extreme -> bigger
appetite, floored at BASE while holding) then VOL-TARGETED so the two curves are
risk-comparable and exposure shrinks into vol spikes (crashes are vol spikes) — that is
what bounds the drawdown. The leverage cap rarely binds, so the edge is NOT leverage.
HONEST NOTE (negative findings kept): (1) The downtrend reversion-short is nearly free but
adds little on train; kept small to honor the bidirectional angle. (2) The momentum
continuation leg (MOM_W) is what distinguishes this from a pure-reversion oscillator — in a
market that trends this hard it earns by riding the overbought regime instead of fading it,
but it ALSO partly degenerates toward trend participation (the honest ceiling for any
direction-on-a-bull rule). The genuine oscillator content is the cross-timed dip entry +
overbought exit cycle plus the DD control from the trend gate + vol-target. (3) A pure
always-on %R weighting (no flat state) degenerated into buy-and-hold (DD blew out); the
hysteresis flat state is what keeps DD modest. Result: an honest, modest combined train
Sharpe at a small DD — a fraction of buy&hold PnL but several-x less drawdown (it
anticipates the dip / rides the strong trend rather than holding through every crash).
CAUSAL: %R uses trailing rolling max(high)/min(low) (<= i); its signal line is a trailing
SMA of %R; the cross compares (%R - sig) at i vs i-1 (past only); the hold-state is a
forward cumulative pass over PAST bars only; the SMA trend filter and vol_target use
trailing data. No shift(-k), no centered windows, no global fit. Verified by causality_ok.
Tuning (train only, combined A&B; coarse->fine sweep + plateau check). The chosen cell sits
on a broad plateau (OB in [-35..-25], MOM_W in [0.3..0.5], SIG_WIN=5, R_WIN in [20..28],
EXIT in [-50..-40], OS=-80, BASE/TVOL/VWD all hold sharpe_min ~1.1..1.29 at DD ~3.3..5.6% —
a plateau, not a spike; SHORT_W is nearly free / marginal):
R_WIN=20, SIG_WIN=5, OS=-80, OB=-35, EXIT=-45, TREND_WIN=150
MOM_W=0.4, SHORT_W=0.4, BASE=0.6, TARGET_VOL=0.25, VOL_WIN_DAYS=35, LEV_CAP=1.5
-> train combined: pnl_mean ~0.46, maxdd_worst ~0.045, sharpe_min ~1.22
"""
import numpy as np
import pandas as pd
import blindlib as bl
R_WIN = 20 # %R lookback (rolling high/low window). 20 > textbook 14 for these trends.
SIG_WIN = 5 # signal line = SMA(%R, SIG_WIN): the line %R crosses (stochastic-style trigger).
OS = -80.0 # oversold: %R below this in an uptrend + cross-up = dip-long entry.
OB = -35.0 # overbought: momentum-ride (uptrend) / reversion-short (downtrend) threshold.
EXIT = -45.0 # dip-long HELD until %R recovers past EXIT (hysteresis entry/exit pair).
TREND_WIN = 150 # long SMA: above = uptrend (dips=long, OB=ride), below = downtrend (OB=short).
MOM_W = 0.4 # weight on the uptrend overbought MOMENTUM-continuation long (the hybrid half).
SHORT_W = 0.4 # weight on the downtrend reversion-short; marginal (see HONEST NOTE).
BASE = 0.6 # base long size while holding a dip (scaled up if %R still oversold).
TARGET_VOL = 0.25
VOL_WIN_DAYS = 35
LEV_CAP = 1.5
def _willr(df, r_win, sig_win):
"""Causal Williams %R + its signal line. %R[i] = -100*(HH-close)/(HH-LL) over the
trailing r_win bars (<= i); sig[i] = SMA(%R, sig_win) (trailing). No look-ahead."""
h = df["high"].values.astype(float)
l = df["low"].values.astype(float)
c = df["close"].values.astype(float)
hh = pd.Series(h).rolling(r_win, min_periods=1).max().values
ll = pd.Series(l).rolling(r_win, min_periods=1).min().values
rng = hh - ll
wr = np.where(rng > 1e-12, -100.0 * (hh - c) / rng, -50.0)
sig = bl.sma(wr, sig_win)
return wr, sig
def signal(df):
c = df["close"].values.astype(float)
n = len(c)
wr, sig = _willr(df, R_WIN, SIG_WIN)
trend_up = c > bl.sma(c, TREND_WIN) # causal trailing SMA trend gate
# --- %R / signal-line crosses (past-only: compares i vs i-1) ---
ds = wr - sig
ds_prev = np.concatenate(([0.0], ds[:-1]))
cross_up = (ds > 0) & (ds_prev <= 0) # %R turns up through its signal line
cross_dn = (ds < 0) & (ds_prev >= 0) # %R turns down through its signal line
# --- smooth appetites (further past the extreme -> bigger) ---
# oversold depth: %R from OS down to -100 -> long appetite 0..1
long_app = np.clip((OS - wr) / (100.0 + OS), 0.0, 1.0)
# overbought depth: %R from OB up to 0 -> 0..1 (used by both momentum-long & rev-short)
ob_app = np.clip((wr - OB) / (0.0 - OB), 0.0, 1.0)
# --- trend-gated Williams %R momentum/reversion hybrid with hysteresis ---
# Forward pass is PURE PAST-ONLY: state at bar i depends only on bars <= i.
held = np.zeros(n)
in_long = False
for i in range(n):
if in_long:
# exit the held dip-long when trend breaks down OR %R has recovered past EXIT
if (not trend_up[i]) or (wr[i] >= EXIT):
in_long = False
else:
# enter a dip-long in an uptrend when %R is oversold AND turns up through its line
if trend_up[i] and (wr[i] < OS) and cross_up[i]:
in_long = True
if in_long:
held[i] = max(BASE, long_app[i]) # ride the recovery, bigger if still oversold
elif trend_up[i]:
# MOMENTUM half of the hybrid: overbought in an uptrend = ride the strong trend
held[i] = MOM_W * ob_app[i]
else:
# downtrend reversion-short: overbought AND %R turning down through its line
if (wr[i] > OB) and cross_dn[i]:
held[i] = -SHORT_W * ob_app[i]
else:
held[i] = 0.0
pos = bl.vol_target(held, 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)