Supports MT5 and forex trading.
Signed-off-by: TIANHE <TIANHE@GMAIL.COM>
This commit is contained in:
@@ -97,6 +97,7 @@ QuantDinger includes a built-in **LLM-based multi-agent research system** that g
|
|||||||
### Guides
|
### Guides
|
||||||
- [Python Strategy Development Guide](docs/STRATEGY_DEV_GUIDE.md)
|
- [Python Strategy Development Guide](docs/STRATEGY_DEV_GUIDE.md)
|
||||||
- [Interactive Brokers (IBKR) Trading Guide](docs/IBKR_TRADING_GUIDE_EN.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
|
### Notification Configuration
|
||||||
- [Telegram Notification Setup](docs/NOTIFICATION_TELEGRAM_CONFIG_EN.md)
|
- [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).
|
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").
|
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**:
|
4. **Execution Mode**:
|
||||||
- **Live Trading**: Direct API execution for 10+ Crypto Exchanges (Binance, OKX, etc.).
|
- **Live Trading**:
|
||||||
- **Signal Notification**: For non-executable markets (Stocks/Forex/Futures), send signals via Telegram, Discord, Email, SMS, or Webhook.
|
- **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
|
### 3. AI Multi-Agent Research
|
||||||
*Your 24/7 AI Investment Committee.*
|
*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 |
|
| Exchange | Markets |
|
||||||
|:--------:|:---------|
|
|:--------:|:---------|
|
||||||
| Binance | Spot, Futures, Margin |
|
| Binance | Spot, Futures, Margin |
|
||||||
| OKX | Spot, Perpetual, Options |
|
| OKX | Spot, Perpetual, Options |
|
||||||
| Bitget | Spot, Futures, Copy Trading |
|
| 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.
|
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 🆕 |
|
| **US Stocks** | Yahoo Finance, Finnhub, Tiingo | ✅ Via IBKR 🆕 |
|
||||||
| **HK Stocks** | AkShare, East Money | ✅ Via IBKR 🆕 |
|
| **HK Stocks** | AkShare, East Money | ✅ Via IBKR 🆕 |
|
||||||
| **CN Stocks (A-shares)** | AkShare, East Money | ⚡ Data only |
|
| **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 |
|
| **Futures** | Exchange APIs, AkShare | ⚡ Data only |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
+24
-7
@@ -97,6 +97,7 @@ QuantDinger 包含一个内置的**基于 LLM 的多智能体研究系统**,
|
|||||||
### 开发指南
|
### 开发指南
|
||||||
- [Python 策略开发指南](docs/STRATEGY_DEV_GUIDE_CN.md)
|
- [Python 策略开发指南](docs/STRATEGY_DEV_GUIDE_CN.md)
|
||||||
- [盈透证券 (IBKR) 实盘交易指南](docs/IBKR_TRADING_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)
|
- [Telegram 通知配置](docs/NOTIFICATION_TELEGRAM_CONFIG_CH.md)
|
||||||
@@ -165,8 +166,11 @@ QuantDinger 包含一个内置的**基于 LLM 的多智能体研究系统**,
|
|||||||
2. **策略配置**:附加风险管理规则(仓位管理、止损、止盈)。
|
2. **策略配置**:附加风险管理规则(仓位管理、止损、止盈)。
|
||||||
3. **回测 & AI 优化**:运行回测,查看丰富的性能指标,并**让 AI 分析结果以建议改进**(例如:“调整 MACD 阈值为 X”)。
|
3. **回测 & AI 优化**:运行回测,查看丰富的性能指标,并**让 AI 分析结果以建议改进**(例如:“调整 MACD 阈值为 X”)。
|
||||||
4. **执行模式**:
|
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 多智能体投研
|
### 3. AI 多智能体投研
|
||||||
*你的 7x24 小时 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 | 现货, 合约, 杠杆 |
|
| Binance | 现货, 合约, 杠杆 |
|
||||||
| OKX | 现货, 永续, 期权 |
|
| OKX | 现货, 永续, 期权 |
|
||||||
| Bitget | 现货, 合约, 跟单交易 |
|
| 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+ 其他交易所用于行情数据。
|
Bybit、Gate.io、Kraken、KuCoin、HTX 以及 100+ 其他交易所用于行情数据。
|
||||||
|
|
||||||
@@ -378,7 +395,7 @@ QuantDinger 为全球用户构建,提供全面的国际化支持:
|
|||||||
| **美股** | Yahoo Finance, Finnhub, Tiingo | ✅ 通过盈透证券 🆕 |
|
| **美股** | Yahoo Finance, Finnhub, Tiingo | ✅ 通过盈透证券 🆕 |
|
||||||
| **港股** | AkShare, 东方财富 | ✅ 通过盈透证券 🆕 |
|
| **港股** | AkShare, 东方财富 | ✅ 通过盈透证券 🆕 |
|
||||||
| **A股** | AkShare, 东方财富 | ⚡ 仅数据 |
|
| **A股** | AkShare, 东方财富 | ⚡ 仅数据 |
|
||||||
| **外汇** | Finnhub, OANDA | ✅ 通过券商 API |
|
| **外汇** | Finnhub, OANDA | ✅ 通过 MT5 🆕 |
|
||||||
| **期货** | 交易所 API, AkShare | ⚡ 仅数据 |
|
| **期货** | 交易所 API, AkShare | ⚡ 仅数据 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
+24
-7
@@ -97,6 +97,7 @@ QuantDingerには、ウェブから金融情報を収集し、ローカル市場
|
|||||||
### 開発ガイド
|
### 開発ガイド
|
||||||
- [Python 戦略開発ガイド](docs/STRATEGY_DEV_GUIDE_JA.md)
|
- [Python 戦略開発ガイド](docs/STRATEGY_DEV_GUIDE_JA.md)
|
||||||
- [Interactive Brokers (IBKR) トレーディングガイド](docs/IBKR_TRADING_GUIDE_EN.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)
|
- [Telegram 通知設定](docs/NOTIFICATION_TELEGRAM_CONFIG_EN.md)
|
||||||
@@ -165,8 +166,11 @@ QuantDingerには、ウェブから金融情報を収集し、ローカル市場
|
|||||||
2. **戦略設定**: リスク管理ルール(ポジションサイジング、ストップロス、利益確定)を追加。
|
2. **戦略設定**: リスク管理ルール(ポジションサイジング、ストップロス、利益確定)を追加。
|
||||||
3. **バックテスト & AI最適化**: バックテストを実行し、豊富なパフォーマンス指標を表示し、**AIに結果を分析させて改善を提案**させます(例:「MACD閾値をXに調整」)。
|
3. **バックテスト & AI最適化**: バックテストを実行し、豊富なパフォーマンス指標を表示し、**AIに結果を分析させて改善を提案**させます(例:「MACD閾値をXに調整」)。
|
||||||
4. **実行モード**:
|
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 マルチエージェントリサーチ
|
### 3. AI マルチエージェントリサーチ
|
||||||
*あなたの24時間365日稼働する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 | 現物, 先物, マージン |
|
| Binance | 現物, 先物, マージン |
|
||||||
| OKX | 現物, 無期限, オプション |
|
| OKX | 現物, 無期限, オプション |
|
||||||
| Bitget | 現物, 先物, コピートレーディング |
|
| 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以上のその他の取引所が市場データ用にサポートされています。
|
Bybit、Gate.io、Kraken、KuCoin、HTX、および100以上のその他の取引所が市場データ用にサポートされています。
|
||||||
|
|
||||||
@@ -334,7 +351,7 @@ QuantDingerは、包括的な国際化対応により、世界中のユーザー
|
|||||||
| **米国株** | Yahoo Finance, Finnhub, Tiingo | ✅ IBKR経由 🆕 |
|
| **米国株** | Yahoo Finance, Finnhub, Tiingo | ✅ IBKR経由 🆕 |
|
||||||
| **香港株** | AkShare, East Money | ✅ IBKR経由 🆕 |
|
| **香港株** | AkShare, East Money | ✅ IBKR経由 🆕 |
|
||||||
| **中国株(A株)** | AkShare, East Money | ⚡ データのみ |
|
| **中国株(A株)** | AkShare, East Money | ⚡ データのみ |
|
||||||
| **FX** | Finnhub, OANDA | ✅ ブローカーAPI経由 |
|
| **FX** | Finnhub, OANDA | ✅ MT5経由 🆕 |
|
||||||
| **先物** | 取引所API, AkShare | ⚡ データのみ |
|
| **先物** | 取引所API, AkShare | ⚡ データのみ |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
+24
-7
@@ -97,6 +97,7 @@ QuantDinger는 웹에서 금융 정보를 수집하고, 로컬 시장 데이터
|
|||||||
### 개발 가이드
|
### 개발 가이드
|
||||||
- [Python 전략 개발 가이드](docs/STRATEGY_DEV_GUIDE_KO.md)
|
- [Python 전략 개발 가이드](docs/STRATEGY_DEV_GUIDE_KO.md)
|
||||||
- [Interactive Brokers (IBKR) 트레이딩 가이드](docs/IBKR_TRADING_GUIDE_EN.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)
|
- [Telegram 알림 설정](docs/NOTIFICATION_TELEGRAM_CONFIG_EN.md)
|
||||||
@@ -165,8 +166,11 @@ QuantDinger는 웹에서 금융 정보를 수집하고, 로컬 시장 데이터
|
|||||||
2. **전략 설정**: 위험 관리 규칙(포지션 사이징, 손절매, 이익실현)을 추가하세요.
|
2. **전략 설정**: 위험 관리 규칙(포지션 사이징, 손절매, 이익실현)을 추가하세요.
|
||||||
3. **백테스트 & AI 최적화**: 백테스트를 실행하고, 풍부한 성과 지표를 확인하며, **AI가 결과를 분석하여 개선 사항을 제안**하도록 하세요(예: "MACD 임계값을 X로 조정").
|
3. **백테스트 & AI 최적화**: 백테스트를 실행하고, 풍부한 성과 지표를 확인하며, **AI가 결과를 분석하여 개선 사항을 제안**하도록 하세요(예: "MACD 임계값을 X로 조정").
|
||||||
4. **실행 모드**:
|
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 멀티 에이전트 리서치
|
### 3. AI 멀티 에이전트 리서치
|
||||||
*연중무휴 24시간 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 | 현물, 선물, 마진 |
|
| Binance | 현물, 선물, 마진 |
|
||||||
| OKX | 현물, 무기한, 옵션 |
|
| OKX | 현물, 무기한, 옵션 |
|
||||||
| Bitget | 현물, 선물, 카피 트레이딩 |
|
| 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개 이상의 기타 거래소가 시장 데이터용으로 지원됩니다.
|
Bybit, Gate.io, Kraken, KuCoin, HTX 및 100개 이상의 기타 거래소가 시장 데이터용으로 지원됩니다.
|
||||||
|
|
||||||
@@ -347,7 +364,7 @@ QuantDinger는 포괄적인 국제화를 통해 글로벌 사용자를 위해
|
|||||||
| **미국 주식** | Yahoo Finance, Finnhub, Tiingo | ✅ IBKR 경유 🆕 |
|
| **미국 주식** | Yahoo Finance, Finnhub, Tiingo | ✅ IBKR 경유 🆕 |
|
||||||
| **홍콩 주식** | AkShare, East Money | ✅ IBKR 경유 🆕 |
|
| **홍콩 주식** | AkShare, East Money | ✅ IBKR 경유 🆕 |
|
||||||
| **중국 주식(A주)** | AkShare, East Money | ⚡ 데이터만 |
|
| **중국 주식(A주)** | AkShare, East Money | ⚡ 데이터만 |
|
||||||
| **외환** | Finnhub, OANDA | ✅ 브로커 API 경유 |
|
| **외환** | Finnhub, OANDA | ✅ MT5 경유 🆕 |
|
||||||
| **선물** | 거래소 API, AkShare | ⚡ 데이터만 |
|
| **선물** | 거래소 API, AkShare | ⚡ 데이터만 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
+24
-7
@@ -97,6 +97,7 @@ QuantDinger 包含一個內置的**基於 LLM 的多智能體研究系統**,
|
|||||||
### 開發指南
|
### 開發指南
|
||||||
- [Python 策略開發指南](docs/STRATEGY_DEV_GUIDE_TW.md)
|
- [Python 策略開發指南](docs/STRATEGY_DEV_GUIDE_TW.md)
|
||||||
- [盈透證券 (IBKR) 實盤交易指南](docs/IBKR_TRADING_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)
|
- [Telegram 通知配置](docs/NOTIFICATION_TELEGRAM_CONFIG_CH.md)
|
||||||
@@ -165,8 +166,11 @@ QuantDinger 包含一個內置的**基於 LLM 的多智能體研究系統**,
|
|||||||
2. **策略配置**:附加風險管理規則(倉位管理、止損、止盈)。
|
2. **策略配置**:附加風險管理規則(倉位管理、止損、止盈)。
|
||||||
3. **回測 & AI 優化**:運行回測,查看豐富的性能指標,並**讓 AI 分析結果以建議改進**(例如:「調整 MACD 閾值為 X」)。
|
3. **回測 & AI 優化**:運行回測,查看豐富的性能指標,並**讓 AI 分析結果以建議改進**(例如:「調整 MACD 閾值為 X」)。
|
||||||
4. **執行模式**:
|
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 多智能體投研
|
### 3. AI 多智能體投研
|
||||||
*你的 7x24 小時 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 | 現貨, 合約, 槓桿 |
|
| Binance | 現貨, 合約, 槓桿 |
|
||||||
| OKX | 現貨, 永續, 期權 |
|
| OKX | 現貨, 永續, 期權 |
|
||||||
| Bitget | 現貨, 合約, 跟單交易 |
|
| 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+ 其他交易所用於行情數據。
|
Bybit、Gate.io、Kraken、KuCoin、HTX 以及 100+ 其他交易所用於行情數據。
|
||||||
|
|
||||||
@@ -348,7 +365,7 @@ QuantDinger 為全球用戶構建,提供全面的國際化支持:
|
|||||||
| **美股** | Yahoo Finance, Finnhub, Tiingo | ✅ 通過盈透證券 🆕 |
|
| **美股** | Yahoo Finance, Finnhub, Tiingo | ✅ 通過盈透證券 🆕 |
|
||||||
| **港股** | AkShare, 東方財富 | ✅ 通過盈透證券 🆕 |
|
| **港股** | AkShare, 東方財富 | ✅ 通過盈透證券 🆕 |
|
||||||
| **A股** | AkShare, 東方財富 | ⚡ 僅數據 |
|
| **A股** | AkShare, 東方財富 | ⚡ 僅數據 |
|
||||||
| **外匯** | Finnhub, OANDA | ✅ 通過券商 API |
|
| **外匯** | Finnhub, OANDA | ✅ 通過 MT5 🆕 |
|
||||||
| **期貨** | 交易所 API, AkShare | ⚡ 僅數據 |
|
| **期貨** | 交易所 API, AkShare | ⚡ 僅數據 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
"""
|
"""
|
||||||
API 路由模块
|
API Routes Module
|
||||||
"""
|
"""
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
|
|
||||||
|
|
||||||
def register_routes(app: Flask):
|
def register_routes(app: Flask):
|
||||||
"""注册所有 API 路由蓝图"""
|
"""Register all API route blueprints"""
|
||||||
from app.routes.kline import kline_bp
|
from app.routes.kline import kline_bp
|
||||||
from app.routes.analysis import analysis_bp
|
from app.routes.analysis import analysis_bp
|
||||||
from app.routes.backtest import backtest_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.settings import settings_bp
|
||||||
from app.routes.portfolio import portfolio_bp
|
from app.routes.portfolio import portfolio_bp
|
||||||
from app.routes.ibkr import ibkr_bp
|
from app.routes.ibkr import ibkr_bp
|
||||||
|
from app.routes.mt5 import mt5_bp
|
||||||
|
|
||||||
app.register_blueprint(health_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(kline_bp, url_prefix='/api/indicator')
|
||||||
app.register_blueprint(analysis_bp, url_prefix='/api/analysis')
|
app.register_blueprint(analysis_bp, url_prefix='/api/analysis')
|
||||||
app.register_blueprint(backtest_bp, url_prefix='/api/indicator')
|
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(settings_bp, url_prefix='/api/settings')
|
||||||
app.register_blueprint(portfolio_bp, url_prefix='/api/portfolio')
|
app.register_blueprint(portfolio_bp, url_prefix='/api/portfolio')
|
||||||
app.register_blueprint(ibkr_bp, url_prefix='/api/ibkr')
|
app.register_blueprint(ibkr_bp, url_prefix='/api/ibkr')
|
||||||
|
app.register_blueprint(mt5_bp, url_prefix='/api/mt5')
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,386 @@
|
|||||||
|
"""
|
||||||
|
MetaTrader 5 Trading API Routes
|
||||||
|
|
||||||
|
Provides REST API for MT5 trading operations.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flask import Blueprint, request, jsonify
|
||||||
|
|
||||||
|
from app.utils.logger import get_logger
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
mt5_bp = Blueprint("mt5", __name__)
|
||||||
|
|
||||||
|
# Lazy import MT5 client to avoid errors if not installed
|
||||||
|
MT5Client = None
|
||||||
|
MT5Config = None
|
||||||
|
_client = None
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_mt5_imports():
|
||||||
|
"""Ensure MT5 modules are imported."""
|
||||||
|
global MT5Client, MT5Config
|
||||||
|
if MT5Client is None or MT5Config is None:
|
||||||
|
try:
|
||||||
|
from app.services.mt5_trading import MT5Client as _MT5Client, MT5Config as _MT5Config
|
||||||
|
MT5Client = _MT5Client
|
||||||
|
MT5Config = _MT5Config
|
||||||
|
except ImportError as e:
|
||||||
|
raise ImportError(
|
||||||
|
"MT5 trading requires MetaTrader5 library. "
|
||||||
|
"Run: pip install MetaTrader5\n"
|
||||||
|
"Note: This library only works on Windows."
|
||||||
|
) from e
|
||||||
|
|
||||||
|
|
||||||
|
def _get_client():
|
||||||
|
"""Get or create MT5 client instance."""
|
||||||
|
global _client
|
||||||
|
if _client is None:
|
||||||
|
_ensure_mt5_imports()
|
||||||
|
_client = MT5Client()
|
||||||
|
return _client
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== Connection Management ====================
|
||||||
|
|
||||||
|
@mt5_bp.route("/status", methods=["GET"])
|
||||||
|
def get_status():
|
||||||
|
"""Get MT5 connection status."""
|
||||||
|
try:
|
||||||
|
_ensure_mt5_imports()
|
||||||
|
client = _get_client()
|
||||||
|
status = client.get_connection_status()
|
||||||
|
return jsonify(status)
|
||||||
|
except ImportError as e:
|
||||||
|
return jsonify({
|
||||||
|
"connected": False,
|
||||||
|
"error": str(e),
|
||||||
|
"hint": "MetaTrader5 library is not installed or not on Windows"
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Get MT5 status failed: {e}")
|
||||||
|
return jsonify({"connected": False, "error": str(e)})
|
||||||
|
|
||||||
|
|
||||||
|
@mt5_bp.route("/connect", methods=["POST"])
|
||||||
|
def connect():
|
||||||
|
"""
|
||||||
|
Connect to MT5 terminal.
|
||||||
|
|
||||||
|
Request body:
|
||||||
|
{
|
||||||
|
"login": 12345678, // MT5 account number
|
||||||
|
"password": "xxx", // MT5 password
|
||||||
|
"server": "ICMarkets-Demo", // Broker server
|
||||||
|
"terminal_path": "" // Optional: path to terminal64.exe
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
global _client
|
||||||
|
|
||||||
|
try:
|
||||||
|
_ensure_mt5_imports()
|
||||||
|
|
||||||
|
data = request.get_json() or {}
|
||||||
|
|
||||||
|
login = data.get("login") or data.get("mt5_login")
|
||||||
|
password = data.get("password") or data.get("mt5_password")
|
||||||
|
server = data.get("server") or data.get("mt5_server")
|
||||||
|
terminal_path = data.get("terminal_path") or data.get("mt5_terminal_path") or ""
|
||||||
|
|
||||||
|
if not login or not password or not server:
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"error": "Missing required fields: login, password, server"
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
config = MT5Config(
|
||||||
|
login=int(login),
|
||||||
|
password=str(password),
|
||||||
|
server=str(server),
|
||||||
|
terminal_path=str(terminal_path),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create new client with config
|
||||||
|
_client = MT5Client(config)
|
||||||
|
|
||||||
|
if _client.connect():
|
||||||
|
account_info = _client.get_account_info()
|
||||||
|
return jsonify({
|
||||||
|
"success": True,
|
||||||
|
"message": "Connected to MT5",
|
||||||
|
"account": account_info
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"error": "Failed to connect to MT5. Check credentials and ensure terminal is running."
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
except ImportError as e:
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"error": str(e)
|
||||||
|
}), 500
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"MT5 connect failed: {e}")
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"error": str(e)
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@mt5_bp.route("/disconnect", methods=["POST"])
|
||||||
|
def disconnect():
|
||||||
|
"""Disconnect from MT5 terminal."""
|
||||||
|
global _client
|
||||||
|
|
||||||
|
try:
|
||||||
|
if _client is not None:
|
||||||
|
_client.disconnect()
|
||||||
|
_client = None
|
||||||
|
return jsonify({"success": True, "message": "Disconnected"})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"MT5 disconnect failed: {e}")
|
||||||
|
return jsonify({"success": False, "error": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== Account Queries ====================
|
||||||
|
|
||||||
|
@mt5_bp.route("/account", methods=["GET"])
|
||||||
|
def get_account():
|
||||||
|
"""Get account information."""
|
||||||
|
try:
|
||||||
|
client = _get_client()
|
||||||
|
if not client.connected:
|
||||||
|
return jsonify({"success": False, "error": "Not connected to MT5"}), 400
|
||||||
|
|
||||||
|
info = client.get_account_info()
|
||||||
|
return jsonify(info)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Get MT5 account failed: {e}")
|
||||||
|
return jsonify({"success": False, "error": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@mt5_bp.route("/positions", methods=["GET"])
|
||||||
|
def get_positions():
|
||||||
|
"""Get open positions."""
|
||||||
|
try:
|
||||||
|
client = _get_client()
|
||||||
|
if not client.connected:
|
||||||
|
return jsonify({"success": False, "error": "Not connected to MT5"}), 400
|
||||||
|
|
||||||
|
symbol = request.args.get("symbol")
|
||||||
|
positions = client.get_positions(symbol=symbol)
|
||||||
|
return jsonify({"success": True, "positions": positions})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Get MT5 positions failed: {e}")
|
||||||
|
return jsonify({"success": False, "error": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@mt5_bp.route("/orders", methods=["GET"])
|
||||||
|
def get_orders():
|
||||||
|
"""Get pending orders."""
|
||||||
|
try:
|
||||||
|
client = _get_client()
|
||||||
|
if not client.connected:
|
||||||
|
return jsonify({"success": False, "error": "Not connected to MT5"}), 400
|
||||||
|
|
||||||
|
symbol = request.args.get("symbol")
|
||||||
|
orders = client.get_orders(symbol=symbol)
|
||||||
|
return jsonify({"success": True, "orders": orders})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Get MT5 orders failed: {e}")
|
||||||
|
return jsonify({"success": False, "error": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@mt5_bp.route("/symbols", methods=["GET"])
|
||||||
|
def get_symbols():
|
||||||
|
"""Get available symbols."""
|
||||||
|
try:
|
||||||
|
client = _get_client()
|
||||||
|
if not client.connected:
|
||||||
|
return jsonify({"success": False, "error": "Not connected to MT5"}), 400
|
||||||
|
|
||||||
|
group = request.args.get("group", "*")
|
||||||
|
symbols = client.get_symbols(group=group)
|
||||||
|
return jsonify({"success": True, "symbols": symbols})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Get MT5 symbols failed: {e}")
|
||||||
|
return jsonify({"success": False, "error": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== Trading ====================
|
||||||
|
|
||||||
|
@mt5_bp.route("/order", methods=["POST"])
|
||||||
|
def place_order():
|
||||||
|
"""
|
||||||
|
Place an order.
|
||||||
|
|
||||||
|
Request body:
|
||||||
|
{
|
||||||
|
"symbol": "EURUSD",
|
||||||
|
"side": "buy", // "buy" or "sell"
|
||||||
|
"volume": 0.1, // Lot size
|
||||||
|
"orderType": "market", // "market" or "limit"
|
||||||
|
"price": 1.0800 // Required for limit orders
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
client = _get_client()
|
||||||
|
if not client.connected:
|
||||||
|
return jsonify({"success": False, "error": "Not connected to MT5"}), 400
|
||||||
|
|
||||||
|
data = request.get_json() or {}
|
||||||
|
|
||||||
|
symbol = data.get("symbol")
|
||||||
|
side = data.get("side")
|
||||||
|
volume = data.get("volume") or data.get("quantity")
|
||||||
|
order_type = data.get("orderType", "market").lower()
|
||||||
|
price = data.get("price")
|
||||||
|
comment = data.get("comment", "QuantDinger")
|
||||||
|
|
||||||
|
if not symbol or not side or not volume:
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"error": "Missing required fields: symbol, side, volume"
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
if order_type == "limit":
|
||||||
|
if not price:
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"error": "Limit order requires price"
|
||||||
|
}), 400
|
||||||
|
result = client.place_limit_order(
|
||||||
|
symbol=symbol,
|
||||||
|
side=side,
|
||||||
|
volume=float(volume),
|
||||||
|
price=float(price),
|
||||||
|
comment=comment,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
result = client.place_market_order(
|
||||||
|
symbol=symbol,
|
||||||
|
side=side,
|
||||||
|
volume=float(volume),
|
||||||
|
comment=comment,
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.success:
|
||||||
|
return jsonify({
|
||||||
|
"success": True,
|
||||||
|
"order_id": result.order_id,
|
||||||
|
"deal_id": result.deal_id,
|
||||||
|
"filled": result.filled,
|
||||||
|
"price": result.price,
|
||||||
|
"status": result.status,
|
||||||
|
"message": result.message,
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"error": result.message
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"MT5 place order failed: {e}")
|
||||||
|
return jsonify({"success": False, "error": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@mt5_bp.route("/close", methods=["POST"])
|
||||||
|
def close_position():
|
||||||
|
"""
|
||||||
|
Close a position.
|
||||||
|
|
||||||
|
Request body:
|
||||||
|
{
|
||||||
|
"ticket": 123456789, // Position ticket
|
||||||
|
"volume": 0.1 // Optional: partial close volume
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
client = _get_client()
|
||||||
|
if not client.connected:
|
||||||
|
return jsonify({"success": False, "error": "Not connected to MT5"}), 400
|
||||||
|
|
||||||
|
data = request.get_json() or {}
|
||||||
|
|
||||||
|
ticket = data.get("ticket")
|
||||||
|
volume = data.get("volume")
|
||||||
|
|
||||||
|
if not ticket:
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"error": "Missing required field: ticket"
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
result = client.close_position(
|
||||||
|
ticket=int(ticket),
|
||||||
|
volume=float(volume) if volume else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.success:
|
||||||
|
return jsonify({
|
||||||
|
"success": True,
|
||||||
|
"order_id": result.order_id,
|
||||||
|
"deal_id": result.deal_id,
|
||||||
|
"filled": result.filled,
|
||||||
|
"price": result.price,
|
||||||
|
"message": result.message,
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"error": result.message
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"MT5 close position failed: {e}")
|
||||||
|
return jsonify({"success": False, "error": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@mt5_bp.route("/order/<int:ticket>", methods=["DELETE"])
|
||||||
|
def cancel_order(ticket: int):
|
||||||
|
"""Cancel a pending order."""
|
||||||
|
try:
|
||||||
|
client = _get_client()
|
||||||
|
if not client.connected:
|
||||||
|
return jsonify({"success": False, "error": "Not connected to MT5"}), 400
|
||||||
|
|
||||||
|
if client.cancel_order(ticket):
|
||||||
|
return jsonify({"success": True, "message": f"Order {ticket} cancelled"})
|
||||||
|
else:
|
||||||
|
return jsonify({"success": False, "error": "Failed to cancel order"}), 400
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"MT5 cancel order failed: {e}")
|
||||||
|
return jsonify({"success": False, "error": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== Market Data ====================
|
||||||
|
|
||||||
|
@mt5_bp.route("/quote", methods=["GET"])
|
||||||
|
def get_quote():
|
||||||
|
"""
|
||||||
|
Get real-time quote.
|
||||||
|
|
||||||
|
Query params:
|
||||||
|
- symbol: Trading symbol (e.g., EURUSD)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
client = _get_client()
|
||||||
|
if not client.connected:
|
||||||
|
return jsonify({"success": False, "error": "Not connected to MT5"}), 400
|
||||||
|
|
||||||
|
symbol = request.args.get("symbol")
|
||||||
|
if not symbol:
|
||||||
|
return jsonify({"success": False, "error": "Missing symbol parameter"}), 400
|
||||||
|
|
||||||
|
quote = client.get_quote(symbol)
|
||||||
|
return jsonify(quote)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"MT5 get quote failed: {e}")
|
||||||
|
return jsonify({"success": False, "error": str(e)}), 500
|
||||||
@@ -61,7 +61,7 @@ def load_strategy_configs(strategy_id: int) -> Dict[str, Any]:
|
|||||||
cur = db.cursor()
|
cur = db.cursor()
|
||||||
cur.execute(
|
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
|
FROM qd_strategies_trading
|
||||||
WHERE id = %s
|
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()
|
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)
|
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()
|
execution_mode = (row.get("execution_mode") or "signal").strip().lower()
|
||||||
|
market_category = (row.get("market_category") or "Crypto").strip()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"strategy_id": int(strategy_id),
|
"strategy_id": int(strategy_id),
|
||||||
@@ -84,6 +85,7 @@ def load_strategy_configs(strategy_id: int) -> Dict[str, Any]:
|
|||||||
"market_type": market_type,
|
"market_type": market_type,
|
||||||
"leverage": leverage,
|
"leverage": leverage,
|
||||||
"execution_mode": execution_mode,
|
"execution_mode": execution_mode,
|
||||||
|
"market_category": market_category,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ Translate a strategy signal into a direct-exchange order call.
|
|||||||
Supports:
|
Supports:
|
||||||
- Crypto exchanges: Binance, OKX, Bitget, Bybit, Coinbase, Kraken, KuCoin, Gate, Bitfinex
|
- Crypto exchanges: Binance, OKX, Bitget, Bybit, Coinbase, Kraken, KuCoin, Gate, Bitfinex
|
||||||
- Traditional brokers: Interactive Brokers (IBKR) for US/HK stocks
|
- Traditional brokers: Interactive Brokers (IBKR) for US/HK stocks
|
||||||
|
- Forex brokers: MetaTrader 5 (MT5)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -28,6 +29,9 @@ from app.services.live_trading.bitfinex import BitfinexClient, BitfinexDerivativ
|
|||||||
# Lazy import IBKR
|
# Lazy import IBKR
|
||||||
IBKRClient = None
|
IBKRClient = None
|
||||||
|
|
||||||
|
# Lazy import MT5
|
||||||
|
MT5Client = None
|
||||||
|
|
||||||
|
|
||||||
def _signal_to_sides(signal_type: str) -> Tuple[str, str, bool]:
|
def _signal_to_sides(signal_type: str) -> Tuple[str, str, bool]:
|
||||||
"""
|
"""
|
||||||
@@ -169,6 +173,24 @@ def place_order_from_signal(
|
|||||||
exchange_config=exchange_config,
|
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)}")
|
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,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ Factory for direct exchange clients.
|
|||||||
Supports:
|
Supports:
|
||||||
- Crypto exchanges: Binance, OKX, Bitget, Bybit, Coinbase, Kraken, KuCoin, Gate, Bitfinex
|
- Crypto exchanges: Binance, OKX, Bitget, Bybit, Coinbase, Kraken, KuCoin, Gate, Bitfinex
|
||||||
- Traditional brokers: Interactive Brokers (IBKR) for US/HK stocks
|
- Traditional brokers: Interactive Brokers (IBKR) for US/HK stocks
|
||||||
|
- Forex brokers: MetaTrader 5 (MT5)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -28,6 +29,10 @@ from app.services.live_trading.bitfinex import BitfinexClient, BitfinexDerivativ
|
|||||||
IBKRClient = None
|
IBKRClient = None
|
||||||
IBKRConfig = 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:
|
def _get(cfg: Dict[str, Any], *keys: str) -> str:
|
||||||
for k in keys:
|
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 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)
|
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":
|
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)
|
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}")
|
raise LiveTradingError(f"Unsupported exchange_id: {exchange_id}")
|
||||||
|
|
||||||
|
|
||||||
@@ -159,3 +172,56 @@ def create_ibkr_client(exchange_config: Dict[str, Any]):
|
|||||||
return client
|
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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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/<id> # 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)
|
||||||
@@ -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",
|
||||||
|
]
|
||||||
@@ -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
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
@@ -42,6 +42,9 @@ from app.utils.logger import get_logger
|
|||||||
# Lazy import IBKR to avoid ImportError if ib_insync not installed
|
# Lazy import IBKR to avoid ImportError if ib_insync not installed
|
||||||
IBKRClient = None
|
IBKRClient = None
|
||||||
|
|
||||||
|
# Lazy import MT5 to avoid ImportError if MetaTrader5 not installed
|
||||||
|
MT5Client = None
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@@ -348,6 +351,32 @@ class PendingOrderWorker:
|
|||||||
except Exception:
|
except Exception:
|
||||||
continue
|
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:
|
else:
|
||||||
# Spot reconciliation is optional; skip for now (keeps self-check low-risk).
|
# 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)}")
|
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 {})
|
exchange_config = resolve_exchange_config(cfg.get("exchange_config") or {})
|
||||||
safe_cfg = safe_exchange_config_for_log(exchange_config)
|
safe_cfg = safe_exchange_config_for_log(exchange_config)
|
||||||
exchange_id = str(exchange_config.get("exchange_id") or "").strip().lower()
|
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 = (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()
|
market_type = str(market_type or "swap").strip().lower()
|
||||||
@@ -719,6 +782,52 @@ class PendingOrderWorker:
|
|||||||
)
|
)
|
||||||
return
|
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:
|
def _make_client_oid(phase: str = "") -> str:
|
||||||
"""
|
"""
|
||||||
Build a client order id.
|
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}")
|
_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))
|
_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(
|
def _mark_sent(
|
||||||
self,
|
self,
|
||||||
order_id: int,
|
order_id: int,
|
||||||
|
|||||||
@@ -13,3 +13,5 @@ PyJWT==2.8.0
|
|||||||
python-dotenv>=1.0.1
|
python-dotenv>=1.0.1
|
||||||
# Interactive Brokers trading (optional, for US/HK stock trading via TWS/IB Gateway)
|
# Interactive Brokers trading (optional, for US/HK stock trading via TWS/IB Gateway)
|
||||||
ib_insync>=0.9.86
|
ib_insync>=0.9.86
|
||||||
|
# MetaTrader 5 trading (optional, for forex trading via MT5 terminal, Windows only)
|
||||||
|
MetaTrader5>=5.0.45
|
||||||
@@ -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/<id> # 撤单
|
||||||
|
```
|
||||||
|
|
||||||
|
### 行情数据
|
||||||
|
|
||||||
|
```
|
||||||
|
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)
|
||||||
@@ -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/<id> # 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)
|
||||||
@@ -1334,6 +1334,8 @@ const locale = {
|
|||||||
'trading-assistant.placeholders.selectBroker': 'Select broker',
|
'trading-assistant.placeholders.selectBroker': 'Select broker',
|
||||||
'trading-assistant.brokerNames': {
|
'trading-assistant.brokerNames': {
|
||||||
'ibkr': 'Interactive Brokers (IBKR)',
|
'ibkr': 'Interactive Brokers (IBKR)',
|
||||||
|
'mt5': 'MetaTrader 5 (MT5)',
|
||||||
|
'mt4': 'MetaTrader 4 (MT4)',
|
||||||
'futu': 'Futu Securities',
|
'futu': 'Futu Securities',
|
||||||
'tiger': 'Tiger Brokers',
|
'tiger': 'Tiger Brokers',
|
||||||
'td': 'TD Ameritrade',
|
'td': 'TD Ameritrade',
|
||||||
@@ -1348,6 +1350,25 @@ const locale = {
|
|||||||
'trading-assistant.placeholders.ibkrAccount': 'Optional, e.g. U1234567',
|
'trading-assistant.placeholders.ibkrAccount': 'Optional, e.g. U1234567',
|
||||||
'trading-assistant.exchange.ibkrConnectionSuccess': 'IBKR connected successfully',
|
'trading-assistant.exchange.ibkrConnectionSuccess': 'IBKR connected successfully',
|
||||||
'trading-assistant.exchange.ibkrConnectionFailed': 'IBKR connection failed. Please check if TWS/Gateway is running.',
|
'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.notifyChannels': 'Notification Channels',
|
||||||
'trading-assistant.form.notifyChannelsHint': 'Choose how you want to receive buy/sell and risk-management signals.',
|
'trading-assistant.form.notifyChannelsHint': 'Choose how you want to receive buy/sell and risk-management signals.',
|
||||||
'trading-assistant.notify.browser': 'Browser',
|
'trading-assistant.notify.browser': 'Browser',
|
||||||
|
|||||||
@@ -1212,6 +1212,8 @@ const locale = {
|
|||||||
'trading-assistant.placeholders.selectBroker': '选择券商',
|
'trading-assistant.placeholders.selectBroker': '选择券商',
|
||||||
'trading-assistant.brokerNames': {
|
'trading-assistant.brokerNames': {
|
||||||
'ibkr': '盈透证券 (Interactive Brokers)',
|
'ibkr': '盈透证券 (Interactive Brokers)',
|
||||||
|
'mt5': 'MetaTrader 5 (MT5)',
|
||||||
|
'mt4': 'MetaTrader 4 (MT4)',
|
||||||
'futu': '富途证券 (Futu)',
|
'futu': '富途证券 (Futu)',
|
||||||
'tiger': '老虎证券 (Tiger Brokers)',
|
'tiger': '老虎证券 (Tiger Brokers)',
|
||||||
'td': 'TD Ameritrade',
|
'td': 'TD Ameritrade',
|
||||||
@@ -1226,6 +1228,25 @@ const locale = {
|
|||||||
'trading-assistant.placeholders.ibkrAccount': '可选,如 U1234567',
|
'trading-assistant.placeholders.ibkrAccount': '可选,如 U1234567',
|
||||||
'trading-assistant.exchange.ibkrConnectionSuccess': '盈透证券连接成功',
|
'trading-assistant.exchange.ibkrConnectionSuccess': '盈透证券连接成功',
|
||||||
'trading-assistant.exchange.ibkrConnectionFailed': '盈透证券连接失败,请检查 TWS/Gateway 是否运行',
|
'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.notifyChannels': '通知渠道',
|
||||||
'trading-assistant.form.notifyChannelsHint': '选择信号触发时的通知方式',
|
'trading-assistant.form.notifyChannelsHint': '选择信号触发时的通知方式',
|
||||||
'trading-assistant.form.notifyEmail': '邮箱地址',
|
'trading-assistant.form.notifyEmail': '邮箱地址',
|
||||||
|
|||||||
@@ -1039,6 +1039,85 @@
|
|||||||
<!-- <template v-else-if="currentBrokerId === 'futu'">...</template> -->
|
<!-- <template v-else-if="currentBrokerId === 'futu'">...</template> -->
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<!-- ========== MT5/Forex Broker Configuration ========== -->
|
||||||
|
<template v-else-if="isMT5Market">
|
||||||
|
<a-form-item :label="$t('trading-assistant.form.forexBroker')">
|
||||||
|
<a-select
|
||||||
|
v-decorator="['forex_broker_id', {
|
||||||
|
initialValue: 'mt5',
|
||||||
|
rules: [{ required: true, message: $t('trading-assistant.validation.brokerRequired') }]
|
||||||
|
}]"
|
||||||
|
:placeholder="$t('trading-assistant.placeholders.selectBroker')"
|
||||||
|
:getPopupContainer="getModalPopupContainer"
|
||||||
|
@change="handleForexBrokerSelectChange"
|
||||||
|
>
|
||||||
|
<a-select-option
|
||||||
|
v-for="broker in forexBrokerOptions"
|
||||||
|
:key="broker.value"
|
||||||
|
:value="broker.value"
|
||||||
|
>
|
||||||
|
{{ broker.displayName }}
|
||||||
|
</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<!-- MT5 specific configuration -->
|
||||||
|
<template v-if="currentBrokerId === 'mt5'">
|
||||||
|
<a-alert
|
||||||
|
type="info"
|
||||||
|
show-icon
|
||||||
|
style="margin-bottom: 16px;"
|
||||||
|
:message="$t('trading-assistant.form.mt5ConnectionTitle')"
|
||||||
|
:description="$t('trading-assistant.form.mt5ConnectionHint')"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<a-form-item :label="$t('trading-assistant.form.mt5Server')">
|
||||||
|
<a-input
|
||||||
|
v-decorator="['mt5_server', {
|
||||||
|
rules: [{ required: true, message: $t('trading-assistant.validation.mt5ServerRequired') }]
|
||||||
|
}]"
|
||||||
|
:placeholder="$t('trading-assistant.placeholders.mt5Server')"
|
||||||
|
@change="handleApiConfigChange"
|
||||||
|
/>
|
||||||
|
<div class="form-item-hint">{{ $t('trading-assistant.form.mt5ServerHint') }}</div>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item :label="$t('trading-assistant.form.mt5Login')">
|
||||||
|
<a-input-number
|
||||||
|
v-decorator="['mt5_login', {
|
||||||
|
rules: [{ required: true, message: $t('trading-assistant.validation.mt5LoginRequired') }]
|
||||||
|
}]"
|
||||||
|
:placeholder="$t('trading-assistant.placeholders.mt5Login')"
|
||||||
|
:min="1"
|
||||||
|
style="width: 100%"
|
||||||
|
@change="handleApiConfigChange"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item :label="$t('trading-assistant.form.mt5Password')">
|
||||||
|
<a-input-password
|
||||||
|
v-decorator="['mt5_password', {
|
||||||
|
rules: [{ required: true, message: $t('trading-assistant.validation.mt5PasswordRequired') }]
|
||||||
|
}]"
|
||||||
|
:placeholder="$t('trading-assistant.placeholders.mt5Password')"
|
||||||
|
@change="handleApiConfigChange"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item :label="$t('trading-assistant.form.mt5TerminalPath')">
|
||||||
|
<a-input
|
||||||
|
v-decorator="['mt5_terminal_path']"
|
||||||
|
:placeholder="$t('trading-assistant.placeholders.mt5TerminalPath')"
|
||||||
|
@change="handleApiConfigChange"
|
||||||
|
/>
|
||||||
|
<div class="form-item-hint">{{ $t('trading-assistant.form.mt5TerminalPathHint') }}</div>
|
||||||
|
</a-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Future forex broker configurations can be added here -->
|
||||||
|
<!-- <template v-else-if="currentBrokerId === 'mt4'">...</template> -->
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- ========== Crypto Exchange Configuration ========== -->
|
<!-- ========== Crypto Exchange Configuration ========== -->
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<a-form-item :label="$t('trading-assistant.form.savedCredential')">
|
<a-form-item :label="$t('trading-assistant.form.savedCredential')">
|
||||||
@@ -1223,6 +1302,14 @@ const BROKER_OPTIONS = [
|
|||||||
// { value: 'tiger', labelKey: 'tiger', name: 'Tiger Brokers (老虎证券)' },
|
// { value: 'tiger', labelKey: 'tiger', name: 'Tiger Brokers (老虎证券)' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// Forex broker options
|
||||||
|
const FOREX_BROKER_OPTIONS = [
|
||||||
|
{ value: 'mt5', labelKey: 'mt5', name: 'MetaTrader 5' }
|
||||||
|
// Future forex brokers can be added here:
|
||||||
|
// { value: 'mt4', labelKey: 'mt4', name: 'MetaTrader 4' },
|
||||||
|
// { value: 'ctrader', labelKey: 'ctrader', name: 'cTrader' },
|
||||||
|
]
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'TradingAssistant',
|
name: 'TradingAssistant',
|
||||||
mixins: [baseMixin],
|
mixins: [baseMixin],
|
||||||
@@ -1242,6 +1329,14 @@ export default {
|
|||||||
isIBKRMarket () {
|
isIBKRMarket () {
|
||||||
return ['USStock', 'HShare'].includes(this.selectedMarketCategory)
|
return ['USStock', 'HShare'].includes(this.selectedMarketCategory)
|
||||||
},
|
},
|
||||||
|
// Check if current market uses MT5 (Forex)
|
||||||
|
isMT5Market () {
|
||||||
|
return this.selectedMarketCategory === 'Forex'
|
||||||
|
},
|
||||||
|
// Check if current market uses any broker (not crypto exchange)
|
||||||
|
isBrokerMarket () {
|
||||||
|
return this.isIBKRMarket || this.isMT5Market
|
||||||
|
},
|
||||||
// 预处理交易所列表,包含显示名称,提升性能
|
// 预处理交易所列表,包含显示名称,提升性能
|
||||||
formattedExchangeOptions () {
|
formattedExchangeOptions () {
|
||||||
return EXCHANGE_OPTIONS.map(exchange => {
|
return EXCHANGE_OPTIONS.map(exchange => {
|
||||||
@@ -1294,7 +1389,7 @@ export default {
|
|||||||
const cat = this.selectedMarketCategory || 'Crypto'
|
const cat = this.selectedMarketCategory || 'Crypto'
|
||||||
return String(cat).toLowerCase() === 'crypto'
|
return String(cat).toLowerCase() === 'crypto'
|
||||||
},
|
},
|
||||||
// Check if selected market supports live trading (Crypto or USStock/HShare with IBKR)
|
// Check if selected market supports live trading (Crypto, USStock/HShare with IBKR, or Forex with MT5)
|
||||||
canUseLiveTrading () {
|
canUseLiveTrading () {
|
||||||
const cat = this.selectedMarketCategory || 'Crypto'
|
const cat = this.selectedMarketCategory || 'Crypto'
|
||||||
// Crypto always supports live trading via crypto exchanges
|
// Crypto always supports live trading via crypto exchanges
|
||||||
@@ -1305,6 +1400,10 @@ export default {
|
|||||||
if (['USStock', 'HShare'].includes(cat)) {
|
if (['USStock', 'HShare'].includes(cat)) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
// Forex can use MT5 for live trading
|
||||||
|
if (cat === 'Forex') {
|
||||||
|
return true
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
},
|
},
|
||||||
// Check if current market + exchange combination supports live trading
|
// Check if current market + exchange combination supports live trading
|
||||||
@@ -1317,7 +1416,11 @@ export default {
|
|||||||
}
|
}
|
||||||
// USStock/HShare use IBKR
|
// USStock/HShare use IBKR
|
||||||
if (['USStock', 'HShare'].includes(cat)) {
|
if (['USStock', 'HShare'].includes(cat)) {
|
||||||
return exchangeId === 'ibkr'
|
return this.currentBrokerId === 'ibkr'
|
||||||
|
}
|
||||||
|
// Forex uses MT5
|
||||||
|
if (cat === 'Forex') {
|
||||||
|
return this.currentBrokerId === 'mt5'
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
},
|
},
|
||||||
@@ -1341,6 +1444,26 @@ export default {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
// Forex broker options (with i18n support)
|
||||||
|
forexBrokerOptions () {
|
||||||
|
return FOREX_BROKER_OPTIONS.map(broker => {
|
||||||
|
let label = ''
|
||||||
|
try {
|
||||||
|
const translationKey = `trading-assistant.brokerNames.${broker.labelKey}`
|
||||||
|
const translated = this.$t(translationKey)
|
||||||
|
if (translated !== translationKey) {
|
||||||
|
label = translated
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
if (!label) {
|
||||||
|
label = broker.name || broker.value.toUpperCase()
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...broker,
|
||||||
|
displayName: label
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
// Crypto exchange options only
|
// Crypto exchange options only
|
||||||
cryptoExchangeOptions () {
|
cryptoExchangeOptions () {
|
||||||
return EXCHANGE_OPTIONS.map(exchange => {
|
return EXCHANGE_OPTIONS.map(exchange => {
|
||||||
@@ -1502,9 +1625,22 @@ export default {
|
|||||||
// Keep selection reactive for Step 3 execution gating
|
// Keep selection reactive for Step 3 execution gating
|
||||||
this.selectedMarketCategory = market || 'Crypto'
|
this.selectedMarketCategory = market || 'Crypto'
|
||||||
|
|
||||||
|
// Auto-set broker ID based on market category
|
||||||
|
if (this.selectedMarketCategory === 'Forex') {
|
||||||
|
this.currentBrokerId = 'mt5'
|
||||||
|
try {
|
||||||
|
this.form && this.form.setFieldsValue && this.form.setFieldsValue({ forex_broker_id: 'mt5' })
|
||||||
|
} catch (e) {}
|
||||||
|
} else if (['USStock', 'HShare'].includes(this.selectedMarketCategory)) {
|
||||||
|
this.currentBrokerId = 'ibkr'
|
||||||
|
try {
|
||||||
|
this.form && this.form.setFieldsValue && this.form.setFieldsValue({ broker_id: 'ibkr' })
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
// Markets without live trading support: force back to signal mode
|
// Markets without live trading support: force back to signal mode
|
||||||
// Crypto, USStock, HShare support live trading; others do not
|
// Crypto, USStock, HShare, Forex support live trading; others do not
|
||||||
const supportsLiveTrading = ['Crypto', 'USStock', 'HShare'].includes(this.selectedMarketCategory)
|
const supportsLiveTrading = ['Crypto', 'USStock', 'HShare', 'Forex'].includes(this.selectedMarketCategory)
|
||||||
if (!supportsLiveTrading) {
|
if (!supportsLiveTrading) {
|
||||||
this.executionModeUi = 'signal'
|
this.executionModeUi = 'signal'
|
||||||
try {
|
try {
|
||||||
@@ -1532,8 +1668,21 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auto-set broker ID based on market category
|
||||||
|
if (this.selectedMarketCategory === 'Forex') {
|
||||||
|
this.currentBrokerId = 'mt5'
|
||||||
|
try {
|
||||||
|
this.form && this.form.setFieldsValue && this.form.setFieldsValue({ forex_broker_id: 'mt5' })
|
||||||
|
} catch (e) {}
|
||||||
|
} else if (['USStock', 'HShare'].includes(this.selectedMarketCategory)) {
|
||||||
|
this.currentBrokerId = 'ibkr'
|
||||||
|
try {
|
||||||
|
this.form && this.form.setFieldsValue && this.form.setFieldsValue({ broker_id: 'ibkr' })
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
// Markets without live trading support: force back to signal mode
|
// Markets without live trading support: force back to signal mode
|
||||||
const supportsLiveTrading = ['Crypto', 'USStock', 'HShare'].includes(this.selectedMarketCategory)
|
const supportsLiveTrading = ['Crypto', 'USStock', 'HShare', 'Forex'].includes(this.selectedMarketCategory)
|
||||||
if (!supportsLiveTrading) {
|
if (!supportsLiveTrading) {
|
||||||
this.executionModeUi = 'signal'
|
this.executionModeUi = 'signal'
|
||||||
try {
|
try {
|
||||||
@@ -1868,8 +2017,9 @@ export default {
|
|||||||
if (strategy.exchange_config) {
|
if (strategy.exchange_config) {
|
||||||
const exchangeId = strategy.exchange_config.exchange_id || ''
|
const exchangeId = strategy.exchange_config.exchange_id || ''
|
||||||
const isLive = this.executionModeUi === 'live'
|
const isLive = this.executionModeUi === 'live'
|
||||||
const supportsLiveTrading = ['Crypto', 'USStock', 'HShare'].includes(this.selectedMarketCategory)
|
const supportsLiveTrading = ['Crypto', 'USStock', 'HShare', 'Forex'].includes(this.selectedMarketCategory)
|
||||||
const isBrokerMarket = ['USStock', 'HShare'].includes(this.selectedMarketCategory)
|
const isBrokerMarket = ['USStock', 'HShare'].includes(this.selectedMarketCategory)
|
||||||
|
const isForexMarket = this.selectedMarketCategory === 'Forex'
|
||||||
|
|
||||||
if (isLive && supportsLiveTrading) {
|
if (isLive && supportsLiveTrading) {
|
||||||
if (isBrokerMarket) {
|
if (isBrokerMarket) {
|
||||||
@@ -1882,6 +2032,16 @@ export default {
|
|||||||
ibkr_client_id: strategy.exchange_config.ibkr_client_id || 1,
|
ibkr_client_id: strategy.exchange_config.ibkr_client_id || 1,
|
||||||
ibkr_account: strategy.exchange_config.ibkr_account || ''
|
ibkr_account: strategy.exchange_config.ibkr_account || ''
|
||||||
})
|
})
|
||||||
|
} else if (isForexMarket) {
|
||||||
|
// MT5 configuration (Forex)
|
||||||
|
this.currentBrokerId = exchangeId || 'mt5'
|
||||||
|
this.form.setFieldsValue({
|
||||||
|
forex_broker_id: exchangeId || 'mt5',
|
||||||
|
mt5_server: strategy.exchange_config.mt5_server || '',
|
||||||
|
mt5_login: strategy.exchange_config.mt5_login || '',
|
||||||
|
mt5_password: strategy.exchange_config.mt5_password || '',
|
||||||
|
mt5_terminal_path: strategy.exchange_config.mt5_terminal_path || ''
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
// Crypto exchange configuration
|
// Crypto exchange configuration
|
||||||
this.currentExchangeId = exchangeId
|
this.currentExchangeId = exchangeId
|
||||||
@@ -1903,8 +2063,8 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update UI state
|
// Update UI state
|
||||||
if (isBrokerMarket) {
|
if (isBrokerMarket || isForexMarket) {
|
||||||
this.currentBrokerId = exchangeId || 'ibkr'
|
this.currentBrokerId = exchangeId || (isForexMarket ? 'mt5' : 'ibkr')
|
||||||
} else {
|
} else {
|
||||||
this.currentExchangeId = exchangeId
|
this.currentExchangeId = exchangeId
|
||||||
}
|
}
|
||||||
@@ -2465,6 +2625,11 @@ export default {
|
|||||||
this.testResult = null
|
this.testResult = null
|
||||||
this.connectionTestResult = null
|
this.connectionTestResult = null
|
||||||
},
|
},
|
||||||
|
handleForexBrokerSelectChange (value) {
|
||||||
|
this.currentBrokerId = value || 'mt5'
|
||||||
|
this.testResult = null
|
||||||
|
this.connectionTestResult = null
|
||||||
|
},
|
||||||
handleExchangeSelectChange (value) {
|
handleExchangeSelectChange (value) {
|
||||||
this.currentExchangeId = value || ''
|
this.currentExchangeId = value || ''
|
||||||
this.testResult = null
|
this.testResult = null
|
||||||
@@ -2601,6 +2766,58 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MT5 uses different connection test (server/login/password)
|
||||||
|
if (this.isMT5Market) {
|
||||||
|
const values = this.form.getFieldsValue(['mt5_server', 'mt5_login', 'mt5_password', 'mt5_terminal_path'])
|
||||||
|
const server = values.mt5_server || ''
|
||||||
|
const login = values.mt5_login || ''
|
||||||
|
const password = values.mt5_password || ''
|
||||||
|
const terminal_path = values.mt5_terminal_path || ''
|
||||||
|
|
||||||
|
if (!server || !login || !password) {
|
||||||
|
this.testResult = {
|
||||||
|
success: false,
|
||||||
|
message: this.$t('trading-assistant.exchange.fillComplete')
|
||||||
|
}
|
||||||
|
this.$message.error(this.testResult.message)
|
||||||
|
this.testing = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Call MT5 connect API
|
||||||
|
const res = await this.$http.post('/api/mt5/connect', {
|
||||||
|
server: server,
|
||||||
|
login: parseInt(login),
|
||||||
|
password: password,
|
||||||
|
terminal_path: terminal_path
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.data && res.data.success) {
|
||||||
|
this.testResult = {
|
||||||
|
success: true,
|
||||||
|
message: this.$t('trading-assistant.exchange.mt5ConnectionSuccess')
|
||||||
|
}
|
||||||
|
this.$message.success(this.$t('trading-assistant.exchange.mt5ConnectionSuccess'))
|
||||||
|
} else {
|
||||||
|
this.testResult = {
|
||||||
|
success: false,
|
||||||
|
message: res.data?.error || this.$t('trading-assistant.exchange.mt5ConnectionFailed')
|
||||||
|
}
|
||||||
|
this.$message.error(this.testResult.message)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.testResult = {
|
||||||
|
success: false,
|
||||||
|
message: error.response?.data?.error || error.message || this.$t('trading-assistant.exchange.mt5ConnectionFailed')
|
||||||
|
}
|
||||||
|
this.$message.error(this.testResult.message)
|
||||||
|
} finally {
|
||||||
|
this.testing = false
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Crypto exchanges: validate api_key/secret_key fields
|
// Crypto exchanges: validate api_key/secret_key fields
|
||||||
const fieldsToValidate = ['exchange_id', 'api_key', 'secret_key']
|
const fieldsToValidate = ['exchange_id', 'api_key', 'secret_key']
|
||||||
if (this.needsPassphrase) {
|
if (this.needsPassphrase) {
|
||||||
@@ -2808,6 +3025,14 @@ export default {
|
|||||||
ibkr_port: values.ibkr_port || 7497,
|
ibkr_port: values.ibkr_port || 7497,
|
||||||
ibkr_client_id: values.ibkr_client_id || 1,
|
ibkr_client_id: values.ibkr_client_id || 1,
|
||||||
ibkr_account: values.ibkr_account || ''
|
ibkr_account: values.ibkr_account || ''
|
||||||
|
} : this.isMT5Market ? {
|
||||||
|
// MT5/Forex broker configuration
|
||||||
|
exchange_id: values.forex_broker_id || this.currentBrokerId || 'mt5',
|
||||||
|
// MT5 specific fields
|
||||||
|
mt5_server: values.mt5_server || '',
|
||||||
|
mt5_login: values.mt5_login || '',
|
||||||
|
mt5_password: values.mt5_password || '',
|
||||||
|
mt5_terminal_path: values.mt5_terminal_path || ''
|
||||||
} : {
|
} : {
|
||||||
// Crypto exchange configuration
|
// Crypto exchange configuration
|
||||||
exchange_id: values.exchange_id,
|
exchange_id: values.exchange_id,
|
||||||
@@ -2882,8 +3107,8 @@ export default {
|
|||||||
const totalCreated = res.data?.total_created || this.selectedSymbols.length
|
const totalCreated = res.data?.total_created || this.selectedSymbols.length
|
||||||
this.$message.success(this.$t('trading-assistant.messages.batchCreateSuccess', { count: totalCreated }))
|
this.$message.success(this.$t('trading-assistant.messages.batchCreateSuccess', { count: totalCreated }))
|
||||||
}
|
}
|
||||||
// Save credential to vault (crypto exchanges only, IBKR doesn't need this)
|
// Save credential to vault (crypto exchanges only, IBKR/MT5 don't need this)
|
||||||
if (isLive && values.save_credential && !this.isIBKRMarket) {
|
if (isLive && values.save_credential && !this.isIBKRMarket && !this.isMT5Market) {
|
||||||
try {
|
try {
|
||||||
await createExchangeCredential({
|
await createExchangeCredential({
|
||||||
user_id: 1,
|
user_id: 1,
|
||||||
|
|||||||
Reference in New Issue
Block a user