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:
+45
-9
@@ -34,16 +34,22 @@ _MULTI_KINDS = ("basket", "rotation", "tsmom")
|
||||
DATA_DIR = Path("data/portfolio_paper")
|
||||
|
||||
# giorni di storia da fetchare per timeframe (TSM01 1d usa 252 barre -> ~440 giorni col buffer)
|
||||
_LOOKBACK_DAYS = {"1h": 90, "4h": 220, "1d": 440}
|
||||
_LOOKBACK_DAYS = {"5m": 7, "15m": 14, "30m": 21, "1h": 90, "4h": 220, "1d": 440}
|
||||
# timeframe SUB-orari: si fetchano DIRETTI da Cerbero (non resamplabili dal 1h).
|
||||
_SUBHOURLY = {"5m", "15m", "30m"}
|
||||
# SH01 (ml) richiede >=4000 barre 1h (train_min di ml_wf_entries); 365g (~8760 barre) danno
|
||||
# margine ampio per il walk-forward. Difensivo: non dipende dal fetch 440g di TSM01/ROT02.
|
||||
_ML_LOOKBACK_DAYS = 365
|
||||
|
||||
|
||||
def pos_for_spec(sid: str, global_ps: float, family_overrides: dict[str, float]) -> float:
|
||||
"""position_size effettivo di uno sleeve: override per-famiglia (chiave =
|
||||
weighting.family_of: PAIRS/FADE/HONEST/SHAPE/TSM) o globale."""
|
||||
def pos_for_spec(sid: str, global_ps: float, family_overrides: dict[str, float],
|
||||
sleeve_ps: float | None = None) -> float:
|
||||
"""position_size effettivo di uno sleeve. Precedenza: override PER-SLEEVE
|
||||
(spec.params['position_size'], es. il 15m a 0.10) > override per-FAMIGLIA
|
||||
(weighting.family_of: PAIRS/FADE/...) > globale."""
|
||||
from src.portfolio.weighting import family_of
|
||||
if sleeve_ps is not None:
|
||||
return float(sleeve_ps)
|
||||
return family_overrides.get(family_of(sid), global_ps)
|
||||
|
||||
|
||||
@@ -301,7 +307,7 @@ def run(config_path: str = "portfolios.yml"):
|
||||
ex, inst = _exec_for(s)
|
||||
pex, pinst = _pairs_exec_for(s)
|
||||
workers[s.sid] = build_worker_for(s, alloc[s.sid], p.leverage,
|
||||
position_size=pos_for_spec(s.sid, position_size, ps_family),
|
||||
position_size=pos_for_spec(s.sid, position_size, ps_family, s.params.get("position_size")),
|
||||
executor=ex, exec_instrument=inst,
|
||||
pairs_executor=pex, exec_instruments=pinst)
|
||||
if ps_family:
|
||||
@@ -312,7 +318,7 @@ def run(config_path: str = "portfolios.yml"):
|
||||
paper_dir = DATA_DIR.parent / "portfolio_paper_stats"
|
||||
paper_workers = {s.sid: build_worker_for(s, paper_notional, p.leverage,
|
||||
data_dir=paper_dir,
|
||||
position_size=pos_for_spec(s.sid, position_size, ps_family))
|
||||
position_size=pos_for_spec(s.sid, position_size, ps_family, s.params.get("position_size")))
|
||||
for s in paper_specs}
|
||||
|
||||
# bootstrap storia full per gli sleeve ML (SH01): parquet locale + feed live.
|
||||
@@ -340,6 +346,18 @@ def run(config_path: str = "portfolios.yml"):
|
||||
for a in assets:
|
||||
asset_days[a] = max(asset_days.get(a, 0), days)
|
||||
|
||||
# timeframe SUB-orari (es. pairs 15m, flat-skip): non resamplabili dal 1h ->
|
||||
# fetch DIRETTO da Cerbero per (asset, tf). Inerte se nessuno sleeve e' sub-orario.
|
||||
subhourly_needs: dict[tuple[str, str], int] = {}
|
||||
for s in supported: # live + paper
|
||||
assets, tf = _spec_assets_tf(s)
|
||||
if tf in _SUBHOURLY:
|
||||
for a in assets:
|
||||
subhourly_needs[(a, tf)] = max(subhourly_needs.get((a, tf), 0),
|
||||
_LOOKBACK_DAYS.get(tf, 14))
|
||||
if subhourly_needs:
|
||||
print(f"[runner] timeframe sub-orari (fetch diretto Cerbero): {sorted(subhourly_needs)}")
|
||||
|
||||
inst_map = dict(INSTRUMENT_MAP)
|
||||
last_day = ""
|
||||
stale_alerted: set[str] = set() # asset con alert STALE_FEED attivo (dedup per episodio)
|
||||
@@ -394,12 +412,30 @@ def run(config_path: str = "portfolios.yml"):
|
||||
raw1h[asset] = df.sort_values("timestamp").reset_index(drop=True)
|
||||
_check_stale_feed(asset, raw1h[asset], stale_alerted)
|
||||
|
||||
# tick di ogni worker col suo timeframe (resample dal 1h)
|
||||
# fetch DIRETTO dei timeframe sub-orari (15m...) per (asset, tf)
|
||||
raw_sub: dict[tuple[str, str], pd.DataFrame] = {}
|
||||
for (asset, tf), days in subhourly_needs.items():
|
||||
inst = inst_map.get(asset, f"{asset}-PERPETUAL")
|
||||
start = end - timedelta(days=days)
|
||||
candles = client.get_historical_v2(inst, start.strftime("%Y-%m-%d"),
|
||||
end.strftime("%Y-%m-%d"), tf)
|
||||
if candles:
|
||||
df = pd.DataFrame(candles)
|
||||
df["timestamp"] = df["timestamp"].astype("int64")
|
||||
raw_sub[(asset, tf)] = df.sort_values("timestamp").reset_index(drop=True)
|
||||
|
||||
def _series_for(a, tf):
|
||||
"""Serie OHLC per (asset, tf): diretta se sub-oraria, altrimenti resample dal 1h."""
|
||||
if tf in _SUBHOURLY:
|
||||
return raw_sub.get((a, tf))
|
||||
return _resample(raw1h[a], tf) if a in raw1h else None
|
||||
|
||||
# tick di ogni worker col suo timeframe
|
||||
def _tick(s, w):
|
||||
assets, tf = _spec_assets_tf(s)
|
||||
if any(a not in raw1h for a in assets):
|
||||
res = {a: _series_for(a, tf) for a in assets}
|
||||
if any(res[a] is None or len(res[a]) == 0 for a in assets):
|
||||
return
|
||||
res = {a: _resample(raw1h[a], tf) for a in assets}
|
||||
if s.kind == "pairs":
|
||||
w.tick(res[s.a], res[s.b])
|
||||
elif s.kind in _MULTI_KINDS:
|
||||
|
||||
Reference in New Issue
Block a user