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:
@@ -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