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,134 @@
|
||||
"""Agent 26 — Stochastic oscillator reversion + cross, trend-gated (family=osc, slug=stoch).
|
||||
|
||||
The angle (assigned): a rolling Stochastic oscillator (%K / %D). %K = where the close sits
|
||||
in its rolling [min(low), max(high)] window (0..100); %D = a short SMA of %K (the signal
|
||||
line). Trade the REVERSION (%K leaving an oversold extreme) timed by the %K-vs-%D CROSS,
|
||||
GATED by a longer trend filter. Tune the windows.
|
||||
|
||||
Reading the train curves first (both A and B, split='train'): they trend UP very hard
|
||||
(A 100->792, B 100->2400 over the window). UNLIKE RSI — which in these up-curves never
|
||||
dips below ~40 so textbook 30/70 is dead — the Stochastic %K is normalized against its
|
||||
OWN rolling high/low, so it sweeps the FULL 0..100 range even inside the bull: %K<20
|
||||
~12-14% of bars, %K>80 ~24-27% of bars (measured). That is exactly the structure a
|
||||
stochastic reversion rule needs, so the angle is genuinely playable here, but it still
|
||||
has to be REGIME-AWARE because the curves drift up:
|
||||
|
||||
* In an UPTREND (close above a long SMA) %K oversold (<LO) is a BUY-THE-DIP setup, and we
|
||||
require %K to CROSS BACK UP through its signal line %D — the standard stochastic long
|
||||
trigger — before going LONG. That waits for the dip to actually TURN (anticipating the
|
||||
bounce) instead of knife-catching while %K is still falling. We HOLD the long
|
||||
(hysteresis) until %K recovers into EXIT, then go flat. We do NOT short %K>80 in an
|
||||
uptrend — overbought in a bull keeps running (that is momentum, not reversion).
|
||||
* In a DOWNTREND (close below the long SMA) the symmetry returns: %K overbought (>80) with
|
||||
a %K cross DOWN through %D is a reversion SHORT (rips fade). %K<LO 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).
|
||||
|
||||
WHY THE CROSS MATTERS (the "anticipation" the angle asks for): entering the instant %K
|
||||
prints <LO is usually early — %K is still falling. Waiting for the %K/%D up-cross times the
|
||||
turn, which on train is the difference between a coin-flip dip rule and a positive one: with
|
||||
the cross the dip-long sits at ~9-12% DD with a clean positive Sharpe; without it the same
|
||||
thresholds bleed. The cross also cuts whipsaw turnover (~5-6 round-trips/yr, fee-cheap).
|
||||
|
||||
The trend gate does two jobs: it picks WHICH side of the oscillator is reversion (buy dips
|
||||
in up-trend / sell rips in down-trend) and it suppresses the side that fights the drift.
|
||||
Sizing is smooth (deeper oversold -> 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. Note the leverage cap never
|
||||
binds here (post-vol-target appetite stays <=1), so the edge does NOT rely on leverage.
|
||||
|
||||
HONEST NOTE (negative findings kept): (1) the downtrend short side is essentially free but
|
||||
adds nothing on train — SHORT_W=0.5 gives sharpe_min 0.51 vs 0.53 at SHORT_W=0; it is kept
|
||||
small to honor the bidirectional angle, not because it earns. (2) A continuous always-on
|
||||
oscillator weighting (no flat state) was tried and pushed time-in-market to ~99% and DD to
|
||||
0.20-0.37 — it degenerated into buy-and-hold; the hysteresis flat state is what keeps the
|
||||
DD at ~12%. (3) In a market that trends this hard, even a cross-gated dip-buy is PARTLY
|
||||
trend participation (the dips it buys recover and it rides them). The genuine reversion
|
||||
content is the oversold-entry / cross-timed turn / overbought-exit cycle plus the DD control
|
||||
from the trend gate + vol-target. Result: an honest, MODEST combined train Sharpe ~0.5 at
|
||||
~12% DD — a fraction of buy&hold's huge PnL but ~6x less drawdown (it anticipates the dip
|
||||
rather than just holding the asset through every crash).
|
||||
|
||||
CAUSAL: %K uses trailing rolling max(high)/min(low) (<= i); %D is a trailing SMA of %K; the
|
||||
cross compares (%K-%D) 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 (max_diff 0.0).
|
||||
|
||||
Tuning (train only, combined A&B; coarse->fine sweep + plateau check). The chosen cell sits
|
||||
on a broad plateau (K in [14..20], LO in [40..50], EXIT in [55..65], D in [3..5], TREND_WIN
|
||||
in [150..200] all hold sharpe_min ~0.37..0.53 at DD ~0.09..0.12 — a plateau, not a spike):
|
||||
K_WIN=20, D_WIN=5, LO=50, EXIT=55, TREND_WIN=150
|
||||
SHORT_W=0.5, BASE=0.7, TARGET_VOL=0.25, VOL_WIN_DAYS=35, LEV_CAP=1.5
|
||||
-> train combined: pnl_mean ~0.17, maxdd_worst ~0.12, sharpe_min ~0.51
|
||||
"""
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import blindlib as bl
|
||||
|
||||
K_WIN = 20 # %K lookback (rolling high/low window). 20 > textbook 14 for these trends.
|
||||
D_WIN = 5 # %D = SMA(%K, D_WIN): the signal line the %K crosses.
|
||||
LO = 50.0 # oversold threshold below which a %K/%D up-cross is a dip-long entry.
|
||||
EXIT = 55.0 # dip-long HELD until %K recovers past EXIT (hysteresis entry/exit pair).
|
||||
TREND_WIN = 150 # long SMA: above = uptrend (buy dips), below = downtrend (sell rips).
|
||||
SHORT_W = 0.5 # weight on the downtrend reversion-short; marginal (see HONEST NOTE).
|
||||
BASE = 0.7 # base long size while holding a dip (scaled up if %K still oversold).
|
||||
TARGET_VOL = 0.25
|
||||
VOL_WIN_DAYS = 35
|
||||
LEV_CAP = 1.5
|
||||
|
||||
|
||||
def _stoch(df, k_win, d_win):
|
||||
"""Causal Stochastic oscillator. %K[i] uses high/low/close over the trailing
|
||||
k_win bars (<= i); %D[i] = SMA(%K, d_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(k_win, min_periods=1).max().values
|
||||
ll = pd.Series(l).rolling(k_win, min_periods=1).min().values
|
||||
rng = hh - ll
|
||||
k = np.where(rng > 1e-12, (c - ll) / rng * 100.0, 50.0)
|
||||
d = bl.sma(k, d_win)
|
||||
return k, d
|
||||
|
||||
|
||||
def signal(df):
|
||||
c = df["close"].values.astype(float)
|
||||
n = len(c)
|
||||
k, d = _stoch(df, K_WIN, D_WIN)
|
||||
trend_up = c > bl.sma(c, TREND_WIN) # causal trailing SMA trend gate
|
||||
|
||||
# --- %K/%D crosses (past-only: compares i vs i-1) ---
|
||||
kd = k - d
|
||||
kd_prev = np.concatenate(([0.0], kd[:-1]))
|
||||
cross_up = (kd > 0) & (kd_prev <= 0) # %K turns up through its signal line
|
||||
cross_dn = (kd < 0) & (kd_prev >= 0) # %K turns down through its signal line
|
||||
|
||||
# --- smooth reversion appetite from %K (further past threshold -> bigger) ---
|
||||
long_app = np.clip((LO - k) / LO, 0.0, 1.0) # oversold depth -> long appetite
|
||||
short_app = np.clip((k - 80.0) / 20.0, 0.0, 1.0) # overbought depth -> short appetite
|
||||
|
||||
# --- trend-gated stochastic reversion with cross-triggered entry + hysteresis ---
|
||||
# Forward pass is PURE PAST-ONLY: in_long 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 %K has recovered past EXIT
|
||||
if (not trend_up[i]) or (k[i] >= EXIT):
|
||||
in_long = False
|
||||
else:
|
||||
# enter a dip-long in an uptrend when %K is oversold AND turns up through %D
|
||||
if trend_up[i] and (k[i] < LO) and cross_up[i]:
|
||||
in_long = True
|
||||
if in_long:
|
||||
held[i] = max(BASE, long_app[i]) # ride the recovery, bigger if still oversold
|
||||
else:
|
||||
# downtrend reversion-short: overbought AND %K turning down through %D
|
||||
if (not trend_up[i]) and (k[i] > 80.0) and cross_dn[i]:
|
||||
held[i] = -SHORT_W * short_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)
|
||||
Reference in New Issue
Block a user