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

135 lines
7.7 KiB
Python

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