Supports MT5 and forex trading.
Signed-off-by: TIANHE <TIANHE@GMAIL.COM>
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
"""
|
||||
API 路由模块
|
||||
API Routes Module
|
||||
"""
|
||||
from flask import Flask
|
||||
|
||||
|
||||
def register_routes(app: Flask):
|
||||
"""注册所有 API 路由蓝图"""
|
||||
"""Register all API route blueprints"""
|
||||
from app.routes.kline import kline_bp
|
||||
from app.routes.analysis import analysis_bp
|
||||
from app.routes.backtest import backtest_bp
|
||||
@@ -20,9 +20,10 @@ def register_routes(app: Flask):
|
||||
from app.routes.settings import settings_bp
|
||||
from app.routes.portfolio import portfolio_bp
|
||||
from app.routes.ibkr import ibkr_bp
|
||||
from app.routes.mt5 import mt5_bp
|
||||
|
||||
app.register_blueprint(health_bp)
|
||||
app.register_blueprint(auth_bp, url_prefix='/api/user') # 兼容前端 /api/user/login
|
||||
app.register_blueprint(auth_bp, url_prefix='/api/user')
|
||||
app.register_blueprint(kline_bp, url_prefix='/api/indicator')
|
||||
app.register_blueprint(analysis_bp, url_prefix='/api/analysis')
|
||||
app.register_blueprint(backtest_bp, url_prefix='/api/indicator')
|
||||
@@ -35,4 +36,5 @@ def register_routes(app: Flask):
|
||||
app.register_blueprint(settings_bp, url_prefix='/api/settings')
|
||||
app.register_blueprint(portfolio_bp, url_prefix='/api/portfolio')
|
||||
app.register_blueprint(ibkr_bp, url_prefix='/api/ibkr')
|
||||
app.register_blueprint(mt5_bp, url_prefix='/api/mt5')
|
||||
|
||||
|
||||
@@ -0,0 +1,386 @@
|
||||
"""
|
||||
MetaTrader 5 Trading API Routes
|
||||
|
||||
Provides REST API for MT5 trading operations.
|
||||
"""
|
||||
|
||||
from flask import Blueprint, request, jsonify
|
||||
|
||||
from app.utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
mt5_bp = Blueprint("mt5", __name__)
|
||||
|
||||
# Lazy import MT5 client to avoid errors if not installed
|
||||
MT5Client = None
|
||||
MT5Config = None
|
||||
_client = None
|
||||
|
||||
|
||||
def _ensure_mt5_imports():
|
||||
"""Ensure MT5 modules are imported."""
|
||||
global MT5Client, MT5Config
|
||||
if MT5Client is None or MT5Config is None:
|
||||
try:
|
||||
from app.services.mt5_trading import MT5Client as _MT5Client, MT5Config as _MT5Config
|
||||
MT5Client = _MT5Client
|
||||
MT5Config = _MT5Config
|
||||
except ImportError as e:
|
||||
raise ImportError(
|
||||
"MT5 trading requires MetaTrader5 library. "
|
||||
"Run: pip install MetaTrader5\n"
|
||||
"Note: This library only works on Windows."
|
||||
) from e
|
||||
|
||||
|
||||
def _get_client():
|
||||
"""Get or create MT5 client instance."""
|
||||
global _client
|
||||
if _client is None:
|
||||
_ensure_mt5_imports()
|
||||
_client = MT5Client()
|
||||
return _client
|
||||
|
||||
|
||||
# ==================== Connection Management ====================
|
||||
|
||||
@mt5_bp.route("/status", methods=["GET"])
|
||||
def get_status():
|
||||
"""Get MT5 connection status."""
|
||||
try:
|
||||
_ensure_mt5_imports()
|
||||
client = _get_client()
|
||||
status = client.get_connection_status()
|
||||
return jsonify(status)
|
||||
except ImportError as e:
|
||||
return jsonify({
|
||||
"connected": False,
|
||||
"error": str(e),
|
||||
"hint": "MetaTrader5 library is not installed or not on Windows"
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Get MT5 status failed: {e}")
|
||||
return jsonify({"connected": False, "error": str(e)})
|
||||
|
||||
|
||||
@mt5_bp.route("/connect", methods=["POST"])
|
||||
def connect():
|
||||
"""
|
||||
Connect to MT5 terminal.
|
||||
|
||||
Request body:
|
||||
{
|
||||
"login": 12345678, // MT5 account number
|
||||
"password": "xxx", // MT5 password
|
||||
"server": "ICMarkets-Demo", // Broker server
|
||||
"terminal_path": "" // Optional: path to terminal64.exe
|
||||
}
|
||||
"""
|
||||
global _client
|
||||
|
||||
try:
|
||||
_ensure_mt5_imports()
|
||||
|
||||
data = request.get_json() or {}
|
||||
|
||||
login = data.get("login") or data.get("mt5_login")
|
||||
password = data.get("password") or data.get("mt5_password")
|
||||
server = data.get("server") or data.get("mt5_server")
|
||||
terminal_path = data.get("terminal_path") or data.get("mt5_terminal_path") or ""
|
||||
|
||||
if not login or not password or not server:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Missing required fields: login, password, server"
|
||||
}), 400
|
||||
|
||||
config = MT5Config(
|
||||
login=int(login),
|
||||
password=str(password),
|
||||
server=str(server),
|
||||
terminal_path=str(terminal_path),
|
||||
)
|
||||
|
||||
# Create new client with config
|
||||
_client = MT5Client(config)
|
||||
|
||||
if _client.connect():
|
||||
account_info = _client.get_account_info()
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"message": "Connected to MT5",
|
||||
"account": account_info
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Failed to connect to MT5. Check credentials and ensure terminal is running."
|
||||
}), 400
|
||||
|
||||
except ImportError as e:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}), 500
|
||||
except Exception as e:
|
||||
logger.error(f"MT5 connect failed: {e}")
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}), 500
|
||||
|
||||
|
||||
@mt5_bp.route("/disconnect", methods=["POST"])
|
||||
def disconnect():
|
||||
"""Disconnect from MT5 terminal."""
|
||||
global _client
|
||||
|
||||
try:
|
||||
if _client is not None:
|
||||
_client.disconnect()
|
||||
_client = None
|
||||
return jsonify({"success": True, "message": "Disconnected"})
|
||||
except Exception as e:
|
||||
logger.error(f"MT5 disconnect failed: {e}")
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
|
||||
# ==================== Account Queries ====================
|
||||
|
||||
@mt5_bp.route("/account", methods=["GET"])
|
||||
def get_account():
|
||||
"""Get account information."""
|
||||
try:
|
||||
client = _get_client()
|
||||
if not client.connected:
|
||||
return jsonify({"success": False, "error": "Not connected to MT5"}), 400
|
||||
|
||||
info = client.get_account_info()
|
||||
return jsonify(info)
|
||||
except Exception as e:
|
||||
logger.error(f"Get MT5 account failed: {e}")
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
|
||||
@mt5_bp.route("/positions", methods=["GET"])
|
||||
def get_positions():
|
||||
"""Get open positions."""
|
||||
try:
|
||||
client = _get_client()
|
||||
if not client.connected:
|
||||
return jsonify({"success": False, "error": "Not connected to MT5"}), 400
|
||||
|
||||
symbol = request.args.get("symbol")
|
||||
positions = client.get_positions(symbol=symbol)
|
||||
return jsonify({"success": True, "positions": positions})
|
||||
except Exception as e:
|
||||
logger.error(f"Get MT5 positions failed: {e}")
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
|
||||
@mt5_bp.route("/orders", methods=["GET"])
|
||||
def get_orders():
|
||||
"""Get pending orders."""
|
||||
try:
|
||||
client = _get_client()
|
||||
if not client.connected:
|
||||
return jsonify({"success": False, "error": "Not connected to MT5"}), 400
|
||||
|
||||
symbol = request.args.get("symbol")
|
||||
orders = client.get_orders(symbol=symbol)
|
||||
return jsonify({"success": True, "orders": orders})
|
||||
except Exception as e:
|
||||
logger.error(f"Get MT5 orders failed: {e}")
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
|
||||
@mt5_bp.route("/symbols", methods=["GET"])
|
||||
def get_symbols():
|
||||
"""Get available symbols."""
|
||||
try:
|
||||
client = _get_client()
|
||||
if not client.connected:
|
||||
return jsonify({"success": False, "error": "Not connected to MT5"}), 400
|
||||
|
||||
group = request.args.get("group", "*")
|
||||
symbols = client.get_symbols(group=group)
|
||||
return jsonify({"success": True, "symbols": symbols})
|
||||
except Exception as e:
|
||||
logger.error(f"Get MT5 symbols failed: {e}")
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
|
||||
# ==================== Trading ====================
|
||||
|
||||
@mt5_bp.route("/order", methods=["POST"])
|
||||
def place_order():
|
||||
"""
|
||||
Place an order.
|
||||
|
||||
Request body:
|
||||
{
|
||||
"symbol": "EURUSD",
|
||||
"side": "buy", // "buy" or "sell"
|
||||
"volume": 0.1, // Lot size
|
||||
"orderType": "market", // "market" or "limit"
|
||||
"price": 1.0800 // Required for limit orders
|
||||
}
|
||||
"""
|
||||
try:
|
||||
client = _get_client()
|
||||
if not client.connected:
|
||||
return jsonify({"success": False, "error": "Not connected to MT5"}), 400
|
||||
|
||||
data = request.get_json() or {}
|
||||
|
||||
symbol = data.get("symbol")
|
||||
side = data.get("side")
|
||||
volume = data.get("volume") or data.get("quantity")
|
||||
order_type = data.get("orderType", "market").lower()
|
||||
price = data.get("price")
|
||||
comment = data.get("comment", "QuantDinger")
|
||||
|
||||
if not symbol or not side or not volume:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Missing required fields: symbol, side, volume"
|
||||
}), 400
|
||||
|
||||
if order_type == "limit":
|
||||
if not price:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Limit order requires price"
|
||||
}), 400
|
||||
result = client.place_limit_order(
|
||||
symbol=symbol,
|
||||
side=side,
|
||||
volume=float(volume),
|
||||
price=float(price),
|
||||
comment=comment,
|
||||
)
|
||||
else:
|
||||
result = client.place_market_order(
|
||||
symbol=symbol,
|
||||
side=side,
|
||||
volume=float(volume),
|
||||
comment=comment,
|
||||
)
|
||||
|
||||
if result.success:
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"order_id": result.order_id,
|
||||
"deal_id": result.deal_id,
|
||||
"filled": result.filled,
|
||||
"price": result.price,
|
||||
"status": result.status,
|
||||
"message": result.message,
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": result.message
|
||||
}), 400
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"MT5 place order failed: {e}")
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
|
||||
@mt5_bp.route("/close", methods=["POST"])
|
||||
def close_position():
|
||||
"""
|
||||
Close a position.
|
||||
|
||||
Request body:
|
||||
{
|
||||
"ticket": 123456789, // Position ticket
|
||||
"volume": 0.1 // Optional: partial close volume
|
||||
}
|
||||
"""
|
||||
try:
|
||||
client = _get_client()
|
||||
if not client.connected:
|
||||
return jsonify({"success": False, "error": "Not connected to MT5"}), 400
|
||||
|
||||
data = request.get_json() or {}
|
||||
|
||||
ticket = data.get("ticket")
|
||||
volume = data.get("volume")
|
||||
|
||||
if not ticket:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Missing required field: ticket"
|
||||
}), 400
|
||||
|
||||
result = client.close_position(
|
||||
ticket=int(ticket),
|
||||
volume=float(volume) if volume else None,
|
||||
)
|
||||
|
||||
if result.success:
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"order_id": result.order_id,
|
||||
"deal_id": result.deal_id,
|
||||
"filled": result.filled,
|
||||
"price": result.price,
|
||||
"message": result.message,
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": result.message
|
||||
}), 400
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"MT5 close position failed: {e}")
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
|
||||
@mt5_bp.route("/order/<int:ticket>", methods=["DELETE"])
|
||||
def cancel_order(ticket: int):
|
||||
"""Cancel a pending order."""
|
||||
try:
|
||||
client = _get_client()
|
||||
if not client.connected:
|
||||
return jsonify({"success": False, "error": "Not connected to MT5"}), 400
|
||||
|
||||
if client.cancel_order(ticket):
|
||||
return jsonify({"success": True, "message": f"Order {ticket} cancelled"})
|
||||
else:
|
||||
return jsonify({"success": False, "error": "Failed to cancel order"}), 400
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"MT5 cancel order failed: {e}")
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
|
||||
# ==================== Market Data ====================
|
||||
|
||||
@mt5_bp.route("/quote", methods=["GET"])
|
||||
def get_quote():
|
||||
"""
|
||||
Get real-time quote.
|
||||
|
||||
Query params:
|
||||
- symbol: Trading symbol (e.g., EURUSD)
|
||||
"""
|
||||
try:
|
||||
client = _get_client()
|
||||
if not client.connected:
|
||||
return jsonify({"success": False, "error": "Not connected to MT5"}), 400
|
||||
|
||||
symbol = request.args.get("symbol")
|
||||
if not symbol:
|
||||
return jsonify({"success": False, "error": "Missing symbol parameter"}), 400
|
||||
|
||||
quote = client.get_quote(symbol)
|
||||
return jsonify(quote)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"MT5 get quote failed: {e}")
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
Reference in New Issue
Block a user