""" core/candle_logger.py — M1-Candle-Logger (Daten-Sammlung, KEIN Strategie-Eingriff) ================================================================================== Schreibt **abgeschlossene** M1-Kerzen (OHLC + Spread + Tick-Volumen) in eine eigene SQLite-Tabelle, damit über die Zeit ein tiefer M1-Datensatz entsteht — die Broker-M1-History ist kurz und rollt weg. M1 ist die Master-Auflösung: daraus lässt sich jede höhere TF (M5/M15/M30/H1) exakt aggregieren. Zeitbasis: `time` = ROHE MT5-Bar-Zeit (**Broker/UTC+3**), wie `copy_rates_from_pos` sie liefert — deckungsgleich mit allen Backtests. Nur ABGESCHLOSSENE Bars (die letzte, noch offene Kerze wird nie geschrieben). Idempotent via PRIMARY KEY + INSERT OR IGNORE, self-healing (jeder Fetch backfillt die letzten N Bars). """ from __future__ import annotations import sqlite3 import threading import time import MetaTrader5 as mt5 from core.mt5_utils import mt5_lock from core.logger import get_logger log = get_logger("candles") _REFRESH_S = 55.0 # M1 ändert sich je 60 s → ~55-s-Takt (kein Sub-Minuten-Fetch) _FETCH_N = 180 # je Fetch die letzten N M1-Bars (backfillt Lücken bis ~3 h) _STARTUP_N = 3000 # erster Lauf: tiefer holen (~2 Handelstage Backfill nach Restart) class CandleLogger: def __init__(self, db_path: str): self.db_path = db_path self._last = 0.0 self._first = True self._lock = threading.Lock() self._ensure() def _ensure(self): try: con = sqlite3.connect(self.db_path, timeout=5.0) con.execute("""CREATE TABLE IF NOT EXISTS candles_m1 ( time INTEGER PRIMARY KEY, -- Broker-Epoch (UTC+3), rohe MT5-Bar-Zeit o REAL, h REAL, l REAL, c REAL, spread REAL, -- in Preis (spread_points × point) tick_volume INTEGER, symbol TEXT )""") con.commit(); con.close() except Exception as e: log.warning(f"candles_m1 anlegen: {e}") def log(self, sym: str): """Im Trend-Loop aufgerufen; self-throttled auf ~55 s. Holt die letzten N M1-Bars und schreibt die abgeschlossenen (INSERT OR IGNORE = dedupliziert).""" if not sym: return now = time.time() with self._lock: if now - self._last < _REFRESH_S: return n = _STARTUP_N if self._first else _FETCH_N try: with mt5_lock(timeout=2) as got: if not got: return bars = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M1, 0, n) si = mt5.symbol_info(sym); point = si.point if si else 0.01 if bars is None or len(bars) < 2: return # letzte Bar ist noch OFFEN → weglassen (nur abgeschlossene schreiben) rows = [(int(b["time"]), float(b["open"]), float(b["high"]), float(b["low"]), float(b["close"]), round(float(b["spread"]) * point, 5), int(b["tick_volume"]), sym) for b in bars[:-1]] con = sqlite3.connect(self.db_path, timeout=5.0) before = con.execute("SELECT COUNT(*) FROM candles_m1").fetchone()[0] con.executemany( "INSERT OR IGNORE INTO candles_m1 (time,o,h,l,c,spread,tick_volume,symbol) " "VALUES (?,?,?,?,?,?,?,?)", rows) con.commit() ins = con.execute("SELECT COUNT(*) FROM candles_m1").fetchone()[0] - before con.close() with self._lock: self._last = now was_first = self._first self._first = False if was_first: log.info(f"M1-Candle-Logger: Start-Backfill +{ins} Bars ({sym})") elif ins > 0: log.debug(f"M1-Candles: +{ins}") except Exception as e: log.warning(f"candle log: {e}") def stats(self) -> dict: try: con = sqlite3.connect(self.db_path, timeout=5.0) r = con.execute("SELECT COUNT(*), MIN(time), MAX(time) FROM candles_m1").fetchone() con.close() return {"n": r[0] or 0, "first": r[1], "last": r[2]} except Exception: return {"n": 0}