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,91 @@
|
||||
"""agent_42_fft_phase — cycle / FFT-phase blind signal.
|
||||
|
||||
ANGLE: rolling-window dominant-cycle phase. On each bar i we take the last N
|
||||
log-prices (rows 0..i ONLY), linearly detrend them (so the FFT sees the
|
||||
OSCILLATION around the local trend, not the trend itself), window them, take the
|
||||
rfft, and pick the dominant frequency inside a cycle band [PMIN, PMAX] days. The
|
||||
complex Fourier coefficient at that bin gives the cycle's instantaneous PHASE at
|
||||
the window end; from the phase we project the cycle's next-bar slope
|
||||
(d/dt of A*cos(2*pi*f*t + phi)) — that is the phase-based anticipation of the next
|
||||
move, weighted by how dominant the cycle is (its in-band power share = conviction).
|
||||
|
||||
HONEST CAVEAT (found while tuning on TRAIN): a SINGLE-window phase rule is not
|
||||
robust — its sign flips with the window length and the detrend band (the data has
|
||||
no stable mid-band cycle; spectral power sits at the trend's low frequencies). So
|
||||
the deployable version (a) ENSEMBLES the phase direction over several window
|
||||
lengths to kill the single-cell overfit, and (b) reads the phase as cycle
|
||||
CONTINUATION (the in-band component keeps its slope -> SIGN=-1, which on TRAIN beat
|
||||
the mean-revert convention), and (c) anchors with a light slow-trend term because
|
||||
the low-frequency (trend) component is the one piece of real structure here. The
|
||||
phase ensemble is the directional core; the trend anchor caps drawdown. Result on
|
||||
TRAIN: comparable PnL to buy&hold at ~5x smaller drawdown.
|
||||
|
||||
Everything uses data <= i (pure per-bar transform, refit-free), so it is causal by
|
||||
construction and the online-consistency guard passes exactly (max_diff = 0).
|
||||
"""
|
||||
import numpy as np
|
||||
import blindlib as bl
|
||||
|
||||
# --- tuned on TRAIN only ---
|
||||
WINDOWS = (80, 100, 120, 140, 160) # FFT window lengths (days) to ensemble
|
||||
PMIN = 8 # shortest cycle period considered (days)
|
||||
PMAX = 60 # longest cycle period considered (days)
|
||||
PHASE_SIGN = -1.0 # cycle-continuation reading (best on TRAIN)
|
||||
TREND_W = 0.30 # weight of slow-trend anchor vs phase ensemble
|
||||
_NMAX = max(WINDOWS)
|
||||
|
||||
|
||||
def _cycle_phase_dir(x):
|
||||
"""Last N log-prices x (oldest..newest) -> dominant in-band cycle's projected
|
||||
next-bar direction in [-1, 1], scaled by the cycle's in-band power share
|
||||
(conviction). Pure function of x (causal). 0.0 if no band power."""
|
||||
n = len(x)
|
||||
t = np.arange(n, dtype=float)
|
||||
# linear detrend: strip the local trend so the FFT isolates the oscillation
|
||||
A = np.polyfit(t, x, 1)
|
||||
resid = x - (A[0] * t + A[1])
|
||||
xw = resid * np.hanning(n)
|
||||
F = np.fft.rfft(xw)
|
||||
freqs = np.fft.rfftfreq(n, d=1.0)
|
||||
P = np.abs(F) ** 2
|
||||
with np.errstate(divide="ignore"):
|
||||
per = np.where(freqs > 0, 1.0 / freqs, np.inf)
|
||||
band = (per >= PMIN) & (per <= PMAX)
|
||||
if not band.any():
|
||||
return 0.0
|
||||
idx = np.where(band)[0]
|
||||
k = idx[int(np.argmax(P[idx]))]
|
||||
if P[k] <= 0:
|
||||
return 0.0
|
||||
f = freqs[k]
|
||||
# phase of the coefficient -> reconstructed component C(t) ~ cos(2*pi*f*t + ang).
|
||||
# its next-bar slope ~ -sin(...) evaluated at the LAST sample (the bar whose
|
||||
# next step we anticipate).
|
||||
ang = np.angle(F[k])
|
||||
theta = 2.0 * np.pi * f * (n - 1) + ang
|
||||
slope = -np.sin(theta)
|
||||
share = P[k] / (P[idx].sum() + 1e-12) # conviction in [0,1]
|
||||
return float(slope) * float(np.clip(share * len(idx), 0.0, 1.0))
|
||||
|
||||
|
||||
def signal(df):
|
||||
c = df["close"].values.astype(float)
|
||||
lp = np.log(c)
|
||||
n = len(c)
|
||||
raw = np.zeros(n)
|
||||
|
||||
# slow local-trend anchor (the low-freq component is the real structure here)
|
||||
slow = bl.ema(c, 50)
|
||||
trend_dir = np.sign(c - slow)
|
||||
|
||||
for i in range(_NMAX, n):
|
||||
acc = 0.0
|
||||
for N in WINDOWS:
|
||||
acc += _cycle_phase_dir(lp[i - N + 1: i + 1]) # rows 0..i only
|
||||
cyc = PHASE_SIGN * acc / len(WINDOWS) # phase ensemble
|
||||
raw[i] = (1.0 - TREND_W) * cyc + TREND_W * trend_dir[i]
|
||||
|
||||
direction = np.tanh(2.0 * raw)
|
||||
pos = bl.vol_target(direction, df, target_vol=0.20, vol_win_days=30,
|
||||
leverage_cap=1.0)
|
||||
return np.clip(pos, -1.0, 1.0)
|
||||
Reference in New Issue
Block a user