Files
silencesdg 430376eb61 feat: 一票制并发锁 + MT5硬止损兜底
- 一票制: _pending_long/_pending_short 计数器防同一周期内多信号穿透
- 硬止损: MT5下单时附带sl/tp,倍率1.5x(止损)/1.3x(止盈),比EA软止损更宽
- config.py: 新增 hard_sl_multiplier/hard_tp_multiplier
- 全部 send_order 链路(abc/remote/live/server/dryrun/backtest) 支持 sl/tp 参数
- 对冲模块: 彻底移除
- 日志: 去重+30轮摘要
2026-05-14 14:07:13 +08:00

169 lines
4.4 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""MT5 代理服务 — python -m run.server"""
import sys
import os
import threading
from contextlib import asynccontextmanager
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from fastapi import FastAPI
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from core.data import LiveDataProvider
from core.data.serializers import (
serialize_account_info,
serialize_position,
serialize_symbol_info,
serialize_order_result,
serialize_rates,
)
from config import SERVER_HOST, SERVER_PORT
from logger import setup_logger, logger
setup_logger("INFO")
# 全局 MT5 连接和线程锁
_provider = LiveDataProvider()
_lock = threading.Lock()
def _ensure_connected():
"""检查 MT5 连接,断开时自动重连"""
import MetaTrader5 as mt5
if mt5.terminal_info() is not None:
return True
logger.warning("MT5 连接已断开,尝试重连...")
with _lock:
ok = _provider.initialize()
if ok:
logger.info("MT5 重连成功")
else:
logger.error("MT5 重连失败")
return ok
@asynccontextmanager
async def lifespan(app: FastAPI):
with _lock:
if not _provider.initialize():
logger.error("MT5 初始化失败,服务启动中止")
sys.exit(1)
logger.info(f"MT5 代理服务启动于 {SERVER_HOST}:{SERVER_PORT}")
yield
with _lock:
_provider.shutdown()
logger.info("MT5 代理服务已关闭")
app = FastAPI(title="MT5 Proxy API", lifespan=lifespan)
# ── 请求模型 ──
class OrderRequest(BaseModel):
symbol: str
order_type: str
volume: float
sl: float = None # ★ 硬止损价格
tp: float = None # ★ 硬止盈价格
class CloseRequest(BaseModel):
ticket: int
symbol: str
volume: float
# ── 端点 ──
@app.post("/api/initialize")
def api_initialize():
with _lock:
return {"success": _provider.initialize()}
@app.post("/api/shutdown")
def api_shutdown():
with _lock:
_provider.shutdown()
return {"success": True}
@app.get("/api/price/{symbol}")
def api_price(symbol: str):
_ensure_connected()
with _lock:
data = _provider.get_current_price(symbol)
if data is None:
return JSONResponse(status_code=503, content={"error": "无法获取价格"})
# time 转为 Unix 时间戳
data['time'] = int(data['time'].timestamp()) if hasattr(data['time'], 'timestamp') else int(data['time'])
return data
@app.get("/api/historical/{symbol}/{timeframe}/{count}")
def api_historical(symbol: str, timeframe: int, count: int):
_ensure_connected()
with _lock:
rates = _provider.get_historical_data(symbol, timeframe, count)
if rates is None:
return JSONResponse(status_code=503, content={"error": "无法获取历史数据"})
return {"rates": serialize_rates(rates)}
@app.get("/api/account")
def api_account():
_ensure_connected()
with _lock:
info = _provider.get_account_info()
if info is None:
return JSONResponse(status_code=503, content={"error": "无法获取账户信息"})
return serialize_account_info(info)
@app.get("/api/positions/{symbol}")
def api_positions(symbol: str):
_ensure_connected()
with _lock:
positions = _provider.get_positions(symbol)
if positions is None:
return {"positions": []}
return {"positions": [serialize_position(p) for p in positions]}
@app.get("/api/symbol/{symbol}")
def api_symbol(symbol: str):
_ensure_connected()
with _lock:
info = _provider.get_symbol_info(symbol)
if info is None:
return JSONResponse(status_code=503, content={"error": "无法获取品种信息"})
return serialize_symbol_info(info)
@app.post("/api/order")
def api_order(req: OrderRequest):
_ensure_connected()
with _lock:
result = _provider.send_order(req.symbol, req.order_type, req.volume,
sl=req.sl, tp=req.tp)
if result is None:
return JSONResponse(status_code=503, content={"error": "下单失败"})
return serialize_order_result(result)
@app.post("/api/close")
def api_close(req: CloseRequest):
_ensure_connected()
with _lock:
success = _provider.close_position(req.ticket, req.symbol, req.volume)
return {"success": success}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host=SERVER_HOST, port=SERVER_PORT)