fix(paper): ETH 5m allineato al tick + hardening GUI/compose
Bug principale: in scripts/run_paper_trading.py il fetch usava
end = now.replace(minute=0,...), troncando sempre all'ora. ETH è
dichiarato timeframe=5m (commit 23b7273) ma di fatto veniva
valutato 1 volta ogni 60 min — 502 poll del run 39e027df hanno
prodotto solo 43 evaluazioni/asset, tutte a HH:00. Il commento
in load_assets segnala esplicitamente che a 1h la strategia
perde -33% su 7y: regressione vs backtest.
Fix: helper _align_end_to_timeframe(now, timeframe) snappa end
al boundary nativo dell'asset. Mappa 1m/5m/15m/30m/1h/4h/1d.
Test regression in src/strategy_crypto/tests con 9 casi.
Hardening accessorio incluso nello stesso commit:
- docker-compose.yml: state/ in RW per strategy-crypto-gui
(SQLite WAL richiede SHM writable anche da reader).
- multi_swarm_core/dashboard/nicegui_app.py: ui.timer ora
deactivate on_disconnect su 3 pagine (index/convergence/genomes)
per evitare leak di timer dopo client disconnect.
- strategy_crypto/frontend/data.py: retry 5s su sqlite.connect
per cold-start race quando GUI parte prima del paper writer.
- state/validation-hardened-001.json: output WFA tooling
multi-fold del run phase1-hardened-001.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -33,6 +33,36 @@ from strategy_crypto.backend import PaperExecutor, PaperRepository, Portfolio
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
# Mapping timeframe stringa Cerbero -> minuti del bar. Le strategie tradano
|
||||
# sul "bar appena chiuso", quindi end deve essere snappato al boundary del
|
||||
# loro timeframe (NON sempre al top dell'ora) per evitare la regressione in
|
||||
# cui ETH 5m veniva valutato una volta sola ogni 60 min.
|
||||
_TIMEFRAME_MINUTES: dict[str, int] = {
|
||||
"1m": 1,
|
||||
"5m": 5,
|
||||
"15m": 15,
|
||||
"30m": 30,
|
||||
"1h": 60,
|
||||
"4h": 240,
|
||||
"1d": 1440,
|
||||
}
|
||||
|
||||
|
||||
def _align_end_to_timeframe(now: datetime, timeframe: str) -> datetime:
|
||||
"""Snap ``now`` al boundary del bar timeframe (UTC, naive seconds).
|
||||
|
||||
Es.: now=14:37:42, tf="5m" -> 14:35:00
|
||||
now=14:37:42, tf="1h" -> 14:00:00
|
||||
now=14:00:00, tf="1h" -> 14:00:00
|
||||
"""
|
||||
bar_min = _TIMEFRAME_MINUTES[timeframe]
|
||||
aligned = now.replace(second=0, microsecond=0)
|
||||
if bar_min >= 1440:
|
||||
return aligned.replace(hour=0, minute=0)
|
||||
total_min = aligned.hour * 60 + aligned.minute
|
||||
snapped = (total_min // bar_min) * bar_min
|
||||
return aligned.replace(hour=snapped // 60, minute=snapped % 60)
|
||||
|
||||
|
||||
def _default_strategies_dir() -> Path:
|
||||
"""Cartella JSON shippata col package strategy_crypto."""
|
||||
@@ -131,9 +161,12 @@ def main() -> None:
|
||||
now = datetime.now(UTC)
|
||||
last_prices: dict[str, float] = {}
|
||||
for asset, executor in zip(assets, executors, strict=True):
|
||||
# fetch OHLCV most recent lookback bars
|
||||
end = now.replace(minute=0, second=0, microsecond=0)
|
||||
start = end - timedelta(hours=args.lookback_bars + 1)
|
||||
# fetch OHLCV most recent lookback bars: end snappato al timeframe
|
||||
# dell'asset, non sempre all'ora (altrimenti ETH 5m veniva valutato
|
||||
# solo ogni 60 min, regressione vs backtest tunato 5m).
|
||||
bar_min = _TIMEFRAME_MINUTES[asset.timeframe]
|
||||
end = _align_end_to_timeframe(now, asset.timeframe)
|
||||
start = end - timedelta(minutes=bar_min * (args.lookback_bars + 1))
|
||||
req = OHLCVRequest(
|
||||
symbol=asset.symbol,
|
||||
timeframe=asset.timeframe,
|
||||
|
||||
Reference in New Issue
Block a user