Files
Axel HocksandClaude Opus 4.8 75d28827e8 Initial commit: Oil Trading Bot (MT5, WTI)
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>
2026-07-24 08:29:23 +02:00

101 lines
4.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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}