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>
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
"""
|
||||
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}
|
||||
Reference in New Issue
Block a user