24565974c0
16 agenti su segnali low-turnover intraday (sessione/funding, reversione post-evento, breakout range del giorno prima) su feed certificati 1h/15m, giudice = marginal scorer indurito + fee-sweep. Lab: intra_score.py (wrappa study_marginal a TF scelto + turnover/fee), meta_intra.py (corr-TP01 + per-cut), verify_intra.py (walk-forward + in-sample-null + drop-one + fee-stress). Esito: 10/16 "earns_slot" -> 5 genuinamente ortogonali (corr<0.4). Combo dei 5: Sharpe 1.80, corr 0.17, leak-free, passa walk-forward (+0.30/+0.37 dove l'ortho dava -0.07), pre-2025 uplift +0.28, drop-one e fee-robusto. Sembrava IL lead. 3 scettici: (1) open_drive = ARTEFATTO etichettatura UTC (shift confine 4h -> uplift negativo); prevday_range_breakout REGGE (unico onesto, eseguibile). (2) combo fallisce il null a corr-zero (20-24° pctl: aggiunge meno del rumore), è HEDGE (corr -0.57..-0.80 a Sharpe-TP01) + tail-luck (80% PnL in top-5 giorni delle gambe revert). (3) robust-plateau ma null-pctl 0.20 = diversificazione di stream ortogonale, non timing-alpha; + finzione fee micro-ribilanciamento a $600. Verdetto: niente in live, resta solo TP01. Lead forward-monitor: prevday_range_breakout. Lezioni harness da codificare: test shift-confine-giorno (artefatti calendar), fee discretizzata a piccolo capitale, causality guard nel lab intraday. Diario 2026-06-21-intraday-microstructure.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
105 lines
5.2 KiB
Python
105 lines
5.2 KiB
Python
"""agent_11_weekly_seasonality — SEASON family, slug=weekly_seasonality (suggested TF 1h).
|
|
|
|
ANGLE (assigned): a CAUSAL EXPANDING day-of-week effect that tilts a long BTC/ETH exposure by
|
|
the historically-strong weekday. Default LONG (capture drift); on the SINGLE weekday whose
|
|
causal expanding mean return is the WEAKEST so far, flip SHORT instead. Low turnover: the
|
|
weekday identity is sticky, so realized turnover is ~65-86 round-trips/yr — under the fee wall.
|
|
|
|
DESIGN PATH (honest): the literal "long-flat, flatten the weak weekday" version (just zero the
|
|
worst day) was NEUTRAL vs TP01 — it stays ~99% long, so it is buy&hold-in-disguise: corr 0.64,
|
|
hold-out uplift ~0.00. The piece that actually ADDS is SHORTING the worst weekday: it removes
|
|
that day's drift and injects a drift-free, trend-orthogonal return. A pure cross-weekday
|
|
long-short (orthogonal but no anchor) was tested and is NOISE OOS (causal long-short Sharpe IS
|
|
~0.1 / OOS -0.3..-1.4). The winning shape is "long the bull, EXCEPT short the worst weekday".
|
|
|
|
WHAT THE SIGNAL CONVERGES TO: the causally-weakest weekday locks onto THURSDAY almost
|
|
immediately and stays there for BOTH BTC and ETH, in-sample AND out-of-sample (>99% of bars).
|
|
So this is effectively "long, short Thursdays" — a Deribit-expiry-adjacent effect (weekly
|
|
options/futures settle Fri 08:00 UTC; pre-expiry de-risking pushes Thursday weak). The
|
|
cross-asset agreement + 7-year persistence is what separates it from a 1-of-7 multiple-testing
|
|
artifact. NB it is still discovered causally per bar — no full-sample weekday mean is used.
|
|
|
|
CAUSALITY: bias[i] for each weekday uses ONLY returns realized at bars 0..i-1 (an expanding
|
|
accumulator updated AFTER bias[i] is read, with a MIN_OBS warm-up). The worst-weekday identity
|
|
is re-decided causally every bar; result is invariant to MIN_OBS in {10,20,40,80}.
|
|
|
|
VERDICT (hardened judge, 1h): abs_grade=PASS, marginal=ADDS, earns_slot=TRUE. Standalone full
|
|
Sharpe BTC 1.59 / ETH 1.42, hold-out 0.86 / 0.98 (both assets). vs TP01: corr 0.44 full /
|
|
0.32 hold, resid Sharpe 1.12, alpha/yr +0.21. Blend 0.75*TP01 + 0.25*cand: hold-out uplift
|
|
+0.40 (full +0.33), DD 11%. Multi-cut persistent (positive uplift EVERY year 2020-2026),
|
|
drop-best-month jackknife +0.25, not a hedge (pays in TP01-up AND TP01-down). Fee-survives to
|
|
0.30% RT (BTC 1.19 / ETH 1.11). HONEST CAVEAT: the whole edge is one weekday ("short Thursday")
|
|
— a single, expiry-driven calendar effect; if Deribit settlement mechanics change, monitor it.
|
|
"""
|
|
import sys
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
|
|
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/alt")
|
|
import altlib as al # noqa: E402
|
|
|
|
# Tunables (kept conservative for LOW turnover).
|
|
_MIN_OBS = 20 # need >=20 past samples of a weekday before trusting its causal bias
|
|
_WORST_N = 1 # tilt only the single weakest weekday (worstN=2 raised turnover & worse OOS)
|
|
_SHORT_FRAC = 1.0 # SHORT the worst weekday (vs merely flat): adds the orthogonal, drift-free
|
|
# piece that lowers TP01-corr and lifts the hold-out (0->1.0 tested)
|
|
_VOL_TARGET = 0.20
|
|
_VOL_WIN_D = 30
|
|
_LEV_CAP = 1.0 # long-1 default, short the worst weekday; vol-targeted, never levered
|
|
|
|
|
|
def _causal_dow_table(daily_r: np.ndarray, dow: np.ndarray) -> np.ndarray:
|
|
"""Expanding mean daily return per UTC day-of-week, strictly causal.
|
|
|
|
Returns table[i, k] = average of past realized daily returns on weekday k using bars
|
|
0..i-1 (the accumulator for the bar's OWN weekday is updated AFTER the row is read, so
|
|
a weekday stays NaN until it has > MIN_OBS prior observations). This is the causal
|
|
analogue of the full-sample 'mean return by weekday' table — it never peeks ahead.
|
|
"""
|
|
n = len(daily_r)
|
|
table = np.full((n, 7), np.nan)
|
|
csum = np.zeros(7)
|
|
ccnt = np.zeros(7)
|
|
for i in range(n):
|
|
for k in range(7):
|
|
if ccnt[k] >= _MIN_OBS:
|
|
table[i, k] = csum[k] / ccnt[k]
|
|
d = dow[i]
|
|
csum[d] += daily_r[i]
|
|
ccnt[d] += 1
|
|
return table
|
|
|
|
|
|
def target(df):
|
|
"""Continuous long-flat position in [0,1] (vol-targeted): long by default, flat on the
|
|
historically-weakest weekday decided causally."""
|
|
c = df["close"].values.astype(float)
|
|
r = al.simple_returns(c)
|
|
dow = pd.to_datetime(df["datetime"], utc=True).dt.dayofweek.values
|
|
table = _causal_dow_table(r, dow)
|
|
|
|
n = len(df)
|
|
base = np.ones(n) # long by default (capture drift)
|
|
for i in range(n):
|
|
row = table[i]
|
|
if np.all(np.isnan(row)): # warm-up: no weekday trusted yet -> stay flat
|
|
base[i] = 0.0
|
|
continue
|
|
# rank weekdays weakest-first; NaN weekdays treated as 'strong' (not tilted)
|
|
order = np.argsort(np.nan_to_num(row, nan=1e9))
|
|
worst = set(order[:_WORST_N].tolist())
|
|
if dow[i] in worst:
|
|
base[i] = -_SHORT_FRAC # short the historically-weakest weekday
|
|
|
|
pos = al.vol_target(base, df, _VOL_TARGET, _VOL_WIN_D, _LEV_CAP)
|
|
return np.nan_to_num(pos, nan=0.0)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
for a in ("BTC", "ETH"):
|
|
d = al.get(a, "1h")
|
|
ev = al.eval_weights(d, target(d))
|
|
print(a, "full", ev["full"]["sharpe"], "hold", ev["holdout"]["sharpe"],
|
|
"turn/yr", ev["turnover_per_year"], "TiM", ev["time_in_market"])
|