From e095f9be7ea576cb54db24b29e82ec0225c47e25 Mon Sep 17 00:00:00 2001 From: TIANHE Date: Tue, 13 Jan 2026 04:01:42 +0800 Subject: [PATCH] Supports MT5 and forex trading. Signed-off-by: TIANHE --- README.md | 31 +- README_CN.md | 31 +- README_JA.md | 31 +- README_KO.md | 31 +- README_TW.md | 31 +- backend_api_python/app/routes/__init__.py | 8 +- backend_api_python/app/routes/mt5.py | 386 +++++++++ .../app/services/exchange_execution.py | 4 +- .../app/services/live_trading/execution.py | 75 ++ .../app/services/live_trading/factory.py | 68 +- .../app/services/mt5_trading/README.md | 173 ++++ .../app/services/mt5_trading/__init__.py | 17 + .../app/services/mt5_trading/client.py | 783 ++++++++++++++++++ .../app/services/mt5_trading/symbols.py | 144 ++++ .../app/services/pending_order_worker.py | 234 ++++++ backend_api_python/requirements.txt | 4 +- docs/MT5_TRADING_GUIDE_CN.md | 215 +++++ docs/MT5_TRADING_GUIDE_EN.md | 215 +++++ quantdinger_vue/src/locales/lang/en-US.js | 21 + quantdinger_vue/src/locales/lang/zh-CN.js | 21 + .../src/views/trading-assistant/index.vue | 245 +++++- 21 files changed, 2717 insertions(+), 51 deletions(-) create mode 100644 backend_api_python/app/routes/mt5.py create mode 100644 backend_api_python/app/services/mt5_trading/README.md create mode 100644 backend_api_python/app/services/mt5_trading/__init__.py create mode 100644 backend_api_python/app/services/mt5_trading/client.py create mode 100644 backend_api_python/app/services/mt5_trading/symbols.py create mode 100644 docs/MT5_TRADING_GUIDE_CN.md create mode 100644 docs/MT5_TRADING_GUIDE_EN.md diff --git a/README.md b/README.md index 2a28f37..a7383d5 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,7 @@ QuantDinger includes a built-in **LLM-based multi-agent research system** that g ### Guides - [Python Strategy Development Guide](docs/STRATEGY_DEV_GUIDE.md) - [Interactive Brokers (IBKR) Trading Guide](docs/IBKR_TRADING_GUIDE_EN.md) 🆕 +- [MetaTrader 5 (MT5) Trading Guide](docs/MT5_TRADING_GUIDE_EN.md) 🆕 ### Notification Configuration - [Telegram Notification Setup](docs/NOTIFICATION_TELEGRAM_CONFIG_EN.md) @@ -165,8 +166,11 @@ QuantDinger includes a built-in **LLM-based multi-agent research system** that g 2. **Strategy Config**: Attach risk management rules (Position sizing, Stop-Loss, Take-Profit). 3. **Backtest & AI Optimization**: Run backtests, view rich performance metrics, and **let AI analyze the result to suggest improvements** (e.g., "Adjust MACD threshold to X"). 4. **Execution Mode**: - - **Live Trading**: Direct API execution for 10+ Crypto Exchanges (Binance, OKX, etc.). - - **Signal Notification**: For non-executable markets (Stocks/Forex/Futures), send signals via Telegram, Discord, Email, SMS, or Webhook. + - **Live Trading**: + - **Cryptocurrency**: Direct API execution for 10+ exchanges (Binance, OKX, Bitget, Bybit, etc.) + - **US/HK Stocks**: Via Interactive Brokers (IBKR) 🆕 + - **Forex**: Via MetaTrader 5 (MT5) 🆕 + - **Signal Notification**: For markets without live trading support (A-shares/Futures), send signals via Telegram, Discord, Email, SMS, or Webhook. ### 3. AI Multi-Agent Research *Your 24/7 AI Investment Committee.* @@ -292,19 +296,32 @@ Config lives in `.env` (see `backend_api_python/env.example`): `ENABLE_AGENT_MEM --- -## 🔌 Supported Exchanges +## 🔌 Supported Exchanges & Brokers -QuantDinger supports direct API connections to major cryptocurrency exchanges for execution, and uses CCXT for broad market data coverage. +QuantDinger supports multiple execution methods for different market types: -### Direct API Support +### Cryptocurrency Exchanges (Direct API) | Exchange | Markets | |:--------:|:---------| | Binance | Spot, Futures, Margin | | OKX | Spot, Perpetual, Options | | Bitget | Spot, Futures, Copy Trading | +| Bybit | Spot, Linear Futures | +| Coinbase Exchange | Spot | +| Kraken | Spot, Futures | +| KuCoin | Spot, Futures | +| Gate.io | Spot, Futures | +| Bitfinex | Spot, Derivatives | -### Also Supported via CCXT +### Traditional Brokers + +| Broker | Markets | Platform | +|:------:|:--------|:---------| +| **Interactive Brokers (IBKR)** | US Stocks, HK Stocks | TWS / IB Gateway 🆕 | +| **MetaTrader 5 (MT5)** | Forex | MT5 Terminal 🆕 | + +### Market Data (via CCXT) Bybit, Gate.io, Kraken, KuCoin, HTX, and 100+ other exchanges for market data. @@ -339,7 +356,7 @@ All UI elements, error messages, and documentation are fully translated. Languag | **US Stocks** | Yahoo Finance, Finnhub, Tiingo | ✅ Via IBKR 🆕 | | **HK Stocks** | AkShare, East Money | ✅ Via IBKR 🆕 | | **CN Stocks (A-shares)** | AkShare, East Money | ⚡ Data only | -| **Forex** | Finnhub, OANDA | ✅ Via broker API | +| **Forex** | Finnhub, OANDA | ✅ Via MT5 🆕 | | **Futures** | Exchange APIs, AkShare | ⚡ Data only | --- diff --git a/README_CN.md b/README_CN.md index 35aee55..68acdbe 100644 --- a/README_CN.md +++ b/README_CN.md @@ -97,6 +97,7 @@ QuantDinger 包含一个内置的**基于 LLM 的多智能体研究系统**, ### 开发指南 - [Python 策略开发指南](docs/STRATEGY_DEV_GUIDE_CN.md) - [盈透证券 (IBKR) 实盘交易指南](docs/IBKR_TRADING_GUIDE_CN.md) 🆕 +- [MetaTrader 5 (MT5) 外汇实盘交易指南](docs/MT5_TRADING_GUIDE_CN.md) 🆕 ### 通知配置 - [Telegram 通知配置](docs/NOTIFICATION_TELEGRAM_CONFIG_CH.md) @@ -165,8 +166,11 @@ QuantDinger 包含一个内置的**基于 LLM 的多智能体研究系统**, 2. **策略配置**:附加风险管理规则(仓位管理、止损、止盈)。 3. **回测 & AI 优化**:运行回测,查看丰富的性能指标,并**让 AI 分析结果以建议改进**(例如:“调整 MACD 阈值为 X”)。 4. **执行模式**: - - **实盘交易**:直接 API 执行,支持 10+ 加密货币交易所(Binance, OKX 等)。 - - **信号通知**:针对非实盘执行的市场(股票/外汇/期货),通过 Telegram, Discord, Email, SMS 或 Webhook 发送信号。 + - **实盘交易**: + - **加密货币**:直接 API 执行,支持 10+ 交易所(Binance, OKX, Bitget, Bybit 等) + - **美股/港股**:通过盈透证券 (IBKR) 🆕 + - **外汇**:通过 MetaTrader 5 (MT5) 🆕 + - **信号通知**:针对不支持实盘交易的市场(A股/期货),通过 Telegram, Discord, Email, SMS 或 Webhook 发送信号。 ### 3. AI 多智能体投研 *你的 7x24 小时 AI 投委会。* @@ -330,19 +334,32 @@ score = w_{sim}\cdot sim + w_{recency}\cdot recency + w_{returns}\cdot returns\_ --- -## 🔌 支持的交易所 +## 🔌 支持的交易所和券商 -QuantDinger 支持直接 API 连接到主要加密货币交易所进行执行,并使用 CCXT 获取广泛的行情数据。 +QuantDinger 支持多种市场类型的执行方式: -### 直接 API 支持 +### 加密货币交易所(直接 API) | 交易所 | 市场 | |:--------:|:---------| | Binance | 现货, 合约, 杠杆 | | OKX | 现货, 永续, 期权 | | Bitget | 现货, 合约, 跟单交易 | +| Bybit | 现货, 线性合约 | +| Coinbase Exchange | 现货 | +| Kraken | 现货, 合约 | +| KuCoin | 现货, 合约 | +| Gate.io | 现货, 合约 | +| Bitfinex | 现货, 衍生品 | -### 也支持通过 CCXT +### 传统券商 + +| 券商 | 市场 | 平台 | +|:------:|:--------|:---------| +| **盈透证券 (IBKR)** | 美股, 港股 | TWS / IB Gateway 🆕 | +| **MetaTrader 5 (MT5)** | 外汇 | MT5 终端 🆕 | + +### 行情数据(通过 CCXT) Bybit、Gate.io、Kraken、KuCoin、HTX 以及 100+ 其他交易所用于行情数据。 @@ -378,7 +395,7 @@ QuantDinger 为全球用户构建,提供全面的国际化支持: | **美股** | Yahoo Finance, Finnhub, Tiingo | ✅ 通过盈透证券 🆕 | | **港股** | AkShare, 东方财富 | ✅ 通过盈透证券 🆕 | | **A股** | AkShare, 东方财富 | ⚡ 仅数据 | -| **外汇** | Finnhub, OANDA | ✅ 通过券商 API | +| **外汇** | Finnhub, OANDA | ✅ 通过 MT5 🆕 | | **期货** | 交易所 API, AkShare | ⚡ 仅数据 | --- diff --git a/README_JA.md b/README_JA.md index 68d02ec..8315b15 100644 --- a/README_JA.md +++ b/README_JA.md @@ -97,6 +97,7 @@ QuantDingerには、ウェブから金融情報を収集し、ローカル市場 ### 開発ガイド - [Python 戦略開発ガイド](docs/STRATEGY_DEV_GUIDE_JA.md) - [Interactive Brokers (IBKR) トレーディングガイド](docs/IBKR_TRADING_GUIDE_EN.md) 🆕 +- [MetaTrader 5 (MT5) トレーディングガイド](docs/MT5_TRADING_GUIDE_EN.md) 🆕 ### 通知設定 - [Telegram 通知設定](docs/NOTIFICATION_TELEGRAM_CONFIG_EN.md) @@ -165,8 +166,11 @@ QuantDingerには、ウェブから金融情報を収集し、ローカル市場 2. **戦略設定**: リスク管理ルール(ポジションサイジング、ストップロス、利益確定)を追加。 3. **バックテスト & AI最適化**: バックテストを実行し、豊富なパフォーマンス指標を表示し、**AIに結果を分析させて改善を提案**させます(例:「MACD閾値をXに調整」)。 4. **実行モード**: - - **実取引**: 直接API実行、10以上の暗号資産取引所(Binance, OKXなど)をサポート。 - - **シグナル通知**: 実取引を実行しない市場(株式/FX/先物)向けに、Telegram, Discord, Email, SMS, Webhook経由でシグナルを送信。 + - **実取引**: + - **暗号資産**: 直接API実行、10以上の取引所(Binance, OKX, Bitget, Bybitなど)をサポート + - **米国/香港株式**: Interactive Brokers (IBKR)経由 🆕 + - **外国為替**: MetaTrader 5 (MT5)経由 🆕 + - **シグナル通知**: 実取引をサポートしていない市場(A株/先物)向けに、Telegram, Discord, Email, SMS, Webhook経由でシグナルを送信。 ### 3. AI マルチエージェントリサーチ *あなたの24時間365日稼働するAI投資委員会。* @@ -300,19 +304,32 @@ score = w_{sim}\cdot sim + w_{recency}\cdot recency + w_{returns}\cdot returns\_ --- -## 🔌 対応取引所 +## 🔌 対応取引所とブローカー -QuantDingerは実行のための主要な暗号資産取引所への直接API接続をサポートし、広範な市場データカバレッジのためにCCXTを使用します。 +QuantDingerは複数の市場タイプに対して複数の実行方法をサポートしています: -### 直接APIサポート +### 暗号資産取引所(直接API) | 取引所 | 市場 | |:--------:|:---------| | Binance | 現物, 先物, マージン | | OKX | 現物, 無期限, オプション | | Bitget | 現物, 先物, コピートレーディング | +| Bybit | 現物, リニア先物 | +| Coinbase Exchange | 現物 | +| Kraken | 現物, 先物 | +| KuCoin | 現物, 先物 | +| Gate.io | 現物, 先物 | +| Bitfinex | 現物, デリバティブ | -### CCXT経由でもサポート +### 伝統的なブローカー + +| ブローカー | 市場 | プラットフォーム | +|:------:|:--------|:---------| +| **Interactive Brokers (IBKR)** | 米国株式, 香港株式 | TWS / IB Gateway 🆕 | +| **MetaTrader 5 (MT5)** | 外国為替 | MT5ターミナル 🆕 | + +### 市場データ(CCXT経由) Bybit、Gate.io、Kraken、KuCoin、HTX、および100以上のその他の取引所が市場データ用にサポートされています。 @@ -334,7 +351,7 @@ QuantDingerは、包括的な国際化対応により、世界中のユーザー | **米国株** | Yahoo Finance, Finnhub, Tiingo | ✅ IBKR経由 🆕 | | **香港株** | AkShare, East Money | ✅ IBKR経由 🆕 | | **中国株(A株)** | AkShare, East Money | ⚡ データのみ | -| **FX** | Finnhub, OANDA | ✅ ブローカーAPI経由 | +| **FX** | Finnhub, OANDA | ✅ MT5経由 🆕 | | **先物** | 取引所API, AkShare | ⚡ データのみ | --- diff --git a/README_KO.md b/README_KO.md index dd854e8..3b2dcce 100644 --- a/README_KO.md +++ b/README_KO.md @@ -97,6 +97,7 @@ QuantDinger는 웹에서 금융 정보를 수집하고, 로컬 시장 데이터 ### 개발 가이드 - [Python 전략 개발 가이드](docs/STRATEGY_DEV_GUIDE_KO.md) - [Interactive Brokers (IBKR) 트레이딩 가이드](docs/IBKR_TRADING_GUIDE_EN.md) 🆕 +- [MetaTrader 5 (MT5) 트레이딩 가이드](docs/MT5_TRADING_GUIDE_EN.md) 🆕 ### 알림 설정 - [Telegram 알림 설정](docs/NOTIFICATION_TELEGRAM_CONFIG_EN.md) @@ -165,8 +166,11 @@ QuantDinger는 웹에서 금융 정보를 수집하고, 로컬 시장 데이터 2. **전략 설정**: 위험 관리 규칙(포지션 사이징, 손절매, 이익실현)을 추가하세요. 3. **백테스트 & AI 최적화**: 백테스트를 실행하고, 풍부한 성과 지표를 확인하며, **AI가 결과를 분석하여 개선 사항을 제안**하도록 하세요(예: "MACD 임계값을 X로 조정"). 4. **실행 모드**: - - **실거래**: 직접 API 실행, 10개 이상의 암호화폐 거래소(Binance, OKX 등) 지원. - - **신호 알림**: 실거래를 실행하지 않는 시장(주식/외환/선물)의 경우, Telegram, Discord, Email, SMS 또는 Webhook을 통해 신호를 전송하세요. + - **실거래**: + - **암호화폐**: 직접 API 실행, 10개 이상의 거래소(Binance, OKX, Bitget, Bybit 등) 지원 + - **미국/홍콩 주식**: Interactive Brokers (IBKR) 경유 🆕 + - **외환**: MetaTrader 5 (MT5) 경유 🆕 + - **신호 알림**: 실거래를 지원하지 않는 시장(A주/선물)의 경우, Telegram, Discord, Email, SMS 또는 Webhook을 통해 신호를 전송하세요. ### 3. AI 멀티 에이전트 리서치 *연중무휴 24시간 AI 투자 위원회.* @@ -300,19 +304,32 @@ score = w_{sim}\cdot sim + w_{recency}\cdot recency + w_{returns}\cdot returns\_ --- -## 🔌 지원되는 거래소 +## 🔌 지원되는 거래소 및 브로커 -QuantDinger는 실행을 위한 주요 암호화폐 거래소에 대한 직접 API 연결을 지원하며, 광범위한 시장 데이터 커버리지를 위해 CCXT를 사용합니다. +QuantDinger는 다양한 시장 유형에 대해 여러 실행 방법을 지원합니다: -### 직접 API 지원 +### 암호화폐 거래소(직접 API) | 거래소 | 시장 | |:--------:|:---------| | Binance | 현물, 선물, 마진 | | OKX | 현물, 무기한, 옵션 | | Bitget | 현물, 선물, 카피 트레이딩 | +| Bybit | 현물, 선형 선물 | +| Coinbase Exchange | 현물 | +| Kraken | 현물, 선물 | +| KuCoin | 현물, 선물 | +| Gate.io | 현물, 선물 | +| Bitfinex | 현물, 파생상품 | -### CCXT를 통해서도 지원 +### 전통적인 브로커 + +| 브로커 | 시장 | 플랫폼 | +|:------:|:--------|:---------| +| **Interactive Brokers (IBKR)** | 미국 주식, 홍콩 주식 | TWS / IB Gateway 🆕 | +| **MetaTrader 5 (MT5)** | 외환 | MT5 터미널 🆕 | + +### 시장 데이터(CCXT 경유) Bybit, Gate.io, Kraken, KuCoin, HTX 및 100개 이상의 기타 거래소가 시장 데이터용으로 지원됩니다. @@ -347,7 +364,7 @@ QuantDinger는 포괄적인 국제화를 통해 글로벌 사용자를 위해 | **미국 주식** | Yahoo Finance, Finnhub, Tiingo | ✅ IBKR 경유 🆕 | | **홍콩 주식** | AkShare, East Money | ✅ IBKR 경유 🆕 | | **중국 주식(A주)** | AkShare, East Money | ⚡ 데이터만 | -| **외환** | Finnhub, OANDA | ✅ 브로커 API 경유 | +| **외환** | Finnhub, OANDA | ✅ MT5 경유 🆕 | | **선물** | 거래소 API, AkShare | ⚡ 데이터만 | --- diff --git a/README_TW.md b/README_TW.md index 984a1d3..4ce356d 100644 --- a/README_TW.md +++ b/README_TW.md @@ -97,6 +97,7 @@ QuantDinger 包含一個內置的**基於 LLM 的多智能體研究系統**, ### 開發指南 - [Python 策略開發指南](docs/STRATEGY_DEV_GUIDE_TW.md) - [盈透證券 (IBKR) 實盤交易指南](docs/IBKR_TRADING_GUIDE_CN.md) 🆕 +- [MetaTrader 5 (MT5) 外匯實盤交易指南](docs/MT5_TRADING_GUIDE_CN.md) 🆕 ### 通知配置 - [Telegram 通知配置](docs/NOTIFICATION_TELEGRAM_CONFIG_CH.md) @@ -165,8 +166,11 @@ QuantDinger 包含一個內置的**基於 LLM 的多智能體研究系統**, 2. **策略配置**:附加風險管理規則(倉位管理、止損、止盈)。 3. **回測 & AI 優化**:運行回測,查看豐富的性能指標,並**讓 AI 分析結果以建議改進**(例如:「調整 MACD 閾值為 X」)。 4. **執行模式**: - - **實盤交易**:直接 API 執行,支持 10+ 加密貨幣交易所(Binance, OKX 等)。 - - **信號通知**:針對非實盤執行的市場(股票/外匯/期貨),通過 Telegram, Discord, Email, SMS 或 Webhook 發送信號。 + - **實盤交易**: + - **加密貨幣**:直接 API 執行,支持 10+ 交易所(Binance, OKX, Bitget, Bybit 等) + - **美股/港股**:通過盈透證券 (IBKR) 🆕 + - **外匯**:通過 MetaTrader 5 (MT5) 🆕 + - **信號通知**:針對不支持實盤交易的市場(A股/期貨),通過 Telegram, Discord, Email, SMS 或 Webhook 發送信號。 ### 3. AI 多智能體投研 *你的 7x24 小時 AI 投委會。* @@ -300,19 +304,32 @@ score = w_{sim}\cdot sim + w_{recency}\cdot recency + w_{returns}\cdot returns\_ --- -## 🔌 支持的交易所 +## 🔌 支持的交易所和券商 -QuantDinger 支持直接 API 連接到主要加密貨幣交易所進行執行,並使用 CCXT 獲取廣泛的行情數據。 +QuantDinger 支持多種市場類型的執行方式: -### 直接 API 支持 +### 加密貨幣交易所(直接 API) | 交易所 | 市場 | |:--------:|:---------| | Binance | 現貨, 合約, 槓桿 | | OKX | 現貨, 永續, 期權 | | Bitget | 現貨, 合約, 跟單交易 | +| Bybit | 現貨, 線性合約 | +| Coinbase Exchange | 現貨 | +| Kraken | 現貨, 合約 | +| KuCoin | 現貨, 合約 | +| Gate.io | 現貨, 合約 | +| Bitfinex | 現貨, 衍生品 | -### 也支持通過 CCXT +### 傳統券商 + +| 券商 | 市場 | 平台 | +|:------:|:--------|:---------| +| **盈透證券 (IBKR)** | 美股, 港股 | TWS / IB Gateway 🆕 | +| **MetaTrader 5 (MT5)** | 外匯 | MT5 終端 🆕 | + +### 行情數據(通過 CCXT) Bybit、Gate.io、Kraken、KuCoin、HTX 以及 100+ 其他交易所用於行情數據。 @@ -348,7 +365,7 @@ QuantDinger 為全球用戶構建,提供全面的國際化支持: | **美股** | Yahoo Finance, Finnhub, Tiingo | ✅ 通過盈透證券 🆕 | | **港股** | AkShare, 東方財富 | ✅ 通過盈透證券 🆕 | | **A股** | AkShare, 東方財富 | ⚡ 僅數據 | -| **外匯** | Finnhub, OANDA | ✅ 通過券商 API | +| **外匯** | Finnhub, OANDA | ✅ 通過 MT5 🆕 | | **期貨** | 交易所 API, AkShare | ⚡ 僅數據 | --- diff --git a/backend_api_python/app/routes/__init__.py b/backend_api_python/app/routes/__init__.py index 4d30367..8c52f2f 100644 --- a/backend_api_python/app/routes/__init__.py +++ b/backend_api_python/app/routes/__init__.py @@ -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') diff --git a/backend_api_python/app/routes/mt5.py b/backend_api_python/app/routes/mt5.py new file mode 100644 index 0000000..0026e4f --- /dev/null +++ b/backend_api_python/app/routes/mt5.py @@ -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/", 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 diff --git a/backend_api_python/app/services/exchange_execution.py b/backend_api_python/app/services/exchange_execution.py index 3bc6c44..439628a 100644 --- a/backend_api_python/app/services/exchange_execution.py +++ b/backend_api_python/app/services/exchange_execution.py @@ -61,7 +61,7 @@ def load_strategy_configs(strategy_id: int) -> Dict[str, Any]: cur = db.cursor() cur.execute( """ - SELECT id, exchange_config, trading_config, market_type, leverage, execution_mode + SELECT id, exchange_config, trading_config, market_type, leverage, execution_mode, market_category FROM qd_strategies_trading WHERE id = %s """, @@ -76,6 +76,7 @@ def load_strategy_configs(strategy_id: int) -> Dict[str, Any]: market_type = (row.get("market_type") or exchange_config.get("market_type") or "swap").strip() leverage = float(row.get("leverage") or trading_config.get("leverage") or exchange_config.get("leverage") or 1.0) execution_mode = (row.get("execution_mode") or "signal").strip().lower() + market_category = (row.get("market_category") or "Crypto").strip() return { "strategy_id": int(strategy_id), @@ -84,6 +85,7 @@ def load_strategy_configs(strategy_id: int) -> Dict[str, Any]: "market_type": market_type, "leverage": leverage, "execution_mode": execution_mode, + "market_category": market_category, } diff --git a/backend_api_python/app/services/live_trading/execution.py b/backend_api_python/app/services/live_trading/execution.py index 5716e0f..3b2ba9c 100644 --- a/backend_api_python/app/services/live_trading/execution.py +++ b/backend_api_python/app/services/live_trading/execution.py @@ -4,6 +4,7 @@ Translate a strategy signal into a direct-exchange order call. Supports: - Crypto exchanges: Binance, OKX, Bitget, Bybit, Coinbase, Kraken, KuCoin, Gate, Bitfinex - Traditional brokers: Interactive Brokers (IBKR) for US/HK stocks +- Forex brokers: MetaTrader 5 (MT5) """ from __future__ import annotations @@ -28,6 +29,9 @@ from app.services.live_trading.bitfinex import BitfinexClient, BitfinexDerivativ # Lazy import IBKR IBKRClient = None +# Lazy import MT5 +MT5Client = None + def _signal_to_sides(signal_type: str) -> Tuple[str, str, bool]: """ @@ -169,6 +173,24 @@ def place_order_from_signal( exchange_config=exchange_config, ) + # Check for MT5 client (lazy import to avoid circular dependency) + global MT5Client + if MT5Client is None: + try: + from app.services.mt5_trading import MT5Client as _MT5Client + MT5Client = _MT5Client + except ImportError: + pass + + if MT5Client is not None and isinstance(client, MT5Client): + return _place_mt5_order( + client=client, + signal_type=signal_type, + symbol=symbol, + amount=qty, + exchange_config=exchange_config, + ) + raise LiveTradingError(f"Unsupported client type: {type(client)}") @@ -228,3 +250,56 @@ def _place_ibkr_order( ) +def _place_mt5_order( + client, + *, + signal_type: str, + symbol: str, + amount: float, + exchange_config: Optional[Dict[str, Any]] = None, +) -> LiveOrderResult: + """ + Place order via MT5 for forex trading. + + Signal mapping for forex: + - open_long / add_long -> BUY + - close_long / reduce_long -> SELL + - open_short / add_short -> SELL + - close_short / reduce_short -> BUY + """ + sig = (signal_type or "").strip().lower() + + # Determine action based on signal + if sig in ("open_long", "add_long"): + action = "buy" + elif sig in ("close_long", "reduce_long"): + action = "sell" + elif sig in ("open_short", "add_short"): + action = "sell" + elif sig in ("close_short", "reduce_short"): + action = "buy" + else: + raise LiveTradingError(f"Unsupported signal_type for MT5: {signal_type}") + + # Place market order + result = client.place_market_order( + symbol=symbol, + side=action, + volume=amount, + comment="QuantDinger", + ) + + # Convert MT5Client result to LiveOrderResult format + return LiveOrderResult( + success=result.success, + exchange_order_id=str(result.order_id) if result.order_id else "", + filled=result.filled, + avg_price=result.price, + raw={ + "status": result.status, + "message": result.message, + "deal_id": result.deal_id, + "raw": result.raw, + }, + ) + diff --git a/backend_api_python/app/services/live_trading/factory.py b/backend_api_python/app/services/live_trading/factory.py index 0bbcc29..366bf93 100644 --- a/backend_api_python/app/services/live_trading/factory.py +++ b/backend_api_python/app/services/live_trading/factory.py @@ -4,6 +4,7 @@ Factory for direct exchange clients. Supports: - Crypto exchanges: Binance, OKX, Bitget, Bybit, Coinbase, Kraken, KuCoin, Gate, Bitfinex - Traditional brokers: Interactive Brokers (IBKR) for US/HK stocks +- Forex brokers: MetaTrader 5 (MT5) """ from __future__ import annotations @@ -28,6 +29,10 @@ from app.services.live_trading.bitfinex import BitfinexClient, BitfinexDerivativ IBKRClient = None IBKRConfig = None +# Lazy import MT5 to avoid ImportError if MetaTrader5 not installed +MT5Client = None +MT5Config = None + def _get(cfg: Dict[str, Any], *keys: str) -> str: for k in keys: @@ -109,10 +114,18 @@ def create_client(exchange_config: Dict[str, Any], *, market_type: str = "swap") return BitfinexClient(api_key=api_key, secret_key=secret_key, base_url=base_url) return BitfinexDerivativesClient(api_key=api_key, secret_key=secret_key, base_url=base_url) - # Traditional brokers (IBKR for US/HK stocks) + # Traditional brokers (IBKR for US/HK stocks only) if exchange_id == "ibkr": + # Note: Market category validation should be done at the caller level + # This factory only creates clients based on exchange_id return create_ibkr_client(exchange_config) + # Forex brokers (MT5 for Forex only) + if exchange_id == "mt5": + # Note: Market category validation should be done at the caller level + # This factory only creates clients based on exchange_id + return create_mt5_client(exchange_config) + raise LiveTradingError(f"Unsupported exchange_id: {exchange_id}") @@ -159,3 +172,56 @@ def create_ibkr_client(exchange_config: Dict[str, Any]): return client +def create_mt5_client(exchange_config: Dict[str, Any]): + """ + Create MT5 client for forex trading. + + exchange_config should contain: + - mt5_login: MT5 account number + - mt5_password: MT5 password + - mt5_server: Broker server name (e.g., "ICMarkets-Demo") + - mt5_terminal_path: Optional path to terminal64.exe + """ + global MT5Client, MT5Config + + # Lazy import to avoid ImportError if MetaTrader5 not installed + 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: + raise LiveTradingError( + "MT5 trading requires MetaTrader5 library. Run: pip install MetaTrader5\n" + "Note: This library only works on Windows." + ) + + login = int(exchange_config.get("mt5_login") or 0) + password = str(exchange_config.get("mt5_password") or "").strip() + server = str(exchange_config.get("mt5_server") or "").strip() + terminal_path = str(exchange_config.get("mt5_terminal_path") or "").strip() + + if not login or not password or not server: + raise LiveTradingError("MT5 requires login, password, and server") + + config = MT5Config( + login=login, + password=password, + server=server, + terminal_path=terminal_path, + ) + + client = MT5Client(config) + + # Connect immediately + if not client.connect(): + raise LiveTradingError( + "Failed to connect to MT5 terminal. Please check:\n" + "1. MT5 terminal is running\n" + "2. Credentials are correct\n" + "3. You are on Windows" + ) + + return client + + diff --git a/backend_api_python/app/services/mt5_trading/README.md b/backend_api_python/app/services/mt5_trading/README.md new file mode 100644 index 0000000..3eaca50 --- /dev/null +++ b/backend_api_python/app/services/mt5_trading/README.md @@ -0,0 +1,173 @@ +# MetaTrader 5 Trading Module + +Supports forex and CFD trading via MetaTrader 5 terminal. + +## Requirements + +- **Windows platform** (MT5 Python library is Windows-only) +- MetaTrader 5 terminal installed +- MT5 account with a broker + +## Installation + +```bash +pip install MetaTrader5 +``` + +Or the dependency is already in `requirements.txt`. + +## MT5 Terminal Configuration + +1. Download and install MetaTrader 5 from your broker or [official website](https://www.metatrader5.com/) +2. Login to your trading account +3. Go to **Tools** → **Options** → **Expert Advisors** +4. Enable: + - ✅ Allow algorithmic trading + - ✅ Allow DLL imports (optional, may be needed for some features) +5. Click OK + +## Strategy Configuration + +When creating a forex strategy, configure the MT5 connection in the "Live Trading" section: + +| Field | Description | Example | +|-------|-------------|---------| +| **Broker** | Select "MetaTrader 5" | - | +| **Server** | Broker server name | `ICMarkets-Demo` | +| **Account** | MT5 login number | `12345678` | +| **Password** | MT5 password | `****` | + +## Symbol Format + +| Market | Format | Examples | +|--------|--------|----------| +| Forex | Currency pair | `EURUSD`, `GBPUSD`, `USDJPY` | +| Metals | XAU/XAG pairs | `XAUUSD`, `XAGUSD` | +| Indices | CFD symbols | `US30`, `US500`, `DE40` | +| Crypto | Symbol pairs | `BTCUSD`, `ETHUSD` | + +> **Note**: Symbol names may vary by broker. Some brokers use suffixes like `EURUSDm`, `EURUSD.raw`, etc. + +## Lot Size Reference + +| Type | Units | Example | +|------|-------|---------| +| Standard Lot | 100,000 | 1.0 lot = 100,000 EUR | +| Mini Lot | 10,000 | 0.1 lot = 10,000 EUR | +| Micro Lot | 1,000 | 0.01 lot = 1,000 EUR | + +## API Endpoints + +### Connection Management + +``` +GET /api/mt5/status # Get connection status +POST /api/mt5/connect # Connect to MT5 terminal +POST /api/mt5/disconnect # Disconnect +``` + +### Account Queries + +``` +GET /api/mt5/account # Account information +GET /api/mt5/positions # Open positions +GET /api/mt5/orders # Pending orders +GET /api/mt5/symbols # Available symbols +``` + +### Trading + +``` +POST /api/mt5/order # Place order +POST /api/mt5/close # Close position +DELETE /api/mt5/order/ # Cancel pending order +``` + +### Market Data + +``` +GET /api/mt5/quote?symbol=EURUSD +``` + +## Usage Examples + +### Connect + +```bash +curl -X POST http://localhost:5000/api/mt5/connect \ + -H "Content-Type: application/json" \ + -d '{"login": 12345678, "password": "your_password", "server": "ICMarkets-Demo"}' +``` + +### Place Market Order + +```bash +# Buy 0.1 lot EURUSD +curl -X POST http://localhost:5000/api/mt5/order \ + -H "Content-Type: application/json" \ + -d '{"symbol": "EURUSD", "side": "buy", "volume": 0.1}' + +# Sell 0.5 lot XAUUSD +curl -X POST http://localhost:5000/api/mt5/order \ + -H "Content-Type: application/json" \ + -d '{"symbol": "XAUUSD", "side": "sell", "volume": 0.5}' +``` + +### Place Limit Order + +```bash +curl -X POST http://localhost:5000/api/mt5/order \ + -H "Content-Type: application/json" \ + -d '{"symbol": "EURUSD", "side": "buy", "volume": 0.1, "orderType": "limit", "price": 1.0800}' +``` + +### Close Position + +```bash +curl -X POST http://localhost:5000/api/mt5/close \ + -H "Content-Type: application/json" \ + -d '{"ticket": 123456789}' +``` + +## Important Notes + +1. **MT5 terminal must be running**: The terminal must be open and logged in +2. **Windows only**: The MetaTrader5 Python library only works on Windows +3. **Broker symbol names**: Symbol names vary by broker, check your broker's symbol list +4. **Demo account first**: Test with a demo account before using real funds +5. **Market hours**: Forex trades 24/5, check specific market hours for other instruments + +## Troubleshooting + +| Error | Cause | Solution | +|-------|-------|----------| +| ImportError | MetaTrader5 not installed | `pip install MetaTrader5` | +| ImportError | Not on Windows | Use a Windows machine or VM | +| Connection failed | Terminal not running | Start MT5 and login | +| Connection failed | Wrong credentials | Verify login/password/server | +| Symbol not found | Invalid symbol | Check broker's symbol list | +| Trade not allowed | Trading disabled | Enable algo trading in MT5 options | + +## Security Recommendations + +- Use a dedicated trading account +- Test with demo account first +- Set appropriate lot sizes and risk limits +- Monitor positions regularly +- Keep MT5 terminal updated + +## Removing This Module + +To remove this module, delete: + +``` +backend_api_python/app/services/mt5_trading/ # Entire directory +backend_api_python/app/routes/mt5.py # Route file +``` + +Then remove the related import and registration code in `app/routes/__init__.py`. + +## See Also + +- [Python Strategy Development Guide](../../docs/STRATEGY_DEV_GUIDE.md) +- [MetaTrader 5 Python Documentation](https://www.mql5.com/en/docs/python_metatrader5) diff --git a/backend_api_python/app/services/mt5_trading/__init__.py b/backend_api_python/app/services/mt5_trading/__init__.py new file mode 100644 index 0000000..54d1810 --- /dev/null +++ b/backend_api_python/app/services/mt5_trading/__init__.py @@ -0,0 +1,17 @@ +""" +MetaTrader 5 Trading Module + +Provides forex trading capabilities via MT5 terminal. +Requires Windows platform and MetaTrader5 Python library. +""" + +from app.services.mt5_trading.client import MT5Client, MT5Config, OrderResult +from app.services.mt5_trading.symbols import normalize_symbol, parse_symbol + +__all__ = [ + "MT5Client", + "MT5Config", + "OrderResult", + "normalize_symbol", + "parse_symbol", +] diff --git a/backend_api_python/app/services/mt5_trading/client.py b/backend_api_python/app/services/mt5_trading/client.py new file mode 100644 index 0000000..cfdf877 --- /dev/null +++ b/backend_api_python/app/services/mt5_trading/client.py @@ -0,0 +1,783 @@ +""" +MetaTrader 5 Trading Client + +Uses official MetaTrader5 Python library to connect to MT5 terminal for trading. +Note: Requires Windows platform and MT5 terminal installed. +""" + +import time +import threading +from dataclasses import dataclass, field +from typing import Optional, Dict, Any, List +from datetime import datetime + +from app.utils.logger import get_logger +from app.services.mt5_trading.symbols import normalize_symbol, parse_symbol + +logger = get_logger(__name__) + +# Lazy import MetaTrader5 to allow other features to work without it installed +mt5 = None + + +def _ensure_mt5(): + """Ensure MetaTrader5 is imported.""" + global mt5 + if mt5 is None: + try: + import MetaTrader5 as _mt5 + mt5 = _mt5 + except ImportError: + raise ImportError( + "MetaTrader5 is not installed. Run: pip install MetaTrader5\n" + "Note: This library only works on Windows with MT5 terminal installed." + ) + return mt5 + + +@dataclass +class MT5Config: + """MT5 connection configuration.""" + login: int = 0 # MT5 account number + password: str = "" # MT5 password + server: str = "" # Broker server name (e.g., "ICMarkets-Demo") + terminal_path: str = "" # Optional: path to terminal64.exe + timeout: int = 60000 # Connection timeout in milliseconds + magic_number: int = 123456 # EA magic number for identifying orders + + +@dataclass +class OrderResult: + """Order execution result.""" + success: bool + order_id: int = 0 + deal_id: int = 0 + filled: float = 0.0 + price: float = 0.0 + status: str = "" + message: str = "" + raw: Dict[str, Any] = field(default_factory=dict) + + +class MT5Client: + """ + MetaTrader 5 Trading Client + + Usage: + config = MT5Config( + login=12345678, + password="your_password", + server="ICMarkets-Demo" + ) + client = MT5Client(config) + + if client.connect(): + # Place order + result = client.place_market_order("EURUSD", "buy", 0.1) + + # Get positions + positions = client.get_positions() + + client.disconnect() + """ + + def __init__(self, config: Optional[MT5Config] = None): + self.config = config or MT5Config() + self._connected = False + self._lock = threading.Lock() + + @property + def connected(self) -> bool: + """Check if connected to MT5 terminal.""" + if not self._connected: + return False + try: + _ensure_mt5() + info = mt5.terminal_info() + return info is not None and info.connected + except Exception: + return False + + def connect(self) -> bool: + """ + Connect to MT5 terminal. + + Returns: + True if connected successfully + """ + with self._lock: + if self.connected: + return True + + try: + _ensure_mt5() + + # Initialize MT5 connection + init_params = {} + + if self.config.terminal_path: + init_params["path"] = self.config.terminal_path + + if self.config.login and self.config.password and self.config.server: + init_params["login"] = self.config.login + init_params["password"] = self.config.password + init_params["server"] = self.config.server + init_params["timeout"] = self.config.timeout + + logger.info(f"Connecting to MT5: server={self.config.server}, login={self.config.login}") + + if init_params: + initialized = mt5.initialize(**init_params) + else: + # Connect to already running terminal + initialized = mt5.initialize() + + if not initialized: + error = mt5.last_error() + logger.error(f"MT5 initialization failed: {error}") + return False + + self._connected = True + + # Log account info + account_info = mt5.account_info() + if account_info: + logger.info(f"MT5 connected: account={account_info.login}, " + f"server={account_info.server}, balance={account_info.balance}") + else: + logger.warning("MT5 connected but account info not available") + + return True + + except Exception as e: + logger.error(f"MT5 connection failed: {e}") + self._connected = False + return False + + def disconnect(self): + """Disconnect from MT5 terminal.""" + with self._lock: + if self._connected: + try: + _ensure_mt5() + mt5.shutdown() + except Exception as e: + logger.warning(f"MT5 disconnect exception: {e}") + finally: + self._connected = False + logger.info("MT5 disconnected") + + def _ensure_connected(self): + """Ensure connection is established.""" + if not self.connected: + if not self.connect(): + raise ConnectionError("Cannot connect to MT5 terminal") + + # ==================== Order Methods ==================== + + def place_market_order( + self, + symbol: str, + side: str, + volume: float, + deviation: int = 20, + comment: str = "QuantDinger", + ) -> OrderResult: + """ + Place a market order. + + Args: + symbol: Trading symbol (e.g., "EURUSD") + side: Direction ("buy" or "sell") + volume: Lot size (e.g., 0.1 = 1 mini lot) + deviation: Maximum price deviation in points + comment: Order comment + + Returns: + OrderResult + """ + try: + self._ensure_connected() + _ensure_mt5() + + # Normalize symbol + symbol = normalize_symbol(symbol) + + # Get symbol info + symbol_info = mt5.symbol_info(symbol) + if symbol_info is None: + return OrderResult( + success=False, + message=f"Symbol not found: {symbol}" + ) + + if not symbol_info.visible: + # Enable symbol in Market Watch + if not mt5.symbol_select(symbol, True): + return OrderResult( + success=False, + message=f"Failed to select symbol: {symbol}" + ) + + # Get current price + tick = mt5.symbol_info_tick(symbol) + if tick is None: + return OrderResult( + success=False, + message=f"Failed to get tick for: {symbol}" + ) + + # Determine order type and price + if side.lower() == "buy": + order_type = mt5.ORDER_TYPE_BUY + price = tick.ask + else: + order_type = mt5.ORDER_TYPE_SELL + price = tick.bid + + # Prepare order request + request = { + "action": mt5.TRADE_ACTION_DEAL, + "symbol": symbol, + "volume": float(volume), + "type": order_type, + "price": price, + "deviation": deviation, + "magic": self.config.magic_number, + "comment": comment, + "type_time": mt5.ORDER_TIME_GTC, + "type_filling": mt5.ORDER_FILLING_IOC, + } + + # Send order + result = mt5.order_send(request) + + if result is None: + error = mt5.last_error() + return OrderResult( + success=False, + message=f"Order send failed: {error}" + ) + + if result.retcode != mt5.TRADE_RETCODE_DONE: + return OrderResult( + success=False, + order_id=result.order if hasattr(result, 'order') else 0, + status=str(result.retcode), + message=f"Order rejected: {result.comment}", + raw=result._asdict() if hasattr(result, '_asdict') else {} + ) + + return OrderResult( + success=True, + order_id=result.order, + deal_id=result.deal, + filled=result.volume, + price=result.price, + status="filled", + message="Order executed", + raw=result._asdict() if hasattr(result, '_asdict') else {} + ) + + except Exception as e: + logger.error(f"Market order failed: {e}") + return OrderResult( + success=False, + message=str(e) + ) + + def place_limit_order( + self, + symbol: str, + side: str, + volume: float, + price: float, + comment: str = "QuantDinger", + ) -> OrderResult: + """ + Place a pending limit order. + + Args: + symbol: Trading symbol + side: Direction ("buy" or "sell") + volume: Lot size + price: Limit price + comment: Order comment + + Returns: + OrderResult + """ + try: + self._ensure_connected() + _ensure_mt5() + + symbol = normalize_symbol(symbol) + + symbol_info = mt5.symbol_info(symbol) + if symbol_info is None: + return OrderResult( + success=False, + message=f"Symbol not found: {symbol}" + ) + + if not symbol_info.visible: + mt5.symbol_select(symbol, True) + + tick = mt5.symbol_info_tick(symbol) + if tick is None: + return OrderResult( + success=False, + message=f"Failed to get tick for: {symbol}" + ) + + # Determine order type based on side and price relative to market + if side.lower() == "buy": + if price < tick.ask: + order_type = mt5.ORDER_TYPE_BUY_LIMIT + else: + order_type = mt5.ORDER_TYPE_BUY_STOP + else: + if price > tick.bid: + order_type = mt5.ORDER_TYPE_SELL_LIMIT + else: + order_type = mt5.ORDER_TYPE_SELL_STOP + + request = { + "action": mt5.TRADE_ACTION_PENDING, + "symbol": symbol, + "volume": float(volume), + "type": order_type, + "price": price, + "magic": self.config.magic_number, + "comment": comment, + "type_time": mt5.ORDER_TIME_GTC, + } + + result = mt5.order_send(request) + + if result is None: + error = mt5.last_error() + return OrderResult( + success=False, + message=f"Order send failed: {error}" + ) + + if result.retcode != mt5.TRADE_RETCODE_DONE: + return OrderResult( + success=False, + status=str(result.retcode), + message=f"Order rejected: {result.comment}", + ) + + return OrderResult( + success=True, + order_id=result.order, + price=price, + status="pending", + message="Pending order placed", + raw=result._asdict() if hasattr(result, '_asdict') else {} + ) + + except Exception as e: + logger.error(f"Limit order failed: {e}") + return OrderResult( + success=False, + message=str(e) + ) + + def close_position( + self, + ticket: int, + volume: Optional[float] = None, + deviation: int = 20, + comment: str = "QuantDinger close", + ) -> OrderResult: + """ + Close an open position. + + Args: + ticket: Position ticket number + volume: Volume to close (None = close all) + deviation: Maximum price deviation + comment: Order comment + + Returns: + OrderResult + """ + try: + self._ensure_connected() + _ensure_mt5() + + # Get position info + position = mt5.positions_get(ticket=ticket) + if not position: + return OrderResult( + success=False, + message=f"Position not found: {ticket}" + ) + + pos = position[0] + symbol = pos.symbol + + # Get tick + tick = mt5.symbol_info_tick(symbol) + if tick is None: + return OrderResult( + success=False, + message=f"Failed to get tick for: {symbol}" + ) + + # Determine close direction and price + if pos.type == mt5.POSITION_TYPE_BUY: + order_type = mt5.ORDER_TYPE_SELL + price = tick.bid + else: + order_type = mt5.ORDER_TYPE_BUY + price = tick.ask + + close_volume = volume if volume else pos.volume + + request = { + "action": mt5.TRADE_ACTION_DEAL, + "symbol": symbol, + "volume": float(close_volume), + "type": order_type, + "position": ticket, + "price": price, + "deviation": deviation, + "magic": self.config.magic_number, + "comment": comment, + "type_time": mt5.ORDER_TIME_GTC, + "type_filling": mt5.ORDER_FILLING_IOC, + } + + result = mt5.order_send(request) + + if result is None or result.retcode != mt5.TRADE_RETCODE_DONE: + return OrderResult( + success=False, + message=f"Close failed: {result.comment if result else 'Unknown error'}" + ) + + return OrderResult( + success=True, + order_id=result.order, + deal_id=result.deal, + filled=result.volume, + price=result.price, + status="closed", + message="Position closed", + ) + + except Exception as e: + logger.error(f"Close position failed: {e}") + return OrderResult( + success=False, + message=str(e) + ) + + def cancel_order(self, ticket: int) -> bool: + """ + Cancel a pending order. + + Args: + ticket: Order ticket number + + Returns: + True if cancelled successfully + """ + try: + self._ensure_connected() + _ensure_mt5() + + request = { + "action": mt5.TRADE_ACTION_REMOVE, + "order": ticket, + } + + result = mt5.order_send(request) + + if result is None or result.retcode != mt5.TRADE_RETCODE_DONE: + logger.warning(f"Cancel order failed: {result.comment if result else 'Unknown'}") + return False + + logger.info(f"Order {ticket} cancelled") + return True + + except Exception as e: + logger.error(f"Cancel order failed: {e}") + return False + + # ==================== Query Methods ==================== + + def get_account_info(self) -> Dict[str, Any]: + """ + Get account information. + + Returns: + Account info dictionary + """ + try: + self._ensure_connected() + _ensure_mt5() + + info = mt5.account_info() + if info is None: + return {"success": False, "error": "Failed to get account info"} + + return { + "success": True, + "login": info.login, + "server": info.server, + "name": info.name, + "currency": info.currency, + "balance": info.balance, + "equity": info.equity, + "margin": info.margin, + "margin_free": info.margin_free, + "margin_level": info.margin_level, + "profit": info.profit, + "leverage": info.leverage, + "trade_allowed": info.trade_allowed, + "trade_expert": info.trade_expert, + } + + except Exception as e: + logger.error(f"Get account info failed: {e}") + return {"success": False, "error": str(e)} + + def get_positions(self, symbol: Optional[str] = None) -> List[Dict[str, Any]]: + """ + Get open positions. + + Args: + symbol: Filter by symbol (optional) + + Returns: + List of positions + """ + try: + self._ensure_connected() + _ensure_mt5() + + if symbol: + positions = mt5.positions_get(symbol=normalize_symbol(symbol)) + else: + positions = mt5.positions_get() + + if positions is None: + return [] + + result = [] + for pos in positions: + result.append({ + "ticket": pos.ticket, + "symbol": pos.symbol, + "type": "buy" if pos.type == mt5.POSITION_TYPE_BUY else "sell", + "volume": pos.volume, + "price_open": pos.price_open, + "price_current": pos.price_current, + "sl": pos.sl, + "tp": pos.tp, + "profit": pos.profit, + "swap": pos.swap, + "magic": pos.magic, + "comment": pos.comment, + "time": datetime.fromtimestamp(pos.time).isoformat(), + }) + + return result + + except Exception as e: + logger.error(f"Get positions failed: {e}") + return [] + + def get_orders(self, symbol: Optional[str] = None) -> List[Dict[str, Any]]: + """ + Get pending orders. + + Args: + symbol: Filter by symbol (optional) + + Returns: + List of orders + """ + try: + self._ensure_connected() + _ensure_mt5() + + if symbol: + orders = mt5.orders_get(symbol=normalize_symbol(symbol)) + else: + orders = mt5.orders_get() + + if orders is None: + return [] + + result = [] + for order in orders: + order_type_map = { + mt5.ORDER_TYPE_BUY_LIMIT: "buy_limit", + mt5.ORDER_TYPE_SELL_LIMIT: "sell_limit", + mt5.ORDER_TYPE_BUY_STOP: "buy_stop", + mt5.ORDER_TYPE_SELL_STOP: "sell_stop", + } + + result.append({ + "ticket": order.ticket, + "symbol": order.symbol, + "type": order_type_map.get(order.type, str(order.type)), + "volume_initial": order.volume_initial, + "volume_current": order.volume_current, + "price_open": order.price_open, + "price_current": order.price_current, + "sl": order.sl, + "tp": order.tp, + "magic": order.magic, + "comment": order.comment, + "time_setup": datetime.fromtimestamp(order.time_setup).isoformat(), + }) + + return result + + except Exception as e: + logger.error(f"Get orders failed: {e}") + return [] + + def get_quote(self, symbol: str) -> Dict[str, Any]: + """ + Get real-time quote. + + Args: + symbol: Symbol code + + Returns: + Quote data + """ + try: + self._ensure_connected() + _ensure_mt5() + + symbol = normalize_symbol(symbol) + + # Select symbol + symbol_info = mt5.symbol_info(symbol) + if symbol_info is None: + return {"success": False, "error": f"Symbol not found: {symbol}"} + + if not symbol_info.visible: + mt5.symbol_select(symbol, True) + + tick = mt5.symbol_info_tick(symbol) + if tick is None: + return {"success": False, "error": f"Failed to get tick: {symbol}"} + + return { + "success": True, + "symbol": symbol, + "bid": tick.bid, + "ask": tick.ask, + "last": tick.last, + "volume": tick.volume, + "time": datetime.fromtimestamp(tick.time).isoformat(), + "spread": round((tick.ask - tick.bid) / symbol_info.point, 1), + } + + except Exception as e: + logger.error(f"Get quote failed: {e}") + return {"success": False, "error": str(e)} + + def get_symbols(self, group: str = "*") -> List[Dict[str, Any]]: + """ + Get available symbols. + + Args: + group: Filter by group pattern (e.g., "*USD*", "Forex*") + + Returns: + List of symbol info + """ + try: + self._ensure_connected() + _ensure_mt5() + + symbols = mt5.symbols_get(group=group) + if symbols is None: + return [] + + result = [] + for s in symbols: + result.append({ + "name": s.name, + "description": s.description, + "path": s.path, + "currency_base": s.currency_base, + "currency_profit": s.currency_profit, + "digits": s.digits, + "point": s.point, + "trade_mode": s.trade_mode, + "volume_min": s.volume_min, + "volume_max": s.volume_max, + "volume_step": s.volume_step, + }) + + return result + + except Exception as e: + logger.error(f"Get symbols failed: {e}") + return [] + + def get_connection_status(self) -> Dict[str, Any]: + """Get connection status.""" + try: + _ensure_mt5() + terminal_info = mt5.terminal_info() if self._connected else None + account_info = mt5.account_info() if self._connected else None + + return { + "connected": self.connected, + "login": self.config.login, + "server": self.config.server, + "account_login": account_info.login if account_info else None, + "account_server": account_info.server if account_info else None, + "terminal_connected": terminal_info.connected if terminal_info else False, + "trade_allowed": terminal_info.trade_allowed if terminal_info else False, + } + except Exception as e: + return { + "connected": False, + "error": str(e), + } + + +# Global singleton (optional) +_global_client: Optional[MT5Client] = None +_global_lock = threading.Lock() + + +def get_mt5_client(config: Optional[MT5Config] = None) -> MT5Client: + """ + Get global MT5 client singleton. + + Args: + config: Configuration (only effective on first call) + + Returns: + MT5Client instance + """ + global _global_client + + with _global_lock: + if _global_client is None: + _global_client = MT5Client(config) + return _global_client + + +def reset_mt5_client(): + """Reset global client (disconnect and clear instance).""" + global _global_client + + with _global_lock: + if _global_client is not None: + _global_client.disconnect() + _global_client = None diff --git a/backend_api_python/app/services/mt5_trading/symbols.py b/backend_api_python/app/services/mt5_trading/symbols.py new file mode 100644 index 0000000..4af1bd5 --- /dev/null +++ b/backend_api_python/app/services/mt5_trading/symbols.py @@ -0,0 +1,144 @@ +""" +Symbol Mapping and Conversion for MT5 + +Handles forex symbol normalization and parsing. +""" + +from typing import Tuple, Optional + + +# Common forex pairs with their typical MT5 symbol format +FOREX_PAIRS = { + # Major pairs + "EURUSD", "GBPUSD", "USDJPY", "USDCHF", "AUDUSD", "USDCAD", "NZDUSD", + # Cross pairs + "EURGBP", "EURJPY", "EURCHF", "EURAUD", "EURCAD", "EURNZD", + "GBPJPY", "GBPCHF", "GBPAUD", "GBPCAD", "GBPNZD", + "AUDJPY", "AUDCHF", "AUDCAD", "AUDNZD", + "NZDJPY", "NZDCHF", "NZDCAD", + "CADJPY", "CADCHF", + "CHFJPY", + # Exotic pairs + "USDMXN", "USDZAR", "USDTRY", "USDHKD", "USDSGD", "USDNOK", "USDSEK", "USDDKK", + "EURTRY", "EURMXN", "EURNOK", "EURSEK", "EURDKK", "EURPLN", "EURHUF", "EURCZK", + # Metals + "XAUUSD", "XAGUSD", "XAUEUR", + # Indices (CFD) + "US30", "US500", "USTEC", "UK100", "DE40", "JP225", "AU200", + # Crypto (if broker supports) + "BTCUSD", "ETHUSD", "LTCUSD", "XRPUSD", +} + + +def normalize_symbol(symbol: str, broker_suffix: str = "") -> str: + """ + Normalize symbol to MT5 format. + + Different brokers may use different suffixes: + - No suffix: "EURUSD" + - With suffix: "EURUSDm", "EURUSD.raw", "EURUSD-ECN" + + Args: + symbol: Symbol code (e.g., "EUR/USD", "EURUSD", "eurusd") + broker_suffix: Broker-specific suffix (e.g., "m", ".raw", "-ECN") + + Returns: + Normalized MT5 symbol + """ + # Remove common separators and convert to uppercase + normalized = (symbol or "").strip().upper() + normalized = normalized.replace("/", "").replace("-", "").replace("_", "").replace(" ", "") + + # Add broker suffix if provided + if broker_suffix: + # Check if symbol already has the suffix + if not normalized.endswith(broker_suffix.upper()): + normalized = normalized + broker_suffix + + return normalized + + +def parse_symbol(symbol: str) -> Tuple[str, Optional[str]]: + """ + Parse symbol and extract base/quote currencies. + + Args: + symbol: MT5 symbol (e.g., "EURUSD", "EURUSDm") + + Returns: + (clean_symbol, market_type) + """ + clean = (symbol or "").strip().upper() + + # Remove common broker suffixes + for suffix in ["M", ".RAW", "-ECN", ".STD", ".PRO", ".", "#"]: + if clean.endswith(suffix): + clean = clean[:-len(suffix)] + + # Determine market type based on symbol pattern + if clean in FOREX_PAIRS or (len(clean) == 6 and clean.isalpha()): + return clean, "forex" + + if clean.startswith("XAU") or clean.startswith("XAG"): + return clean, "metal" + + if clean.startswith("BTC") or clean.startswith("ETH") or clean.startswith("LTC"): + return clean, "crypto" + + if any(idx in clean for idx in ["US30", "US500", "USTEC", "UK100", "DE40", "JP225"]): + return clean, "index" + + # Default to forex + return clean, "forex" + + +def get_lot_size_info(symbol: str) -> dict: + """ + Get lot size information for a symbol. + + Standard forex lot sizes: + - Standard lot: 100,000 units + - Mini lot: 10,000 units + - Micro lot: 1,000 units + - Nano lot: 100 units + + Args: + symbol: MT5 symbol + + Returns: + Dict with lot size information + """ + clean, market_type = parse_symbol(symbol) + + if market_type == "forex": + return { + "standard_lot": 100000, + "min_lot": 0.01, + "lot_step": 0.01, + "max_lot": 100.0, + } + + if market_type == "metal": + # Gold/Silver typically uses oz + return { + "standard_lot": 100, # 100 oz + "min_lot": 0.01, + "lot_step": 0.01, + "max_lot": 50.0, + } + + if market_type == "index": + return { + "standard_lot": 1, + "min_lot": 0.1, + "lot_step": 0.1, + "max_lot": 100.0, + } + + # Default + return { + "standard_lot": 1, + "min_lot": 0.01, + "lot_step": 0.01, + "max_lot": 100.0, + } diff --git a/backend_api_python/app/services/pending_order_worker.py b/backend_api_python/app/services/pending_order_worker.py index dfcb7b1..a02858e 100644 --- a/backend_api_python/app/services/pending_order_worker.py +++ b/backend_api_python/app/services/pending_order_worker.py @@ -42,6 +42,9 @@ from app.utils.logger import get_logger # Lazy import IBKR to avoid ImportError if ib_insync not installed IBKRClient = None +# Lazy import MT5 to avoid ImportError if MetaTrader5 not installed +MT5Client = None + logger = get_logger(__name__) @@ -348,6 +351,32 @@ class PendingOrderWorker: except Exception: continue + # Check for MT5 client (forex) + if MT5Client is None: + try: + from app.services.mt5_trading import MT5Client as _MT5Client + MT5Client = _MT5Client + except ImportError: + pass + if MT5Client is not None and isinstance(client, MT5Client): + # MT5 forex positions + positions = client.get_positions() + if isinstance(positions, list): + for p in positions: + if not isinstance(p, dict): + continue + sym = str(p.get("symbol") or "").strip() + pos_type = str(p.get("type") or "").strip().lower() + try: + vol = float(p.get("volume") or 0.0) + except Exception: + vol = 0.0 + if not sym or vol <= 0: + continue + # MT5: type "buy" = long, "sell" = short + side = "long" if pos_type == "buy" else "short" + exch_size.setdefault(sym, {"long": 0.0, "short": 0.0})[side] = float(vol) + # Continue to reconciliation logic below else: # Spot reconciliation is optional; skip for now (keeps self-check low-risk). logger.debug(f"position sync: skip unsupported market/client: sid={sid}, cfg={safe_cfg}, market_type={market_type}, client={type(client)}") @@ -681,6 +710,40 @@ class PendingOrderWorker: exchange_config = resolve_exchange_config(cfg.get("exchange_config") or {}) safe_cfg = safe_exchange_config_for_log(exchange_config) exchange_id = str(exchange_config.get("exchange_id") or "").strip().lower() + market_category = str(cfg.get("market_category") or "Crypto").strip() + + # Validate market category and exchange_id combination for live trading + # AShare and Futures do not support live trading + if market_category in ("AShare", "Futures"): + self._mark_failed(order_id=order_id, error=f"live_trading_not_supported_for_{market_category.lower()}") + _console_print(f"[worker] order rejected: strategy_id={strategy_id} pending_id={order_id} {market_category} does not support live trading") + _notify_live_best_effort(status="failed", error=f"live_trading_not_supported_for_{market_category.lower()}") + return + + # Validate IBKR only for USStock/HShare + if exchange_id == "ibkr": + if market_category not in ("USStock", "HShare"): + self._mark_failed(order_id=order_id, error=f"ibkr_only_supports_usstock_hshare_got_{market_category.lower()}") + _console_print(f"[worker] order rejected: strategy_id={strategy_id} pending_id={order_id} IBKR only supports USStock/HShare, got {market_category}") + _notify_live_best_effort(status="failed", error=f"ibkr_only_supports_usstock_hshare_got_{market_category.lower()}") + return + + # Validate MT5 only for Forex + if exchange_id == "mt5": + if market_category != "Forex": + self._mark_failed(order_id=order_id, error=f"mt5_only_supports_forex_got_{market_category.lower()}") + _console_print(f"[worker] order rejected: strategy_id={strategy_id} pending_id={order_id} MT5 only supports Forex, got {market_category}") + _notify_live_best_effort(status="failed", error=f"mt5_only_supports_forex_got_{market_category.lower()}") + return + + # Validate crypto exchanges only for Crypto market + crypto_exchanges = ["binance", "okx", "bitget", "bybit", "coinbaseexchange", "kraken", "kucoin", "gate", "bitfinex"] + if exchange_id in crypto_exchanges: + if market_category != "Crypto": + self._mark_failed(order_id=order_id, error=f"crypto_exchange_only_supports_crypto_got_{market_category.lower()}") + _console_print(f"[worker] order rejected: strategy_id={strategy_id} pending_id={order_id} {exchange_id} only supports Crypto, got {market_category}") + _notify_live_best_effort(status="failed", error=f"crypto_exchange_only_supports_crypto_got_{market_category.lower()}") + return market_type = (payload.get("market_type") or order_row.get("market_type") or cfg.get("market_type") or exchange_config.get("market_type") or "swap") market_type = str(market_type or "swap").strip().lower() @@ -719,6 +782,52 @@ class PendingOrderWorker: ) return + # Check if this is an MT5 client (Forex) + global MT5Client + if MT5Client is None: + try: + from app.services.mt5_trading import MT5Client as _MT5Client + MT5Client = _MT5Client + except ImportError: + pass + + if MT5Client is not None and isinstance(client, MT5Client): + # Execute MT5 order (separate flow for forex) + self._execute_mt5_order( + order_id=order_id, + order_row=order_row, + payload=payload, + client=client, + strategy_id=strategy_id, + exchange_config=exchange_config, + _notify_live_best_effort=_notify_live_best_effort, + _console_print=_console_print, + ) + return + + # Check if this is an MT5 client (Forex) + global MT5Client + if MT5Client is None: + try: + from app.services.mt5_trading import MT5Client as _MT5Client + MT5Client = _MT5Client + except ImportError: + pass + + if MT5Client is not None and isinstance(client, MT5Client): + # Execute MT5 order (separate flow for forex) + self._execute_mt5_order( + order_id=order_id, + order_row=order_row, + payload=payload, + client=client, + strategy_id=strategy_id, + exchange_config=exchange_config, + _notify_live_best_effort=_notify_live_best_effort, + _console_print=_console_print, + ) + return + def _make_client_oid(phase: str = "") -> str: """ Build a client order id. @@ -1702,6 +1811,131 @@ class PendingOrderWorker: _console_print(f"[worker] IBKR order exception: strategy_id={strategy_id} pending_id={order_id} err={e}") _notify_live_best_effort(status="failed", error=str(e)) + def _execute_mt5_order( + self, + *, + order_id: int, + order_row: Dict[str, Any], + payload: Dict[str, Any], + client, # MT5Client instance + strategy_id: int, + exchange_config: Dict[str, Any], + _notify_live_best_effort, + _console_print, + ) -> None: + """ + Execute order via MetaTrader 5 for forex trading. + + Simplified flow compared to crypto (no maker->market fallback): + - Place market order directly + - Wait for fill + - Record trade + """ + signal_type = payload.get("signal_type") or order_row.get("signal_type") + symbol = payload.get("symbol") or order_row.get("symbol") + amount = float(payload.get("amount") or order_row.get("amount") or 0.0) + ref_price = float(payload.get("ref_price") or payload.get("price") or order_row.get("price") or 0.0) + + sig = str(signal_type or "").strip().lower() + + # Map signal to action + if sig in ("open_long", "add_long"): + action = "buy" + elif sig in ("close_long", "reduce_long"): + action = "sell" + elif sig in ("open_short", "add_short"): + action = "sell" + elif sig in ("close_short", "reduce_short"): + action = "buy" + else: + self._mark_failed(order_id=order_id, error=f"mt5_unsupported_signal:{signal_type}") + _console_print(f"[worker] MT5 order rejected: strategy_id={strategy_id} pending_id={order_id} unsupported signal {signal_type}") + _notify_live_best_effort(status="failed", error=f"mt5_unsupported_signal:{signal_type}") + return + + try: + # Place market order via MT5 + result = client.place_market_order( + symbol=symbol, + side=action, + volume=amount, + comment="QuantDinger", + ) + + if not result.success: + self._mark_failed(order_id=order_id, error=f"mt5_order_failed:{result.message}") + _console_print(f"[worker] MT5 order failed: strategy_id={strategy_id} pending_id={order_id} err={result.message}") + _notify_live_best_effort(status="failed", error=f"mt5_order_failed:{result.message}") + return + + filled = float(result.filled or 0.0) + avg_price = float(result.price or 0.0) + exchange_order_id = str(result.order_id or "") + + # Use ref_price if avg_price not available + if avg_price <= 0 and ref_price > 0: + avg_price = ref_price + if filled <= 0: + filled = amount + + executed_at = int(time.time()) + + # Mark order as sent + self._mark_sent( + order_id=order_id, + note="mt5_order_sent", + exchange_id="mt5", + exchange_order_id=exchange_order_id, + exchange_response_json=json.dumps(result.raw or {}, ensure_ascii=False), + filled=filled, + avg_price=avg_price, + executed_at=executed_at, + ) + _console_print(f"[worker] MT5 order sent: strategy_id={strategy_id} pending_id={order_id} order_id={exchange_order_id} filled={filled} avg={avg_price}") + + # Record trade and update position + try: + if filled > 0 and avg_price > 0: + logger.info( + f"MT5 record begin: pending_id={order_id} strategy_id={strategy_id} symbol={symbol} " + f"signal={signal_type} filled={filled} avg_price={avg_price}" + ) + profit, _pos = apply_fill_to_local_position( + strategy_id=strategy_id, + symbol=str(symbol), + signal_type=str(signal_type), + filled=filled, + avg_price=avg_price, + ) + record_trade( + strategy_id=strategy_id, + symbol=str(symbol), + trade_type=str(signal_type), + price=avg_price, + amount=filled, + commission=0.0, # MT5 commission is complex, skip for now + commission_ccy="USD", + profit=profit, + ) + logger.info(f"MT5 record done: pending_id={order_id} strategy_id={strategy_id} symbol={symbol}") + except Exception as e: + logger.warning(f"MT5 record_trade/update_position failed: pending_id={order_id}, err={e}") + + # Notify success + _notify_live_best_effort( + status="sent", + exchange_id="mt5", + exchange_order_id=exchange_order_id, + price_hint=avg_price, + amount_hint=filled, + ) + + except Exception as e: + logger.error(f"MT5 order execution failed: pending_id={order_id}, strategy_id={strategy_id}, err={e}") + self._mark_failed(order_id=order_id, error=f"mt5_exception:{e}") + _console_print(f"[worker] MT5 order exception: strategy_id={strategy_id} pending_id={order_id} err={e}") + _notify_live_best_effort(status="failed", error=str(e)) + def _mark_sent( self, order_id: int, diff --git a/backend_api_python/requirements.txt b/backend_api_python/requirements.txt index 47ae1b4..b55a201 100644 --- a/backend_api_python/requirements.txt +++ b/backend_api_python/requirements.txt @@ -12,4 +12,6 @@ SQLAlchemy>=2.0.0 PyJWT==2.8.0 python-dotenv>=1.0.1 # Interactive Brokers trading (optional, for US/HK stock trading via TWS/IB Gateway) -ib_insync>=0.9.86 \ No newline at end of file +ib_insync>=0.9.86 +# MetaTrader 5 trading (optional, for forex trading via MT5 terminal, Windows only) +MetaTrader5>=5.0.45 \ No newline at end of file diff --git a/docs/MT5_TRADING_GUIDE_CN.md b/docs/MT5_TRADING_GUIDE_CN.md new file mode 100644 index 0000000..6a842b0 --- /dev/null +++ b/docs/MT5_TRADING_GUIDE_CN.md @@ -0,0 +1,215 @@ +# MetaTrader 5 (MT5) 外汇实盘交易指南 + +QuantDinger 支持通过 MetaTrader 5 终端进行外汇实盘交易。 + +## 概述 + +此功能可通过您的 MetaTrader 5 账户实现外汇的自动化交易执行。配置完成后,您的交易策略可以通过 MT5 API 自动下单。 + +## 前置条件 + +- MetaTrader 5 外汇账户 +- 已安装 MT5 终端(仅支持 Windows) +- 已订阅市场数据(用于实时报价) + +## 安装 + +`MetaTrader5` 库已包含在 `requirements.txt` 中。如需手动安装: + +```bash +pip install MetaTrader5 +``` + +> **注意**:MetaTrader5 Python 库仅支持 Windows 平台。Linux/Mac 部署请考虑使用 Windows VM 或远程 Windows 服务器。 + +## MT5 终端配置 + +1. 从您的券商或[官网](https://www.metatrader5.com/)下载并安装 MetaTrader 5 +2. 登录您的交易账户 +3. 进入 **工具** → **选项** → **智能交易系统** +4. 启用: + - ✅ 允许自动交易 + - ✅ 允许 DLL 导入(可选,某些功能可能需要) +5. 点击 确定 + +## 策略配置 + +创建外汇策略时,在"实盘交易"部分配置 MT5 连接: + +| 字段 | 说明 | 示例 | +|------|------|------| +| **外汇券商** | 选择"MetaTrader 5" | - | +| **服务器** | 券商服务器名称 | `ICMarkets-Demo` | +| **账户号** | MT5 登录账号 | `12345678` | +| **密码** | MT5 密码 | `****` | +| **MT5 终端路径** | 终端路径(可选) | `C:\Program Files\MetaTrader 5\terminal64.exe` | + +> **注意**:如果 MT5 终端安装在默认位置,可以留空"MT5 终端路径"字段。只有在自定义安装位置时才需要填写完整路径。 + +## 代码格式 + +| 市场 | 格式 | 示例 | +|------|------|------| +| 外汇 | 货币对 | `EURUSD`, `GBPUSD`, `USDJPY` | +| 贵金属 | XAU/XAG 对 | `XAUUSD`, `XAGUSD` | +| 指数 | CFD 代码 | `US30`, `US500`, `DE40` | + +> **注意**:代码名称可能因券商而异。部分券商使用后缀,如 `EURUSDm`、`EURUSD.raw` 等。请查看您券商的代码列表。 + +## 手数参考 + +| 类型 | 单位 | 示例 | +|------|------|------| +| 标准手 | 100,000 | 1.0 手 = 100,000 EUR | +| 迷你手 | 10,000 | 0.1 手 = 10,000 EUR | +| 微手 | 1,000 | 0.01 手 = 1,000 EUR | + +## 交易流程 + +``` +策略信号 → 待执行订单队列 → MT5 执行 → 持仓更新 +``` + +1. 您的策略生成买入/卖出信号 +2. 信号作为待执行订单入队 +3. 后台工作线程连接 MT5 并执行订单 +4. 更新持仓和交易记录 + +## 支持的信号类型 + +| 信号 | 动作 | 说明 | +|------|------|------| +| `open_long` | 买入 | 开多仓 | +| `add_long` | 买入 | 加多仓 | +| `close_long` | 卖出 | 平多仓 | +| `reduce_long` | 卖出 | 减多仓 | +| `open_short` | 卖出 | 开空仓 | +| `add_short` | 卖出 | 加空仓 | +| `close_short` | 买入 | 平空仓 | +| `reduce_short` | 买入 | 减空仓 | + +## API 接口 + +### 连接管理 + +``` +GET /api/mt5/status # 获取连接状态 +POST /api/mt5/connect # 连接到 MT5 终端 +POST /api/mt5/disconnect # 断开连接 +``` + +### 账户查询 + +``` +GET /api/mt5/account # 账户信息 +GET /api/mt5/positions # 当前持仓 +GET /api/mt5/orders # 未成交订单 +GET /api/mt5/symbols # 可用代码列表 +``` + +### 交易 + +``` +POST /api/mt5/order # 下单 +POST /api/mt5/close # 平仓 +DELETE /api/mt5/order/ # 撤单 +``` + +### 行情数据 + +``` +GET /api/mt5/quote?symbol=EURUSD +``` + +## 使用示例 + +### 测试连接(通过 curl) + +```bash +# 使用默认终端路径 +curl -X POST http://localhost:5000/api/mt5/connect \ + -H "Content-Type: application/json" \ + -d '{"login": 12345678, "password": "your_password", "server": "ICMarkets-Demo"}' + +# 指定自定义终端路径 +curl -X POST http://localhost:5000/api/mt5/connect \ + -H "Content-Type: application/json" \ + -d '{"login": 12345678, "password": "your_password", "server": "ICMarkets-Demo", "terminal_path": "C:\\Program Files\\MetaTrader 5\\terminal64.exe"}' +``` + +### 下市价单 + +```bash +# 买入 0.1 手 EURUSD +curl -X POST http://localhost:5000/api/mt5/order \ + -H "Content-Type: application/json" \ + -d '{"symbol": "EURUSD", "side": "buy", "volume": 0.1}' + +# 卖出 0.5 手 XAUUSD +curl -X POST http://localhost:5000/api/mt5/order \ + -H "Content-Type: application/json" \ + -d '{"symbol": "XAUUSD", "side": "sell", "volume": 0.5}' +``` + +### 下限价单 + +```bash +curl -X POST http://localhost:5000/api/mt5/order \ + -H "Content-Type: application/json" \ + -d '{"symbol": "EURUSD", "side": "buy", "volume": 0.1, "orderType": "limit", "price": 1.0800}' +``` + +### 平仓 + +```bash +curl -X POST http://localhost:5000/api/mt5/close \ + -H "Content-Type: application/json" \ + -d '{"ticket": 123456789}' +``` + +## 重要说明 + +1. **MT5 终端必须运行**:交易前确保 MT5 终端已打开并登录 +2. **仅支持 Windows**:MetaTrader5 Python 库仅支持 Windows +3. **券商代码名称**:代码名称因券商而异,请查看您券商的代码列表 +4. **先使用模拟账户**:使用真实资金前,请先用模拟账户测试 +5. **交易时间**:外汇 24/5 交易,其他品种请查看具体交易时间 +6. **杠杆**:外汇交易使用杠杆,请注意保证金要求 + +## 常见问题排查 + +| 错误 | 原因 | 解决方案 | +|------|------|----------| +| ImportError | MetaTrader5 未安装 | `pip install MetaTrader5` | +| ImportError | 非 Windows 系统 | 使用 Windows 机器或 VM | +| 连接失败 | 终端未运行 | 启动 MT5 并登录 | +| 连接失败 | 凭证错误 | 验证登录账号/密码/服务器 | +| 代码未找到 | 无效代码 | 查看券商的代码列表 | +| 交易被禁用 | 交易未启用 | 在 MT5 选项中启用自动交易 | +| 订单被拒绝 | 保证金不足 | 检查账户余额和保证金 | + +## Docker 部署 + +在 Docker 中运行 QuantDinger 时,MT5 交易需要: + +1. **Windows 主机**:Windows 上的 Docker Desktop 或 Windows Server +2. **主机上的 MT5**:在 Windows 主机上运行 MT5 终端 +3. **网络访问**:容器必须能访问主机的 MT5 终端 + +对于 Linux/Mac 部署,请考虑: +- 在 Windows VM 上运行 QuantDinger 后端 +- 使用远程 Windows 服务器进行 MT5 连接 + +## 安全建议 + +- 使用专用交易账户 +- 先使用模拟账户测试 +- 设置适当的手数和风险限制 +- 定期监控您的持仓 +- 保持 MT5 终端更新 +- 使用强密码 + +## 参见 + +- [Python 策略开发指南](STRATEGY_DEV_GUIDE_CN.md) +- [MetaTrader 5 Python 文档](https://www.mql5.com/en/docs/python_metatrader5) diff --git a/docs/MT5_TRADING_GUIDE_EN.md b/docs/MT5_TRADING_GUIDE_EN.md new file mode 100644 index 0000000..bd63558 --- /dev/null +++ b/docs/MT5_TRADING_GUIDE_EN.md @@ -0,0 +1,215 @@ +# MetaTrader 5 (MT5) Trading Guide + +QuantDinger supports forex live trading via MetaTrader 5 terminal. + +## Overview + +This feature enables automated forex trading execution through your MetaTrader 5 account. Once configured, your trading strategies can automatically place orders via the MT5 API. + +## Prerequisites + +- MetaTrader 5 account with a forex broker +- MT5 terminal installed (Windows only) +- Market data subscription (for real-time quotes) + +## Installation + +The `MetaTrader5` library is already included in `requirements.txt`. If you need to install manually: + +```bash +pip install MetaTrader5 +``` + +> **Note**: The MetaTrader5 Python library only works on Windows. For Linux/Mac deployments, consider using a Windows VM or a remote Windows server. + +## MT5 Terminal Configuration + +1. Download and install MetaTrader 5 from your broker or [official website](https://www.metatrader5.com/) +2. Login to your trading account +3. Go to **Tools** → **Options** → **Expert Advisors** +4. Enable: + - ✅ Allow algorithmic trading + - ✅ Allow DLL imports (optional, may be needed for some features) +5. Click OK + +## Strategy Configuration + +When creating a forex strategy, configure the MT5 connection in the "Live Trading" section: + +| Field | Description | Example | +|-------|-------------|---------| +| **Forex Broker** | Select "MetaTrader 5" | - | +| **Server** | Broker server name | `ICMarkets-Demo` | +| **Account Number** | MT5 login number | `12345678` | +| **Password** | MT5 password | `****` | +| **MT5 Terminal Path** | Terminal path (optional) | `C:\Program Files\MetaTrader 5\terminal64.exe` | + +> **Note**: If MT5 terminal is installed in the default location, you can leave the "MT5 Terminal Path" field empty. Only fill in the full path if MT5 is installed in a custom location. + +## Symbol Format + +| Market | Format | Examples | +|--------|--------|----------| +| Forex | Currency pair | `EURUSD`, `GBPUSD`, `USDJPY` | +| Metals | XAU/XAG pairs | `XAUUSD`, `XAGUSD` | +| Indices | CFD symbols | `US30`, `US500`, `DE40` | + +> **Note**: Symbol names may vary by broker. Some brokers use suffixes like `EURUSDm`, `EURUSD.raw`, etc. Check your broker's symbol list. + +## Lot Size Reference + +| Type | Units | Example | +|------|-------|---------| +| Standard Lot | 100,000 | 1.0 lot = 100,000 EUR | +| Mini Lot | 10,000 | 0.1 lot = 10,000 EUR | +| Micro Lot | 1,000 | 0.01 lot = 1,000 EUR | + +## Trading Flow + +``` +Strategy Signal → Pending Order Queue → MT5 Execution → Position Update +``` + +1. Your strategy generates a buy/sell signal +2. The signal is queued as a pending order +3. The background worker connects to MT5 and executes the order +4. Position and trade records are updated + +## Supported Signal Types + +| Signal | Action | Description | +|--------|--------|-------------| +| `open_long` | BUY | Open a long position | +| `add_long` | BUY | Add to existing long position | +| `close_long` | SELL | Close long position | +| `reduce_long` | SELL | Reduce long position | +| `open_short` | SELL | Open a short position | +| `add_short` | SELL | Add to existing short position | +| `close_short` | BUY | Close short position | +| `reduce_short` | BUY | Reduce short position | + +## API Endpoints + +### Connection Management + +``` +GET /api/mt5/status # Get connection status +POST /api/mt5/connect # Connect to MT5 terminal +POST /api/mt5/disconnect # Disconnect +``` + +### Account Queries + +``` +GET /api/mt5/account # Account information +GET /api/mt5/positions # Open positions +GET /api/mt5/orders # Pending orders +GET /api/mt5/symbols # Available symbols +``` + +### Trading + +``` +POST /api/mt5/order # Place order +POST /api/mt5/close # Close position +DELETE /api/mt5/order/ # Cancel pending order +``` + +### Market Data + +``` +GET /api/mt5/quote?symbol=EURUSD +``` + +## Usage Examples + +### Test Connection (via curl) + +```bash +# Using default terminal path +curl -X POST http://localhost:5000/api/mt5/connect \ + -H "Content-Type: application/json" \ + -d '{"login": 12345678, "password": "your_password", "server": "ICMarkets-Demo"}' + +# With custom terminal path +curl -X POST http://localhost:5000/api/mt5/connect \ + -H "Content-Type: application/json" \ + -d '{"login": 12345678, "password": "your_password", "server": "ICMarkets-Demo", "terminal_path": "C:\\Program Files\\MetaTrader 5\\terminal64.exe"}' +``` + +### Place Market Order + +```bash +# Buy 0.1 lot EURUSD +curl -X POST http://localhost:5000/api/mt5/order \ + -H "Content-Type: application/json" \ + -d '{"symbol": "EURUSD", "side": "buy", "volume": 0.1}' + +# Sell 0.5 lot XAUUSD +curl -X POST http://localhost:5000/api/mt5/order \ + -H "Content-Type: application/json" \ + -d '{"symbol": "XAUUSD", "side": "sell", "volume": 0.5}' +``` + +### Place Limit Order + +```bash +curl -X POST http://localhost:5000/api/mt5/order \ + -H "Content-Type: application/json" \ + -d '{"symbol": "EURUSD", "side": "buy", "volume": 0.1, "orderType": "limit", "price": 1.0800}' +``` + +### Close Position + +```bash +curl -X POST http://localhost:5000/api/mt5/close \ + -H "Content-Type: application/json" \ + -d '{"ticket": 123456789}' +``` + +## Important Notes + +1. **MT5 terminal must be running**: The terminal must be open and logged in before trading +2. **Windows only**: The MetaTrader5 Python library only works on Windows +3. **Broker symbol names**: Symbol names vary by broker, check your broker's symbol list +4. **Demo account first**: Test with a demo account before using real funds +5. **Market hours**: Forex trades 24/5, check specific market hours for other instruments +6. **Leverage**: Forex trading uses leverage. Be aware of margin requirements + +## Troubleshooting + +| Error | Cause | Solution | +|-------|-------|----------| +| ImportError | MetaTrader5 not installed | `pip install MetaTrader5` | +| ImportError | Not on Windows | Use a Windows machine or VM | +| Connection failed | Terminal not running | Start MT5 and login | +| Connection failed | Wrong credentials | Verify login/password/server | +| Symbol not found | Invalid symbol | Check broker's symbol list | +| Trade not allowed | Trading disabled | Enable algo trading in MT5 options | +| Order rejected | Insufficient margin | Check account balance and margin | + +## Docker Deployment + +When running QuantDinger in Docker, MT5 trading requires: + +1. **Windows host**: Docker Desktop on Windows, or Windows Server +2. **MT5 on host**: Run MT5 terminal on the Windows host +3. **Network access**: Container must be able to access the host's MT5 terminal + +For Linux/Mac deployments, consider: +- Running QuantDinger backend on a Windows VM +- Using a remote Windows server for MT5 connection + +## Security Recommendations + +- Use a dedicated trading account +- Test with demo account first +- Set appropriate lot sizes and risk limits +- Monitor positions regularly +- Keep MT5 terminal updated +- Use strong passwords + +## See Also + +- [Python Strategy Development Guide](STRATEGY_DEV_GUIDE.md) +- [MetaTrader 5 Python Documentation](https://www.mql5.com/en/docs/python_metatrader5) diff --git a/quantdinger_vue/src/locales/lang/en-US.js b/quantdinger_vue/src/locales/lang/en-US.js index 7dbca1d..edd142c 100644 --- a/quantdinger_vue/src/locales/lang/en-US.js +++ b/quantdinger_vue/src/locales/lang/en-US.js @@ -1334,6 +1334,8 @@ const locale = { 'trading-assistant.placeholders.selectBroker': 'Select broker', 'trading-assistant.brokerNames': { 'ibkr': 'Interactive Brokers (IBKR)', + 'mt5': 'MetaTrader 5 (MT5)', + 'mt4': 'MetaTrader 4 (MT4)', 'futu': 'Futu Securities', 'tiger': 'Tiger Brokers', 'td': 'TD Ameritrade', @@ -1348,6 +1350,25 @@ const locale = { 'trading-assistant.placeholders.ibkrAccount': 'Optional, e.g. U1234567', 'trading-assistant.exchange.ibkrConnectionSuccess': 'IBKR connected successfully', 'trading-assistant.exchange.ibkrConnectionFailed': 'IBKR connection failed. Please check if TWS/Gateway is running.', + // MT5/Forex configuration + 'trading-assistant.form.forexBroker': 'Forex Broker', + 'trading-assistant.form.mt5ConnectionTitle': 'MetaTrader 5 Connection', + 'trading-assistant.form.mt5ConnectionHint': 'Make sure MT5 terminal is running and logged in (Windows only)', + 'trading-assistant.form.mt5Server': 'Server', + 'trading-assistant.form.mt5ServerHint': 'Broker server name (e.g., ICMarkets-Demo)', + 'trading-assistant.form.mt5Login': 'Account Number', + 'trading-assistant.form.mt5Password': 'Password', + 'trading-assistant.form.mt5TerminalPath': 'MT5 Terminal Path (Optional)', + 'trading-assistant.form.mt5TerminalPathHint': 'If MT5 terminal is not installed in the default location, specify the full path to terminal64.exe (e.g., C:\\Program Files\\MetaTrader 5\\terminal64.exe)', + 'trading-assistant.placeholders.mt5Server': 'e.g., ICMarkets-Demo', + 'trading-assistant.placeholders.mt5Login': 'e.g., 12345678', + 'trading-assistant.placeholders.mt5Password': 'Your MT5 password', + 'trading-assistant.placeholders.mt5TerminalPath': 'e.g., C:\\Program Files\\MetaTrader 5\\terminal64.exe', + 'trading-assistant.validation.mt5ServerRequired': 'Please enter MT5 server', + 'trading-assistant.validation.mt5LoginRequired': 'Please enter MT5 account number', + 'trading-assistant.validation.mt5PasswordRequired': 'Please enter MT5 password', + 'trading-assistant.exchange.mt5ConnectionSuccess': 'MT5 connected successfully', + 'trading-assistant.exchange.mt5ConnectionFailed': 'MT5 connection failed. Please check if terminal is running.', 'trading-assistant.form.notifyChannels': 'Notification Channels', 'trading-assistant.form.notifyChannelsHint': 'Choose how you want to receive buy/sell and risk-management signals.', 'trading-assistant.notify.browser': 'Browser', diff --git a/quantdinger_vue/src/locales/lang/zh-CN.js b/quantdinger_vue/src/locales/lang/zh-CN.js index 092e2f6..2cfbca1 100644 --- a/quantdinger_vue/src/locales/lang/zh-CN.js +++ b/quantdinger_vue/src/locales/lang/zh-CN.js @@ -1212,6 +1212,8 @@ const locale = { 'trading-assistant.placeholders.selectBroker': '选择券商', 'trading-assistant.brokerNames': { 'ibkr': '盈透证券 (Interactive Brokers)', + 'mt5': 'MetaTrader 5 (MT5)', + 'mt4': 'MetaTrader 4 (MT4)', 'futu': '富途证券 (Futu)', 'tiger': '老虎证券 (Tiger Brokers)', 'td': 'TD Ameritrade', @@ -1226,6 +1228,25 @@ const locale = { 'trading-assistant.placeholders.ibkrAccount': '可选,如 U1234567', 'trading-assistant.exchange.ibkrConnectionSuccess': '盈透证券连接成功', 'trading-assistant.exchange.ibkrConnectionFailed': '盈透证券连接失败,请检查 TWS/Gateway 是否运行', +// MT5/Forex 配置 +'trading-assistant.form.forexBroker': '外汇券商', +'trading-assistant.form.mt5ConnectionTitle': 'MetaTrader 5 连接配置', +'trading-assistant.form.mt5ConnectionHint': '请确保 MT5 终端已启动并登录(仅支持 Windows)', +'trading-assistant.form.mt5Server': '服务器', +'trading-assistant.form.mt5ServerHint': '券商服务器名称(如:ICMarkets-Demo)', +'trading-assistant.form.mt5Login': '账户号', +'trading-assistant.form.mt5Password': '密码', +'trading-assistant.form.mt5TerminalPath': 'MT5 终端路径(可选)', +'trading-assistant.form.mt5TerminalPathHint': '如果 MT5 终端未安装在默认位置,请指定 terminal64.exe 的完整路径(例如:C:\\Program Files\\MetaTrader 5\\terminal64.exe)', +'trading-assistant.placeholders.mt5Server': '例如:ICMarkets-Demo', +'trading-assistant.placeholders.mt5Login': '例如:12345678', +'trading-assistant.placeholders.mt5Password': '您的 MT5 密码', +'trading-assistant.placeholders.mt5TerminalPath': '例如:C:\\Program Files\\MetaTrader 5\\terminal64.exe', +'trading-assistant.validation.mt5ServerRequired': '请输入 MT5 服务器', +'trading-assistant.validation.mt5LoginRequired': '请输入 MT5 账户号', +'trading-assistant.validation.mt5PasswordRequired': '请输入 MT5 密码', +'trading-assistant.exchange.mt5ConnectionSuccess': 'MT5 连接成功', +'trading-assistant.exchange.mt5ConnectionFailed': 'MT5 连接失败,请检查终端是否运行', 'trading-assistant.form.notifyChannels': '通知渠道', 'trading-assistant.form.notifyChannelsHint': '选择信号触发时的通知方式', 'trading-assistant.form.notifyEmail': '邮箱地址', diff --git a/quantdinger_vue/src/views/trading-assistant/index.vue b/quantdinger_vue/src/views/trading-assistant/index.vue index 979d1a2..2302548 100644 --- a/quantdinger_vue/src/views/trading-assistant/index.vue +++ b/quantdinger_vue/src/views/trading-assistant/index.vue @@ -1039,6 +1039,85 @@ + + +