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:
+34
-11
@@ -51,6 +51,11 @@ class PairsWorker:
|
||||
self.z_exit = float(p.get("z_exit", 0.75))
|
||||
self.max_bars = int(p.get("max_bars", 72))
|
||||
self.jump_max = float(p.get("jump_max", 0.08))
|
||||
# flat-skip (timeframe sub-orari, es. 15m): non entrare/uscire su candele flat
|
||||
# (O=H=L=C, prezzo stale/liquidita' zero -> fill non eseguibile). LIVE-REALIZABLE:
|
||||
# l'uscita arma exit_ready e si esegue alla prima barra PULITA. Parita' col backtest
|
||||
# pairs_research.pairs_sim_flat(flat_skip=True). Default off = comportamento 1h storico.
|
||||
self.flat_skip = bool(p.get("flat_skip", False))
|
||||
|
||||
self.initial_capital = capital
|
||||
self.position_size = position_size
|
||||
@@ -71,6 +76,7 @@ class PairsWorker:
|
||||
self.entry_z = 0.0
|
||||
self.entry_time = ""
|
||||
self.bars_held = 0
|
||||
self.exit_ready = False # flat-skip: condizione di uscita armata, attende barra pulita
|
||||
self.total_trades = 0
|
||||
self.total_wins = 0
|
||||
self.last_bar_ts = 0
|
||||
@@ -117,6 +123,7 @@ class PairsWorker:
|
||||
self.entry_z = s.get("entry_z", 0.0)
|
||||
self.entry_time = s.get("entry_time", "")
|
||||
self.bars_held = s.get("bars_held", 0)
|
||||
self.exit_ready = s.get("exit_ready", False)
|
||||
self.total_trades = s.get("total_trades", 0)
|
||||
self.total_wins = s.get("total_wins", 0)
|
||||
self.last_bar_ts = s.get("last_bar_ts", 0)
|
||||
@@ -145,7 +152,8 @@ class PairsWorker:
|
||||
"capital": round(self.capital, 2), "in_position": self.in_position,
|
||||
"direction": self.direction, "entry_a": self.entry_a, "entry_b": self.entry_b,
|
||||
"entry_z": round(self.entry_z, 4), "entry_time": self.entry_time,
|
||||
"bars_held": self.bars_held, "total_trades": self.total_trades,
|
||||
"bars_held": self.bars_held, "exit_ready": self.exit_ready,
|
||||
"total_trades": self.total_trades,
|
||||
"total_wins": self.total_wins, "last_bar_ts": self.last_bar_ts,
|
||||
"started_at": self.started_at, "last_update": datetime.now(timezone.utc).isoformat(),
|
||||
"real_capital": round(self.real_capital, 4), "real_in_position": self.real_in_position,
|
||||
@@ -185,6 +193,7 @@ class PairsWorker:
|
||||
self.entry_a, self.entry_b, self.entry_z = ca, cb, z
|
||||
self.entry_time = datetime.now(timezone.utc).isoformat()
|
||||
self.bars_held = 0
|
||||
self.exit_ready = False
|
||||
data = {"direction": "long_ratio" if d == 1 else "short_ratio",
|
||||
"long_leg": self.asset_a if d == 1 else self.asset_b,
|
||||
"short_leg": self.asset_b if d == 1 else self.asset_a,
|
||||
@@ -287,9 +296,13 @@ class PairsWorker:
|
||||
"""Chiamato ad ogni poll con gli OHLCV aggiornati delle due gambe."""
|
||||
if df_a is None or df_b is None or df_a.empty or df_b.empty:
|
||||
return
|
||||
m = df_a[["timestamp", "close"]].rename(columns={"close": "ca"}).merge(
|
||||
df_b[["timestamp", "close"]].rename(columns={"close": "cb"}), on="timestamp", how="inner"
|
||||
).sort_values("timestamp").reset_index(drop=True)
|
||||
# merge OHLC quando disponibile (serve a rilevare le candele flat per il flat-skip);
|
||||
# se le colonne OHLC mancano, flat resta False -> comportamento close-only invariato.
|
||||
ohlc = ["open", "high", "low", "close"]
|
||||
keep_a = ["timestamp"] + [c for c in ohlc if c in df_a.columns]
|
||||
keep_b = ["timestamp"] + [c for c in ohlc if c in df_b.columns]
|
||||
m = df_a[keep_a].merge(df_b[keep_b], on="timestamp", how="inner",
|
||||
suffixes=("_a", "_b")).sort_values("timestamp").reset_index(drop=True)
|
||||
# Scarta la barra IN FORMAZIONE: entry ED exit valutati SOLO sul close di
|
||||
# barra COMPLETA, come il backtest (pairs_research: close settled) —
|
||||
# lezione EXIT-16. Detection condivisa: src.live.bars.
|
||||
@@ -298,7 +311,7 @@ class PairsWorker:
|
||||
m = m.iloc[:-1]
|
||||
if len(m) < self.n + 2:
|
||||
return
|
||||
ca, cb = m["ca"].values, m["cb"].values
|
||||
ca, cb = m["close_a"].values, m["close_b"].values
|
||||
z, dr = self._zscore(ca, cb)
|
||||
i = len(m) - 1
|
||||
cur_ts = int(m["timestamp"].iloc[i])
|
||||
@@ -306,19 +319,29 @@ class PairsWorker:
|
||||
if np.isnan(zi):
|
||||
self._save_state(); return
|
||||
|
||||
# flat della barra corrente (entrambe le gambe): O=H=L=C in una delle due
|
||||
flat_i = False
|
||||
if self.flat_skip and {"open_a", "high_a", "low_a"}.issubset(m.columns) \
|
||||
and {"open_b", "high_b", "low_b"}.issubset(m.columns):
|
||||
fa = (m["open_a"].iloc[i] == m["high_a"].iloc[i] == m["low_a"].iloc[i] == ca[i])
|
||||
fb = (m["open_b"].iloc[i] == m["high_b"].iloc[i] == m["low_b"].iloc[i] == cb[i])
|
||||
flat_i = bool(fa or fb)
|
||||
|
||||
if self.in_position:
|
||||
if cur_ts > self.last_bar_ts:
|
||||
self.bars_held += 1
|
||||
self.last_bar_ts = cur_ts
|
||||
if abs(zi) <= self.z_exit:
|
||||
self._close(float(ca[i]), float(cb[i]), float(zi), "mean_revert")
|
||||
elif self.bars_held >= self.max_bars:
|
||||
self._close(float(ca[i]), float(cb[i]), float(zi), "time_limit")
|
||||
# arma l'uscita: |z|<=z_exit (rientro) o time-limit; poi esegui alla 1a barra pulita
|
||||
if not self.exit_ready and (abs(zi) <= self.z_exit or self.bars_held >= self.max_bars):
|
||||
self.exit_ready = True
|
||||
if self.exit_ready and not flat_i:
|
||||
reason = "mean_revert" if abs(zi) <= self.z_exit else "time_limit"
|
||||
self._close(float(ca[i]), float(cb[i]), float(zi), reason)
|
||||
self._save_state()
|
||||
return
|
||||
|
||||
# flat: cerca ingresso (no look-ahead: z[i] usa solo dati <= i)
|
||||
if dr[i] <= self.jump_max:
|
||||
# cerca ingresso (no look-ahead: z[i] usa solo dati <= i); mai su barra stale
|
||||
if dr[i] <= self.jump_max and not flat_i:
|
||||
if zi <= -self.z_in:
|
||||
self._open(1, float(ca[i]), float(cb[i]), float(zi)); self.last_bar_ts = cur_ts
|
||||
elif zi >= self.z_in:
|
||||
|
||||
Reference in New Issue
Block a user