Files

208 lines
7.7 KiB
Python

"""
forcaster_dmm_patch.py — Data Management Module integration for forcasterv21.
This module provides drop-in replacement functions for the forecaster's
fetch_daily_data() and fetch_intraday_data() that route through the DMM
instead of calling MT5 directly.
INTEGRATION (3 steps in forcasterv21_institutional_anchors.py):
─────────────────────────────────────────────────────────────────
STEP 1 — Add import near line 28 (after other module imports):
from data_manager import DataManager
STEP 2 — Replace the fetch functions (lines 66-93) with:
# ==========================================
# 📊 DATA INGESTION (via Data Management Module)
# ==========================================
_dm = None # Initialized in __main__ after MT5 init
def fetch_daily_data(ticker, days_back=1500):
if _dm is None:
return _fetch_daily_data_legacy(ticker, days_back)
df = _dm.get_bars_as_forecaster(ticker, "D1", n_bars=days_back)
return df
def fetch_intraday_data(ticker, start_time, end_time=None):
if _dm is None:
return _fetch_intraday_data_legacy(ticker, start_time, end_time)
df = _dm.get_bars_as_forecaster(ticker, "M15", n_bars=20000)
if df is None: return None
df = df[df.index >= start_time]
if end_time is not None:
df = df[df.index <= end_time]
return df
# Legacy fallback (direct MT5) — used if DMM init fails
def _fetch_daily_data_legacy(ticker, days_back=1500):
if not mt5.initialize(path=MT5_TERMINAL_PATH):
print(f"[!] MT5 Init failed for {ticker}: {mt5.last_error()}")
return None
rates = mt5.copy_rates_from_pos(ticker, mt5.TIMEFRAME_D1, 0, days_back)
if rates is None or len(rates) == 0: return None
df = pd.DataFrame(rates)
df['time'] = pd.to_datetime(df['time'], unit='s')
df.rename(columns={'open': 'Open', 'high': 'High', 'low': 'Low',
'close': 'Close', 'tick_volume': 'Volume'}, inplace=True)
df.set_index('time', inplace=True)
return df
def _fetch_intraday_data_legacy(ticker, start_time, end_time=None):
rates = mt5.copy_rates_from_pos(ticker, mt5.TIMEFRAME_M15, 0, 20000)
if rates is None or len(rates) == 0: return None
df = pd.DataFrame(rates)
df['time'] = pd.to_datetime(df['time'], unit='s')
df.rename(columns={'open': 'Open', 'high': 'High', 'low': 'Low',
'close': 'Close'}, inplace=True)
df.set_index('time', inplace=True)
df = df[df.index >= start_time]
if end_time is not None:
df = df[df.index <= end_time]
return df
STEP 3 — In __main__ block (after MT5 init, ~line 1855), add DMM initialization:
if __name__ == "__main__":
if not mt5.initialize(path=MT5_TERMINAL_PATH):
print("[!] MT5 Init failed.")
quit()
# >>> NEW: Initialize Data Management Module <<<
try:
_dm = DataManager(mt5_terminal_path=MT5_TERMINAL_PATH)
print(f"[DMM] Data library: {_dm.library_root} | Engine: {_dm.store.engine}")
except Exception as e:
print(f"[DMM] Init failed ({e}) — falling back to direct MT5")
_dm = None
# ... rest of __main__ unchanged ...
That's it. Three changes. Everything else stays the same.
"""
# ─── Standalone helper for quick integration testing ─────────────────────────
# You can run this file directly to test DMM + forecaster compatibility:
# python forcaster_dmm_patch.py
import sys
from pathlib import Path
def test_integration():
"""Test that DMM produces forecaster-compatible DataFrames."""
sys.path.insert(0, str(Path(__file__).resolve().parent))
from data_manager import DataManager
from datetime import datetime, timezone
import pandas as pd
import numpy as np
print("=" * 60)
print(" FORECASTER ↔ DMM INTEGRATION TEST")
print("=" * 60)
dm = DataManager(
library_root=Path(__file__).resolve().parent / "_test_fcst_lib",
cache_enabled=True,
)
# Inject mock to simulate MT5
class MockMT5Fetcher:
name = "mock_mt5"
def fetch_bars(self, symbol, timeframe, date_from, date_to):
n = max(1, (date_to - date_from).days + 1)
return _make_synthetic(date_from, n)
def fetch_bars_n(self, symbol, timeframe, n_bars):
return _make_synthetic(datetime(2020, 1, 1, tzinfo=timezone.utc), n_bars)
def is_available(self):
return True
def _make_synthetic(start, n):
if hasattr(start, 'tzinfo') and start.tzinfo is None:
start = start.replace(tzinfo=timezone.utc)
from datetime import timedelta
dates = pd.DatetimeIndex(
[start + timedelta(days=i) for i in range(n)],
name="time"
)
rng = np.random.default_rng(42)
close = 1800.0 + np.cumsum(rng.normal(0, 10, n))
return pd.DataFrame({
"open": close + rng.normal(0, 2, n),
"high": close + np.abs(rng.normal(0, 5, n)),
"low": close - np.abs(rng.normal(0, 5, n)),
"close": close,
"tick_volume": rng.integers(100, 5000, n),
"spread": rng.integers(1, 30, n),
"real_volume": np.zeros(n, dtype=int),
}, index=dates)
dm._fetcher = MockMT5Fetcher()
passed = 0
failed = 0
# Test 1: get_bars_as_forecaster matches expected schema
df = dm.get_bars_as_forecaster("XAUUSD", "D1", n_bars=1500)
if df is not None and all(c in df.columns for c in ["Open", "High", "Low", "Close", "Volume"]):
print(" ✓ Column names: Open, High, Low, Close, Volume")
passed += 1
else:
print(f" ✗ Column names wrong: {df.columns.tolist() if df is not None else 'None'}")
failed += 1
if df is not None and df.index.tz is None:
print(" ✓ Timezone-naive index (forecaster compat)")
passed += 1
else:
print(" ✗ Index has timezone (forecaster expects tz-naive)")
failed += 1
if df is not None and len(df) == 1500:
print(" ✓ Correct row count (1500)")
passed += 1
else:
print(f" ✗ Row count: {len(df) if df is not None else 'None'}")
failed += 1
if df is not None and isinstance(df.index, pd.DatetimeIndex):
print(" ✓ DatetimeIndex preserved")
passed += 1
else:
print(" ✗ Index is not DatetimeIndex")
failed += 1
# Test 2: Intraday M15 with date filtering
from datetime import timedelta
df15 = dm.get_bars_as_forecaster("XAUUSD", "M15", n_bars=500)
if df15 is not None and len(df15) == 500:
print(" ✓ M15 intraday fetch (500 bars)")
passed += 1
else:
print(f" ✗ M15 fetch: {len(df15) if df15 is not None else 'None'}")
failed += 1
# Test 3: Second fetch is faster (from cache)
import time
t0 = time.time()
dm.get_bars_as_forecaster("XAUUSD", "D1", n_bars=1500)
elapsed = time.time() - t0
print(f" ✓ Cached D1 fetch: {elapsed*1000:.0f}ms" if elapsed < 1 else f" ⚠ Cached fetch slow: {elapsed:.1f}s")
passed += 1
# Cleanup
import shutil
test_lib = Path(__file__).resolve().parent / "_test_fcst_lib"
if test_lib.exists():
shutil.rmtree(test_lib)
print(f"\n RESULTS: {passed} passed, {failed} failed")
print("=" * 60)
return failed == 0
if __name__ == "__main__":
success = test_integration()
sys.exit(0 if success else 1)