e095f9be7e
Signed-off-by: TIANHE <TIANHE@GMAIL.COM>
387 lines
12 KiB
Python
387 lines
12 KiB
Python
"""
|
|
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
|