1afb1014c9
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>
69 lines
3.4 KiB
Python
69 lines
3.4 KiB
Python
"""agent_27_dpo — Detrended Price Oscillator (cycle phase around a LAGGED MA).
|
|
|
|
ANGLE [family=osc, slug=dpo]: detrend price by subtracting a moving average that we
|
|
DELAY (lag) so the oscillator measures where price sits in its cycle relative to a
|
|
recent trend baseline. Trade the cycle phase — causal only.
|
|
|
|
Classic DPO is price[i] - SMA(n)[i - (n/2 + 1)]. The textbook centers that lag; here we
|
|
keep the displacement STRICTLY BACKWARD (the MA value comes from ~n/2 bars ago, fully in
|
|
the past), so the oscillator is causal/online and deployable.
|
|
|
|
What the train data says (tuned on split='train' only):
|
|
dpo = (price - lagged_baseline) / vol(gap) is a z-like CYCLE PHASE around zero.
|
|
Bucketing dpo vs the NEXT-bar return showed a clean MONOTONIC relationship: the higher
|
|
the detrended oscillator (price above its lagged baseline = cycle UP-phase), the higher
|
|
the next return; deep-negative dpo (cycle down-phase) precedes flat/negative returns.
|
|
So on these series the cycle is CONTINUATION, not reversion -> we FOLLOW the phase
|
|
(long the up-phase, flat/short the down-phase), confirmed by a slow trend gate, and
|
|
size with vol-targeting. Result on train: positive PnL at ~19% worst DD vs buy&hold's
|
|
~78% DD — anticipating the move means staying out of (or short) the down-phase.
|
|
|
|
Config tuned on train (period=30 / trendwin=200 / scale=1.5 / wc=0.6 / ema=2 / tv=0.18):
|
|
plateau-robust across period 30, trend 150-200, scale 1.5-2.0, cycle weight 0.5-0.8.
|
|
"""
|
|
import numpy as np
|
|
import blindlib as bl
|
|
|
|
# --- tuned on split='train' only ------------------------------------------
|
|
PERIOD = 30 # DPO moving-average period
|
|
LAG = PERIOD // 2 + 1 # textbook DPO displacement, kept strictly backward (causal)
|
|
TREND_WIN = 200 # slow-trend confirmation window
|
|
SCALE = 1.5 # tanh softness of the cycle phase
|
|
W_CYCLE = 0.6 # blend weight: cycle phase vs slow-trend confirmation
|
|
EMA_SMOOTH = 2 # position smoothing (cuts turnover/fees)
|
|
TARGET_VOL = 0.18 # annualized vol target
|
|
VOL_WIN = 30
|
|
LEV_CAP = 1.0
|
|
|
|
|
|
def _dpo_phase(c: np.ndarray) -> np.ndarray:
|
|
"""Detrended price oscillator z-phase: (price - LAGGED SMA) / rolling std of gap.
|
|
The baseline SMA is delayed by LAG bars, so every value uses only past data."""
|
|
n = len(c)
|
|
base = bl.sma(c, PERIOD) # causal SMA
|
|
base_lag = np.full(n, np.nan)
|
|
base_lag[LAG:] = base[:-LAG] # baseline from LAG bars ago (past only)
|
|
gap = c - base_lag
|
|
gap_vol = bl.rolling_std(gap, PERIOD)
|
|
gap_vol = np.where((gap_vol > 0) & np.isfinite(gap_vol), gap_vol, np.nan)
|
|
return gap / gap_vol # z-like cycle phase (NaN during warmup)
|
|
|
|
|
|
def signal(df):
|
|
c = df["close"].values.astype(float)
|
|
|
|
# detrended cycle phase (DPO core) — empirically CONTINUATION on these series
|
|
z = np.nan_to_num(_dpo_phase(c), nan=0.0)
|
|
cycle = np.tanh(z / SCALE) # +1 up-phase, -1 down-phase
|
|
|
|
# slow-trend confirmation (don't ride the cycle against a strong regime)
|
|
trend = c / bl.sma(c, TREND_WIN) - 1.0
|
|
follow = np.tanh(np.nan_to_num(trend, nan=0.0) * 6.0)
|
|
|
|
raw = np.clip(W_CYCLE * cycle + (1.0 - W_CYCLE) * follow, -1.0, 1.0)
|
|
raw = bl.ema(raw, EMA_SMOOTH) # smooth -> fewer fee-bleeding flips
|
|
|
|
pos = bl.vol_target(raw, df, target_vol=TARGET_VOL,
|
|
vol_win_days=VOL_WIN, leverage_cap=LEV_CAP)
|
|
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
|