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>
107 lines
4.7 KiB
Python
107 lines
4.7 KiB
Python
"""Agent 48 — Multi-timescale agreement (family=mix, slug=multiscale).
|
|
|
|
The angle (assigned): build a weekly-ish momentum by rolling aggregation up to i and
|
|
combine it with a daily momentum, going long/short only when the timescales AGREE.
|
|
|
|
Why agreement, not just averaging: a single horizon whipsaws when its window straddles
|
|
a chop. By measuring momentum at DAILY (1-bar EMA slope), WEEKLY (~5-bar aggregated
|
|
returns) and MONTHLY (~21-bar) timescales and requiring them to point the same way, we
|
|
filter the rule down to the bars where the trend is coherent across scales. The position
|
|
size = the (weighted) fraction of timescales that agree, so a unanimous up-vote is full
|
|
size and a split vote is light/flat. A vol-target then makes the two curves risk-
|
|
comparable and shrinks size into every vol spike (i.e. into every crash), turning the
|
|
~77-79% buy&hold drawdown into a ~0.23 one at comparable PnL.
|
|
|
|
Multi-timescale construction (all causal, value at i uses rows <= i only):
|
|
* DAILY momentum: sign of close vs a short EMA (fast trend state).
|
|
* WEEKLY momentum: rolling aggregation — mean of the last WEEK_WIN daily log-returns
|
|
(= ~WEEK_WIN/5 weeks of weekly drift) up to i. This is the "weekly-ish momentum by
|
|
rolling aggregation up to i" the angle asks for.
|
|
* MONTHLY momentum: sign of the past-MONTH_H-bar return (slow ~6-month macro trend).
|
|
The three signs are combined with weights into a -1..+1 direction; the short side is
|
|
zeroed (SHORT_W=0 -> long-flat) because both curves trend structurally up, so any short
|
|
bleeds by shorting the dips — tuning on train, long-flat dominated every de-weighted
|
|
short on sharpe_min (1.475 vs 1.45 at SHORT_W=0.3).
|
|
|
|
CAUSAL: EMAs / rolling means / past-return signs all use data <= i; vol_target uses a
|
|
trailing realized-vol window. No look-ahead, no centered windows, no global fit.
|
|
Verified by causality_ok (max_diff 0.0).
|
|
|
|
Tuning (split='train' only, combined A&B). Coarse->fine sweep on the timescale set,
|
|
weights, the short weight and the vol-target block; one-axis neighbor check confirms the
|
|
cell is interior on a wide plateau (ema 6-10, wk 30-35, mo 110-126, tv 0.26-0.30, vw
|
|
30-35 all give sharpe_min 1.42-1.50). Chosen cell:
|
|
DAILY_EMA=8, WEEK_WIN=35 (~7 weeks of daily drift), MONTH_H=126
|
|
weights (daily,weekly,monthly) = (0.15, 0.40, 0.45)
|
|
SHORT_W=0.0 (long-flat), TARGET_VOL=0.28, VOL_WIN=35d, LEV_CAP=1.5
|
|
-> train combined: pnl_mean ~3.62, maxdd_worst ~0.23, sharpe_min ~1.48.
|
|
"""
|
|
import numpy as np
|
|
import blindlib as bl
|
|
|
|
# timescale set
|
|
DAILY_EMA = 8 # daily-ish trend state (fast EMA)
|
|
WEEK_WIN = 35 # rolling window of daily log-returns (~7 weeks of weekly drift)
|
|
MONTH_H = 126 # ~6-month macro lookback (monthly-ish slow trend)
|
|
|
|
# combination weights (sum ~1) — weekly + monthly carry the agreement
|
|
W_DAILY = 0.15
|
|
W_WEEK = 0.40
|
|
W_MONTH = 0.45
|
|
SHORT_W = 0.0 # zero the short side (curves trend up) -> long-flat
|
|
|
|
# sizing
|
|
TARGET_VOL = 0.28
|
|
VOL_WIN_DAYS = 35
|
|
LEV_CAP = 1.5
|
|
|
|
|
|
def _daily_mom(c: np.ndarray) -> np.ndarray:
|
|
"""Sign of close vs a short EMA — the fast (daily) trend state, causal."""
|
|
e = bl.ema(c, DAILY_EMA)
|
|
return np.sign(c / e - 1.0)
|
|
|
|
|
|
def _weekly_mom(c: np.ndarray) -> np.ndarray:
|
|
"""Weekly-ish momentum by ROLLING AGGREGATION up to i (the assigned angle).
|
|
Aggregate daily log-returns into the average drift over the last WEEK_WIN bars
|
|
(~7 weeks), then take its sign. Causal: at bar i it only averages r[i-W+1..i].
|
|
Vectorized via a prefix-sum so it is O(n)."""
|
|
lr = bl.log_returns(c) # lr[i] = log(c[i]/c[i-1]), causal
|
|
win = WEEK_WIN
|
|
s = np.concatenate([[0.0], np.cumsum(lr)]) # prefix sums, s[k] = sum(lr[:k])
|
|
out = np.zeros(len(c))
|
|
idx = np.arange(len(c))
|
|
lo = np.maximum(0, idx - win + 1)
|
|
full = idx >= (win - 1) # only emit once the full window exists
|
|
means = (s[idx + 1] - s[lo]) / win
|
|
out[full] = np.sign(means[full])
|
|
return out
|
|
|
|
|
|
def _monthly_mom(c: np.ndarray) -> np.ndarray:
|
|
"""Sign of the past-MONTH_H-bar return — the slow macro trend, causal."""
|
|
out = np.zeros(len(c))
|
|
h = MONTH_H
|
|
if h < len(c):
|
|
out[h:] = np.sign(c[h:] / c[:-h] - 1.0)
|
|
return out
|
|
|
|
|
|
def signal(df):
|
|
c = df["close"].values.astype(float)
|
|
|
|
d = _daily_mom(c)
|
|
w = _weekly_mom(c)
|
|
m = _monthly_mom(c)
|
|
|
|
# weighted multi-timescale agreement -> direction in [-1, +1]
|
|
sig = W_DAILY * d + W_WEEK * w + W_MONTH * m
|
|
|
|
# asymmetric long-short: keep longs full size, de-weight shorts
|
|
raw = np.where(sig >= 0.0, sig, sig * SHORT_W)
|
|
|
|
pos = bl.vol_target(raw, 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)
|