- simulation.py: price-series loader (Yahoo snapshot) + allocate_capital 50/20/30 with min-100 shares, bucket3 excludes bucket1, cash fallback - /api/v1/simulation POST: combines theme 60/40 score + Siamchart dividend + Yahoo price; labels output paper/backtest non-PIT (never validated) - mt5_bridge.py: MT5 order interface, dry-run default; live dispatch needs MT5_SEND_ORDERS=1 AND approval (Windows-only MetaTrader5) - 12 new tests (simulation/mt5/api); full suite 184 OK; live verified (1M -> buckets)
110 lines
3.4 KiB
Python
110 lines
3.4 KiB
Python
"""MT5 order bridge — interface only; LIVE dispatch is strictly gated off.
|
|
|
|
The forward test shares the backtest engine and may *optionally* dispatch orders
|
|
to MetaTrader 5. This module provides the bridge *interface* with a structural
|
|
kill switch: no real order path exists in code unless BOTH of these hold:
|
|
|
|
1. the environment flag MT5_ENABLE_ORDER=1, AND
|
|
2. explicit per-run approval is passed.
|
|
|
|
`MetaTrader5` is a Windows-only third-party module; it is imported lazily and
|
|
guarded so the rest of the platform (and tests) run fine without it. On non-
|
|
Windows (or when not installed) the bridge reports `unavailable` rather than
|
|
failing the platform.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from dataclasses import dataclass
|
|
from typing import Optional
|
|
|
|
|
|
class MT5UnavailableError(Exception):
|
|
"""Raised when MetaTrader5 is not available / connected."""
|
|
|
|
|
|
@dataclass
|
|
class MT5OrderRequest:
|
|
symbol: str
|
|
action: str # BUY / SELL
|
|
lots: float
|
|
price: Optional[float] = None
|
|
sl: Optional[float] = None
|
|
tp: Optional[float] = None
|
|
|
|
|
|
@dataclass
|
|
class MT5OrderResult:
|
|
symbol: str
|
|
ticket: int
|
|
executed: bool
|
|
dry_run: bool
|
|
message: str = ""
|
|
|
|
|
|
def _mt5():
|
|
"""Return the MetaTrader5 module or None (Windows-only + installed)."""
|
|
try:
|
|
import MetaTrader5 as mt5 # type: ignore
|
|
return mt5
|
|
except ImportError:
|
|
return None
|
|
|
|
|
|
def is_available() -> bool:
|
|
return _mt5() is not None
|
|
|
|
|
|
def enabled() -> bool:
|
|
"""Live dispatch is enabled only when MT5_SEND_ORDERS=1 is set explicitly."""
|
|
return os.environ.get("MT5_SEND_ORDERS", "").strip() == "1"
|
|
|
|
|
|
def place_order(req: MT5OrderRequest, approve: bool = False) -> MT5OrderResult:
|
|
"""Place an order.
|
|
|
|
- Without `enabled()` (flag) or explicit approval, this is a DRY-RUN: no
|
|
order is sent; we return an order-shaped result with executed=False.
|
|
- The caller (simulation forward mode) decides whether to actually call the
|
|
live path; default is dry-run. This module never self-authorizes.
|
|
"""
|
|
if not (enabled() and approve):
|
|
return MT5OrderResult(req.symbol, 0, False, dry_run=True,
|
|
message="live dispatch gated: MT5_SEND_ORDERS=1 AND approval required")
|
|
|
|
mt5 = _mt5()
|
|
if mt5 is None:
|
|
return MT5OrderResult(req.symbol, 0, False, dry_run=True,
|
|
message="MetaTrader5 not available (Windows-only module)")
|
|
|
|
if not mt5.initialize():
|
|
raise MT5UnavailableError(mt5.last_error())
|
|
|
|
symbol_info = mt5.symbol_info(req.symbol)
|
|
if symbol_info is None:
|
|
raise MT5UnavailableError(f"unknown symbol {req.symbol}")
|
|
|
|
request = {
|
|
"action": mt5.TRADE_ACTION_DEAL,
|
|
"symbol": req.symbol,
|
|
"volume": req.lots,
|
|
"type": mt5.ORDER_TYPE_BUY if req.action == "BUY" else mt5.ORDER_TYPE_SELL,
|
|
"price": req.price or symbol_info.ask if req.action == "BUY" else req.price or symbol_info.bid,
|
|
"sl": req.sl,
|
|
"tp": req.tp,
|
|
"deviation": 20,
|
|
"magic": 567890,
|
|
"comment": "SET50 alternative-data sim",
|
|
"type_time": mt5.ORDER_TIME_GTC,
|
|
"type_filling": mt5.ORDER_FILLING_IOC,
|
|
}
|
|
result = mt5.order_send(request)
|
|
return MT5OrderResult(
|
|
req.symbol,
|
|
ticket=result.order if result else 0,
|
|
executed=bool(result and result.retcode == mt5.TRADE_RETCODE_DONE),
|
|
dry_run=False,
|
|
message=str(result),
|
|
)
|