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,203 @@
|
||||
"""Check candele FLAT (O=H=L=C, liquidita' zero) sui pairs ETH/BTC a 15m.
|
||||
|
||||
Rischio noto (CLAUDE.md): ETH 15m ha 14-30%/anno di candele flat per bassa liquidita'
|
||||
del perpetuo. Su un pairs, un close stale gonfia lo z-score (l'altra gamba si muove,
|
||||
questa e' ferma) -> segnale di "reversione" FINTO che rientra solo quando la gamba
|
||||
stale si sblocca: profitto NON eseguibile dal vivo. Questo gonfierebbe il backtest 15m.
|
||||
|
||||
Test:
|
||||
[1] prevalenza candele flat per anno (ETH 15m, BTC 15m).
|
||||
[2] quanti trade del pairs 15m hanno ENTRY/EXIT su una candela flat (gamba stale).
|
||||
[3] re-sim flat-aware: entry/exit SOLO su barre pulite (non-flat in ENTRAMBE le gambe)
|
||||
-> quanto sopravvive l'edge? (parita': senza flat-skip == pairs_sim).
|
||||
[4] gate PORT06 col 15m flat-filtrato vs baseline 1h.
|
||||
|
||||
uv run python scripts/analysis/pairs15m_flatcheck.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 src.data.downloader import load_data
|
||||
from scripts.analysis.pairs_research import pairs_sim, OOS_FRAC, FEE_RT, LEV, POS, BARS_YEAR
|
||||
from scripts.analysis.report_families import daily_from
|
||||
from scripts.analysis.combine_portfolio import metrics, SPLIT, OOS_DATE
|
||||
from scripts.analysis.pairs15m_port06_gate import port_metrics, eth_btc_daily, UNIV_1H, GAME_15M
|
||||
from scripts.portfolios._defs import PORTFOLIOS
|
||||
from src.portfolio.sleeves import all_sleeve_equities
|
||||
|
||||
|
||||
def aligned2(a, b, tf="15m"):
|
||||
"""Merge con OHLC di ENTRAMBE le gambe (serve per rilevare i flat su entrambe)."""
|
||||
da = load_data(a, tf)[["timestamp", "open", "high", "low", "close"]].rename(
|
||||
columns=lambda x: x + "_a" if x != "timestamp" else x)
|
||||
db = load_data(b, tf)[["timestamp", "open", "high", "low", "close"]].rename(
|
||||
columns=lambda x: x + "_b" if x != "timestamp" else x)
|
||||
m = da.merge(db, on="timestamp", how="inner").reset_index(drop=True)
|
||||
m["dt"] = pd.to_datetime(m["timestamp"], unit="ms", utc=True)
|
||||
return m
|
||||
|
||||
|
||||
def is_flat(o, h, l, c):
|
||||
return (o == h) & (h == l) & (l == c)
|
||||
|
||||
|
||||
def flat_prevalence(asset, tf="15m"):
|
||||
d = load_data(asset, tf)
|
||||
d = d.copy()
|
||||
d["dt"] = pd.to_datetime(d["timestamp"], unit="ms", utc=True)
|
||||
fl = is_flat(d["open"].values, d["high"].values, d["low"].values, d["close"].values)
|
||||
d["flat"] = fl
|
||||
by = d.groupby(d["dt"].dt.year)["flat"].mean() * 100
|
||||
return by, fl.mean() * 100
|
||||
|
||||
|
||||
def pairs_sim_flataware(a, b, tf="15m", n=66, z_in=1.674, z_exit=1.0, max_bars=35,
|
||||
jump_max=0.08, fee_rt=FEE_RT, lev=LEV, pos=POS,
|
||||
split_frac=0.0, skip_flat=True):
|
||||
"""Come pairs_sim ma: entry/exit consentiti SOLO su barre pulite (se skip_flat).
|
||||
Ritorna anche n_entry_flat / n_exit_flat (diagnostica, calcolata sempre)."""
|
||||
m = aligned2(a, b, tf)
|
||||
ca, cb = m["close_a"].values, m["close_b"].values
|
||||
flat_a = is_flat(m["open_a"].values, m["high_a"].values, m["low_a"].values, ca)
|
||||
flat_b = is_flat(m["open_b"].values, m["high_b"].values, m["low_b"].values, cb)
|
||||
flat = flat_a | flat_b # barra "sporca" se una delle due gambe e' flat
|
||||
r = np.log(ca / cb)
|
||||
dr = np.abs(np.diff(r, prepend=r[0]))
|
||||
ma = pd.Series(r).rolling(n).mean().values
|
||||
sd = pd.Series(r).rolling(n).std().values
|
||||
z = (r - ma) / np.where(sd == 0, np.nan, sd)
|
||||
ts = m["dt"]; N = len(r)
|
||||
split = int(N * split_frac)
|
||||
fee = 2 * fee_rt * lev
|
||||
cap = peak = 1000.0; dd = 0.0; last = -1
|
||||
trades = wins = 0; rets = []; yearly = {}
|
||||
eq_ts, eq_v = [], []
|
||||
n_entry_flat = n_exit_flat = 0
|
||||
for i in range(n + 1, N - 1):
|
||||
if i < split or np.isnan(z[i]) or dr[i] > jump_max or i <= last:
|
||||
continue
|
||||
if z[i] <= -z_in:
|
||||
d = 1
|
||||
elif z[i] >= z_in:
|
||||
d = -1
|
||||
else:
|
||||
continue
|
||||
if flat[i]:
|
||||
n_entry_flat += 1
|
||||
if skip_flat:
|
||||
continue # non si entra su una gamba stale
|
||||
# exit: |z|<=z_exit o max_bars; se skip_flat, salta le barre flat come uscita
|
||||
j = min(i + max_bars, N - 1)
|
||||
for k in range(1, max_bars + 1):
|
||||
jj = i + k
|
||||
if jj >= N:
|
||||
j = N - 1; break
|
||||
if skip_flat and flat[jj]:
|
||||
j = jj # avanza, non esce su barra stale
|
||||
continue
|
||||
if abs(z[jj]) <= z_exit:
|
||||
j = jj; break
|
||||
j = jj
|
||||
if flat[j]:
|
||||
n_exit_flat += 1
|
||||
if skip_flat:
|
||||
# spingi all'ultima barra pulita entro l'orizzonte
|
||||
back = j
|
||||
while back > i and flat[back]:
|
||||
back -= 1
|
||||
j = back if back > i else j
|
||||
retA = (ca[j] - ca[i]) / ca[i]
|
||||
retB = (cb[j] - cb[i]) / cb[i]
|
||||
ret = (retA - retB) * d * lev - fee
|
||||
cap = max(cap + cap * pos * ret, 10.0)
|
||||
peak = max(peak, cap); dd = max(dd, (peak - cap) / peak)
|
||||
trades += 1; wins += ret > 0; rets.append(ret * pos); last = j
|
||||
eq_ts.append(ts.iloc[j]); eq_v.append(cap)
|
||||
yearly[ts.iloc[i].year] = yearly.get(ts.iloc[i].year, 0.0) + ret * 100
|
||||
yrs_span = (ts.iloc[-1] - ts.iloc[max(split, 0)]).days / 365.25 or 1
|
||||
sharpe = 0.0
|
||||
if len(rets) > 1 and np.std(rets) > 0:
|
||||
sharpe = float(np.mean(rets) / np.std(rets) * np.sqrt(trades / yrs_span))
|
||||
ret_tot = (cap / 1000 - 1) * 100
|
||||
return dict(trades=trades, win=wins / trades * 100 if trades else 0, ret=ret_tot,
|
||||
dd=dd * 100, sharpe=sharpe, yearly=yearly, eq_ts=eq_ts, eq_v=eq_v,
|
||||
n_entry_flat=n_entry_flat, n_exit_flat=n_exit_flat)
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 100)
|
||||
print(" CHECK FLAT-CANDLE — ETH/BTC pairs 15m (gate condizionato)")
|
||||
print("=" * 100)
|
||||
|
||||
# [1] prevalenza
|
||||
print("\n[1] Prevalenza candele flat (O=H=L=C) per anno, 15m:")
|
||||
for asset in ("ETH", "BTC"):
|
||||
by, tot = flat_prevalence(asset, "15m")
|
||||
print(f" {asset}: media {tot:.1f}% | " +
|
||||
" ".join(f"{y}:{v:.0f}%" for y, v in by.items()))
|
||||
|
||||
# [2] quanti trade toccano un flat (sim SENZA skip per diagnostica)
|
||||
diag = pairs_sim_flataware("ETH", "BTC", **GAME_15M, skip_flat=False)
|
||||
tr = diag["trades"]
|
||||
print(f"\n[2] Trade 15m totali: {tr} | entry su barra flat: {diag['n_entry_flat']} "
|
||||
f"({diag['n_entry_flat']/tr*100:.1f}%) | exit su barra flat: {diag['n_exit_flat']} "
|
||||
f"({diag['n_exit_flat']/tr*100:.1f}%)")
|
||||
|
||||
# [3] parita' + edge filtrato
|
||||
print("\n[3] Edge 15m: NO-skip (== pairs_sim) vs FLAT-AWARE (entry/exit solo barre pulite):")
|
||||
# parita': flataware skip_flat=False deve ~== pairs_sim
|
||||
base_ps = pairs_sim("ETH", "BTC", **GAME_15M, pos=POS, lev=LEV)
|
||||
print(f" parita' pairs_sim : trd {base_ps['trades']:>5d} Sh {base_ps['sharpe']:.2f} "
|
||||
f"DD {base_ps['dd']:.0f}% ret {base_ps['ret']:+.0f}%")
|
||||
print(f" flataware (no-skip) : trd {diag['trades']:>5d} Sh {diag['sharpe']:.2f} "
|
||||
f"DD {diag['dd']:.0f}% ret {diag['ret']:+.0f}%")
|
||||
filt = pairs_sim_flataware("ETH", "BTC", **GAME_15M, skip_flat=True)
|
||||
filt_o = pairs_sim_flataware("ETH", "BTC", **GAME_15M, skip_flat=True, split_frac=1 - OOS_FRAC)
|
||||
print(f" FLAT-AWARE (skip) : trd {filt['trades']:>5d} Sh {filt['sharpe']:.2f} "
|
||||
f"DD {filt['dd']:.0f}% ret {filt['ret']:+.0f}% | OOS Sh {filt_o['sharpe']:.2f} DD {filt_o['dd']:.0f}%")
|
||||
drop = (1 - filt['trades'] / diag['trades']) * 100
|
||||
sh_keep = filt['sharpe'] / diag['sharpe'] * 100 if diag['sharpe'] else 0
|
||||
verdict = "EDGE NON artefatto flat" if sh_keep > 70 else "EDGE in larga parte ARTEFATTO flat"
|
||||
print(f" -> rimossi {drop:.1f}% dei trade; Sharpe trattenuto {sh_keep:.0f}% ({verdict})")
|
||||
|
||||
# [4] gate PORT06 col 15m flat-filtrato
|
||||
print("\n[4] GATE PORT06 — ETH/BTC: baseline 1h vs SWAP 15m-FLATAWARE vs BLEND:")
|
||||
p = PORTFOLIOS["PORT06"]
|
||||
pair_ids = [s.sid for s in p.sleeves if s.sid.startswith("PR_")]
|
||||
eq_base = dict(all_sleeve_equities())
|
||||
e1h, _ = eth_btc_daily(UNIV_1H)
|
||||
e15f = daily_from(filt["eq_ts"], filt["eq_v"])
|
||||
# blend 1h + 15m-flataware (50/50 daily-rebalanced)
|
||||
from scripts.analysis.pairs15m_port06_gate import blend
|
||||
eblend = blend(e1h, e15f, 0.5)
|
||||
corr = e1h.pct_change().fillna(0).corr(e15f.pct_change().fillna(0))
|
||||
print(f" corr 1h vs 15m-flataware: {corr:.3f}")
|
||||
print(f" {'variante':<18s} | {'FULL Sh':>8s}{'FULL DD%':>9s}{'CAGR':>6s} | {'OOS Sh':>7s}{'OOS DD%':>8s}")
|
||||
print(" " + "-" * 70)
|
||||
res = {}
|
||||
for tag, eth in [("baseline 1h", e1h), ("SWAP 15m-flat", e15f), ("BLEND 1h+15m-flat", eblend)]:
|
||||
members = dict(eq_base); members["PR_ETHBTC"] = eth
|
||||
f, o = port_metrics(members, p)
|
||||
res[tag] = (f, o)
|
||||
print(f" {tag:<18s} | {f['sharpe']:>8.2f}{f['dd']:>9.2f}{f['cagr']:>5.0f}%"
|
||||
f" | {o['sharpe']:>7.2f}{o['dd']:>8.2f}")
|
||||
|
||||
fb, ob = res["baseline 1h"]
|
||||
print("\n VERDETTO (vs baseline 1h, fee backtest): Sharpe non peggiora E DD <= baseline")
|
||||
for tag in ("SWAP 15m-flat", "BLEND 1h+15m-flat"):
|
||||
f, o = res[tag]
|
||||
ok = o["sharpe"] >= ob["sharpe"] - 0.02 and o["dd"] <= ob["dd"] + 1e-9 and f["sharpe"] >= fb["sharpe"] - 0.02 and f["dd"] <= fb["dd"] + 1e-9
|
||||
print(f" {tag:<18s}: OOS {ob['sharpe']:.2f}->{o['sharpe']:.2f} DD {ob['dd']:.2f}->{o['dd']:.2f}"
|
||||
f" | FULL {fb['sharpe']:.2f}->{f['sharpe']:.2f} DD {fb['dd']:.2f}->{f['dd']:.2f} => {'PROMOSSO' if ok else 'bocciato'}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,62 @@
|
||||
"""GATE PORT06 FINALE — ETH/BTC 15m flat-skip, engine canonico pairs_sim_flat.
|
||||
|
||||
Usa pairs_sim_flat(flat_skip=True), cioe' la STESSA semantica live-realizable del
|
||||
PairsWorker (uscita alla prima barra pulita), validata da validate_worker_pairs.
|
||||
Conferma i numeri deployabili: baseline 1h vs SWAP 15m vs BLEND 1h+15m.
|
||||
|
||||
uv run python scripts/analysis/pairs15m_gate_final.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from scripts.analysis.pairs_research import pairs_sim_flat
|
||||
from scripts.analysis.report_families import daily_from
|
||||
from scripts.analysis.pairs15m_port06_gate import (port_metrics, eth_btc_daily, blend,
|
||||
UNIV_1H, POS, LEV)
|
||||
from scripts.portfolios._defs import PORTFOLIOS
|
||||
from src.portfolio.sleeves import all_sleeve_equities
|
||||
|
||||
CFG_15M = dict(n=66, z_in=1.674, z_exit=1.0, max_bars=35)
|
||||
|
||||
|
||||
def main():
|
||||
p = PORTFOLIOS["PORT06"]
|
||||
eq_base = dict(all_sleeve_equities())
|
||||
e1h, _ = eth_btc_daily(UNIV_1H)
|
||||
r15 = pairs_sim_flat("ETH", "BTC", tf="15m", **CFG_15M, flat_skip=True, pos=POS, lev=LEV)
|
||||
e15 = daily_from(r15["eq_ts"], r15["eq_v"])
|
||||
eblend = blend(e1h, e15, 0.5)
|
||||
corr = e1h.pct_change().fillna(0).corr(e15.pct_change().fillna(0))
|
||||
|
||||
print("=" * 92)
|
||||
print(" GATE PORT06 FINALE — ETH/BTC 15m flat-skip (pairs_sim_flat == worker live)")
|
||||
print(f" 15m: {r15['trades']} trade, {r15['n_skip_entry']} ingressi flat saltati | "
|
||||
f"corr 1h vs 15m = {corr:.3f}")
|
||||
print("=" * 92)
|
||||
print(f" {'variante':<18s} | {'FULL Sh':>8s}{'FULL DD%':>9s}{'CAGR':>6s} | {'OOS Sh':>7s}{'OOS DD%':>8s}")
|
||||
print(" " + "-" * 70)
|
||||
res = {}
|
||||
for tag, eth in [("baseline 1h", e1h), ("SWAP 15m-flat", e15), ("BLEND 1h+15m", eblend)]:
|
||||
members = dict(eq_base); members["PR_ETHBTC"] = eth
|
||||
f, o = port_metrics(members, p)
|
||||
res[tag] = (f, o)
|
||||
print(f" {tag:<18s} | {f['sharpe']:>8.2f}{f['dd']:>9.2f}{f['cagr']:>5.0f}%"
|
||||
f" | {o['sharpe']:>7.2f}{o['dd']:>8.2f}")
|
||||
fb, ob = res["baseline 1h"]
|
||||
print("\n Promosso se OOS Sharpe non peggiora E DD<=baseline (PORT06):")
|
||||
for tag in ("SWAP 15m-flat", "BLEND 1h+15m"):
|
||||
f, o = res[tag]
|
||||
ok = o["sharpe"] >= ob["sharpe"] - 0.02 and o["dd"] <= ob["dd"] + 1e-9 \
|
||||
and f["sharpe"] >= fb["sharpe"] - 0.02 and f["dd"] <= fb["dd"] + 1e-9
|
||||
print(f" {tag:<18s}: OOS {ob['sharpe']:.2f}->{o['sharpe']:.2f} DD {ob['dd']:.2f}->{o['dd']:.2f}"
|
||||
f" | FULL {fb['sharpe']:.2f}->{f['sharpe']:.2f} DD {fb['dd']:.2f}->{f['dd']:.2f}"
|
||||
f" => {'PROMOSSO' if ok else 'bocciato'}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,81 @@
|
||||
"""Smoke LIVE del nuovo percorso 15m: fetch DIRETTO 15m da Cerbero per ETH/BTC +
|
||||
freschezza + flat-fraction + un tick reale del PairsWorker(flat_skip).
|
||||
|
||||
Verifica cio' che il backtest non vede: che Cerbero serva candele 15m fresche per
|
||||
entrambe le gambe (il runner ora le fetcha dirette, non resamplate dal 1h) e che il
|
||||
worker 15m le processi senza errori. NON apre ordini reali (l'esecuzione a 2 gambe e'
|
||||
gia' coperta da live_pairs_smoke.py, indipendente dal timeframe).
|
||||
|
||||
uv run python scripts/analysis/pairs15m_live_smoke.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import shutil
|
||||
import tempfile
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
import pandas as pd
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from src.live.cerbero_client import CerberoClient
|
||||
from src.live.multi_runner import INSTRUMENT_MAP
|
||||
from src.live.pairs_worker import PairsWorker
|
||||
|
||||
CFG = {"n": 66, "z_in": 1.674, "z_exit": 1.0, "max_bars": 35, "flat_skip": True}
|
||||
|
||||
|
||||
def fetch15(cli, asset, days=14):
|
||||
inst = INSTRUMENT_MAP.get(asset, f"{asset}-PERPETUAL")
|
||||
end = datetime.now(timezone.utc)
|
||||
start = end - timedelta(days=days)
|
||||
candles = cli.get_historical_v2(inst, start.strftime("%Y-%m-%d"),
|
||||
end.strftime("%Y-%m-%d"), "15m")
|
||||
if not candles:
|
||||
return inst, None
|
||||
df = pd.DataFrame(candles)
|
||||
df["timestamp"] = df["timestamp"].astype("int64")
|
||||
return inst, df.sort_values("timestamp").reset_index(drop=True)
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 84)
|
||||
print(" SMOKE LIVE — ETH/BTC pairs 15m (fetch diretto Cerbero + tick worker flat-skip)")
|
||||
print("=" * 84)
|
||||
cli = CerberoClient()
|
||||
inst_a, da = fetch15(cli, "ETH")
|
||||
inst_b, db = fetch15(cli, "BTC")
|
||||
ok = True
|
||||
for asset, inst, df in [("ETH", inst_a, da), ("BTC", inst_b, db)]:
|
||||
if df is None or df.empty:
|
||||
print(f" {asset} ({inst}): NESSUNA candela 15m -> FAIL"); ok = False; continue
|
||||
last = pd.to_datetime(df["timestamp"].iloc[-1], unit="ms", utc=True)
|
||||
age_min = (datetime.now(timezone.utc) - last).total_seconds() / 60
|
||||
flat = ((df["open"] == df["high"]) & (df["high"] == df["low"]) &
|
||||
(df["low"] == df["close"])).mean() * 100
|
||||
fresh = age_min < 60
|
||||
print(f" {asset} ({inst}): {len(df)} barre 15m | ultima {last:%Y-%m-%d %H:%M} "
|
||||
f"({age_min:.0f} min fa, {'FRESCO' if fresh else 'STALE'}) | flat {flat:.1f}%")
|
||||
ok &= fresh
|
||||
if da is None or db is None:
|
||||
print("\n ESITO: FAIL (feed 15m assente)."); return
|
||||
# tick reale del worker 15m
|
||||
tmp = Path(tempfile.mkdtemp(prefix="smoke15m_"))
|
||||
try:
|
||||
w = PairsWorker("ETH", "BTC", "15m", params=CFG, fee_rt=0.001, data_dir=tmp)
|
||||
df_a = pd.DataFrame({"timestamp": da["timestamp"], "open": da["open"], "high": da["high"],
|
||||
"low": da["low"], "close": da["close"]})
|
||||
df_b = pd.DataFrame({"timestamp": db["timestamp"], "open": db["open"], "high": db["high"],
|
||||
"low": db["low"], "close": db["close"]})
|
||||
w.tick(df_a, df_b)
|
||||
print(f"\n Worker 15m flat_skip={w.flat_skip} -> tick OK | {w.status_summary}")
|
||||
print(f" ESITO: {'OK — feed 15m fresco e worker ticca' if ok else 'ATTENZIONE: feed 15m stale/parziale'}")
|
||||
finally:
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -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()
|
||||
@@ -95,6 +95,98 @@ def pairs_sim(a, b, tf="1h", n=50, z_in=2.0, z_exit=0.5, max_bars=72,
|
||||
eq_ts=eq_ts, eq_v=eq_v)
|
||||
|
||||
|
||||
def aligned_ohlc(a: str, b: str, tf: str = "1h"):
|
||||
"""Come aligned ma con OHLC di ENTRAMBE le gambe (serve a rilevare candele flat)."""
|
||||
da = load_data(a, tf)[["timestamp", "open", "high", "low", "close"]].rename(
|
||||
columns=lambda x: x + "_a" if x != "timestamp" else x)
|
||||
db = load_data(b, tf)[["timestamp", "open", "high", "low", "close"]].rename(
|
||||
columns=lambda x: x + "_b" if x != "timestamp" else x)
|
||||
m = da.merge(db, on="timestamp", how="inner").reset_index(drop=True)
|
||||
m["dt"] = pd.to_datetime(m["timestamp"], unit="ms", utc=True)
|
||||
return m
|
||||
|
||||
|
||||
def is_flat_ohlc(o, h, l, c):
|
||||
"""Candela flat (O=H=L=C): prezzo fermo / liquidita' zero -> fill non eseguibile."""
|
||||
return (o == h) & (h == l) & (l == c)
|
||||
|
||||
|
||||
def pairs_sim_flat(a, b, tf="1h", n=50, z_in=2.0, z_exit=0.5, max_bars=72,
|
||||
jump_max=0.08, fee_rt=FEE_RT, lev=LEV, pos=POS, split_frac=0.0,
|
||||
flat_skip=False, scan_buffer=192):
|
||||
"""Engine pairs GENERALIZZATO con opzione flat-skip LIVE-REALIZABLE.
|
||||
|
||||
Identico a pairs_sim quando flat_skip=False (regression-lock verificato).
|
||||
Con flat_skip=True:
|
||||
- ENTRY: saltata se la barra d'ingresso e' flat in UNA delle due gambe (prezzo stale).
|
||||
- EXIT: la condizione di uscita (|z|<=z_exit O bars>=max_bars) arma 'exit_ready';
|
||||
si esce al CLOSE della PRIMA barra PULITA successiva (mai a un prezzo passato).
|
||||
scan_buffer = barre extra oltre max_bars concesse per trovare la barra pulita.
|
||||
Questa e' la stessa regola implementata nel PairsWorker live (flat_skip) -> parita'.
|
||||
"""
|
||||
m = aligned_ohlc(a, b, tf)
|
||||
ca, cb = m["close_a"].values, m["close_b"].values
|
||||
N = len(ca)
|
||||
if flat_skip:
|
||||
flat = (is_flat_ohlc(m["open_a"].values, m["high_a"].values, m["low_a"].values, ca)
|
||||
| is_flat_ohlc(m["open_b"].values, m["high_b"].values, m["low_b"].values, cb))
|
||||
else:
|
||||
flat = np.zeros(N, dtype=bool)
|
||||
r = np.log(ca / cb)
|
||||
dr = np.abs(np.diff(r, prepend=r[0]))
|
||||
ma = pd.Series(r).rolling(n).mean().values
|
||||
sd = pd.Series(r).rolling(n).std().values
|
||||
z = (r - ma) / np.where(sd == 0, np.nan, sd)
|
||||
ts = m["dt"]
|
||||
split = int(N * split_frac)
|
||||
fee = 2 * fee_rt * lev
|
||||
cap = peak = 1000.0; dd = 0.0; last = -1
|
||||
trades = wins = 0; rets = []; yearly = {}
|
||||
eq_ts, eq_v = [], []
|
||||
n_skip_entry = 0
|
||||
kmax = max_bars + (scan_buffer if flat_skip else 0)
|
||||
for i in range(n + 1, N - 1):
|
||||
if i < split or np.isnan(z[i]) or dr[i] > jump_max or i <= last:
|
||||
continue
|
||||
if z[i] <= -z_in:
|
||||
d = 1
|
||||
elif z[i] >= z_in:
|
||||
d = -1
|
||||
else:
|
||||
continue
|
||||
if flat[i]:
|
||||
n_skip_entry += 1
|
||||
continue # niente ingresso su barra stale
|
||||
# uscita live-realizable: arma a |z|<=z_exit o max_bars, esci alla prima barra pulita
|
||||
exit_ready = False; j = i
|
||||
for k in range(1, kmax + 1):
|
||||
jj = i + k
|
||||
if jj >= N:
|
||||
j = N - 1; break
|
||||
if not exit_ready and (abs(z[jj]) <= z_exit or k >= max_bars):
|
||||
exit_ready = True
|
||||
if exit_ready and not flat[jj]:
|
||||
j = jj; break
|
||||
j = jj
|
||||
retA = (ca[j] - ca[i]) / ca[i]
|
||||
retB = (cb[j] - cb[i]) / cb[i]
|
||||
ret = (retA - retB) * d * lev - fee
|
||||
cap = max(cap + cap * pos * ret, 10.0)
|
||||
peak = max(peak, cap); dd = max(dd, (peak - cap) / peak)
|
||||
trades += 1; wins += ret > 0; rets.append(ret * pos); last = j
|
||||
eq_ts.append(ts.iloc[j]); eq_v.append(cap)
|
||||
yearly[ts.iloc[i].year] = yearly.get(ts.iloc[i].year, 0.0) + ret * 100
|
||||
yrs_span = (ts.iloc[-1] - ts.iloc[max(split, 0)]).days / 365.25 or 1
|
||||
sharpe = 0.0
|
||||
if len(rets) > 1 and np.std(rets) > 0:
|
||||
sharpe = float(np.mean(rets) / np.std(rets) * np.sqrt(trades / yrs_span))
|
||||
ret_tot = (cap / 1000 - 1) * 100
|
||||
cagr = ((cap / 1000) ** (1 / yrs_span) - 1) * 100 if cap > 0 else -100
|
||||
return dict(trades=trades, win=wins / trades * 100 if trades else 0, ret=ret_tot,
|
||||
cagr=cagr, dd=dd * 100, sharpe=sharpe, yearly=yearly,
|
||||
eq_ts=eq_ts, eq_v=eq_v, n_skip_entry=n_skip_entry)
|
||||
|
||||
|
||||
def check_no_lookahead():
|
||||
"""Perturba il FUTURO del ratio e verifica che z[i] non cambi (causalita')."""
|
||||
m = aligned("ETH", "BTC")
|
||||
|
||||
@@ -28,7 +28,7 @@ from scripts.analysis.combine_portfolio import (
|
||||
build_all_sleeves, port_returns, metrics, yearly_returns, SPLIT, OOS_DATE, IDX,
|
||||
)
|
||||
from scripts.analysis.honest_improve2 import _daily_equity, _norm
|
||||
from scripts.analysis.pairs_research import pairs_sim
|
||||
from scripts.analysis.pairs_research import pairs_sim, pairs_sim_flat
|
||||
from scripts.analysis.tsmom_research import tsmom_sim
|
||||
from scripts.strategies.PR01_pairs_reversion import PAIRS
|
||||
from scripts.analysis.shape_ml_validate import shape_daily_equity
|
||||
@@ -46,6 +46,16 @@ def build_everything():
|
||||
for a, b, p in PAIRS:
|
||||
r = pairs_sim(a, b, **p)
|
||||
pairs[f"PR_{a}{b}"] = daily_from(r["eq_ts"], r["eq_v"])
|
||||
# BLEND ETH/BTC 15m flat-skip (gioco Blind Traders -> gate PORT06, decorrelato 0.37
|
||||
# dal 1h, edge non-artefatto-flat, worker validato). Engine LIVE-REALIZABLE identico
|
||||
# al PairsWorker (pairs_sim_flat). Diari 2026-06-09-pairs15m-*.md.
|
||||
# MEZZA size (pos 0.075 = meta' della canonica 0.15): a peso uguale il 15m, piu'
|
||||
# volatile, contribuirebbe ~26% del rischio PORT06 (vs ~9% del 1h). Dimezzarlo lo
|
||||
# riporta in linea col 1h -> blend-tilt, non scommessa dominante (col caveat slippage).
|
||||
# Coerente col live (params.position_size=0.10 = meta' del family PAIRS 0.20).
|
||||
r15 = pairs_sim_flat("ETH", "BTC", tf="15m", n=66, z_in=1.674, z_exit=1.0,
|
||||
max_bars=35, flat_skip=True, pos=0.075)
|
||||
pairs["PR_ETHBTC_15M"] = daily_from(r15["eq_ts"], r15["eq_v"])
|
||||
t = tsmom_sim()
|
||||
tsm = {"TSM01": daily_from(t["eq_ts"], t["eq_v"])}
|
||||
shape = {f"SH_{a}": _norm(shape_daily_equity(a, IDX)) for a in ("BTC", "ETH")}
|
||||
|
||||
@@ -18,56 +18,70 @@ PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from src.live.pairs_worker import PairsWorker
|
||||
from scripts.analysis.pairs_research import aligned, pairs_sim
|
||||
from scripts.analysis.pairs_research import aligned, aligned_ohlc, pairs_sim, pairs_sim_flat
|
||||
from scripts.strategies.PR01_pairs_reversion import PAIRS
|
||||
|
||||
WINDOW = 60 # finestra trailing minima (>= n+2): z[i] corretto, replay veloce
|
||||
# Config 15m promossa dal gate (gioco Blind Traders + flat-skip): vedi
|
||||
# docs/diary/2026-06-09-pairs15m-port06-gate.md
|
||||
CFG_15M = dict(n=66, z_in=1.674, z_exit=1.0, max_bars=35, flat_skip=True)
|
||||
|
||||
|
||||
def replay(a: str, b: str, params: dict, data_dir: Path) -> PairsWorker:
|
||||
m = aligned(a, b)
|
||||
df_a = m[["timestamp"]].copy(); df_a["close"] = m["close_a"].values
|
||||
df_b = m[["timestamp"]].copy(); df_b["close"] = m["close_b"].values
|
||||
w = PairsWorker(a, b, "1h", params=params, fee_rt=0.001, data_dir=data_dir)
|
||||
# replay veloce: niente I/O su file / log / notifiche ad ogni tick (servono solo le metriche finali)
|
||||
def replay(a, b, params, data_dir, tf="1h", ohlc=False) -> PairsWorker:
|
||||
if ohlc:
|
||||
m = aligned_ohlc(a, b, tf)
|
||||
df_a = pd.DataFrame({"timestamp": m["timestamp"], "open": m["open_a"],
|
||||
"high": m["high_a"], "low": m["low_a"], "close": m["close_a"]})
|
||||
df_b = pd.DataFrame({"timestamp": m["timestamp"], "open": m["open_b"],
|
||||
"high": m["high_b"], "low": m["low_b"], "close": m["close_b"]})
|
||||
else:
|
||||
m = aligned(a, b, tf)
|
||||
df_a = m[["timestamp"]].copy(); df_a["close"] = m["close_a"].values
|
||||
df_b = m[["timestamp"]].copy(); df_b["close"] = m["close_b"].values
|
||||
w = PairsWorker(a, b, tf, params=params, fee_rt=0.001, data_dir=data_dir)
|
||||
w._save_state = lambda: None
|
||||
w._log = lambda *a, **k: None
|
||||
w._notify = lambda *a, **k: None
|
||||
n = w.n
|
||||
for k in range(n + 2, len(m) + 1):
|
||||
lo = max(0, k - WINDOW)
|
||||
window = max(60, w.n + 6) # finestra trailing >= n+? : z[i] corretto
|
||||
for k in range(w.n + 2, len(m) + 1):
|
||||
lo = max(0, k - window)
|
||||
w.tick(df_a.iloc[lo:k], df_b.iloc[lo:k])
|
||||
# chiudi eventuale posizione aperta a fine serie (come fa il backtest col troncamento)
|
||||
return w
|
||||
|
||||
|
||||
def _row(label, w, bt):
|
||||
bt_cap = 1000.0 * (1 + bt["ret"] / 100)
|
||||
cap_match = abs(w.capital - bt_cap) / bt_cap < 0.02 if bt_cap else False
|
||||
trd_match = abs(w.total_trades - bt["trades"]) <= max(2, bt["trades"] * 0.02)
|
||||
ok = "OK" if (cap_match and trd_match) else "DIFF"
|
||||
ww = w.total_wins / w.total_trades * 100 if w.total_trades else 0
|
||||
print(f" {label:<16s}{w.capital:>13.0f}{w.total_trades:>6d}{ww:>6.1f} | "
|
||||
f"{bt_cap:>14.0f}{bt['trades']:>6d}{bt['win']:>6.1f} {ok}")
|
||||
return ok == "OK"
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 96)
|
||||
print(" VALIDAZIONE PairsWorker — replay live vs backtest pairs_sim (fee 0.20% RT/coppia)")
|
||||
print("=" * 96)
|
||||
print(f" {'coppia':<10s}{'WORKER cap':>12s}{'trd':>5s}{'win%':>6s} | {'BACKTEST cap':>13s}{'trd':>5s}{'win%':>6s} match?")
|
||||
print(" " + "-" * 88)
|
||||
# Sottoinsieme rappresentativo: il codice del worker e' identico per ogni coppia,
|
||||
# quindi 2 coppie con strutture diverse (alt/major e major/alt) bastano a provare
|
||||
# l'equivalenza. ~135s/coppia su 73k barre orarie. Per validarle tutte: usa PAIRS.
|
||||
subset = [pp for pp in PAIRS if (pp[0], pp[1]) in {("ETH", "BTC"), ("BTC", "LTC")}]
|
||||
print("=" * 100)
|
||||
print(" VALIDAZIONE PairsWorker — replay live == backtest (fee 0.20% RT/coppia)")
|
||||
print("=" * 100)
|
||||
print(f" {'caso':<16s}{'WORKER cap':>13s}{'trd':>6s}{'win%':>6s} | "
|
||||
f"{'BACKTEST cap':>14s}{'trd':>6s}{'win%':>6s} match?")
|
||||
print(" " + "-" * 92)
|
||||
tmp = Path(tempfile.mkdtemp(prefix="pairs_validate_"))
|
||||
allok = True
|
||||
try:
|
||||
for a, b, p in subset:
|
||||
w = replay(a, b, p, tmp)
|
||||
bt = pairs_sim(a, b, **p)
|
||||
bt_cap = 1000.0 * (1 + bt["ret"] / 100)
|
||||
cap_match = abs(w.capital - bt_cap) / bt_cap < 0.02 if bt_cap else False
|
||||
trd_match = abs(w.total_trades - bt["trades"]) <= max(2, bt["trades"] * 0.02)
|
||||
ok = "OK" if (cap_match and trd_match) else "DIFF"
|
||||
ww = w.total_wins / w.total_trades * 100 if w.total_trades else 0
|
||||
print(f" {a+'/'+b:<10s}{w.capital:>12.0f}{w.total_trades:>5d}{ww:>6.1f} | "
|
||||
f"{bt_cap:>13.0f}{bt['trades']:>5d}{bt['win']:>6.1f} {ok}")
|
||||
# [A] REGRESSIONE 1h (flat_skip=False, close-only) vs pairs_sim
|
||||
for a, b, p in [pp for pp in PAIRS if (pp[0], pp[1]) in {("ETH", "BTC"), ("BTC", "LTC")}]:
|
||||
w = replay(a, b, p, tmp, tf="1h", ohlc=False)
|
||||
allok &= _row(f"{a}/{b} 1h", w, pairs_sim(a, b, **p))
|
||||
# [B] NUOVO: 15m flat-skip (OHLC) vs pairs_sim_flat
|
||||
w = replay("ETH", "BTC", CFG_15M, tmp, tf="15m", ohlc=True)
|
||||
bt = pairs_sim_flat("ETH", "BTC", tf="15m", **CFG_15M)
|
||||
allok &= _row("ETH/BTC 15m-flat", w, bt)
|
||||
finally:
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
print(" " + "-" * 88)
|
||||
print(" match = capitale entro 2% e trade entro 2% del backtest. Differenze minime sono")
|
||||
print(" attese (gestione bar finale/troncamento), ma la semantica deve coincidere.")
|
||||
print(" " + "-" * 92)
|
||||
print(" match = capitale e trade entro 2% del backtest (diff minime = bar finale aperta).")
|
||||
print(f" ESITO COMPLESSIVO: {'TUTTO OK' if allok else 'DIFFERENZE -> INDAGARE'}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user