feat(pairs): attiva ETH/BTC 15m flat-skip in PORT06 (BLEND, mezza size)
Origine: gioco "Blind Traders" (100 agenti ciechi su BTC/ETH anonimizzati) -> vincitore = spread ETH/BTC reversion a 15m. Testato sul serio col gate PORT06: non duplicato (corr 1h vs 15m = 0.37), robusto (16/16 celle Sharpe>1), edge NON artefatto delle candele flat ETH 15m (filtrandole resta l'83% dello Sharpe). Percorso live costruito e validato: - pairs_research.pairs_sim_flat: engine generalizzato con exit LIVE-REALIZABLE (arma exit_ready, esce alla 1a barra pulita); regression-lock a pairs_sim. - PairsWorker: flat_skip + exit_ready + rilevamento flat da OHLC (1h byte-exact). - runner: fetch diretto dei timeframe sub-orari + override position_size per-sleeve. - validate_worker_pairs: replay worker == backtest a 15m (8452 vs 8453 trade). - _defs/build_everything: sleeve PR_ETHBTC_15M (mezza size, pos 0.10) -> PORT06 FULL 6.43->7.20, OOS 8.58->9.66, DD giu'. Rischio bilanciato col 1h. - smoke live: Cerbero serve candele 15m fresche; worker ticca. Diari docs/diary/2026-06-09-*. Caveat slippage: mezza size = blend-tilt prudente. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
"""GATE PORT06 — ETH/BTC pairs a 15m (origine: gioco "Blind Traders", vincitore #43).
|
||||
|
||||
Domanda onesta sollevata dal gioco: la coppia ETH/BTC (gia' deployata in PR01 a 1h,
|
||||
config UNIV n=50 z_in=2.0 z_exit=0.75 max_bars=72) MIGLIORA se girata a 15m con la
|
||||
config trovata dal gioco (n=66 z_in=1.67 z_exit=1.0 max_bars=35), oppure e' solo una
|
||||
variante piu' veloce, correlata, dello STESSO spread?
|
||||
|
||||
Metodo (engine di PRODUZIONE pairs_sim, NON il motore-giocattolo del gioco):
|
||||
[1] PARITA': pairs_sim ETH/BTC 1h UNIV (pos0.15 lev3) == sleeve canonico PR_ETHBTC.
|
||||
[2] CORRELAZIONE 1h vs 15m (rendimenti giornalieri): se ~1 e' ridondante.
|
||||
[3] STANDALONE 1h vs 15m (+ griglia robustezza n x z_in su 15m, + stress fee 2x).
|
||||
[4] GATE PORT06: baseline(1h) vs SWAP(15m) vs BLEND(0.5*1h+0.5*15m) per la sleeve
|
||||
ETH/BTC; promosso se vs baseline l'OOS Sharpe non peggiora E il DD scende
|
||||
(PORT06 e famiglia), come gli altri gate del progetto.
|
||||
|
||||
uv run python scripts/analysis/pairs15m_port06_gate.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from scripts.analysis.combine_portfolio import port_returns, metrics, SPLIT, OOS_DATE, IDX
|
||||
from scripts.analysis.pairs_research import pairs_sim, OOS_FRAC
|
||||
from scripts.analysis.report_families import daily_from
|
||||
from scripts.portfolios._defs import PORTFOLIOS
|
||||
from src.portfolio import weighting as W
|
||||
|
||||
POS, LEV = 0.15, 3.0 # config CANONICA (== build_everything)
|
||||
UNIV_1H = dict(tf="1h", n=50, z_in=2.0, z_exit=0.75, max_bars=72)
|
||||
GAME_15M = dict(tf="15m", n=66, z_in=1.674, z_exit=1.0, max_bars=35) # vincitore gioco
|
||||
|
||||
|
||||
def eth_btc_daily(cfg):
|
||||
r = pairs_sim("ETH", "BTC", **{**cfg, "pos": POS, "lev": LEV})
|
||||
return daily_from(r["eq_ts"], r["eq_v"]), r
|
||||
|
||||
|
||||
def std_metrics(cfg, fee_rt=0.001):
|
||||
f = pairs_sim("ETH", "BTC", **{**cfg, "pos": POS, "lev": LEV, "fee_rt": fee_rt})
|
||||
o = pairs_sim("ETH", "BTC", **{**cfg, "pos": POS, "lev": LEV, "fee_rt": fee_rt,
|
||||
"split_frac": 1 - OOS_FRAC})
|
||||
yrs = f["yearly"]; pos_y = sum(1 for v in yrs.values() if v > 0)
|
||||
return f, o, pos_y, len(yrs)
|
||||
|
||||
|
||||
def port_metrics(members, p):
|
||||
ids = p.sleeve_ids
|
||||
dr = pd.DataFrame({i: members[i].pct_change().fillna(0.0) for i in ids})
|
||||
w = W.weight_vector(p.weighting, ids, dr, weights=p.weights,
|
||||
caps=p.caps, clusters=p.clusters, lookback=p.vol_lookback)
|
||||
drp = port_returns({i: members[i] for i in ids}, w)
|
||||
return metrics(drp), metrics(drp, lo=SPLIT)
|
||||
|
||||
|
||||
def fam_metrics(eqs):
|
||||
dr = port_returns(eqs)
|
||||
return metrics(dr), metrics(dr, lo=SPLIT)
|
||||
|
||||
|
||||
def blend(e1, e2, w1=0.5):
|
||||
"""Sleeve combinata: media pesata dei rendimenti giornalieri (ribilancio 1D)."""
|
||||
r1 = e1.reindex(IDX).ffill().bfill().pct_change().fillna(0.0)
|
||||
r2 = e2.reindex(IDX).ffill().bfill().pct_change().fillna(0.0)
|
||||
rb = w1 * r1 + (1 - w1) * r2
|
||||
eq = (1 + rb).cumprod()
|
||||
return eq / eq.iloc[0]
|
||||
|
||||
|
||||
def main():
|
||||
p = PORTFOLIOS["PORT06"]
|
||||
pair_ids = [s.sid for s in p.sleeves if s.sid.startswith("PR_")]
|
||||
print("=" * 100)
|
||||
print(" GATE PORT06 — ETH/BTC pairs 15m (vincitore gioco) vs 1h deployato")
|
||||
print(f" pos={POS} lev={LEV} (canonico) | OOS da {OOS_DATE} | coppie PORT06: {pair_ids}")
|
||||
print("=" * 100)
|
||||
|
||||
from src.portfolio.sleeves import all_sleeve_equities
|
||||
eq_base = dict(all_sleeve_equities())
|
||||
|
||||
# [1] PARITA'
|
||||
print("\n[1] PARITA' pairs_sim ETH/BTC 1h UNIV (pos0.15 lev3) == sleeve canonico PR_ETHBTC:")
|
||||
e1h, r1h = eth_btc_daily(UNIV_1H)
|
||||
base = eq_base["PR_ETHBTC"]
|
||||
corr = base.pct_change().fillna(0).corr(e1h.pct_change().fillna(0))
|
||||
rb = (base.iloc[-1] / base.iloc[0] - 1) * 100
|
||||
rr = (e1h.iloc[-1] / e1h.iloc[0] - 1) * 100
|
||||
par_ok = corr > 0.999 and abs(rr - rb) <= max(1.0, abs(rb) * 0.01)
|
||||
print(f" corr={corr:.5f} ret canon {rb:+.0f}% vs replay {rr:+.0f}% "
|
||||
f"{'OK' if par_ok else '<-- MISMATCH (STOP)'}")
|
||||
if not par_ok:
|
||||
return
|
||||
|
||||
# [2] CORRELAZIONE 1h vs 15m
|
||||
e15, r15 = eth_btc_daily(GAME_15M)
|
||||
c = e1h.pct_change().fillna(0).corr(e15.pct_change().fillna(0))
|
||||
print(f"\n[2] CORRELAZIONE rendimenti giornalieri ETH/BTC 1h vs 15m: {c:.3f}")
|
||||
print(f" {'(quasi-duplicato se >0.8; diversificatore se <0.5)':<60s}")
|
||||
|
||||
# [3] STANDALONE 1h vs 15m
|
||||
print("\n[3] STANDALONE ETH/BTC (netto fee 0.20% RT/coppia, leva 3x):")
|
||||
print(f" {'cfg':<10s}{'trd':>6s}{'win%':>6s}{'FULL%':>9s}{'OOS%':>9s}{'CAGR%':>7s}"
|
||||
f"{'DD%':>6s}{'oDD%':>7s}{'Shrp':>6s}{'anni+':>7s}{'fee2x FULL%':>12s}")
|
||||
for tag, cfg in [("1h UNIV", UNIV_1H), ("15m gioco", GAME_15M)]:
|
||||
f, o, py, ny = std_metrics(cfg)
|
||||
f2, _, _, _ = std_metrics(cfg, fee_rt=0.002)
|
||||
print(f" {tag:<10s}{f['trades']:>6d}{f['win']:>6.1f}{f['ret']:>+9.0f}{o['ret']:>+9.0f}"
|
||||
f"{f['cagr']:>7.0f}{f['dd']:>6.0f}{o['dd']:>7.0f}{f['sharpe']:>6.2f}"
|
||||
f"{f'{py}/{ny}':>7s}{f2['ret']:>+12.0f}")
|
||||
|
||||
# robustezza: plateau n x z_in su 15m (Sharpe>1?)
|
||||
print("\n Robustezza 15m (Sharpe full, griglia n x z_in, z_exit=1.0 max_bars=35):")
|
||||
ns = [40, 50, 66, 80]; zs = [1.5, 1.7, 2.0, 2.5]
|
||||
cells = 0; tot = 0
|
||||
hdr = " n\\z_in " + "".join(f"{z:>7.1f}" for z in zs)
|
||||
print(hdr)
|
||||
for n in ns:
|
||||
row = f" {n:>6d} "
|
||||
for z in zs:
|
||||
s = pairs_sim("ETH", "BTC", tf="15m", n=n, z_in=z, z_exit=1.0,
|
||||
max_bars=35, pos=POS, lev=LEV)["sharpe"]
|
||||
tot += 1; cells += s > 1
|
||||
row += f"{s:>7.2f}"
|
||||
print(row)
|
||||
print(f" -> {cells}/{tot} celle Sharpe>1 (plateau se ~tutte; picco se poche)")
|
||||
|
||||
# [4] GATE PORT06
|
||||
print("\n[4] GATE PORT06 — sleeve ETH/BTC: baseline(1h) vs SWAP(15m) vs BLEND(50/50):")
|
||||
variants = {
|
||||
"baseline 1h": e1h,
|
||||
"SWAP 15m": e15,
|
||||
"BLEND 1h+15m": blend(e1h, e15, 0.5),
|
||||
}
|
||||
print(f" {'variante':<14s} | {'FULL Sh':>8s}{'FULL DD%':>9s}{'CAGR':>6s}"
|
||||
f" | {'OOS Sh':>7s}{'OOS DD%':>8s} | {'famSh':>6s}{'famDD%':>7s}")
|
||||
print(" " + "-" * 78)
|
||||
res = {}
|
||||
for tag, eth in variants.items():
|
||||
members = dict(eq_base)
|
||||
members["PR_ETHBTC"] = eth
|
||||
f, o = port_metrics(members, p)
|
||||
fam_eqs = {sid: (eth if sid == "PR_ETHBTC" else eq_base[sid]) for sid in pair_ids}
|
||||
ff, _ = fam_metrics(fam_eqs)
|
||||
res[tag] = (f, o, ff)
|
||||
print(f" {tag:<14s} | {f['sharpe']:>8.2f}{f['dd']:>9.2f}{f['cagr']:>5.0f}%"
|
||||
f" | {o['sharpe']:>7.2f}{o['dd']:>8.2f} | {ff['sharpe']:>6.2f}{ff['dd']:>7.1f}")
|
||||
|
||||
# VERDETTO
|
||||
fb, ob, _ = res["baseline 1h"]
|
||||
print("\n" + "=" * 100)
|
||||
print(" VERDETTO vs baseline 1h: promosso se OOS Sharpe non peggiora E DD scende (PORT06 e famiglia)")
|
||||
print("=" * 100)
|
||||
for tag in ("SWAP 15m", "BLEND 1h+15m"):
|
||||
f, o, ff = res[tag]
|
||||
ok = (o["sharpe"] >= ob["sharpe"] - 0.02 and o["dd"] <= ob["dd"] + 1e-9
|
||||
and f["sharpe"] >= fb["sharpe"] - 0.02)
|
||||
print(f" {tag:<14s}: OOS Sh {ob['sharpe']:.2f}->{o['sharpe']:.2f} "
|
||||
f"DD {ob['dd']:.2f}->{o['dd']:.2f} | FULL Sh {fb['sharpe']:.2f}->{f['sharpe']:.2f} "
|
||||
f"DD {fb['dd']:.2f}->{f['dd']:.2f} => {'PROMOSSO' if ok else 'bocciato'}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user