Compare commits
2 Commits
be85a454e0
...
cc9a380a63
| Author | SHA1 | Date | |
|---|---|---|---|
| cc9a380a63 | |||
| 8c5372c487 |
@@ -357,6 +357,16 @@ queste fade, ma va confermato col paper trader live prima di rischiare capitale
|
||||
- **SH01 SENZA STOP-LOSS — by design, CONFERMATO da ricerca (2026-06-05).** Dopo il crash ETH (−15.6% su un trade SH01 live), ricerca multi-agente con harness dedicato `scripts/analysis/sh01_exit_lab.py` (cache segnali walk-forward, engine con **fill gap-aware** `worse(livello, open)`, parity esatta con explore_lab, protocollo train≤2023-11-01/OOS): **11 famiglie di stop testate (ATR intrabar/close-confirm, %, chandelier, breakeven, giveback, loser-timestop, disaster-cap close+intrabar, swing, vol-regime), 0 sopravvissute** al gate (ETH migliorato senza degradare BTC, train E oos, plateau). Pattern: ogni stop stretto abbastanza da toccare la coda ETH rompe BTC; ogni stop largo non arriva alla coda; nei crash il fill è al gap, non al livello (lo stop "protettivo" PEGGIORA la coda OOS). Mitigazione adottata: **cap famiglia SHAPE a 0.0588 in PORT06** (≈ dimezzata; costo OOS Sharpe −0.24, FULL DD −0.14pp) — la prossima coda impatta il conto per metà. NON impostare mai `sl`/`sl_confirm_atr` su SH_BTC/SH_ETH. Direzione futura: liquidity-gate sull'entry (skip dopo feed flat). Diario `docs/diary/2026-06-05-sh01-sl-research.md`.
|
||||
- **Data layer Cerbero v2:** `get_historical_v2` unificato + `get_instruments` (naming robusto) + `get_ticker_batch`. Trading su Deribit.
|
||||
- **SCOPE LIVE (fase 2 completata):** il runner esegue TUTTI gli sleeve di PORT06. Worker: single `StrategyWorker` (fade MR01/02/07, DIP01, **e SH01**), `PairsWorker` (PR01 2 gambe), e i multi-asset dedicati `BasketTrendWorker` (TR01 4h), `RotationWorker` (ROT02 1d), `TsmomWorker` (TSM01 1d). Il runner fetcha 1h da Cerbero v2 e **resampla a 4h/1d** (lookback dimensionato sui daily: TSM01 usa 252g). Validazione: runner pool/ribilancio/ledger == backtest (`validate_portfolio_runner.py`, identico); worker multi-asset == reference (`validate_honest_workers.py`: TSM01 esatto, ROT02 +1303% canonico, TR01 stesso ordine — differenza di convenzione capitale-unico vs media-equity).
|
||||
- **SH01 bootstrap full-history (punto-10, 2026-06-07).** La ri-validazione col train-window del
|
||||
regime live (`sh01_trainwindow_validate.py`) ha mostrato che SH01 a train 365g **NON è robusto**
|
||||
(BTC fee-2x FULL −42%, ETH Sharpe −0.02, trade-rate 22-26% vs 10% validato: LogReg over-confident,
|
||||
soglia inerte — la diagnosi sweep era esatta). L'edge è MONOTONO nella memoria: solo l'expanding
|
||||
full-history passa il gate. Fix live: il runner passa agli sleeve `ml` la storia FULL
|
||||
(`_with_history`: parquet locale + feed, gap-guard con WARN) e `ml_wf_entries(last_block_only=True)`
|
||||
fitta SOLO l'ultimo blocco del walk-forward → segnali **identici per costruzione** al WF completo
|
||||
(parity test esatto), 0.6 s/tick su 73k barre. Manutenzione: tenere fresco il parquet
|
||||
(`download_all()`). MAI ri-tunare la soglia nel regime corto (instabile/incoerente fra asset).
|
||||
Diario `docs/diary/2026-06-07-sh01-trainwindow.md`.
|
||||
- **FIX SH01 wiring (2026-06-01).** SH01 gira come **`StrategyWorker` NORMALE** (NON il vecchio `MLWorkerWrapper` di `multi_runner`, che usava il `SignalEngine` **squeeze SCARTATO**: apriva senza metadata ed usciva a `hold_bars=3`, ignorando del tutto SH01_shape_ml). `SH01_shape_ml.generate_signals` fa il walk-forward (retraining) internamente ad ogni tick ed emette `metadata.max_bars=12` → exit a orizzonte via `StrategyWorker.tick`. Serve ≥4000 barre 1h (`_ML_LOOKBACK_DAYS=365`). Vedi `docs/diary/2026-06-01-sh01-wiring-squeeze-bug.md`.
|
||||
- **Altri fix StrategyWorker (2026-06-01).** Exit a orizzonte puro per strategie senza TP/SL (`elif self.max_bars`, SH01 esce a H=12 non hold_bars=3); `is_win = net > 0` (win NETTO fee, non lordo); filtro `min_tp_frac` (salta micro-scalp col TP entro le fee); loss-guard `hurst_max=0.55` sulle fade (vedi sopra).
|
||||
- **Exit intrabar (fase 3, risolto):** lo `StrategyWorker` ora esce sui TP/SL toccati INTRABAR (high/low della barra, al livello, SL prioritario) come il backtest — non più solo sul close. Allinea fade/DIP01 live al backtest intrabar (`tests/portfolio/test_intrabar_exit.py`). Caveat residuo onesto: nel paper trading l'high/low usato è quello della barra in corso al poll; su un fill reale conterebbe il momento del tocco.
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
# 2026-06-07 — SH01 punto-10: il regime live (train 365g) NON è robusto → bootstrap full-history
|
||||
|
||||
Punto 10 della roadmap sweep: ri-validare SH01 col train-window del regime live
|
||||
(~8760 barre, il `_ML_LOOKBACK_DAYS=365` del runner) invece dell'expanding
|
||||
full-history usato da tutta la validazione storica.
|
||||
|
||||
## Ri-validazione (`scripts/analysis/sh01_trainwindow_validate.py`)
|
||||
|
||||
`ml_wf_entries(train_window=...)` rolling su storia piena ≈ il regime live.
|
||||
Protocollo explore_lab (FULL / OOS 30% / sweep fee / anni positivi), W24 H12 th0.58.
|
||||
|
||||
| BTC train | trade-rate | FULL / OOS | Sharpe | fee 0.2%: FULL/OOS | anni+ | robusto |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 8760 (live) | **21.7%** | +82 / +74 | 1.31 | **−42** / +20 | 6/9 | ❌ |
|
||||
| 17520 (2y) | 15.4% | +166 / +96 | 2.05 | +9 / +47 | 6/9 | ❌ |
|
||||
| 26280 (3y) | 11.8% | +348 / +95 | 3.09 | +109 / +61 | 7/9 | ❌ |
|
||||
| EXPANDING | 9.8% | +219 / +42 | 2.72 | +60 / +26 | 8/9 | ✅ |
|
||||
|
||||
ETH a tw=8760: **FULL −33%, Sharpe −0.02** (lancio di moneta che paga fee).
|
||||
|
||||
Conferme: (a) la diagnosi del sweep era esatta (trade-rate 22-26% live vs 10%
|
||||
validato — soglia inerte per over-confidence su train piccolo); (b) la
|
||||
progressione è MONOTONA: l'edge di SH01 *è* la memoria lunga; (c) lo sweep della
|
||||
soglia nel regime corto dà risultati instabili e incoerenti fra asset → ri-tunare
|
||||
th sarebbe curve-fitting sul rumore di calibrazione (come da warning del piano).
|
||||
|
||||
## Decisione (utente): bootstrap full-history
|
||||
|
||||
Ripristinare in live ESATTAMENTE il regime validato (expanding full-history):
|
||||
|
||||
1. **`ml_wf_entries(last_block_only=True)`** — fitta/predice SOLO l'ultimo blocco
|
||||
del walk-forward. I confini dei blocchi sono deterministici (start + k·retrain)
|
||||
e al worker servono solo i segnali recenti → le entries dell'ultimo blocco sono
|
||||
**identiche per costruzione** al tail del WF completo (parity test esatto in
|
||||
`tests/portfolio/test_sh01_last_block.py`). Costo live: **0.6 s/tick** su 73k
|
||||
barre (1 fit LogReg) vs ~140 fit del WF completo.
|
||||
2. **Bootstrap storia nel runner**: per gli sleeve `ml` il tick riceve
|
||||
`concat(parquet_locale < feed_start, feed_live)` (`runner._with_history`).
|
||||
Gap-guard: se il parquet è stantio oltre il lookback del feed → solo feed +
|
||||
WARN una-tantum (meglio il regime corto dichiarato che una serie col buco).
|
||||
Manutenzione: tenere fresco il parquet con `download_all()` (oggi al 2026-05-28,
|
||||
il feed copre 365g → margine ~11 mesi).
|
||||
3. `_defs.py`: `params={"last_block_only": True}` sugli sleeve SHAPE (solo path
|
||||
live; il backtest canonico resta WF completo).
|
||||
|
||||
Effetto atteso live: trade-rate SH01 da ~25% a ~10% delle barre, selettività
|
||||
della soglia ripristinata, WR atteso ≈ validazione (acc OOS ~56% BTC).
|
||||
Il monitor naturale è il report orario (SH01 farà MENO trade).
|
||||
|
||||
Nota: `_ML_LOOKBACK_DAYS` resta 365 (il feed serve comunque per le barre recenti
|
||||
e come fallback se il parquet è stantio).
|
||||
@@ -0,0 +1,75 @@
|
||||
"""Ri-validazione SH01 col train-window del REGIME LIVE (sweep punto 10).
|
||||
|
||||
Diagnosi (improvement-sweep 2026-06-06): tutta la validazione SH01
|
||||
(`shape_ml_validate.py`) usa training EXPANDING full-history (~73k barre dal 2018),
|
||||
ma il worker live fetcha solo `_ML_LOOKBACK_DAYS=365` (~8760 barre 1h) -> la LogReg
|
||||
allenata su train piccolo e' OVER-CONFIDENT: frac(proba>=0.58) 44.8% vs 3.8%,
|
||||
trade-rate live 25.1% delle barre vs 2.9% validato, WR live 1/9. La soglia th=0.58
|
||||
e' di fatto inerte nel regime live.
|
||||
|
||||
Questo harness misura l'edge SH01 (W24 H12 logit th0.58, protocollo explore_lab:
|
||||
FULL / OOS ultimo 30% / sweep fee / anni positivi) in funzione del train-window
|
||||
ROLLING (`ml_wf_entries(train_window=...)`):
|
||||
|
||||
8760 = regime live ATTUALE (365g) <- la validazione mai fatta
|
||||
17520 = 2 anni (gia' noto: BTC regge, +166%/+96%)
|
||||
26280 = 3 anni
|
||||
None = EXPANDING full-history <- la validazione canonica (riferimento)
|
||||
|
||||
+ diagnostica trade-rate per variante e, per il regime live, sweep della soglia
|
||||
(SOLO diagnostica: la lezione del sweep e' di NON ri-tunare th a forza — se
|
||||
l'edge non sopravvive a 8760 la risposta e' cambiare il regime di training del
|
||||
live, non la soglia).
|
||||
|
||||
Esiti possibili:
|
||||
a) edge OK a 8760 -> il live va bene cosi' (e la diagnosi WR 1/9 e' altro);
|
||||
b) edge OK da un lookback X > 8760 -> portare il train live a X
|
||||
(bootstrap storico dal parquet locale + append feed, o lookback maggiore);
|
||||
c) edge solo expanding -> bootstrap parquet full-history o riconsiderare SH01.
|
||||
|
||||
uv run python scripts/analysis/sh01_trainwindow_validate.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.explore_lab import get_df, evaluate, robust # noqa: E402
|
||||
from scripts.analysis.shape_ml_research import ml_wf_entries # noqa: E402
|
||||
|
||||
CFG = dict(W=24, H=12, model="logit", thresh=0.58)
|
||||
WINDOWS = [8760, 17520, 26280, None] # None = expanding (canonico)
|
||||
TH_DIAG = [0.58, 0.62, 0.66, 0.70] # sweep soglia SOLO per il regime live
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 108)
|
||||
print(" SH01 — edge vs train-window ROLLING (W24 H12 logit th0.58, protocollo explore_lab)")
|
||||
print(" regime live = 8760 barre (365g); canonico = expanding full-history")
|
||||
print("=" * 108)
|
||||
|
||||
for asset in ("BTC", "ETH"):
|
||||
df = get_df(asset, "1h")
|
||||
nbars = len(df)
|
||||
print(f"\n--- {asset} ({nbars} barre 1h) ---")
|
||||
for tw in WINDOWS:
|
||||
tag = f"tw={tw}" if tw else "EXPANDING"
|
||||
ents = ml_wf_entries(df, train_window=tw, **CFG)
|
||||
rate = len(ents) / nbars * 100
|
||||
print(f" [{tag:<10s}] trade-rate {rate:4.1f}% delle barre")
|
||||
res = evaluate(f"{asset} {tag}", ents, df)
|
||||
print(f" -> {'ROBUSTO' if robust(res) else 'NON robusto'}")
|
||||
|
||||
print(f"\n diagnostica soglia nel REGIME LIVE (tw=8760), {asset}:")
|
||||
for th in TH_DIAG:
|
||||
ents = ml_wf_entries(df, train_window=8760, **{**CFG, "thresh": th})
|
||||
rate = len(ents) / nbars * 100
|
||||
print(f" [th={th:<5}] trade-rate {rate:4.1f}%")
|
||||
evaluate(f"{asset} tw8760 th{th}", ents, df)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -245,12 +245,18 @@ def analog_feat_entries(df, W=24, H=12, K=60, rebuild=300, min_lib=1500,
|
||||
def ml_wf_entries(df, W=24, H=12, model="gb", thresh=0.58,
|
||||
train_min=4000, retrain=500, n_estimators=80, max_depth=3,
|
||||
tp_atr=None, sl_atr=None, trend_max=None, ema_long=200,
|
||||
train_window=None) -> list[dict]:
|
||||
train_window=None, last_block_only=False) -> list[dict]:
|
||||
"""Walk-forward: a blocchi di `retrain` barre, allena sul passato il cui esito
|
||||
e' noto, predice il blocco corrente. Scaler+modello fittati solo sul train.
|
||||
Entra a close[i] se proba della classe predetta >= thresh. model in {gb, logit}.
|
||||
train_window: se None -> expanding (tutto il passato); se int -> ROLLING (solo le
|
||||
ultime train_window barre prima del blocco) -> test di robustezza piu' severo."""
|
||||
ultime train_window barre prima del blocco) -> test di robustezza piu' severo.
|
||||
last_block_only: PATH LIVE (2026-06-07) — fitta e predice SOLO l'ultimo blocco
|
||||
(un fit invece di ~n/retrain): al worker servono solo i segnali sulla barra
|
||||
corrente e i confini dei blocchi sono deterministici (start + k*retrain), quindi
|
||||
le entries dell'ultimo blocco sono IDENTICHE per costruzione al tail del
|
||||
walk-forward completo (parity test in tests/portfolio/test_sh01_last_block.py).
|
||||
Rende sostenibile il train EXPANDING full-history nel runner (~73k barre/tick)."""
|
||||
c = df["close"].values
|
||||
n = len(c)
|
||||
a = atr(df, 14)
|
||||
@@ -269,6 +275,9 @@ def ml_wf_entries(df, W=24, H=12, model="gb", thresh=0.58,
|
||||
blk = start
|
||||
while blk < n - 1:
|
||||
blk_end = min(blk + retrain, n - 1)
|
||||
if last_block_only and blk_end < n - 1:
|
||||
blk = blk_end # salta i blocchi storici (stessi confini del WF completo)
|
||||
continue
|
||||
# TRAIN: finestre la cui forma E il cui esito (e+H) sono < blk
|
||||
# cioe' e <= blk-1-H (esito realizzato prima del primo test del blocco)
|
||||
lo_end = (blk - 1 - H - train_window) if train_window is not None else (W - 1)
|
||||
|
||||
@@ -67,7 +67,12 @@ PAIRS = [
|
||||
TSM = [SleeveSpec(kind="tsmom", name="TSM01", sid="TSM01", cluster="trend",
|
||||
params={"universe": UNIVERSE8, "tf": "1d",
|
||||
"horizons": [63, 126, 252], "thr": 1.0, "gross": 0.30})]
|
||||
SHAPE = [SleeveSpec(kind="ml", name="SH01", sid=f"SH_{a}", asset=a, cluster="shape")
|
||||
# SH01 live (punto-10, 2026-06-07): il runner passa la storia FULL (parquet+feed) e
|
||||
# last_block_only fitta/predice solo l'ultimo blocco del walk-forward (segnali ==
|
||||
# WF completo per costruzione). Il regime corto train-365g NON e' robusto (BTC
|
||||
# negativo a fee 2x, trade-rate 22% vs 10% validato): sh01_trainwindow_validate.py.
|
||||
SHAPE = [SleeveSpec(kind="ml", name="SH01", sid=f"SH_{a}", asset=a, cluster="shape",
|
||||
params={"last_block_only": True})
|
||||
for a in ("BTC", "ETH")]
|
||||
|
||||
PORTFOLIOS = {
|
||||
|
||||
@@ -75,7 +75,14 @@ class ShapeMLStrategy(Strategy):
|
||||
|
||||
def generate_signals(self, df: pd.DataFrame, ts: pd.DatetimeIndex, **params) -> list[Signal]:
|
||||
cfg = {**CONFIG, **{k: params[k] for k in ("W", "H", "model", "thresh") if k in params}}
|
||||
ents = ml_wf_entries(df, W=cfg["W"], H=cfg["H"], model=cfg["model"], thresh=cfg["thresh"])
|
||||
# last_block_only (live, 2026-06-07): il runner passa la storia FULL (parquet
|
||||
# bootstrap + feed) e qui si fitta/predice solo l'ultimo blocco del walk-forward
|
||||
# (segnali identici al WF completo per costruzione, 1 fit invece di ~140).
|
||||
# La ri-validazione punto-10 ha mostrato che il regime corto (train 365g) NON
|
||||
# e' robusto (BTC negativo a fee 2x, trade-rate 22% vs 10%): l'edge richiede
|
||||
# il training expanding full-history. Vedi sh01_trainwindow_validate.py.
|
||||
ents = ml_wf_entries(df, W=cfg["W"], H=cfg["H"], model=cfg["model"], thresh=cfg["thresh"],
|
||||
last_block_only=bool(params.get("last_block_only")))
|
||||
c = df["close"].values
|
||||
out: list[Signal] = []
|
||||
for e in ents:
|
||||
|
||||
+47
-2
@@ -116,6 +116,32 @@ def rebalance_allocations(ledger: PortfolioLedger, workers: dict, weights: dict[
|
||||
ledger.save()
|
||||
|
||||
|
||||
_OHLCV = ["timestamp", "open", "high", "low", "close", "volume"]
|
||||
|
||||
|
||||
def _with_history(hist: pd.DataFrame | None, live: pd.DataFrame,
|
||||
warned: set | None = None, asset: str = "") -> pd.DataFrame:
|
||||
"""Bootstrap storia per SH01 (punto-10, 2026-06-07): parquet locale PRIMA del
|
||||
feed live (dedup sul timestamp). La ri-validazione ha mostrato che l'edge SH01
|
||||
richiede il training EXPANDING full-history: col solo lookback live (365g) la
|
||||
LogReg e' over-confident e la strategia NON e' robusta. Se c'e' un gap fra
|
||||
parquet e feed (parquet stantio oltre il lookback) si usa il SOLO feed con
|
||||
WARN: meglio il regime corto dichiarato che una serie con un buco."""
|
||||
if hist is None or live is None or live.empty:
|
||||
return live
|
||||
lo = int(live["timestamp"].iloc[0])
|
||||
h = hist[hist["timestamp"] < lo]
|
||||
if h.empty:
|
||||
return live
|
||||
if int(h["timestamp"].iloc[-1]) < lo - 2 * 3_600_000: # buco > 1 barra 1h
|
||||
if warned is not None and asset not in warned:
|
||||
warned.add(asset)
|
||||
print(f"[runner] WARN: gap fra parquet e feed live per {asset} "
|
||||
f"(parquet stantio? rilanciare download_all) — SH01 senza bootstrap")
|
||||
return live
|
||||
return pd.concat([h[_OHLCV], live[_OHLCV]], ignore_index=True)
|
||||
|
||||
|
||||
def _resample(df: pd.DataFrame, tf: str) -> pd.DataFrame:
|
||||
"""Resampla candele 1h -> 4h/1d mantenendo timestamp ms reale (come get_df del backtest)."""
|
||||
if tf == "1h":
|
||||
@@ -247,6 +273,21 @@ def run(config_path: str = "portfolios.yml"):
|
||||
if ps_family:
|
||||
print(f"[runner] position_size globale={position_size} override famiglia={ps_family}")
|
||||
|
||||
# bootstrap storia full per gli sleeve ML (SH01): parquet locale + feed live.
|
||||
# L'edge SH01 richiede train expanding full-history (sh01_trainwindow_validate);
|
||||
# il path live fitta solo l'ultimo blocco (last_block_only nei params SHAPE).
|
||||
ml_hist: dict[str, pd.DataFrame] = {}
|
||||
ml_gap_warned: set[str] = set()
|
||||
for a in sorted({s.asset for s in live_specs if s.kind == "ml"}):
|
||||
try:
|
||||
from src.data.downloader import load_data
|
||||
ml_hist[a] = load_data(a, "1h")
|
||||
last = pd.to_datetime(ml_hist[a]["timestamp"].iloc[-1], unit="ms", utc=True)
|
||||
print(f"[runner] bootstrap storia SH01 {a}: {len(ml_hist[a])} barre parquet "
|
||||
f"(fino a {last:%Y-%m-%d %H:%M})")
|
||||
except Exception as e:
|
||||
print(f"[runner] WARN bootstrap storia {a} fallito: {e} — SH01 col solo feed")
|
||||
|
||||
# lookback (giorni) richiesto per ogni asset = max sui worker che lo usano
|
||||
asset_days: dict[str, int] = {}
|
||||
for s in live_specs:
|
||||
@@ -322,9 +363,13 @@ def run(config_path: str = "portfolios.yml"):
|
||||
w.tick(res[s.a], res[s.b])
|
||||
elif s.kind in _MULTI_KINDS:
|
||||
w.tick(res)
|
||||
elif s.kind == "ml":
|
||||
# SH01: storia full (parquet bootstrap + feed) -> il walk-forward
|
||||
# interno fitta solo l'ultimo blocco (last_block_only nei params).
|
||||
w.tick(_with_history(ml_hist.get(s.asset), res[s.asset],
|
||||
ml_gap_warned, s.asset))
|
||||
else:
|
||||
# single (fade/dip) e ml (SH01): StrategyWorker. SH01 retraina dentro
|
||||
# generate_signals (walk-forward) -> nessun training esterno.
|
||||
# single (fade/dip): StrategyWorker su feed live.
|
||||
w.tick(res[s.asset])
|
||||
|
||||
ledger.update_equity({sid: _worker_equity(wk) for sid, wk in workers.items()})
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
"""SH01 path live: last_block_only == tail del walk-forward completo (parity by
|
||||
construction) + merge storia parquet/feed del runner."""
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import pytest
|
||||
|
||||
from src.portfolio.runner import _with_history
|
||||
|
||||
|
||||
def _df(n=6500, seed=0, t0=1_600_000_000_000):
|
||||
rng = np.random.default_rng(seed)
|
||||
c = 100 * np.exp(np.cumsum(rng.normal(0, 0.01, n)))
|
||||
h = c * (1 + np.abs(rng.normal(0, 0.004, n)))
|
||||
l = c * (1 - np.abs(rng.normal(0, 0.004, n)))
|
||||
o = np.roll(c, 1); o[0] = c[0]
|
||||
ts = t0 + 3_600_000 * np.arange(n)
|
||||
return pd.DataFrame({"timestamp": ts, "open": o, "high": h, "low": l,
|
||||
"close": c, "volume": 1.0})
|
||||
|
||||
|
||||
def test_last_block_only_matches_full_wf_tail():
|
||||
from scripts.analysis.shape_ml_research import ml_wf_entries
|
||||
df = _df()
|
||||
cfg = dict(W=24, H=12, model="logit", thresh=0.55, train_min=4000, retrain=500)
|
||||
full = ml_wf_entries(df, **cfg)
|
||||
last = ml_wf_entries(df, last_block_only=True, **cfg)
|
||||
n = len(df)
|
||||
# confine dell'ultimo blocco: start + k*retrain (deterministico)
|
||||
start = max(cfg["train_min"], cfg["W"] - 1)
|
||||
B = start
|
||||
while B + cfg["retrain"] < n - 1:
|
||||
B += cfg["retrain"]
|
||||
full_tail = [e for e in full if e["i"] >= B]
|
||||
assert last == full_tail # identita' esatta, non solo simile
|
||||
assert all(e["i"] >= B for e in last)
|
||||
|
||||
|
||||
def test_with_history_merges_contiguous():
|
||||
hist = _df(n=100)
|
||||
live = _df(n=50, t0=int(hist["timestamp"].iloc[60])) # overlap: live parte dentro hist
|
||||
out = _with_history(hist, live)
|
||||
assert len(out) == 60 + 50 # hist pre-overlap + live completo
|
||||
ts = out["timestamp"].values
|
||||
assert (np.diff(ts) > 0).all() # serie strettamente crescente
|
||||
|
||||
|
||||
def test_with_history_gap_falls_back_to_live():
|
||||
hist = _df(n=100)
|
||||
gap_start = int(hist["timestamp"].iloc[-1]) + 10 * 3_600_000 # buco di 10 barre
|
||||
live = _df(n=50, t0=gap_start)
|
||||
warned = set()
|
||||
out = _with_history(hist, live, warned, "BTC")
|
||||
assert len(out) == 50 and "BTC" in warned # solo feed + warn deduplicato
|
||||
out2 = _with_history(hist, live, warned, "BTC")
|
||||
assert len(out2) == 50 # secondo giro: nessun nuovo warn
|
||||
|
||||
|
||||
def test_with_history_none_hist_passthrough():
|
||||
live = _df(n=50)
|
||||
assert _with_history(None, live) is live
|
||||
Reference in New Issue
Block a user