Headless FastAPI-Backend (server.py + core/engine.py) mit Mobile-PWA (web/), Strategie-/Backtest-Suite und Doku. Secrets, DB, Logs und Laufzeit-State sind via .gitignore ausgeschlossen; Config-Vorlage: oil_widget_config.ini.example. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
101 lines
4.2 KiB
Python
101 lines
4.2 KiB
Python
"""
|
||
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}
|