173 lines
6.0 KiB
Python
173 lines
6.0 KiB
Python
"""
|
|
================================================================================
|
|
Quantum Terminal — Data Server Configuration
|
|
================================================================================
|
|
Central config for the FastAPI data server (Phase 3C).
|
|
|
|
All paths are relative to PROJECT_ROOT for portability (Rule 3).
|
|
MT5 terminal path is read from local_config.ini (gitignored, Rule 3).
|
|
Universe must match the orchestrator's ASSET_UNIVERSE.
|
|
|
|
Usage:
|
|
from server_config import ServerConfig, PROJECT_ROOT
|
|
config = ServerConfig()
|
|
================================================================================
|
|
"""
|
|
|
|
import configparser
|
|
from pathlib import Path
|
|
from dataclasses import dataclass, field
|
|
from typing import List, Optional, Dict
|
|
|
|
# ── Portable project root (Rule 3) ──
|
|
PROJECT_ROOT = Path(__file__).resolve().parent
|
|
|
|
|
|
# ── Load local_config.ini for machine-specific values (Rule 3) ──
|
|
def _load_mt5_path() -> Optional[str]:
|
|
"""
|
|
Read MT5 terminal path from local_config.ini.
|
|
Falls back to None (MT5 auto-detect) if file or key is missing.
|
|
|
|
Expected format in local_config.ini:
|
|
[mt5]
|
|
terminal_path = C:/Program Files/MetaTrader 5/terminal64.exe
|
|
"""
|
|
ini_path = PROJECT_ROOT / "local_config.ini"
|
|
if not ini_path.exists():
|
|
return None
|
|
cp = configparser.ConfigParser()
|
|
cp.read(str(ini_path))
|
|
return cp.get("mt5", "terminal_path", fallback=None)
|
|
|
|
|
|
MT5_TERMINAL_PATH = _load_mt5_path()
|
|
|
|
|
|
# ── Asset universe — must stay in sync with orchestrator.py ──
|
|
ASSET_UNIVERSE = [
|
|
# Forex
|
|
"EURUSD", "GBPUSD", "USDJPY", "AUDUSD", "USDCHF", "USDCAD",
|
|
# Indices
|
|
"US500", "USTEC", "GER40", "UK100",
|
|
# Commodities
|
|
"XAUUSD", "XAGUSD", "XTIUSD",
|
|
]
|
|
|
|
|
|
# ── Symbol alias map ──
|
|
# Canonical name → list of possible broker names to try (in order).
|
|
# The adapter tries each until one resolves in the connected MT5 terminal.
|
|
# Only needed for symbols whose canonical name doesn't match the broker.
|
|
# Forex pairs (EURUSD etc.) are universal and don't need aliases.
|
|
SYMBOL_ALIASES: Dict[str, List[str]] = {
|
|
"US500": ["US500", "US500.cash", "SPX500", "SP500", "US500Cash", "USA500"],
|
|
"USTEC": ["USTEC", "USTEC.cash", "NAS100", "NSDQ100", "USTECCash", "USTECH"],
|
|
"GER40": ["GER40", "GER40.cash", "DAX40", "DE40", "GER40Cash", "GDAXI"],
|
|
"UK100": ["UK100", "UK100.cash", "FTSE100", "UK100Cash"],
|
|
"XTIUSD": ["XTIUSD", "USOIL", "WTI", "XTIUSD.cash", "USOIL.cash", "CL"],
|
|
"XBRUSD": ["XBRUSD", "UKOIL", "BRENT", "XBRUSD.cash", "UKOIL.cash"],
|
|
"XAUUSD": ["XAUUSD", "GOLD", "XAUUSD.cash"],
|
|
"XAGUSD": ["XAGUSD", "SILVER", "XAGUSD.cash"],
|
|
}
|
|
|
|
|
|
# ── MT5 timeframe mapping (string → MT5 constant) ──
|
|
# These are the actual MetaTrader5 Python API constants.
|
|
# M1-M30 happen to equal their minute value, but H1+ use encoded values.
|
|
MT5_TIMEFRAMES = {
|
|
"M1": 1, # mt5.TIMEFRAME_M1
|
|
"M5": 5, # mt5.TIMEFRAME_M5
|
|
"M15": 15, # mt5.TIMEFRAME_M15
|
|
"M30": 30, # mt5.TIMEFRAME_M30
|
|
"H1": 16385, # mt5.TIMEFRAME_H1
|
|
"H4": 16388, # mt5.TIMEFRAME_H4
|
|
"D1": 16408, # mt5.TIMEFRAME_D1
|
|
"W1": 32769, # mt5.TIMEFRAME_W1
|
|
}
|
|
|
|
|
|
# ── Decimal precision per asset class ──
|
|
ASSET_DECIMALS = {
|
|
# Forex — 5 decimal places (pipettes)
|
|
"EURUSD": 5, "GBPUSD": 5, "AUDUSD": 5, "USDCHF": 5, "USDCAD": 5,
|
|
"USDJPY": 3,
|
|
# Indices — 2 decimal places
|
|
"US500": 2, "USTEC": 2, "GER40": 2, "UK100": 2,
|
|
# Commodities
|
|
"XAUUSD": 2, "XAGUSD": 3, "XTIUSD": 2,
|
|
}
|
|
|
|
|
|
# ── Asset class classification ──
|
|
ASSET_CLASSES = {
|
|
"EURUSD": "FX", "GBPUSD": "FX", "USDJPY": "FX",
|
|
"AUDUSD": "FX", "USDCHF": "FX", "USDCAD": "FX",
|
|
"US500": "INDEX", "USTEC": "INDEX", "GER40": "INDEX", "UK100": "INDEX",
|
|
"XAUUSD": "COMMODITY", "XAGUSD": "COMMODITY",
|
|
"XTIUSD": "COMMODITY",
|
|
# Futures (Rithmic)
|
|
"ES": "FUTURES", "NQ": "FUTURES", "YM": "FUTURES",
|
|
"GC": "FUTURES", "SI": "FUTURES", "CL": "FUTURES", "BZ": "FUTURES",
|
|
}
|
|
|
|
|
|
@dataclass
|
|
class ServerConfig:
|
|
"""
|
|
Master configuration for the data server.
|
|
All file paths are relative to PROJECT_ROOT.
|
|
MT5 terminal path comes from local_config.ini (Rule 3).
|
|
"""
|
|
|
|
# ── Network ──
|
|
host: str = "127.0.0.1"
|
|
port: int = 8501
|
|
|
|
# ── MT5 ──
|
|
mt5_terminal_path: Optional[str] = field(default_factory=lambda: MT5_TERMINAL_PATH)
|
|
|
|
# ── Universe ──
|
|
universe: List[str] = field(default_factory=lambda: list(ASSET_UNIVERSE))
|
|
|
|
# ── MT5 polling intervals (seconds) ──
|
|
tick_interval: float = 1.0 # How often to poll latest ticks
|
|
bar_check_interval: float = 5.0 # How often to check for new closed bars
|
|
|
|
# ── State file paths (relative to PROJECT_ROOT) ──
|
|
terminal_payload: str = "terminal_payload.json"
|
|
daily_signals: str = "daily_signals.json"
|
|
signal_lifecycle: str = "signal_lifecycle.json"
|
|
weekly_state: str = "weekly_state.json"
|
|
bands_data_dir: str = "bands_data"
|
|
|
|
# ── Display defaults ──
|
|
default_timeframe: str = "M15"
|
|
default_lookback_bars: int = 200
|
|
|
|
# ── Frontend CORS ──
|
|
cors_origins: List[str] = field(default_factory=lambda: [
|
|
"http://localhost:3000",
|
|
"http://127.0.0.1:3000",
|
|
])
|
|
|
|
# ── Reconnection ──
|
|
mt5_reconnect_delay: float = 5.0 # Seconds between reconnect attempts
|
|
mt5_max_reconnect_attempts: int = 10 # Then log error, keep trying
|
|
|
|
# ── File watcher ──
|
|
file_watch_debounce: float = 1.0 # Seconds to debounce rapid file changes
|
|
|
|
def resolve_path(self, relative: str) -> Path:
|
|
"""Resolve a config path relative to PROJECT_ROOT."""
|
|
return PROJECT_ROOT / relative
|
|
|
|
@property
|
|
def watched_files(self) -> List[Path]:
|
|
"""All state files that the file watcher should monitor."""
|
|
return [
|
|
self.resolve_path(self.terminal_payload),
|
|
self.resolve_path(self.daily_signals),
|
|
self.resolve_path(self.signal_lifecycle),
|
|
self.resolve_path(self.weekly_state),
|
|
] |