feat: 添加前端界面和市场分析模块

- 新增 Vue 3 + Vuetify 前端界面
- 新增市场分析模块 (market/)
- 更新主服务器和路由
- 更新 MT5 EA 文件
- 添加 .gitignore 排除临时文件
This commit is contained in:
guaiwoluo2020
2026-03-10 17:38:13 +08:00
parent 0c9cf048d2
commit 51b2f30748
40 changed files with 8576 additions and 151 deletions
+20
View File
@@ -0,0 +1,20 @@
# macOS
.DS_Store
# Python
venv/
__pycache__/
*.pyc
*.pyo
# Logs
*.log
# Backups
*.backup
# IDE
.claude/
# Node.js
node_modules/
+268
View File
@@ -0,0 +1,268 @@
# 前端界面集成指南
## 🎉 完成!前端界面已成功集成
你的量化交易服务现在同时拥有了**高性能后端**和**现代化前端界面**!
## 📋 项目概览
### 后端服务 (Python FastAPI)
- **端口**: 8000
- **状态**: ✅ 运行中
- **功能**: 交易指令管理、价格过滤、统计数据收集
### 前端界面 (React)
- **端口**: 3000
- **状态**: ✅ 运行中
- **功能**: 仪表板、交易管理、数据可视化、服务监控
## 🚀 快速启动
### 完整系统启动 (推荐)
**方式1: 脚本启动**
```bash
# 终端1: 启动后端
python main.py
# 终端2: 启动前端
./start_frontend.sh
```
**方式2: 手动启动**
```bash
# 终端1: 启动后端
python main.py
# 终端2: 启动前端
cd frontend && npm start
```
### 验证启动成功
```bash
# 检查后端
curl http://localhost:8000/health
# 应该返回: {"status":"ok"}
# 检查前端
curl -I http://localhost:3000
# 应该返回: HTTP/1.1 200 OK
```
## 🎯 主要功能
### 1. 仪表板 (Dashboard)
- 📊 实时服务状态监控
- 📈 关键指标展示 (待执行指令、统计记录、活跃品种)
- 🔄 自动刷新数据
### 2. 交易指令管理 (Trade Orders)
- 发送新的交易指令
-**智能价格验证**: 自动检查买入/卖出指令的价格合理性
- 📋 查看所有待执行指令
- 🗑️ 批量清空指令
### 3. 统计数据分析 (Statistics)
- 📊 历史数据表格展示
- 📈 价格和账户趋势图表
- 📋 汇总统计指标
### 4. 服务状态监控 (Status)
- ❤️ 健康检查状态
- 📊 详细的服务指标
- 🔄 实时自动刷新 (每5秒)
## 🔧 技术架构
```
┌─────────────────┐ HTTP/JSON ┌─────────────────┐
│ React前端 │◄──────────────►│ FastAPI后端 │
│ (localhost:3000)│ │ (localhost:8000) │
│ │ │ │
│ • Material-UI │ │ • 交易指令管理 │
│ • Axios │ │ • 价格过滤逻辑 │
│ • Recharts │ │ • 统计数据收集 │
│ • 响应式设计 │ │ • RESTful API │
└─────────────────┘ └─────────────────┘
┌─────────────────┐
│ MT5 EA │
│ (MetaTrader) │
│ │
│ • TICK处理 │
│ • 订单执行 │
│ • 风险管理 │
└─────────────────┘
```
## 📱 使用指南
### 访问界面
打开浏览器访问: **http://localhost:3000**
### 发送交易指令
1. 点击顶部导航栏的 **"交易指令"**
2. 填写交易表单:
- **交易品种**: GOLD, EURUSD 等
- **买卖方向**: 买入/卖出
- **手数**: 交易量
- **执行价格**: 指令价格 (用于价格过滤)
- **止损/止盈**: 可选 (自动填充默认值)
3. 点击 **"发送交易指令"**
### 查看统计数据
1. 点击 **"统计数据"**
2. 查看历史统计表格
3. 查看价格趋势图表
4. 调整显示条数 (最多10条)
### 监控服务状态
1. 点击 **"服务状态"**
2. 查看实时服务指标
3. 页面会自动每5秒刷新
## 🔒 安全特性
### 价格验证规则
- **买入指令**: 必须满足 `sl < price < tp`
- **卖出指令**: 必须满足 `tp < price < sl`
- **自动拒绝**: 不符合规则的指令会被服务器拒绝
### 数据验证
- 后端使用 Pydantic 进行数据验证
- 前端进行表单验证和错误处理
- API 请求包含错误处理和重试机制
## 📊 API 接口
### 主要端点
- `GET /health` - 健康检查
- `GET /status` - 服务状态
- `POST /send_trade_instructions` - 发送交易指令
- `GET /query_pending_trades` - 查询待执行指令
- `GET /query_statistics` - 查询统计数据
- `DELETE /clear_trades` - 清空指令
### API 文档
访问: **http://localhost:8000/docs**
## 🛠️ 开发和部署
### 开发环境
```bash
# 后端开发
python main.py # 支持热重载
# 前端开发
cd frontend && npm start # 支持热重载
```
### 生产部署
```bash
# 后端
pip install -r requirements.txt
python main.py
# 前端
cd frontend
npm run build
# 将 build 目录部署到 Web 服务器
```
### Docker 部署
```bash
# 后端
docker build -t trading-backend .
docker run -p 8000:8000 trading-backend
# 前端
cd frontend
npm run build
# 使用 nginx 或其他 Web 服务器部署 build 目录
```
## 🔍 故障排除
### 常见问题
**Q: 前端无法连接后端**
```bash
# 检查后端是否运行
curl http://localhost:8000/health
# 检查前端代理配置
# frontend/package.json 中 proxy 应为 "http://localhost:8000"
```
**Q: 页面显示空白**
```bash
# 检查浏览器控制台错误
# 确认 Node.js 和 npm 版本
node --version && npm --version
```
**Q: API 请求失败**
```bash
# 检查 CORS 配置
# 后端已配置允许所有源
```
**Q: MT5 EA 无法连接**
```bash
# 确保 EA 启用 WebRequest
# 在 MT5 中: 工具 → 选项 → EA交易 → 勾选 WebRequest
```
### 日志查看
```bash
# 后端日志 (控制台输出)
python main.py
# 前端日志 (浏览器开发者工具)
# F12 → Console 标签
```
## 📈 性能优化
- **后端**: FastAPI + uvloop 高性能异步处理
- **前端**: React 虚拟DOM + Material-UI 优化渲染
- **网络**: HTTP/2 支持,压缩传输
- **缓存**: 浏览器缓存静态资源
## 🎯 下一步扩展
### 短期计划
- [ ] 添加用户认证和权限管理
- [ ] 实现实时 WebSocket 推送
- [ ] 添加更多图表类型和指标
### 长期计划
- [ ] 移动端适配优化
- [ ] 多语言支持 (i18n)
- [ ] 主题切换 (暗色模式)
- [ ] 高级数据分析功能
## 📞 支持
如果遇到问题,请检查:
1. 浏览器开发者工具的错误信息
2. 终端的日志输出
3. API 文档的接口说明
## 📝 更新日志
### v1.0.0 (2026-03-05)
- ✅ 完成前后端集成
- ✅ 实现完整的交易指令管理
- ✅ 添加价格验证规则
- ✅ 集成数据可视化图表
- ✅ 实现实时状态监控
---
**恭喜!你的量化交易系统现在拥有了完整的现代化界面!**
🌟 **访问地址**: http://localhost:3000
📚 **API 文档**: http://localhost:8000/docs
+221
View File
@@ -0,0 +1,221 @@
# MT5 WebRequest 配置检查清单
## ⚠️ 重要提示
EA无法发送HTTP请求的最常见原因是**WebRequest权限未正确配置**。请按照以下步骤逐一检查。
## ✅ 配置步骤
### 1. 启用WebRequest权限
在MT5终端中:
1. 打开 **工具(Tools)****选项(Options)** (或按 `Ctrl+O`)
2. 切换到 **EA交易(Expert Advisors)** 标签
3. 勾选以下选项:
-**允许自动交易(Allow Automated Trading)**
-**允许WebRequest用于脚本...(Allow WebRequest for scripts and EA)**
### 2. 添加URL到允许列表
在同一页面:
1. 找到 **WebRequest允许的URL列表** 区域
2. 点击 **添加(Add)** 按钮
3. 输入以下URL(根据你的后端配置):
```
http://localhost:8000
```
或如果使用trading_server.py
```
http://localhost:5858
```
**注意**:
- 必须包含 `http://` 前缀
- 不要在末尾加 `/`
- 如果同时使用两个端口,两个都要添加
### 3. 重启EA
配置完成后:
1. 在图表上右键点击EA → **移除(Remove)**
2. 重新从导航器拖拽EA到图表
3. 确保EA显示为 ✅ **启用(Enabled)** 状态
## 🔍 验证配置
### 方法1: 检查MT5日志
1. 打开 **终端(Terminal)****日志(Journal)** 标签
2. 查找以下消息:
-`Expert initialized successfully`
-`Statistics sent successfully` (每分钟发送)
-`WebRequest is disabled!` → WebRequest未启用
-`Failed to send statistics` → URL未添加或服务未启动
### 方法2: 检查后端日志
启动后端服务后,应该看到:
```bash
# 启动后端
python main.py
# 观察日志,应该看到EA的请求:
# GET /get_trades?symbol=GOLD&price=2035.50
# POST /send_statistics
```
### 方法3: 使用测试工具
运行自动化测试验证服务:
```bash
python test_trading_service.py
```
## 🐛 常见问题排查
### 问题1: WebRequest is disabled
**症状**:
```
WebRequest is disabled! Please enable WebRequest in MT5 Options -> Expert Advisors
```
**解决方案**:
- 按照上述步骤1启用WebRequest权限
- 重启MT5
### 问题2: Connection refused
**症状**:
```
Failed to send statistics. Response code: 404
Failed to send statistics. Response code: 500
```
**解决方案**:
1. 确认后端服务正在运行
```bash
# 检查服务是否启动
lsof -i :8000 # 或 lsof -i :5858
# 如果没有运行,启动服务
python main.py # 端口8000
# 或
python trading_server.py # 端口5858
```
2. 确认端口匹配
- EA配置: `wangxxGold.mq5` 第22行
- 后端配置: `main.py` 第67行 (8000) 或 `trading_server.py` 第494行 (5858)
### 问题3: URL not in allowed list
**症状**:
```
Failed to send statistics. Response code: -1
```
**解决方案**:
- 确保已添加 `http://localhost:8000` 到允许列表
- 不要添加 `https://`(除非你的服务器配置了HTTPS
- 重启EA
### 问题4: Timeout
**症状**:
```
Failed to send statistics. Response code: 408
```
**解决方案**:
- 检查后端服务响应时间
- 检查网络连接
- 尝试在浏览器访问 `http://localhost:8000/health` 测试连接
## 📋 快速检查清单
```
□ MT5选项中启用"允许自动交易"
□ MT5选项中启用"允许WebRequest"
□ 添加 http://localhost:8000 到允许列表
□ 后端服务已启动 (python main.py)
□ EA已重启并启用
□ MT5日志显示"Expert initialized successfully"
□ 后端日志显示EA的HTTP请求
```
## 🧪 测试流程
### 完整测试流程:
```bash
# 终端1: 启动后端
python main.py
# 终端2: 测试后端
curl http://localhost:8000/health
# 应该返回: {"status": "healthy", ...}
# MT5终端:
# 1. 配置WebRequest权限
# 2. 加载EA到图表
# 3. 观察MT5日志和后端日志
# 终端3: 发送测试交易指令
python trade_client.py
# 选择1: 发送买入订单
# 检查EA是否执行交易
```
## 💡 调试技巧
### 启用详细日志
在EA代码中,所有HTTP请求都有详细的日志输出。如果你看不到任何WebRequest相关的日志:
1. **检查EA是否真的在运行**
- 图表右上角应该有EA图标和笑脸 😊
- 如果是哭脸 😢,说明EA初始化失败
2. **检查OnTick是否被调用**
- 每次价格变动都会调用OnTick
- 应该看到统计数据变化
3. **手动触发HTTP请求**
```bash
# 测试后端连接
curl -X GET "http://localhost:8000/get_trades?symbol=GOLD&price=2035.50"
# 测试统计接口
curl -X POST "http://localhost:8000/send_statistics" \
-H "Content-Type: application/json" \
-d '{"tickCount":100,"bidPrice":2035.50}'
```
## 📚 相关文档
- [MT5 WebRequest官方文档](https://www.mql5.com/en/docs/network/webrequest)
- [项目README](README.md)
- [快速开始指南](QUICKSTART.md)
## ❓ 需要帮助?
如果按照以上步骤仍无法解决问题:
1. 检查MT5日志文件(通常在 `MQL5/Logs/` 目录)
2. 检查后端服务日志
3. 使用 `curl` 测试后端接口
4. 确认防火墙没有阻止连接
---
**最后更新**: 2026-03-05
+132 -7
View File
@@ -43,7 +43,36 @@ curl http://localhost:5858/status
### 方式 C: 访问API文档
打开浏览器访问: [http://localhost:5858/docs](http://localhost:5858/docs)
## 4. 使用交易工具
## 4. 启动前端界面 (Vue)
### 方式 A: 使用启动脚本 (推荐)
```bash
cd frontend
./start_vue.sh
```
### 方式 B: 手动启动
```bash
cd frontend
npm install # 首次运行需要
npm run dev
```
### 方式 C: 直接运行
```bash
cd frontend
npx vite --host 0.0.0.0 --port 3001
```
**前端访问地址**: http://localhost:3001
### 前端功能
- 📊 **仪表板**: 实时服务状态和关键指标
- 📋 **交易指令**: 发送和管理交易指令 (带智能价格验证)
- 📈 **统计数据**: 数据可视化和历史分析
- ❤️ **服务状态**: 实时监控和健康检查
## 5. 使用交易工具
### 方式 A: 交互式命令行工具
```bash
@@ -118,7 +147,7 @@ curl "http://localhost:5858/query_pending_trades"
curl "http://localhost:5858/query_statistics?count=5"
```
## 5. MT5 EA集成
## 6. MT5 EA集成
MT5 EA已经配置好,只需确保:
@@ -137,7 +166,7 @@ MT5 EA已经配置好,只需确保:
EA每分钟→ 统计数据 → Python服务 → 交易员查询统计
```
## 6. 常见操作
## 7. 常见操作
### 发送一个黄金买入单
@@ -289,14 +318,99 @@ while True:
time.sleep(1)
```
## 10. 下一步
## 11. Vue 前端详细说明
### 🎯 技术栈
- **Vue 3** + Composition API
- **Vuetify 3** (Material Design)
- **Vue Router 4**
- **Axios** (HTTP 客户端)
- **ECharts** (数据可视化)
- **Vite** (构建工具)
### 🌟 前端特性
- ✅ **现代化UI**: Material Design 设计规范
- ✅ **响应式布局**: 支持桌面和移动设备
- ✅ **实时更新**: 自动刷新数据和状态
- ✅ **智能验证**: 交易指令价格自动验证
- ✅ **数据可视化**: ECharts 图表展示
- ✅ **中文界面**: 完全本地化
### 🎨 界面功能
#### 仪表板 (Dashboard)
- 📊 服务状态指示器
- 📈 关键指标卡片 (待执行指令、统计记录、活跃品种)
- 🔄 自动数据刷新 (30秒间隔)
#### 交易指令 (Trade Orders)
- ➕ 交易指令表单 (品种、方向、手数、价格、止损/止盈)
- ✅ **智能价格验证**:
- 买入: `止损 < 执行价格 < 止盈`
- 卖出: `止盈 < 执行价格 < 止损`
- 📋 待执行指令表格
- 🗑️ 一键清空所有指令
#### 统计数据 (Statistics)
- 📊 历史数据表格 (时间、品种、价格、类型)
- 📈 价格趋势线图 (ECharts)
- 📋 汇总统计 (总记录数、平均价格、最高/最低价)
#### 服务状态 (Status)
- ❤️ 健康检查状态
- 📊 系统指标 (运行时间、内存使用等)
- 🔄 连接状态监控 (后端、MT5)
- ⏰ 实时更新 (5秒间隔)
### 🚀 快速体验
```bash
# 启动完整系统
# 终端1: 启动后端
python main.py
# 终端2: 启动前端
cd frontend && ./start_vue.sh
# 访问前端: http://localhost:3001
```
### 🔧 开发和部署
#### 开发环境
```bash
cd frontend
npm install
npm run dev # 开发服务器
```
#### 生产构建
```bash
cd frontend
npm run build # 构建生产版本
npm run preview # 预览构建结果
```
#### 项目结构
```
frontend/
├── src/
│ ├── views/ # 页面组件
│ ├── api/ # API 接口
│ ├── router/ # 路由配置
│ └── plugins/ # Vuetify 配置
├── public/ # 静态资源
└── README.md # 详细文档
```
## 12. 下一步
- 详细API文档: [README.md](README.md)
- 自动化测试: `python test_trading_service.py`
- API交互式文档: http://localhost:5858/docs
- 性能监控: `curl http://localhost:5858/status`
## 11. 后续优化
## 13. 后续优化
虽然当前HTTP性能足够,但可考虑:
@@ -315,6 +429,17 @@ while True:
## 需要帮助?
查看完整文档: [README.md](README.md)
查看完整文档:
- [主项目文档](README.md)
- [Vue 前端文档](frontend/README.md)
- API交互式文档: http://localhost:8000/docs
- 前端界面: http://localhost:3001
祝您交易愉快!🚀
## 🎉 祝您使用愉快!
现在您拥有了完整的量化交易系统:
- ⚡ 高性能 Python 后端
- 🎨 现代化 Vue 前端界面
- 🤖 智能 MT5 EA 集成
**开始您的量化交易之旅吧!** 🚀
+59 -11
View File
@@ -5,19 +5,67 @@
这是一个为MT5 EA提供支持的高性能交易服务,采用FastAPI框架,支持以下功能:
### 核心功能
1. **EA接口** - MT5 EA与服务通信
- `GET /get_trades` - EA获取待执行的交易指令(按SYMBOL分类)
- `POST /send_statistics` - EA发送每分钟的统计数据
2. **交易员接口** - 交易员下发指令和查询数据
- `POST /send_trade_instructions` - 下发交易指令
- `GET /query_pending_trades` - 查询所有待执行指令
- `DELETE /clear_trades` - 清空交易指令
- `GET /query_statistics` - 查询统计数据(保留最新10条)
#### 1. EA接口 - MT5 EA与服务通信
- `GET /get_trades` - EA获取待执行的交易指令(按SYMBOL分类)
- `POST /send_statistics` - EA发送每分钟的统计数据
- `POST /ea/kline/{period}` - EA推送K线数据 (H4/H1/M15/M5/M1)
- `POST /ea/kline_batch` - EA批量推送多个周期的K线数据
3. **系统接口** - 服务监控和健康检查
- `GET /health` - 健康检查
- `GET /status` - 服务状态
#### 2. 行情分析接口
- `GET /market/kline/{symbol}` - 查询K线数据
- `GET /market/pivots/{symbol}` - 查询转折点数据
- `GET /market/status` - 获取行情存储状态
- `GET /market/thresholds` - 获取各周期接近阈值
- `WebSocket /ws/market` - 实时转折点提醒推送
#### 3. 交易员接口 - 交易员下发指令和查询数据
- `POST /send_trade_instructions` - 下发交易指令
- `GET /query_pending_trades` - 查询所有待执行指令
- `DELETE /clear_trades` - 清空交易指令
- `GET /query_statistics` - 查询统计数据(保留最新10条)
#### 4. 系统接口 - 服务监控和健康检查
- `GET /health` - 健康检查
- `GET /status` - 服务状态
## 新增功能:转折点检测与提醒
### K线数据接收
EA启动后会推送各周期K线数据:
| 周期 | 历史数据要求 |
|------|-------------|
| H4 (4小时) | 最近6个月 |
| H1 (1小时) | 最近1个月 |
| M15 (15分钟) | 最近3天 |
| M5 (5分钟) | 最近24小时 |
| M1 (1分钟) | 最近1小时 |
### 转折点检测
服务自动检测K线的转折点(高点/低点):
- 使用分型识别算法(顶分型/底分型)
- 默认左右各3根K线确认转折
### 接近阈值
各周期距离转折点的提醒阈值:
| 周期 | 阈值 | 说明 |
|------|------|------|
| H4 | 千分之6 | 如GOLD高点5000,当前4980提醒 |
| H1 | 千分之3 | 如GOLD高点5000,当前4985提醒 |
| M15 | 千分之1.5 | - |
| M5 | 千分之0.5 | - |
| M1 | 千分之0.2 | - |
### 实时提醒
EA调用 `/get_trades` 时携带价格参数,服务自动检查是否接近转折点,返回提醒信息。
同时支持WebSocket推送实时提醒到前端页面。
## 安装和运行
+251
View File
@@ -0,0 +1,251 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
行情API调用示例
演示EA端如何推送K线数据到Python服务
"""
import requests
import json
from datetime import datetime, timedelta
import random
# 服务地址
BASE_URL = "http://localhost:8000"
def generate_mock_klines(count: int, base_price: float, period_minutes: int) -> list:
"""
生成模拟K线数据
"""
klines = []
now = datetime.now()
for i in range(count):
# 计算时间戳
ts = now - timedelta(minutes=period_minutes * (count - i - 1))
# 生成随机价格波动
change = random.uniform(-0.01, 0.01) * base_price
open_price = base_price + change
high = open_price + random.uniform(0, 0.005) * base_price
low = open_price - random.uniform(0, 0.005) * base_price
close = open_price + random.uniform(-0.003, 0.003) * base_price
klines.append({
"timestamp": ts.strftime("%Y-%m-%d %H:%M:%S"),
"open": round(open_price, 2),
"high": round(high, 2),
"low": round(low, 2),
"close": round(close, 2),
"volume": random.randint(100, 1000)
})
# 更新基准价格
base_price = close
return klines
def push_kline_single_period(symbol: str, period: str, klines: list, is_full: bool = False):
"""
推送单个周期的K线数据
"""
url = f"{BASE_URL}/ea/kline/{period}"
payload = {
"symbol": symbol,
"is_full": is_full,
"klines": klines
}
print(f"\n推送 {symbol} {period} K线数据 ({len(klines)} 条, {'全量' if is_full else '增量'})")
try:
response = requests.post(url, json=payload)
print(f"状态码: {response.status_code}")
print(f"响应: {response.json()}")
# 检查是否需要全量数据
if response.status_code == 400:
data = response.json()
if data.get('code') == 8888:
print(">>> 服务需要全量数据,请重新发送历史数据")
return 8888
return response.status_code
except Exception as e:
print(f"请求失败: {e}")
return None
def push_kline_batch(symbol: str, kline_data: dict, is_full: bool = False):
"""
批量推送多个周期的K线数据
"""
url = f"{BASE_URL}/ea/kline_batch"
payload = {
"symbol": symbol,
"is_full": is_full,
"data": kline_data
}
print(f"\n批量推送 {symbol} K线数据 ({'全量' if is_full else '增量'})")
try:
response = requests.post(url, json=payload)
print(f"状态码: {response.status_code}")
print(f"响应: {json.dumps(response.json(), indent=2, ensure_ascii=False)}")
return response.status_code
except Exception as e:
print(f"请求失败: {e}")
return None
def get_trades_with_price(symbol: str, price: float):
"""
获取交易指令(携带价格,用于转折点检测)
"""
url = f"{BASE_URL}/get_trades"
params = {
"symbol": symbol,
"price": price
}
try:
response = requests.get(url, params=params)
data = response.json()
print(f"\n获取 {symbol} 交易指令 (价格: {price})")
print(f"交易指令: {data.get('trades', [])}")
# 检查转折点提醒
alerts = data.get('pivot_alerts', [])
if alerts:
print(f"\n⚠️ 转折点提醒 ({len(alerts)} 条):")
for alert in alerts:
print(f" - {alert['period']} {alert['direction'] == 'high' and '高点' or '低点'}")
print(f" 转折点价格: {alert['pivot_price']}")
print(f" 当前价格: {alert['current_price']}")
print(f" 距离: {alert['distance_pct']}%")
else:
print("无转折点提醒")
return data
except Exception as e:
print(f"请求失败: {e}")
return None
def get_klines(symbol: str, period: str, count: int = 100):
"""查询K线数据"""
url = f"{BASE_URL}/market/kline/{symbol}"
params = {"period": period, "count": count}
try:
response = requests.get(url, params=params)
data = response.json()
print(f"\n{symbol} {period} K线数据 ({data['count']} 条):")
for k in data['data'][-3:]:
print(f" {k['timestamp']}: O={k['open']} H={k['high']} L={k['low']} C={k['close']}")
return data
except Exception as e:
print(f"请求失败: {e}")
return None
def get_pivots(symbol: str):
"""查询转折点"""
url = f"{BASE_URL}/market/pivots/{symbol}"
try:
response = requests.get(url)
data = response.json()
print(f"\n{symbol} 转折点数据:")
for period, pivots in data.get('data', {}).items():
if pivots:
print(f" {period}: {len(pivots)} 个转折点")
for p in pivots[:3]:
print(f" - {p['direction'] == 'high' and '高点' or '低点'} @ {p['price']} ({p['timestamp']})")
return data
except Exception as e:
print(f"请求失败: {e}")
return None
def main():
"""
演示完整的EA启动流程
"""
print("=" * 60)
print("EA 行情数据推送示例")
print("=" * 60)
symbol = "GOLD"
base_price = 2030.0
# ==================== 1. EA启动时推送全量K线数据 ====================
print("\n[步骤1] EA启动,推送全量K线数据...")
# 各周期历史数据条数(根据实际需求)
history_counts = {
'H4': 1100, # 6个月约1100根4小时K线
'H1': 720, # 1个月约720根1小时K线
'M15': 288, # 3天约288根15分钟K线
'M5': 288, # 24小时288根5分钟K线
'M1': 60 # 1小时60根1分钟K线
}
kline_data = {}
period_minutes = {'H4': 240, 'H1': 60, 'M15': 15, 'M5': 5, 'M1': 1}
for period, count in history_counts.items():
kline_data[period] = generate_mock_klines(count, base_price, period_minutes[period])
push_kline_batch(symbol, kline_data, is_full=True)
# ==================== 2. 查询K线和转折点 ====================
print("\n[步骤2] 查询K线数据和转折点...")
get_klines(symbol, 'H4', 10)
get_pivots(symbol)
# ==================== 3. 模拟EA轮询获取交易指令 ====================
print("\n[步骤3] EA轮询获取交易指令(携带当前价格)...")
# 模拟当前价格
current_price = base_price + random.uniform(-5, 5)
get_trades_with_price(symbol, current_price)
# ==================== 4. 模拟增量数据推送 ====================
print("\n[步骤4] 推送增量K线数据...")
# 生成一根新的K线
for period in ['H4', 'H1', 'M15', 'M5', 'M1']:
new_klines = generate_mock_klines(1, base_price, period_minutes[period])
push_kline_single_period(symbol, period, new_klines, is_full=False)
# ==================== 5. 查看服务状态 ====================
print("\n[步骤5] 查看服务状态...")
try:
response = requests.get(f"{BASE_URL}/market/status")
data = response.json()
print(json.dumps(data, indent=2, ensure_ascii=False))
except Exception as e:
print(f"请求失败: {e}")
print("\n" + "=" * 60)
print("示例完成")
print("=" * 60)
if __name__ == "__main__":
main()
+207
View File
@@ -0,0 +1,207 @@
# 量化交易系统 - Vue 前端
基于 Vue 3 + Vuetify 的现代化量化交易前端界面
## 🚀 技术栈
- **Vue 3** - 渐进式 JavaScript 框架
- **Vuetify 3** - Material Design 组件库
- **Vue Router 4** - 官方路由管理器
- **Axios** - HTTP 客户端
- **ECharts** - 数据可视化图表
- **Vite** - 快速构建工具
## 📦 安装依赖
```bash
cd frontend
npm install
```
## 🏃‍♂️ 开发运行
```bash
# 启动开发服务器
npm run dev
# 或直接使用 npx
npx vite --host 0.0.0.0 --port 3000
```
## 🏗️ 生产构建
```bash
# 构建生产版本
npm run build
# 预览构建结果
npm run preview
```
## 🌐 访问地址
- **开发环境**: http://localhost:3001
- **生产环境**: 根据部署配置
## 📱 功能特性
### 1. 仪表板 (Dashboard)
- 📊 实时服务状态监控
- 📈 关键指标展示 (待执行指令、统计记录、活跃品种)
- 🔄 自动刷新数据
### 2. 交易指令管理 (Trade Orders)
- 发送新的交易指令
-**智能价格验证**: 自动检查买入/卖出指令的价格合理性
- 📋 查看所有待执行指令
- 🗑️ 批量清空指令
### 3. 统计数据分析 (Statistics)
- 📊 历史数据表格展示
- 📈 价格趋势图表
- 📋 汇总统计指标
### 4. 服务状态监控 (Status)
- ❤️ 健康检查状态
- 📊 详细的服务指标
- 🔄 实时自动刷新 (每5秒)
## 🔧 项目结构
```
frontend/
├── public/ # 静态资源
├── src/
│ ├── api/ # API 接口
│ │ └── trading.js # 交易 API
│ ├── components/ # 组件 (预留)
│ ├── plugins/ # 插件配置
│ │ └── vuetify.js # Vuetify 配置
│ ├── router/ # 路由配置
│ │ └── index.js # 路由定义
│ ├── views/ # 页面视图
│ │ ├── Dashboard.vue # 仪表板
│ │ ├── TradeOrders.vue # 交易指令
│ │ ├── Statistics.vue # 统计数据
│ │ └── Status.vue # 服务状态
│ ├── App.vue # 根组件
│ ├── main.js # 入口文件
│ └── style.css # 全局样式
├── package.json # 项目配置
├── vite.config.js # Vite 配置
└── index.html # HTML 模板
```
## 🔗 API 集成
前端通过代理自动连接到后端 API
- **后端地址**: http://localhost:8000
- **代理路径**: `/api/*``http://localhost:8000/*`
## 🎨 UI 设计
- **Material Design**: 使用 Google Material Design 规范
- **响应式布局**: 支持桌面和移动设备
- **深色主题**: 支持亮色/暗色主题切换
- **中文界面**: 完全本地化的中文界面
## 🔒 安全特性
### 价格验证规则
- **买入指令**: 必须满足 `sl < price < tp`
- **卖出指令**: 必须满足 `tp < price < sl`
- **自动拒绝**: 不符合规则的指令会被服务器拒绝
## 📊 数据可视化
- **ECharts 图表**: 价格趋势线图
- **实时更新**: 图表数据自动刷新
- **交互式**: 支持缩放、拖拽等交互
## 🚀 性能优化
- **Vite 构建**: 快速的冷启动和热重载
- **代码分割**: 自动路由级代码分割
- **懒加载**: 组件按需加载
- **缓存优化**: 浏览器缓存策略
## 🛠️ 开发工具
- **ESLint**: 代码规范检查
- **Vue DevTools**: Vue 开发调试工具
- **热重载**: 修改代码即时预览
## 📝 开发指南
### 添加新页面
1.`src/views/` 创建 Vue 组件
2.`src/router/index.js` 添加路由配置
3.`src/App.vue` 的菜单中添加导航项
### API 调用
```javascript
import { tradingAPI } from '@/api/trading'
// 发送交易指令
await tradingAPI.sendTradeInstructions(instructions)
// 查询统计数据
const stats = await tradingAPI.getStatistics()
```
### 组件开发
```vue
<template>
<v-card>
<v-card-title>我的组件</v-card-title>
<v-card-text>
<!-- 组件内容 -->
</v-card-text>
</v-card>
</template>
<script>
export default {
name: 'MyComponent',
setup() {
// 组件逻辑
return {
// 响应式数据
}
}
}
</script>
```
## 🔄 与 React 版本对比
| 特性 | Vue 版本 | React 版本 |
|------|----------|------------|
| 框架 | Vue 3 + Composition API | React 18 + Hooks |
| UI库 | Vuetify 3 | Material-UI 5 |
| 路由 | Vue Router 4 | React Router 6 |
| 图表 | ECharts | Recharts |
| 构建 | Vite | Create React App |
| 学习曲线 | 较平缓 | 较陡峭 |
| 性能 | 优秀 | 优秀 |
| 生态 | 成熟 | 庞大 |
## 🎯 优势特点
1. **Vue 生态**: 你熟悉的 Vue 框架和语法
2. **Vuetify**: 功能完整的 Material Design 组件库
3. **TypeScript 支持**: 可选的 TypeScript 支持
4. **开发体验**: 优秀的开发工具和热重载
5. **性能优化**: Vue 3 的优秀性能表现
## 📞 技术支持
如果遇到问题,请检查:
1. 浏览器开发者工具的错误信息
2. 终端的日志输出
3. 确保后端服务正在运行 (http://localhost:8000)
---
**🌟 享受 Vue 开发的乐趣!**
+15
View File
@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>量化交易系统</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900&display=swap">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@5.x/css/materialdesignicons.min.css">
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
+1628
View File
File diff suppressed because it is too large Load Diff
+24
View File
@@ -0,0 +1,24 @@
{
"name": "lianghua-trading-frontend-vue",
"version": "1.0.0",
"description": "Vue 3 前端界面 for 高性能交易服务",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.4.0",
"vue-router": "^4.2.0",
"axios": "^1.6.0",
"vuetify": "^3.5.0",
"echarts": "^5.4.0",
"vue-echarts": "^6.6.0",
"@mdi/font": "^7.4.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.0",
"vite": "^5.0.0"
}
}
+63
View File
@@ -0,0 +1,63 @@
<template>
<v-app>
<v-app-bar app color="primary" dark>
<v-app-bar-nav-icon @click="drawer = !drawer"></v-app-bar-nav-icon>
<v-toolbar-title>量化交易系统</v-toolbar-title>
<v-spacer></v-spacer>
<v-btn icon>
<v-icon>mdi-refresh</v-icon>
</v-btn>
</v-app-bar>
<v-navigation-drawer v-model="drawer" app>
<v-list>
<v-list-item
v-for="item in menuItems"
:key="item.title"
:to="item.path"
link
>
<v-list-item-icon>
<v-icon>{{ item.icon }}</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>{{ item.title }}</v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list>
</v-navigation-drawer>
<v-main>
<router-view />
</v-main>
</v-app>
</template>
<script>
import { ref } from 'vue'
export default {
name: 'App',
setup() {
const drawer = ref(false)
const menuItems = [
{ title: '仪表板', path: '/', icon: 'mdi-view-dashboard' },
{ title: '交易指令', path: '/trades', icon: 'mdi-format-list-bulleted' },
{ title: '行情分析', path: '/market', icon: 'mdi-chart-candlestick' },
{ title: '统计数据', path: '/statistics', icon: 'mdi-chart-line' },
{ title: '服务状态', path: '/status', icon: 'mdi-information' },
]
return {
drawer,
menuItems,
}
},
}
</script>
<style scoped>
.v-app-bar {
z-index: 1000;
}
</style>
+149
View File
@@ -0,0 +1,149 @@
import axios from 'axios'
const api = axios.create({
baseURL: 'http://localhost:8000',
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
})
export const marketAPI = {
// 获取所有symbol列表
async getSymbols() {
const response = await api.get('/market/symbols')
return response.data
},
// 获取K线数据
async getKlines(symbol, period = 'M5', count = 100) {
const encodedSymbol = encodeURIComponent(symbol)
const response = await api.get(`/market/kline/${encodedSymbol}`, {
params: { period, count }
})
return response.data
},
// 获取转折点数据
async getPivots(symbol, period = null, direction = null, count = 50) {
const params = { count }
if (period) params.period = period
if (direction) params.direction = direction
const encodedSymbol = encodeURIComponent(symbol)
const response = await api.get(`/market/pivots/${encodedSymbol}`, { params })
return response.data
},
// 获取行情状态
async getStatus() {
const response = await api.get('/market/status')
return response.data
},
// 获取阈值配置
async getThresholds() {
const response = await api.get('/market/thresholds')
return response.data
},
// 创建WebSocket连接
createWebSocket(onMessage, onError, onOpen, onClose) {
const ws = new WebSocket('ws://localhost:8000/ws/market')
ws.onopen = () => {
console.log('WebSocket 连接成功')
if (onOpen) onOpen()
}
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data)
if (onMessage) onMessage(data)
} catch (e) {
console.error('WebSocket 消息解析错误:', e)
}
}
ws.onerror = (error) => {
console.error('WebSocket 错误:', error)
if (onError) onError(error)
}
ws.onclose = () => {
console.log('WebSocket 连接关闭')
if (onClose) onClose()
}
return ws
},
// 发送心跳
sendPing(ws) {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'ping' }))
}
},
// 获取趋势分析
async getTrend(symbol) {
const response = await api.get(`/trend/${encodeURIComponent(symbol)}`)
return response.data
},
// 生成交易建议
async generateTradeOrder(symbol) {
const response = await api.post(`/trend/generate_order/${encodeURIComponent(symbol)}`)
return response.data
},
// 获取待确认订单
async getPendingOrders(symbol = null) {
const params = symbol ? { symbol } : {}
const response = await api.get('/pending_orders', { params })
return response.data
},
// 确认订单
async confirmOrder(orderId) {
const response = await api.post(`/pending_orders/${orderId}/confirm`)
return response.data
},
// 确认订单并更新参数
async confirmOrderWithUpdate(orderId, updateData) {
const response = await api.post(`/pending_orders/${orderId}/confirm`, updateData)
return response.data
},
// 拒绝订单
async rejectOrder(orderId) {
const response = await api.post(`/pending_orders/${orderId}/reject`)
return response.data
},
// 获取交易配置
async getTradeConfig() {
const response = await api.get('/trade_config')
return response.data
},
// 更新交易配置
async updateTradeConfig(config) {
const response = await api.post('/trade_config', config)
return response.data
},
// 获取统计数据(包含持仓)
async getStatistics(count = 1) {
const response = await api.get('/query_statistics', { params: { count } })
return response.data
},
// 平仓
async closePosition(ticket, symbol) {
const response = await api.post('/close_position', { ticket, symbol })
return response.data
}
}
export default api
+71
View File
@@ -0,0 +1,71 @@
import axios from 'axios'
const api = axios.create({
baseURL: 'http://localhost:8000',
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
})
// 请求拦截器
api.interceptors.request.use(
(config) => {
// 可以在这里添加认证token等
return config
},
(error) => {
return Promise.reject(error)
}
)
// 响应拦截器
api.interceptors.response.use(
(response) => {
return response
},
(error) => {
console.error('API Error:', error)
return Promise.reject(error)
}
)
export const tradingAPI = {
// 健康检查
async health() {
const response = await api.get('/health')
return response.data
},
// 获取服务状态
async getStatus() {
const response = await api.get('/status')
return response.data
},
// 发送交易指令
async sendTradeInstructions(instructions) {
const response = await api.post('/send_trade_instructions', instructions)
return response.data
},
// 查询待执行指令
async getPendingTrades() {
const response = await api.get('/query_pending_trades')
return response.data
},
// 查询统计数据
async getStatistics() {
const response = await api.get('/query_statistics')
return response.data
},
// 清空指令
async clearTrades() {
const response = await api.delete('/clear_trades')
return response.data
},
}
export default api
+13
View File
@@ -0,0 +1,13 @@
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import vuetify from './plugins/vuetify'
import './style.css'
const app = createApp(App)
app.use(router)
app.use(vuetify)
app.mount('#app')
+34
View File
@@ -0,0 +1,34 @@
// plugins/vuetify.js
import 'vuetify/styles'
import { createVuetify } from 'vuetify'
import * as components from 'vuetify/components'
import * as directives from 'vuetify/directives'
import { aliases, mdi } from 'vuetify/iconsets/mdi'
export default createVuetify({
components,
directives,
icons: {
defaultSet: 'mdi',
aliases,
sets: {
mdi,
},
},
theme: {
defaultTheme: 'light',
themes: {
light: {
colors: {
primary: '#1976D2',
secondary: '#424242',
accent: '#82B1FF',
error: '#FF5252',
info: '#2196F3',
success: '#4CAF50',
warning: '#FFC107',
},
},
},
},
})
+41
View File
@@ -0,0 +1,41 @@
import { createRouter, createWebHistory } from 'vue-router'
import Dashboard from '../views/Dashboard.vue'
import TradeOrders from '../views/TradeOrders.vue'
import Statistics from '../views/Statistics.vue'
import Status from '../views/Status.vue'
import Market from '../views/Market.vue'
const routes = [
{
path: '/',
name: 'Dashboard',
component: Dashboard
},
{
path: '/trades',
name: 'TradeOrders',
component: TradeOrders
},
{
path: '/statistics',
name: 'Statistics',
component: Statistics
},
{
path: '/status',
name: 'Status',
component: Status
},
{
path: '/market',
name: 'Market',
component: Market
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
+11
View File
@@ -0,0 +1,11 @@
/* style.css */
#app {
font-family: 'Roboto', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
margin: 0;
padding: 0;
}
+136
View File
@@ -0,0 +1,136 @@
<template>
<v-container fluid>
<v-row>
<v-col cols="12">
<h1 class="mb-4">仪表板</h1>
</v-col>
</v-row>
<!-- 状态卡片 -->
<v-row>
<v-col cols="12" sm="6" md="3">
<v-card>
<v-card-title class="d-flex align-center">
<v-icon class="me-2">mdi-heart</v-icon>
服务状态
</v-card-title>
<v-card-text>
<v-chip
:color="status.ok ? 'success' : 'error'"
variant="flat"
>
{{ status.ok ? '正常' : '异常' }}
</v-chip>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" sm="6" md="3">
<v-card>
<v-card-title class="d-flex align-center">
<v-icon class="me-2">mdi-format-list-bulleted</v-icon>
待执行指令
</v-card-title>
<v-card-text>
<div class="text-h4">{{ pendingTradesCount }}</div>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" sm="6" md="3">
<v-card>
<v-card-title class="d-flex align-center">
<v-icon class="me-2">mdi-chart-line</v-icon>
统计记录
</v-card-title>
<v-card-text>
<div class="text-h4">{{ statisticsCount }}</div>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" sm="6" md="3">
<v-card>
<v-card-title class="d-flex align-center">
<v-icon class="me-2">mdi-currency-usd</v-icon>
活跃品种
</v-card-title>
<v-card-text>
<div class="text-h4">{{ activeSymbolsCount }}</div>
</v-card-text>
</v-card>
</v-col>
</v-row>
<!-- 错误信息 -->
<v-row v-if="error">
<v-col cols="12">
<v-alert type="error" dismissible>
{{ error }}
</v-alert>
</v-col>
</v-row>
</v-container>
</template>
<script>
import { ref, onMounted } from 'vue'
import { tradingAPI } from '@/api/trading'
export default {
name: 'Dashboard',
setup() {
const status = ref({ ok: false })
const pendingTradesCount = ref(0)
const statisticsCount = ref(0)
const activeSymbolsCount = ref(0)
const error = ref('')
const loadData = async () => {
try {
error.value = ''
// 获取服务状态
const statusData = await tradingAPI.getStatus()
status.value = { ok: statusData.status === 'ok' }
// 获取待执行指令数量
const pendingTrades = await tradingAPI.getPendingTrades()
pendingTradesCount.value = pendingTrades.length || 0
// 获取统计数据数量
const statistics = await tradingAPI.getStatistics()
statisticsCount.value = (statistics.statistics || []).length
// 计算活跃品种数量
const symbols = new Set()
if (pendingTrades && pendingTrades.length > 0) {
pendingTrades.forEach(trade => {
if (trade.symbol) symbols.add(trade.symbol)
})
}
activeSymbolsCount.value = symbols.size
} catch (err) {
error.value = `加载数据失败: ${err.message}`
console.error('Dashboard error:', err)
}
}
onMounted(() => {
loadData()
// 每30秒自动刷新
setInterval(loadData, 30000)
})
return {
status,
pendingTradesCount,
statisticsCount,
activeSymbolsCount,
error,
loadData,
}
},
}
</script>
File diff suppressed because it is too large Load Diff
+248
View File
@@ -0,0 +1,248 @@
<template>
<v-container fluid>
<v-row>
<v-col cols="12">
<h1 class="mb-4">统计数据分析</h1>
</v-col>
</v-row>
<!-- 汇总统计 -->
<v-row>
<v-col cols="12" md="3">
<v-card>
<v-card-title class="d-flex align-center">
<v-icon class="me-2">mdi-counter</v-icon>
总记录数
</v-card-title>
<v-card-text>
<div class="text-h4">{{ totalRecords }}</div>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" md="3">
<v-card>
<v-card-title class="d-flex align-center">
<v-icon class="me-2">mdi-trending-up</v-icon>
平均价格
</v-card-title>
<v-card-text>
<div class="text-h4">{{ averagePrice.toFixed(5) }}</div>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" md="3">
<v-card>
<v-card-title class="d-flex align-center">
<v-icon class="me-2">mdi-chart-line</v-icon>
最高价格
</v-card-title>
<v-card-text>
<div class="text-h4">{{ maxPrice.toFixed(5) }}</div>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" md="3">
<v-card>
<v-card-title class="d-flex align-center">
<v-icon class="me-2">mdi-chart-line-variant</v-icon>
最低价格
</v-card-title>
<v-card-text>
<div class="text-h4">{{ minPrice.toFixed(5) }}</div>
</v-card-text>
</v-card>
</v-col>
</v-row>
<!-- 图表 -->
<v-row>
<v-col cols="12">
<v-card>
<v-card-title>价格趋势图</v-card-title>
<v-card-text>
<div ref="chartContainer" style="width: 100%; height: 400px;"></div>
</v-card-text>
</v-card>
</v-col>
</v-row>
<!-- 数据表格 -->
<v-row>
<v-col cols="12">
<v-card>
<v-card-title>详细数据</v-card-title>
<v-card-text>
<v-data-table
:headers="tableHeaders"
:items="statistics"
:loading="loading"
:items-per-page="itemsPerPage"
no-data-text="暂无统计数据"
density="compact"
>
<template v-slot:item.bidPrice="{ item }">
{{ item.bidPrice.toFixed(2) }}
</template>
<template v-slot:item.askPrice="{ item }">
{{ item.askPrice.toFixed(2) }}
</template>
<template v-slot:item.balance="{ item }">
{{ item.balance.toFixed(2) }}
</template>
</v-data-table>
</v-card-text>
</v-card>
</v-col>
</v-row>
<!-- 错误信息 -->
<v-row v-if="error">
<v-col cols="12">
<v-alert type="error" dismissible>
{{ error }}
</v-alert>
</v-col>
</v-row>
</v-container>
</template>
<script>
import { ref, onMounted, nextTick } from 'vue'
import * as echarts from 'echarts'
import { tradingAPI } from '@/api/trading'
export default {
name: 'Statistics',
setup() {
const chartContainer = ref(null)
const chart = ref(null)
const loading = ref(false)
const error = ref('')
const statistics = ref([])
const itemsPerPage = ref(10)
const totalRecords = ref(0)
const averagePrice = ref(0)
const maxPrice = ref(0)
const minPrice = ref(0)
const tableHeaders = [
{ title: '时间', key: 'timestamp', width: '20%' },
{ title: '品种', key: 'symbol', width: '15%' },
{ title: '买价', key: 'bidPrice', width: '15%' },
{ title: '卖价', key: 'askPrice', width: '15%' },
{ title: 'Tick数', key: 'tickCount', width: '15%' },
{ title: '余额', key: 'balance', width: '20%' },
]
const loadStatistics = async () => {
try {
loading.value = true
error.value = ''
const data = await tradingAPI.getStatistics()
statistics.value = data.statistics || []
// 计算统计信息
if (statistics.value.length > 0) {
totalRecords.value = statistics.value.length
const prices = statistics.value.map(item => (item.bidPrice + item.askPrice) / 2)
averagePrice.value = prices.reduce((a, b) => a + b, 0) / prices.length
maxPrice.value = Math.max(...prices)
minPrice.value = Math.min(...prices)
} else {
totalRecords.value = 0
averagePrice.value = 0
maxPrice.value = 0
minPrice.value = 0
}
// 更新图表
updateChart()
} catch (err) {
error.value = `加载统计数据失败: ${err.message}`
console.error('Load statistics error:', err)
} finally {
loading.value = false
}
}
const updateChart = async () => {
await nextTick()
if (!chartContainer.value) return
if (chart.value) {
chart.value.dispose()
}
chart.value = echarts.init(chartContainer.value)
const option = {
title: {
text: '价格趋势'
},
tooltip: {
trigger: 'axis'
},
xAxis: {
type: 'category',
data: statistics.value.map(item => item.timestamp)
},
yAxis: {
type: 'value',
name: '价格'
},
series: [{
name: '买价',
type: 'line',
data: statistics.value.map(item => item.bidPrice),
smooth: true,
lineStyle: {
color: '#1976D2'
}
}, {
name: '卖价',
type: 'line',
data: statistics.value.map(item => item.askPrice),
smooth: true,
lineStyle: {
color: '#4CAF50'
}
}]
}
chart.value.setOption(option)
}
const formatTime = (timestamp) => {
if (!timestamp) return ''
return new Date(timestamp * 1000).toLocaleString('zh-CN')
}
onMounted(() => {
loadStatistics()
// 每30秒自动刷新
setInterval(loadStatistics, 30000)
})
return {
chartContainer,
loading,
error,
statistics,
itemsPerPage,
totalRecords,
averagePrice,
maxPrice,
minPrice,
tableHeaders,
loadStatistics,
formatTime,
}
},
}
</script>
+250
View File
@@ -0,0 +1,250 @@
<template>
<v-container fluid>
<v-row>
<v-col cols="12">
<h1 class="mb-4">服务状态监控</h1>
</v-col>
</v-row>
<!-- 健康状态 -->
<v-row>
<v-col cols="12" md="6">
<v-card>
<v-card-title class="d-flex align-center">
<v-icon class="me-2" :color="healthStatus.ok ? 'success' : 'error'">
mdi-heart
</v-icon>
服务健康状态
</v-card-title>
<v-card-text>
<v-chip
:color="healthStatus.ok ? 'success' : 'error'"
variant="flat"
size="large"
>
{{ healthStatus.ok ? '服务正常' : '服务异常' }}
</v-chip>
<div class="mt-2 text-caption">
最后检查: {{ lastCheckTime }}
</div>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" md="6">
<v-card>
<v-card-title class="d-flex align-center">
<v-icon class="me-2">mdi-information</v-icon>
系统信息
</v-card-title>
<v-card-text>
<div class="d-flex flex-column ga-2">
<div><strong>版本:</strong> {{ systemInfo.version || '未知' }}</div>
<div><strong>运行时间:</strong> {{ formatUptime(systemInfo.uptime) }}</div>
<div><strong>内存使用:</strong> {{ formatMemory(systemInfo.memory) }}</div>
</div>
</v-card-text>
</v-card>
</v-col>
</v-row>
<!-- 详细指标 -->
<v-row>
<v-col cols="12">
<v-card>
<v-card-title>详细服务指标</v-card-title>
<v-card-text>
<v-row>
<v-col cols="12" sm="6" md="3">
<v-card variant="outlined">
<v-card-text class="text-center">
<div class="text-h4">{{ serviceMetrics.pendingTrades }}</div>
<div class="text-caption">待执行指令</div>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" sm="6" md="3">
<v-card variant="outlined">
<v-card-text class="text-center">
<div class="text-h4">{{ serviceMetrics.totalTrades }}</div>
<div class="text-caption">总交易次数</div>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" sm="6" md="3">
<v-card variant="outlined">
<v-card-text class="text-center">
<div class="text-h4">{{ serviceMetrics.activeSymbols }}</div>
<div class="text-caption">活跃品种</div>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" sm="6" md="3">
<v-card variant="outlined">
<v-card-text class="text-center">
<div class="text-h4">{{ serviceMetrics.successRate }}%</div>
<div class="text-caption">成功率</div>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-col>
</v-row>
<!-- 连接状态 -->
<v-row>
<v-col cols="12">
<v-card>
<v-card-title>连接状态</v-card-title>
<v-card-text>
<v-row>
<v-col cols="12" sm="6">
<v-card variant="outlined" class="pa-3">
<div class="d-flex align-center">
<v-icon
:color="connectionStatus.backend ? 'success' : 'error'"
class="me-2"
>
mdi-server
</v-icon>
<div>
<div class="font-weight-bold">后端服务</div>
<div class="text-caption">
{{ connectionStatus.backend ? '已连接' : '未连接' }}
</div>
</div>
</div>
</v-card>
</v-col>
<v-col cols="12" sm="6">
<v-card variant="outlined" class="pa-3">
<div class="d-flex align-center">
<v-icon
:color="connectionStatus.mt5 ? 'success' : 'warning'"
class="me-2"
>
mdi-chart-line
</v-icon>
<div>
<div class="font-weight-bold">MT5 连接</div>
<div class="text-caption">
{{ connectionStatus.mt5 ? '已连接' : '未连接' }}
</div>
</div>
</div>
</v-card>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-col>
</v-row>
<!-- 错误信息 -->
<v-row v-if="error">
<v-col cols="12">
<v-alert type="error" dismissible>
{{ error }}
</v-alert>
</v-col>
</v-row>
</v-container>
</template>
<script>
import { ref, onMounted } from 'vue'
import { tradingAPI } from '@/api/trading'
export default {
name: 'Status',
setup() {
const healthStatus = ref({ ok: false })
const systemInfo = ref({})
const serviceMetrics = ref({
pendingTrades: 0,
totalTrades: 0,
activeSymbols: 0,
successRate: 0,
})
const connectionStatus = ref({
backend: false,
mt5: false,
})
const lastCheckTime = ref('')
const error = ref('')
const checkStatus = async () => {
try {
error.value = ''
// 检查健康状态
const health = await tradingAPI.health()
healthStatus.value = health
// 获取详细状态
const status = await tradingAPI.getStatus()
systemInfo.value = status.system || {}
// 适配实际API响应结构
serviceMetrics.value = {
pendingTrades: status.pending_instructions || 0,
totalTrades: status.statistics_records || 0,
activeSymbols: status.symbols?.length || 0,
successRate: status.success_rate || 0,
}
// 检查连接状态
connectionStatus.value = {
backend: health.ok,
mt5: status.mt5_connected || false,
}
lastCheckTime.value = new Date().toLocaleString('zh-CN')
} catch (err) {
error.value = `检查状态失败: ${err.message}`
healthStatus.value = { ok: false }
connectionStatus.value = { backend: false, mt5: false }
console.error('Status check error:', err)
}
}
const formatUptime = (seconds) => {
if (!seconds) return '未知'
const hours = Math.floor(seconds / 3600)
const minutes = Math.floor((seconds % 3600) / 60)
return `${hours}小时 ${minutes}分钟`
}
const formatMemory = (bytes) => {
if (!bytes) return '未知'
const mb = (bytes / 1024 / 1024).toFixed(1)
return `${mb} MB`
}
onMounted(() => {
checkStatus()
// 每5秒自动检查
setInterval(checkStatus, 5000)
})
return {
healthStatus,
systemInfo,
serviceMetrics,
connectionStatus,
lastCheckTime,
error,
checkStatus,
formatUptime,
formatMemory,
}
},
}
</script>
+308
View File
@@ -0,0 +1,308 @@
<template>
<v-container fluid>
<v-row>
<v-col cols="12">
<h1 class="mb-4">交易指令管理</h1>
</v-col>
</v-row>
<!-- 发送交易指令表单 -->
<v-row>
<v-col cols="12" md="6">
<v-card>
<v-card-title>发送交易指令</v-card-title>
<v-card-text>
<v-form ref="form" v-model="formValid">
<v-select
v-model="tradeForm.symbol"
:items="symbols"
label="交易品种"
required
:rules="[v => !!v || '请选择交易品种']"
></v-select>
<v-select
v-model="tradeForm.direction"
:items="directions"
label="买卖方向"
required
:rules="[v => !!v || '请选择买卖方向']"
></v-select>
<v-text-field
v-model.number="tradeForm.volume"
label="手数"
type="number"
step="0.01"
required
:rules="[v => v > 0 || '手数必须大于0']"
></v-text-field>
<v-text-field
v-model.number="tradeForm.price"
label="执行价格"
type="number"
step="0.00001"
required
:rules="[v => v > 0 || '执行价格必须大于0']"
></v-text-field>
<v-text-field
v-model.number="tradeForm.sl"
label="止损价格"
type="number"
step="0.00001"
:rules="[v => !v || v > 0 || '止损价格必须大于0']"
></v-text-field>
<v-text-field
v-model.number="tradeForm.tp"
label="止盈价格"
type="number"
step="0.00001"
:rules="[v => !v || v > 0 || '止盈价格必须大于0']"
></v-text-field>
<v-btn
color="primary"
:disabled="!formValid"
:loading="sending"
@click="sendTrade"
block
class="mt-4"
>
发送交易指令
</v-btn>
</v-form>
</v-card-text>
</v-card>
</v-col>
<!-- 待执行指令列表 -->
<v-col cols="12" md="6">
<v-card>
<v-card-title class="d-flex align-center justify-space-between">
待执行指令
<v-btn
color="error"
size="small"
:loading="clearing"
@click="clearAllTrades"
>
清空全部
</v-btn>
</v-card-title>
<v-card-text>
<v-data-table
:headers="tradeHeaders"
:items="pendingTrades"
:loading="loadingTrades"
no-data-text="暂无待执行指令"
density="compact"
>
<template v-slot:item.direction="{ item }">
<v-chip
:color="item.direction === 'BUY' ? 'success' : 'error'"
size="small"
>
{{ item.direction === 'BUY' ? '买入' : '卖出' }}
</v-chip>
</template>
<template v-slot:item.timestamp="{ item }">
{{ formatTime(item.timestamp) }}
</template>
</v-data-table>
</v-card-text>
</v-card>
</v-col>
</v-row>
<!-- 错误信息 -->
<v-row v-if="error">
<v-col cols="12">
<v-alert type="error" dismissible>
{{ error }}
</v-alert>
</v-col>
</v-row>
<!-- 成功信息 -->
<v-row v-if="success">
<v-col cols="12">
<v-alert type="success" dismissible>
{{ success }}
</v-alert>
</v-col>
</v-row>
</v-container>
</template>
<script>
import { ref, onMounted } from 'vue'
import { tradingAPI } from '@/api/trading'
import { marketAPI } from '@/api/market'
export default {
name: 'TradeOrders',
setup() {
const form = ref(null)
const formValid = ref(false)
const sending = ref(false)
const loadingTrades = ref(false)
const clearing = ref(false)
const error = ref('')
const success = ref('')
const tradeForm = ref({
symbol: '',
direction: '',
volume: 0.01,
price: 0,
sl: 0,
tp: 0,
})
const symbols = ref([])
const directions = [
{ title: '买入', value: 'BUY' },
{ title: '卖出', value: 'SELL' },
]
const tradeHeaders = [
{ title: '品种', key: 'symbol', width: '20%' },
{ title: '方向', key: 'direction', width: '15%' },
{ title: '手数', key: 'volume', width: '15%' },
{ title: '价格', key: 'price', width: '20%' },
{ title: '时间', key: 'timestamp', width: '30%' },
]
const pendingTrades = ref([])
const loadPendingTrades = async () => {
try {
loadingTrades.value = true
error.value = ''
const data = await tradingAPI.getPendingTrades()
// 将对象格式转换为数组格式
const tradesObj = data.pending_trades || {}
const tradesArray = []
Object.keys(tradesObj).forEach(symbol => {
tradesObj[symbol].forEach(trade => {
tradesArray.push({
...trade,
direction: trade.action === 'b' ? 'BUY' : 'SELL',
volume: trade.mount
})
})
})
pendingTrades.value = tradesArray
} catch (err) {
error.value = `加载指令失败: ${err.message}`
console.error('Load trades error:', err)
} finally {
loadingTrades.value = false
}
}
const sendTrade = async () => {
try {
sending.value = true
error.value = ''
success.value = ''
// 价格验证
if (tradeForm.value.sl > 0 && tradeForm.value.tp > 0) {
if (tradeForm.value.direction === 'BUY' && !(tradeForm.value.sl < tradeForm.value.price && tradeForm.value.price < tradeForm.value.tp)) {
error.value = '买入指令必须满足: 止损 < 执行价格 < 止盈'
return
}
if (tradeForm.value.direction === 'SELL' && !(tradeForm.value.tp < tradeForm.value.price && tradeForm.value.price < tradeForm.value.sl)) {
error.value = '卖出指令必须满足: 止盈 < 执行价格 < 止损'
return
}
}
// 转换数据格式
const instruction = {
symbol: tradeForm.value.symbol,
action: tradeForm.value.direction === 'BUY' ? 'b' : 's',
mount: tradeForm.value.volume,
price: tradeForm.value.price,
sl: tradeForm.value.sl || 0,
tp: tradeForm.value.tp || 0
}
await tradingAPI.sendTradeInstructions([instruction])
success.value = '交易指令发送成功!'
form.value.reset()
await loadPendingTrades()
} catch (err) {
error.value = `发送指令失败: ${err.message}`
console.error('Send trade error:', err)
} finally {
sending.value = false
}
}
const clearAllTrades = async () => {
if (!confirm('确定要清空所有待执行指令吗?')) return
try {
clearing.value = true
error.value = ''
success.value = ''
await tradingAPI.clearTrades()
success.value = '已清空所有指令!'
await loadPendingTrades()
} catch (err) {
error.value = `清空指令失败: ${err.message}`
console.error('Clear trades error:', err)
} finally {
clearing.value = false
}
}
const formatTime = (timestamp) => {
if (!timestamp) return ''
return new Date(timestamp * 1000).toLocaleString('zh-CN')
}
const loadSymbols = async () => {
try {
const data = await marketAPI.getSymbols()
symbols.value = data.symbols || []
} catch (err) {
console.error('加载品种列表失败:', err)
}
}
onMounted(() => {
loadSymbols()
loadPendingTrades()
// 每10秒自动刷新
setInterval(loadPendingTrades, 10000)
})
return {
form,
formValid,
sending,
loadingTrades,
clearing,
error,
success,
tradeForm,
symbols,
directions,
tradeHeaders,
pendingTrades,
loadPendingTrades,
sendTrade,
clearAllTrades,
formatTime,
}
},
}
</script>
+33
View File
@@ -0,0 +1,33 @@
#!/bin/bash
# Vue 前端启动脚本
echo "🚀 启动量化交易系统 - Vue 前端"
echo "================================="
# 检查是否在正确的目录
if [ ! -f "package.json" ]; then
echo "❌ 错误: 请在 frontend 目录下运行此脚本"
echo " cd frontend && ./start_vue.sh"
exit 1
fi
# 检查依赖是否已安装
if [ ! -d "node_modules" ]; then
echo "📦 安装依赖..."
npm install
if [ $? -ne 0 ]; then
echo "❌ 依赖安装失败"
exit 1
fi
fi
echo "🌐 启动 Vue 开发服务器..."
echo " 前端地址: http://localhost:3001"
echo " 后端地址: http://localhost:8000"
echo ""
echo "按 Ctrl+C 停止服务器"
echo ""
# 启动开发服务器
npx vite --host 0.0.0.0 --port 3001
+22
View File
@@ -0,0 +1,22 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { fileURLToPath, URL } from 'node:url'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
server: {
proxy: {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
})
+28 -2
View File
@@ -20,6 +20,7 @@ from server import TradingServer
from routes_ea import create_ea_routes
from routes_trader import create_trader_routes
from routes_system import create_system_routes
from routes_market import create_market_routes
def create_app():
@@ -31,8 +32,26 @@ def create_app():
# 创建 FastAPI 应用
app = FastAPI(
title="高频交易服务 (HFT Trading Service)",
description="连接 MT5 EA 和交易指令源的高性能交易中心",
version="1.0.0"
description="""
连接 MT5 EA 和交易指令源的高性能交易中心
## 功能模块
### 交易指令
- EA获取交易指令
- 交易员下发交易指令
- 查询待执行指令
### 行情分析
- K线数据接收与存储 (H4/H1/M15/M5/M1)
- 转折点自动检测
- 实时转折点提醒 (WebSocket)
### 系统监控
- 健康检查
- 服务状态查询
""",
version="2.0.0"
)
# 添加 CORS 中间件
@@ -48,6 +67,13 @@ def create_app():
app.include_router(create_ea_routes(server))
app.include_router(create_trader_routes(server))
app.include_router(create_system_routes(server))
app.include_router(create_market_routes(
server.market_store,
server.pivot_detector,
server.pivot_monitor,
server.trend_analyzer,
server.pending_orders
))
return app
+12
View File
@@ -0,0 +1,12 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
行情分析模块
"""
from .store import MarketStore
from .pivot_detector import PivotDetector
from .monitor import PivotMonitor
from .trend_analyzer import TrendAnalyzer
__all__ = ['MarketStore', 'PivotDetector', 'PivotMonitor', 'TrendAnalyzer']
+120
View File
@@ -0,0 +1,120 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
K线合并模块
处理增量K线数据的合并逻辑
"""
from typing import List, Dict, Optional
from datetime import datetime
from .store import KlineData
class KlineMerger:
"""K线合并器"""
@staticmethod
def merge_klines(existing: List[KlineData], new_klines: List[KlineData]) -> List[KlineData]:
"""
合并K线数据
Args:
existing: 现有K线数据
new_klines: 新增K线数据
Returns:
合并后的K线数据
"""
if not new_klines:
return existing
if not existing:
return new_klines
# 使用字典来去重,以时间戳为key
kline_dict = {}
# 添加现有数据
for k in existing:
ts = KlineMerger._normalize_timestamp(k.timestamp)
kline_dict[ts] = k
# 添加或更新新数据
for k in new_klines:
ts = KlineMerger._normalize_timestamp(k.timestamp)
kline_dict[ts] = k
# 按时间排序
merged = sorted(kline_dict.values(), key=lambda x: KlineMerger._normalize_timestamp(x.timestamp))
return merged
@staticmethod
def _normalize_timestamp(ts) -> str:
"""标准化时间戳"""
if isinstance(ts, datetime):
return ts.strftime("%Y-%m-%d %H:%M:%S")
return str(ts)
@staticmethod
def detect_gaps(klines: List[KlineData], period: str) -> List[Dict]:
"""
检测K线数据缺口
Args:
klines: K线数据
period: 周期
Returns:
缺口列表
"""
if len(klines) < 2:
return []
# 各周期对应的分钟数
period_minutes = {
'H4': 240,
'H1': 60,
'M15': 15,
'M5': 5,
'M1': 1
}
interval = period_minutes.get(period, 1)
gaps = []
for i in range(1, len(klines)):
prev_ts = KlineMerger._parse_timestamp(klines[i-1].timestamp)
curr_ts = KlineMerger._parse_timestamp(klines[i].timestamp)
if prev_ts and curr_ts:
expected_diff = interval * 60 # 秒
actual_diff = (curr_ts - prev_ts).total_seconds()
# 如果实际差值大于预期的1.5倍,认为有缺口
if actual_diff > expected_diff * 1.5:
gaps.append({
"start": klines[i-1].timestamp,
"end": klines[i].timestamp,
"missing_bars": int(actual_diff / expected_diff) - 1
})
return gaps
@staticmethod
def _parse_timestamp(ts):
"""解析时间戳"""
if isinstance(ts, datetime):
return ts
if isinstance(ts, str):
try:
return datetime.strptime(ts, "%Y-%m-%d %H:%M:%S")
except:
try:
return datetime.strptime(ts, "%Y-%m-%d %H:%M")
except:
return None
return None
+412
View File
@@ -0,0 +1,412 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
转折点监控模块
实时监控价格与转折点的接近程度,并通过WebSocket推送提醒
"""
from typing import Dict, List, Optional, Set
from datetime import datetime
import threading
import asyncio
import json
from .store import MarketStore, normalize_symbol
from .pivot_detector import PivotDetector
from .pending_orders import PendingOrderManager
# 交易配置
class TradeConfig:
"""交易配置"""
_instance = None
_lock = threading.Lock()
def __init__(self):
self.enabled = True # 是否启用自动生成
# 默认配置
self.default_volume = 0.01 # 默认手数
self.default_sl_offset = 0.05 # 默认止损偏移(固定点数)
# 按品种配置: {symbol: {"volume": 0.01, "sl_offset": 0.05}}
self.symbol_config = {
"GOLD#": {"volume": 0.01, "sl_offset": 0.5},
"OILCASH#": {"volume": 0.01, "sl_offset": 0.05},
}
@classmethod
def get_instance(cls):
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = cls()
return cls._instance
def get_symbol_config(self, symbol: str) -> Dict:
"""获取品种配置,如果未配置则返回默认值"""
symbol = symbol.upper()
if symbol in self.symbol_config:
config = self.symbol_config[symbol]
return {
"volume": config.get("volume", self.default_volume),
"sl_offset": config.get("sl_offset", self.default_sl_offset)
}
return {
"volume": self.default_volume,
"sl_offset": self.default_sl_offset
}
def to_dict(self) -> Dict:
return {
"enabled": self.enabled,
"default_volume": self.default_volume,
"default_sl_offset": self.default_sl_offset,
"symbol_config": self.symbol_config
}
def update(self, data: Dict):
if "enabled" in data:
self.enabled = bool(data["enabled"])
if "default_volume" in data:
self.default_volume = float(data["default_volume"])
if "default_sl_offset" in data:
self.default_sl_offset = float(data["default_sl_offset"])
if "symbol_config" in data:
self.symbol_config = data["symbol_config"]
class PivotMonitor:
"""转折点监控器"""
def __init__(self, store: MarketStore, detector: PivotDetector,
pending_orders: PendingOrderManager = None):
self.store = store
self.detector = detector
self.pending_orders = pending_orders
self.trade_config = TradeConfig.get_instance()
# WebSocket连接管理
self._ws_clients: Set = set()
self._ws_lock = threading.Lock()
# 已提醒的转折点(避免重复提醒)
# 结构: {(symbol, period, timestamp, price): datetime}
self._alerted_pivots: Dict[tuple, datetime] = {}
self._alert_lock = threading.Lock()
# 提醒冷却时间(秒)
self.alert_cooldown = 300 # 5分钟内同一转折点不重复提醒
print("[PivotMonitor] 转折点监控器已初始化")
def check_and_alert(self, symbol: str, current_price: float) -> List[Dict]:
"""
检查价格是否接近转折点,并发送提醒
Args:
symbol: 交易品种
current_price: 当前价格
Returns:
接近的转折点列表
"""
symbol = normalize_symbol(symbol)
# 检查是否接近转折点
near_pivots = self.detector.check_near_pivot(symbol, current_price)
if not near_pivots:
return []
# 过滤已提醒过的转折点
new_alerts = []
current_time = datetime.now()
with self._alert_lock:
for pivot in near_pivots:
key = (
pivot['symbol'],
pivot['period'],
pivot['timestamp'],
pivot['price']
)
# 检查是否已提醒过
if key in self._alerted_pivots:
last_alert = self._alerted_pivots[key]
elapsed = (current_time - last_alert).total_seconds()
# 如果在冷却时间内,跳过
if elapsed < self.alert_cooldown:
continue
# 记录提醒时间
self._alerted_pivots[key] = current_time
# 构建提醒消息
is_breakthrough = pivot.get('is_breakthrough', False)
alert_type = pivot.get('alert_type', '')
period = pivot['period']
# 根据类型生成不同的消息
if is_breakthrough:
if 'high' in alert_type:
message = f"{pivot['symbol']} {period} 已突破高点 {pivot['price']}, 当前价格 {pivot['current_price']}"
else:
message = f"{pivot['symbol']} {period} 已突破低点 {pivot['price']}, 当前价格 {pivot['current_price']}"
else:
if 'high' in alert_type:
message = f"{pivot['symbol']} {period} 接近高点 {pivot['price']}, 当前价格 {pivot['current_price']}, 距离 {pivot['distance_pct']}%"
else:
message = f"{pivot['symbol']} {period} 接近低点 {pivot['price']}, 当前价格 {pivot['current_price']}, 距离 {pivot['distance_pct']}%"
alert = {
"type": "pivot_alert",
"symbol": pivot['symbol'],
"period": period,
"direction": pivot['direction'],
"pivot_price": pivot['price'],
"current_price": pivot['current_price'],
"distance_pct": pivot['distance_pct'],
"threshold_pct": pivot['threshold_pct'],
"timestamp": current_time.isoformat(),
"alert_type": alert_type,
"is_breakthrough": is_breakthrough,
"message": message
}
# M1和M5周期接近转折点时,自动生成交易指令
pending_order = None
if period in ['M1', 'M5'] and not is_breakthrough:
pending_order = self._auto_generate_order(pivot, current_time)
# 如果生成了订单,加入通知中
if pending_order:
alert["pending_order"] = pending_order
new_alerts.append(alert)
# 异步推送WebSocket消息
self._broadcast_alert(alert)
# 清理过期的提醒记录
self._cleanup_alerted()
return new_alerts
def _auto_generate_order(self, pivot: Dict, current_time: datetime) -> Optional[Dict]:
"""
M1周期接近转折点时,自动生成交易指令
Args:
pivot: 转折点信息
current_time: 当前时间
Returns:
生成的订单信息,包含order_id
"""
if not self.pending_orders:
return None
if not self.trade_config.enabled:
return None
symbol = pivot['symbol']
current_price = pivot['current_price']
pivot_price = pivot['price']
direction = pivot['direction']
alert_type = pivot['alert_type']
# 只处理"接近"类型(near_high, near_low
if not alert_type.startswith('near_'):
return None
# 获取品种配置
config = self.trade_config.get_symbol_config(symbol)
volume = config["volume"]
sl_offset = config["sl_offset"] # 固定点数偏移
order = None
if alert_type == 'near_low':
# 接近低点 → 买入
# 止损 = 低点 - 配置的偏移
sl = pivot_price - sl_offset
# 止盈 = 最近的高点
tp = self._find_nearest_pivot_price(symbol, 'high', current_price)
if tp and tp > current_price:
order = {
"symbol": symbol,
"action": "b", # 买入
"price": current_price,
"mount": volume,
"sl": round(sl, 2),
"tp": round(tp, 2),
"reason": f"M1接近低点{pivot_price:.2f},建议买入,止损{sl:.2f},止盈{tp:.2f}",
"source": "auto_pivot_m1",
"pivot_price": pivot_price,
"generated_at": current_time.isoformat()
}
elif alert_type == 'near_high':
# 接近高点 → 卖出
# 止损 = 高点 + 配置的偏移
sl = pivot_price + sl_offset
# 止盈 = 最近的低点
tp = self._find_nearest_pivot_price(symbol, 'low', current_price)
if tp and tp < current_price:
order = {
"symbol": symbol,
"action": "s", # 卖出
"price": current_price,
"mount": volume,
"sl": round(sl, 2),
"tp": round(tp, 2),
"reason": f"M1接近高点{pivot_price:.2f},建议卖出,止损{sl:.2f},止盈{tp:.2f}",
"source": "auto_pivot_m1",
"pivot_price": pivot_price,
"generated_at": current_time.isoformat()
}
if order:
order_id = self.pending_orders.add_order(order)
print(f"[PivotMonitor] 自动生成交易指令: {order_id} - {order['action']} {symbol} @ {current_price}")
# 返回订单信息(包含order_id)
order["order_id"] = order_id
return order
return None
def _find_nearest_pivot_price(self, symbol: str, direction: str,
current_price: float) -> Optional[float]:
"""
找到离当前价格最近的转折点价格
Args:
symbol: 交易品种
direction: 'high''low'
current_price: 当前价格
Returns:
最近的转折点价格,如果没有返回None
"""
symbol = normalize_symbol(symbol)
nearest_price = None
min_distance = float('inf')
with self.detector._lock:
for period in self.detector._pivots[symbol]:
pivots = self.detector._pivots[symbol][period]
for pivot in pivots:
if pivot.direction != direction:
continue
# 对于高点,只考虑价格高于当前价的
# 对于低点,只考虑价格低于当前价的
if direction == 'high' and pivot.price <= current_price:
continue
if direction == 'low' and pivot.price >= current_price:
continue
distance = abs(pivot.price - current_price)
if distance < min_distance:
min_distance = distance
nearest_price = pivot.price
return nearest_price
def _broadcast_new_order(self, order_id: str, order: Dict) -> None:
"""广播新订单通知"""
message = json.dumps({
"type": "new_order",
"order_id": order_id,
"order": order
})
with self._ws_lock:
clients = list(self._ws_clients)
for client in clients:
try:
asyncio.create_task(self._send_to_client(client, message))
except Exception as e:
print(f"[PivotMonitor] 发送新订单通知失败: {e}")
def _cleanup_alerted(self):
"""清理过期的提醒记录"""
current_time = datetime.now()
with self._alert_lock:
keys_to_remove = []
for key, alert_time in self._alerted_pivots.items():
elapsed = (current_time - alert_time).total_seconds()
if elapsed > self.alert_cooldown * 2:
keys_to_remove.append(key)
for key in keys_to_remove:
del self._alerted_pivots[key]
def _broadcast_alert(self, alert: Dict):
"""广播提醒到所有WebSocket客户端"""
message = json.dumps(alert)
with self._ws_lock:
clients = list(self._ws_clients)
# 在事件循环中发送消息
for client in clients:
try:
asyncio.create_task(self._send_to_client(client, message))
except Exception as e:
print(f"[PivotMonitor] 发送WebSocket消息失败: {e}")
async def _send_to_client(self, client, message: str):
"""发送消息到客户端"""
try:
await client.send_text(message)
except Exception as e:
print(f"[PivotMonitor] 发送消息到客户端失败: {e}")
# 移除失效的客户端
with self._ws_lock:
self._ws_clients.discard(client)
def add_ws_client(self, client):
"""添加WebSocket客户端"""
with self._ws_lock:
self._ws_clients.add(client)
print(f"[PivotMonitor] WebSocket客户端已连接, 当前连接数: {len(self._ws_clients)}")
def remove_ws_client(self, client):
"""移除WebSocket客户端"""
with self._ws_lock:
self._ws_clients.discard(client)
print(f"[PivotMonitor] WebSocket客户端已断开, 当前连接数: {len(self._ws_clients)}")
def get_ws_client_count(self) -> int:
"""获取WebSocket客户端数量"""
with self._ws_lock:
return len(self._ws_clients)
def clear_symbol(self, symbol: str):
"""清除某个Symbol的提醒记录"""
symbol = normalize_symbol(symbol)
with self._alert_lock:
keys_to_remove = [k for k in self._alerted_pivots if k[0] == symbol]
for key in keys_to_remove:
del self._alerted_pivots[key]
def get_status(self) -> Dict:
"""获取监控状态"""
with self._alert_lock:
alerted_count = len(self._alerted_pivots)
return {
"ws_clients": self.get_ws_client_count(),
"alerted_pivots": alerted_count,
"alert_cooldown": self.alert_cooldown
}
+199
View File
@@ -0,0 +1,199 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
待确认订单管理模块
存储交易员待确认的交易指令
"""
from collections import defaultdict
from typing import List, Dict, Optional, Callable
from datetime import datetime, timedelta
import threading
import uuid
class PendingOrderManager:
"""待确认订单管理器"""
# 订单超时时间(秒)
ORDER_TIMEOUT = 180 # 3分钟
def __init__(self):
# 待确认订单: {SYMBOL: [Order, ...]}
self._pending_orders = defaultdict(list)
self._lock = threading.RLock()
# 订单ID到订单的映射
self._order_by_id = {}
# 订单确认回调(确认后将订单加入交易队列)
self._confirm_callback: Optional[Callable] = None
# 启动超时清理线程
self._start_cleanup_thread()
print("[PendingOrderManager] 待确认订单管理器已初始化")
def set_confirm_callback(self, callback: Callable):
"""设置订单确认回调函数"""
self._confirm_callback = callback
def _start_cleanup_thread(self):
"""启动超时清理线程"""
def cleanup_loop():
while True:
try:
self._cleanup_expired_orders()
except Exception as e:
print(f"[PendingOrderManager] 清理线程异常: {e}")
threading.Event().wait(10) # 每10秒检查一次
thread = threading.Thread(target=cleanup_loop, daemon=True)
thread.start()
def _cleanup_expired_orders(self):
"""清理超时订单"""
current_time = datetime.now()
expired_orders = []
with self._lock:
for order_id, order in list(self._order_by_id.items()):
created_at = datetime.fromisoformat(order['created_at'])
elapsed = (current_time - created_at).total_seconds()
if elapsed > self.ORDER_TIMEOUT:
expired_orders.append(order_id)
for order_id in expired_orders:
order = self._order_by_id[order_id]
symbol = order.get('symbol', 'UNKNOWN')
self._pending_orders[symbol] = [
o for o in self._pending_orders[symbol] if o['order_id'] != order_id
]
del self._order_by_id[order_id]
print(f"[PendingOrderManager] 订单超时自动移除: {order_id}")
def add_order(self, order: Dict) -> str:
"""
添加待确认订单
Args:
order: 订单信息
Returns:
订单ID
"""
# 生成订单ID
order_id = str(uuid.uuid4())[:8]
order_with_id = {
**order,
"order_id": order_id,
"status": "pending",
"created_at": datetime.now().isoformat(),
"expires_at": (datetime.now() + timedelta(seconds=self.ORDER_TIMEOUT)).isoformat()
}
symbol = order.get('symbol', 'UNKNOWN')
with self._lock:
self._pending_orders[symbol].append(order_with_id)
self._order_by_id[order_id] = order_with_id
print(f"[PendingOrderManager] 添加待确认订单: {order_id} {symbol} {order.get('action')}")
return order_id
def confirm_order(self, order_id: str) -> Optional[Dict]:
"""
确认订单(交易员确认后调用)
Returns:
确认后的订单,用于加入正式交易队列
"""
with self._lock:
if order_id not in self._order_by_id:
return None
order = self._order_by_id[order_id]
symbol = order.get('symbol', 'UNKNOWN')
# 从待确认列表中移除
self._pending_orders[symbol] = [
o for o in self._pending_orders[symbol] if o['order_id'] != order_id
]
del self._order_by_id[order_id]
# 标记为已确认
order['status'] = 'confirmed'
order['confirmed_at'] = datetime.now().isoformat()
print(f"[PendingOrderManager] 订单已确认: {order_id}")
# 调用确认回调(将订单加入交易队列)
if self._confirm_callback:
try:
self._confirm_callback(order)
print(f"[PendingOrderManager] 订单已加入交易队列: {order_id}")
except Exception as e:
print(f"[PendingOrderManager] 加入交易队列失败: {e}")
return order
def reject_order(self, order_id: str) -> bool:
"""
拒绝订单(交易员点击放弃)
Returns:
是否成功
"""
with self._lock:
if order_id not in self._order_by_id:
return False
order = self._order_by_id[order_id]
symbol = order.get('symbol', 'UNKNOWN')
# 从待确认列表中移除
self._pending_orders[symbol] = [
o for o in self._pending_orders[symbol] if o['order_id'] != order_id
]
del self._order_by_id[order_id]
print(f"[PendingOrderManager] 订单已拒绝: {order_id}")
return True
def get_pending_orders(self, symbol: str = None) -> List[Dict]:
"""获取待确认订单列表"""
with self._lock:
if symbol:
return list(self._pending_orders.get(symbol, []))
else:
# 返回所有
orders = []
for sym, order_list in self._pending_orders.items():
orders.extend(order_list)
return sorted(orders, key=lambda x: x['created_at'], reverse=True)
def get_order_by_id(self, order_id: str) -> Optional[Dict]:
"""根据ID获取订单"""
with self._lock:
return self._order_by_id.get(order_id)
def get_pending_count(self, symbol: str = None) -> int:
"""获取待确认订单数量"""
with self._lock:
if symbol:
return len(self._pending_orders.get(symbol, []))
return len(self._order_by_id)
def clear_all(self) -> int:
"""清空所有待确认订单"""
with self._lock:
count = len(self._order_by_id)
self._pending_orders.clear()
self._order_by_id.clear()
return count
+414
View File
@@ -0,0 +1,414 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
转折点检测模块
识别K线的高点和低点(分型识别)
"""
from collections import defaultdict
from datetime import datetime
from typing import List, Dict, Optional, Tuple
import threading
from .store import KlineData, normalize_symbol
class PivotPoint:
"""转折点数据结构"""
def __init__(self, symbol: str, period: str, timestamp, price: float,
direction: str, strength: int = 3):
self.symbol = normalize_symbol(symbol)
self.period = period
self.timestamp = timestamp
self.price = price
self.direction = direction # "high" 或 "low"
self.strength = strength # 转折强度(左右各N根K线)
def to_dict(self) -> Dict:
"""转换为字典"""
ts = self.timestamp
if isinstance(ts, datetime):
ts_str = ts.strftime("%Y-%m-%d %H:%M:%S")
else:
ts_str = str(ts)
return {
"symbol": self.symbol,
"period": self.period,
"timestamp": ts_str,
"price": self.price,
"direction": self.direction,
"strength": self.strength
}
class PivotDetector:
"""转折点检测器"""
# 各周期接近阈值(千分比)
THRESHOLDS = {
'H4': 0.0015, # 千分之1.5
'H1': 0.0015, # 千分之1.5
'M15': 0.0015, # 千分之1.5
'M5': 0.0005, # 千分之0.5
'M1': 0.0002 # 千分之0.2
}
def __init__(self):
# 存储转折点: {SYMBOL: {PERIOD: [PivotPoint, ...]}}
self._pivots = defaultdict(lambda: defaultdict(list))
self._lock = threading.RLock()
# 默认转折强度(左右各N根K线)
self.default_strength = 3
print("[PivotDetector] 转折点检测器已初始化")
def detect_pivots(self, symbol: str, period: str, klines: List[KlineData],
strength: int = None) -> List[PivotPoint]:
"""
检测转折点
Args:
symbol: 交易品种
period: 周期
klines: K线数据列表
strength: 转折强度(左右各N根K线)
Returns:
检测到的转折点列表
"""
if strength is None:
strength = self.default_strength
if len(klines) < 2 * strength + 1:
return []
pivots = []
# 遍历K线,检测分型
for i in range(strength, len(klines) - strength):
current = klines[i]
# 检查是否为高点(顶分型)
is_high = True
for j in range(1, strength + 1):
if klines[i - j].high >= current.high or klines[i + j].high >= current.high:
is_high = False
break
if is_high:
pivot = PivotPoint(
symbol=symbol,
period=period,
timestamp=current.timestamp,
price=current.high,
direction="high",
strength=strength
)
pivots.append(pivot)
# 检查是否为低点(底分型)
is_low = True
for j in range(1, strength + 1):
if klines[i - j].low <= current.low or klines[i + j].low <= current.low:
is_low = False
break
if is_low:
pivot = PivotPoint(
symbol=symbol,
period=period,
timestamp=current.timestamp,
price=current.low,
direction="low",
strength=strength
)
pivots.append(pivot)
return pivots
def _merge_pivots(self, pivots: List[PivotPoint], klines: List[KlineData]) -> List[PivotPoint]:
"""
合并相近的转折点
合并规则:
- K线距离小于26根
- 价格相差在万分之三范围内
- 高点合并:取较高的价格
- 低点合并:取较低的价格
Args:
pivots: 原始转折点列表
klines: K线数据(用于计算K线索引)
Returns:
合并后的转折点列表
"""
if len(pivots) < 2:
return pivots
# 建立K线时间戳到索引的映射
kline_index = {str(k.timestamp): i for i, k in enumerate(klines)}
# 按时间排序
pivots = sorted(pivots, key=lambda p: str(p.timestamp))
# 分开处理高点和低点
high_pivots = [p for p in pivots if p.direction == "high"]
low_pivots = [p for p in pivots if p.direction == "low"]
# 合并高点
merged_highs = self._merge_same_direction(
high_pivots, kline_index, "high"
)
# 合并低点
merged_lows = self._merge_same_direction(
low_pivots, kline_index, "low"
)
# 合并结果
result = merged_highs + merged_lows
return result
def _merge_same_direction(self, pivots: List[PivotPoint],
kline_index: Dict[str, int],
direction: str) -> List[PivotPoint]:
"""
合并同方向的转折点
"""
if len(pivots) < 2:
return pivots
merged = []
i = 0
while i < len(pivots):
current = pivots[i]
current_idx = kline_index.get(str(current.timestamp), -1)
if current_idx < 0:
i += 1
continue
# 查找需要合并的转折点
group = [current]
j = i + 1
while j < len(pivots):
next_pivot = pivots[j]
next_idx = kline_index.get(str(next_pivot.timestamp), -1)
if next_idx < 0:
j += 1
continue
# 检查K线距离
kline_distance = abs(next_idx - current_idx)
if kline_distance >= 26:
break
# 检查价格差距(万分之三)
if current.price > 0:
price_diff_pct = abs(next_pivot.price - current.price) / current.price
if price_diff_pct <= 0.0003: # 万分之三
group.append(next_pivot)
j += 1
continue
break
# 从组中选择代表性转折点
if direction == "high":
# 高点:取价格最高的
best = max(group, key=lambda p: p.price)
else:
# 低点:取价格最低的
best = min(group, key=lambda p: p.price)
merged.append(best)
i = j
return merged
def update_pivots(self, symbol: str, period: str, klines: List[KlineData],
strength: int = None) -> int:
"""
更新转折点数据
Returns:
更新后的转折点数量
"""
symbol = normalize_symbol(symbol)
pivots = self.detect_pivots(symbol, period, klines, strength)
# 合并相近的转折点
merged_pivots = self._merge_pivots(pivots, klines)
with self._lock:
self._pivots[symbol][period] = merged_pivots
count = len(merged_pivots)
original_count = len(pivots)
if original_count != count:
print(f"[PivotDetector] {symbol} {period} 检测到 {original_count} 个转折点,合并后 {count}")
else:
print(f"[PivotDetector] {symbol} {period} 检测到 {count} 个转折点")
return count
def get_pivots(self, symbol: str, period: str, direction: str = None,
count: int = 50) -> List[Dict]:
"""
获取转折点数据
Args:
symbol: 交易品种
period: 周期
direction: "high""low"None表示全部
count: 返回数量
Returns:
转折点列表
"""
symbol = normalize_symbol(symbol)
with self._lock:
pivots = self._pivots[symbol][period]
if direction:
pivots = [p for p in pivots if p.direction == direction]
# 按时间排序,返回最新的
pivots = sorted(pivots, key=lambda x: str(x.timestamp), reverse=True)[:count]
return [p.to_dict() for p in pivots]
def get_recent_pivots(self, symbol: str, period: str, count: int = 10) -> List[Dict]:
"""获取最近的转折点(按时间倒序)"""
symbol = normalize_symbol(symbol)
with self._lock:
pivots = self._pivots[symbol][period]
pivots = sorted(pivots, key=lambda x: str(x.timestamp), reverse=True)[:count]
return [p.to_dict() for p in pivots]
def check_near_pivot(self, symbol: str, current_price: float) -> List[Dict]:
"""
检查当前价格是否接近某个转折点
Args:
symbol: 交易品种
current_price: 当前价格
Returns:
接近的转折点列表,包含距离信息
预警逻辑:
- 接近高点:当前价格 < 高点价格 且 距离在阈值范围内
- 接近低点:当前价格 > 低点价格 且 距离在阈值范围内
- 突破高点:当前价格超过高点价格的万分之一点二(基于实时价格)
- 突破低点:当前价格低于低点价格的万分之一点二(基于实时价格)
- 超过千分之一不再提示
"""
symbol = normalize_symbol(symbol)
near_pivots = []
# 突破阈值:万分之一点二
BREAKTHROUGH_THRESHOLD = 0.00012
# 最大提示范围:千分之一
MAX_ALERT_THRESHOLD = 0.001
with self._lock:
for period in self._pivots[symbol]:
pivots = self._pivots[symbol][period]
threshold = self.THRESHOLDS.get(period, 0.001)
for pivot in pivots:
if pivot.price == 0 or current_price == 0:
continue
# 基于实时价格计算阈值
breakthrough_value = current_price * BREAKTHROUGH_THRESHOLD # 万分之一点二
max_alert_value = current_price * MAX_ALERT_THRESHOLD # 千分之一
# 判断是接近还是突破
is_near = False
is_breakthrough = False
alert_type = ""
if pivot.direction == "high":
# 高点转折
if current_price > pivot.price:
# 当前价格高于高点,判断是否突破
# 突破:超过高点的距离在万分之一点二到千分之一之间
distance = current_price - pivot.price
if distance >= breakthrough_value and distance < max_alert_value:
is_breakthrough = True
alert_type = "breakthrough_high"
# 超过千分之一不再提示
else:
# 当前价格低于高点
distance_pct = (pivot.price - current_price) / current_price
if distance_pct <= threshold:
is_near = True
alert_type = "near_high"
elif pivot.direction == "low":
# 低点转折
if current_price < pivot.price:
# 当前价格低于低点,判断是否突破
# 突破:低于低点的距离在万分之一点二到千分之一之间
distance = pivot.price - current_price
if distance >= breakthrough_value and distance < max_alert_value:
is_breakthrough = True
alert_type = "breakthrough_low"
# 超过千分之一不再提示
else:
# 当前价格高于低点
distance_pct = (current_price - pivot.price) / current_price
if distance_pct <= threshold:
is_near = True
alert_type = "near_low"
if is_near or is_breakthrough:
distance_pct = abs(current_price - pivot.price) / current_price
near_pivots.append({
**pivot.to_dict(),
"current_price": current_price,
"distance_pct": round(distance_pct * 100, 4),
"threshold_pct": round(threshold * 100, 4),
"distance": round(current_price - pivot.price, 2),
"alert_type": alert_type,
"is_breakthrough": is_breakthrough
})
# 按距离排序,最近的优先
near_pivots.sort(key=lambda x: x['distance_pct'])
return near_pivots
def get_threshold(self, period: str) -> float:
"""获取某个周期的接近阈值"""
return self.THRESHOLDS.get(period, 0.001)
def clear_symbol(self, symbol: str):
"""清除某个Symbol的转折点数据"""
symbol = normalize_symbol(symbol)
with self._lock:
if symbol in self._pivots:
del self._pivots[symbol]
def get_status(self) -> Dict:
"""获取状态"""
with self._lock:
status = {}
for symbol in self._pivots:
status[symbol] = {}
for period in self._pivots[symbol]:
count = len(self._pivots[symbol][period])
status[symbol][period] = {"pivot_count": count}
return status
+259
View File
@@ -0,0 +1,259 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
K线数据存储模块
按周期和Symbol存储K线数据
"""
from collections import defaultdict
from datetime import datetime
from typing import List, Dict, Optional
import threading
def normalize_symbol(symbol: str) -> str:
"""
标准化品种名称(保持原样)
"""
return symbol if symbol else ""
class KlineData:
"""K线数据结构"""
def __init__(self, symbol: str, period: str, timestamp, open_price: float,
high: float, low: float, close: float, volume: float = 0):
self.symbol = normalize_symbol(symbol)
self.period = period # H4, H1, M15, M5, M1
self.timestamp = timestamp
self.open = open_price
self.high = high
self.low = low
self.close = close
self.volume = volume
def to_dict(self) -> Dict:
"""转换为字典"""
ts = self.timestamp
if isinstance(ts, datetime):
ts_str = ts.strftime("%Y-%m-%d %H:%M:%S")
else:
ts_str = str(ts)
return {
"symbol": self.symbol,
"period": self.period,
"timestamp": ts_str,
"open": self.open,
"high": self.high,
"low": self.low,
"close": self.close,
"volume": self.volume
}
class MarketStore:
"""K线数据存储"""
# 支持的周期
PERIODS = ['H4', 'H1', 'M15', 'M5', 'M1']
# 各周期最大存储条数
MAX_KLINES = {
'H4': 1500, # 4小时,6个月约1100根,留余量
'H1': 1000, # 1小时,1个月约720根
'M15': 500, # 15分钟,3天约288根
'M5': 400, # 5分钟,24小时288根
'M1': 100 # 1分钟,1小时60根
}
def __init__(self):
# 存储结构: {SYMBOL: {PERIOD: [KlineData, ...]}}
self._klines = defaultdict(lambda: defaultdict(list))
self._lock = threading.RLock()
# 标记每个symbol每个周期是否已收到全量数据
# 结构: {SYMBOL: {PERIOD: True/False}}
self._initialized = defaultdict(lambda: defaultdict(bool))
print("[MarketStore] K线存储已初始化")
def save_klines(self, symbol: str, period: str, klines: List[Dict],
is_full: bool = False) -> Dict:
"""
保存K线数据
Args:
symbol: 交易品种
period: 周期 (H4/H1/M15/M5/M1)
klines: K线数据列表
is_full: 是否为全量数据
Returns:
{"status": "ok", "count": N, "is_full": bool}
"""
symbol = normalize_symbol(symbol)
period = period.upper()
if period not in self.PERIODS:
return {"status": "error", "message": f"不支持的周期: {period}"}
with self._lock:
if is_full:
# 全量数据,直接覆盖
self._klines[symbol][period] = []
# 解析并存储K线数据
new_count = 0
for k in klines:
kline = KlineData(
symbol=symbol,
period=period,
timestamp=k.get('timestamp') or k.get('time'),
open_price=float(k.get('open', 0)),
high=float(k.get('high', 0)),
low=float(k.get('low', 0)),
close=float(k.get('close', 0)),
volume=float(k.get('volume', 0))
)
# 检查是否已存在相同时间戳的数据
existing = self._klines[symbol][period]
ts = kline.timestamp
# 查找是否已存在
found_idx = -1
for i, existing_kline in enumerate(existing):
if self._normalize_timestamp(existing_kline.timestamp) == self._normalize_timestamp(ts):
found_idx = i
break
if found_idx >= 0:
# 更新已有数据
existing[found_idx] = kline
else:
# 添加新数据
existing.append(kline)
new_count += 1
# 按时间排序
self._klines[symbol][period].sort(
key=lambda x: self._normalize_timestamp(x.timestamp)
)
# 限制最大条数,保留最新的
max_count = self.MAX_KLINES.get(period, 500)
if len(self._klines[symbol][period]) > max_count:
self._klines[symbol][period] = self._klines[symbol][period][-max_count:]
# 标记已初始化
self._initialized[symbol][period] = True
total = len(self._klines[symbol][period])
print(f"[MarketStore] {symbol} {period} 保存了 {new_count} 条新数据, 当前共 {total}")
return {
"status": "ok",
"count": new_count,
"total": total,
"is_full": is_full
}
def get_klines(self, symbol: str, period: str, count: int = 100) -> List[Dict]:
"""获取K线数据"""
symbol = normalize_symbol(symbol)
period = period.upper()
with self._lock:
klines = self._klines[symbol][period][-count:]
return [k.to_dict() for k in klines]
def get_all_klines(self, symbol: str, period: str) -> List[Dict]:
"""获取所有K线数据"""
symbol = normalize_symbol(symbol)
period = period.upper()
with self._lock:
return [k.to_dict() for k in self._klines[symbol][period]]
def get_latest_price(self, symbol: str) -> Optional[float]:
"""获取最新价格(从K线的最新close,优先M1,依次尝试其他周期)"""
symbol = normalize_symbol(symbol)
with self._lock:
# 尝试找到匹配的symbol(支持带#后缀的symbol
actual_symbol = None
if symbol in self._klines:
actual_symbol = symbol
else:
# 尝试添加#后缀
for s in self._klines:
if s.upper().startswith(symbol.upper()):
actual_symbol = s
break
if not actual_symbol:
return None
# 按优先级尝试各周期(M1优先,然后更短周期)
for period in ['M1', 'M5', 'M15', 'H1', 'H4']:
klines = self._klines[actual_symbol][period]
if klines:
return klines[-1].close
return None
def is_initialized(self, symbol: str, period: str) -> bool:
"""检查某个周期的数据是否已初始化"""
symbol = normalize_symbol(symbol)
period = period.upper()
return self._initialized[symbol][period]
def check_all_initialized(self, symbol: str) -> bool:
"""检查所有周期是否都已初始化"""
symbol = normalize_symbol(symbol)
return all(self._initialized[symbol][p] for p in self.PERIODS)
def clear_symbol(self, symbol: str):
"""清除某个Symbol的数据"""
symbol = normalize_symbol(symbol)
with self._lock:
if symbol in self._klines:
del self._klines[symbol]
if symbol in self._initialized:
del self._initialized[symbol]
def get_status(self) -> Dict:
"""获取存储状态"""
with self._lock:
status = {}
for symbol in self._klines:
status[symbol] = {}
for period in self.PERIODS:
count = len(self._klines[symbol][period])
initialized = self._initialized[symbol][period]
status[symbol][period] = {
"count": count,
"initialized": initialized
}
return status
def get_symbols(self) -> List[str]:
"""获取所有有实际数据的symbol列表"""
with self._lock:
symbols = []
for symbol in self._klines:
# 检查是否有实际数据(任一周期有K线数据)
has_data = False
for period in self.PERIODS:
if len(self._klines[symbol][period]) > 0:
has_data = True
break
if has_data:
symbols.append(symbol)
return symbols
def _normalize_timestamp(self, ts) -> str:
"""标准化时间戳为字符串"""
if isinstance(ts, datetime):
return ts.strftime("%Y-%m-%d %H:%M:%S")
return str(ts)
+389
View File
@@ -0,0 +1,389 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
趋势分析模块
基于均线和ADX判断趋势方向和强度
"""
from collections import defaultdict
from typing import List, Dict, Optional
from datetime import datetime
import threading
from .store import KlineData, normalize_symbol
class TrendAnalyzer:
"""趋势分析器"""
# 支持的周期
PERIODS = ['H4', 'H1', 'M15', 'M5', 'M1']
# ADX阈值
ADX_TREND_THRESHOLD = 25 # ADX > 25 表示有趋势
ADX_STRONG_THRESHOLD = 40 # ADX > 40 表示强趋势
# 均线周期
MA_FAST = 10 # 快线周期
MA_SLOW = 20 # 慢线周期
def __init__(self):
# 存储各周期趋势状态: {SYMBOL: {PERIOD: TrendState}}
self._trend_states = defaultdict(lambda: defaultdict(dict))
self._lock = threading.RLock()
# 趋势转换历史
self._trend_changes = defaultdict(list)
print("[TrendAnalyzer] 趋势分析器已初始化")
def analyze_trend(self, symbol: str, period: str, klines: List[KlineData]) -> Dict:
"""
分析单个周期的趋势
Args:
symbol: 交易品种
period: 周期
klines: K线数据
Returns:
{
"trend": "up" / "down" / "sideways",
"strength": 0-100,
"adx": float,
"ma_fast": float,
"ma_slow": float,
"price": float,
"change_signal": bool, # 是否发生趋势转换
"timestamp": str
}
"""
if len(klines) < 30: # 至少需要30根K线
return {
"trend": "unknown",
"strength": 0,
"adx": 0,
"ma_fast": 0,
"ma_slow": 0,
"price": 0,
"change_signal": False,
"reason": "K线数据不足(需≥30根)",
"timestamp": datetime.now().isoformat()
}
# 计算均线
closes = [k.close for k in klines]
ma_fast = self._calculate_ma(closes, self.MA_FAST)
ma_slow = self._calculate_ma(closes, self.MA_SLOW)
current_price = closes[-1]
# 计算ADX
adx = self._calculate_adx(klines)
# 判断趋势方向和原因
reason_parts = []
if adx < self.ADX_TREND_THRESHOLD:
# ADX较低,震荡行情
trend = "sideways"
reason_parts.append(f"ADX={adx:.1f}<25 无明显趋势")
else:
# 根据均线和价格判断方向
if ma_fast > ma_slow and current_price > ma_fast:
trend = "up"
reason_parts.append(f"MA{self.MA_FAST}({ma_fast:.2f}) > MA{self.MA_SLOW}({ma_slow:.2f})")
reason_parts.append(f"价格({current_price:.2f}) > MA{self.MA_FAST}")
reason_parts.append(f"ADX={adx:.1f}≥25 确认趋势")
elif ma_fast < ma_slow and current_price < ma_fast:
trend = "down"
reason_parts.append(f"MA{self.MA_FAST}({ma_fast:.2f}) < MA{self.MA_SLOW}({ma_slow:.2f})")
reason_parts.append(f"价格({current_price:.2f}) < MA{self.MA_FAST}")
reason_parts.append(f"ADX={adx:.1f}≥25 确认趋势")
else:
trend = "sideways"
if ma_fast > ma_slow:
reason_parts.append(f"MA{self.MA_FAST}({ma_fast:.2f}) > MA{self.MA_SLOW}({ma_slow:.2f})")
reason_parts.append(f"但价格({current_price:.2f})低于MA{self.MA_FAST}")
else:
reason_parts.append(f"MA{self.MA_FAST}({ma_fast:.2f}) < MA{self.MA_SLOW}({ma_slow:.2f})")
reason_parts.append(f"且价格({current_price:.2f})高于MA{self.MA_FAST}")
reason_parts.append("信号矛盾,判定震荡")
reason = "".join(reason_parts)
# 计算趋势强度 (基于ADX)
if adx >= self.ADX_STRONG_THRESHOLD:
strength = min(100, int(adx + 20))
elif adx >= self.ADX_TREND_THRESHOLD:
strength = int(adx + 10)
else:
strength = int(adx)
# 检查趋势转换
symbol_key = normalize_symbol(symbol)
change_signal = False
previous_trend = None
with self._lock:
if period in self._trend_states[symbol_key]:
previous_trend = self._trend_states[symbol_key][period].get('trend')
if previous_trend and previous_trend != trend and previous_trend != "unknown":
change_signal = True
# 记录转换历史
self._trend_changes[symbol_key].append({
"period": period,
"from_trend": previous_trend,
"to_trend": trend,
"timestamp": datetime.now().isoformat(),
"price": current_price
})
# 只保留最近20条
if len(self._trend_changes[symbol_key]) > 20:
self._trend_changes[symbol_key] = self._trend_changes[symbol_key][-20:]
# 更新状态
self._trend_states[symbol_key][period] = {
"trend": trend,
"strength": strength,
"adx": round(adx, 2),
"ma_fast": round(ma_fast, 4),
"ma_slow": round(ma_slow, 4),
"price": current_price,
"change_signal": change_signal,
"previous_trend": previous_trend,
"reason": reason,
"timestamp": datetime.now().isoformat()
}
return self._trend_states[symbol_key][period]
def analyze_resonance(self, symbol: str) -> Dict:
"""
分析多周期共振
Returns:
{
"resonance": "up" / "down" / "none",
"strength": 0-100,
"periods": {period: trend_state},
"aligned_count": int,
"signal": str
}
"""
symbol_key = normalize_symbol(symbol)
with self._lock:
states = dict(self._trend_states[symbol_key])
if not states:
return {
"resonance": "none",
"strength": 0,
"periods": {},
"aligned_count": 0,
"signal": "等待数据"
}
# 统计各趋势数量
up_count = sum(1 for s in states.values() if s.get('trend') == 'up')
down_count = sum(1 for s in states.values() if s.get('trend') == 'down')
sideways_count = sum(1 for s in states.values() if s.get('trend') == 'sideways')
# 计算平均强度
strengths = [s.get('strength', 0) for s in states.values() if s.get('trend') != 'sideways']
avg_strength = sum(strengths) / len(strengths) if strengths else 0
# 判断共振
total = len(states)
if up_count >= total * 0.6: # 60%以上周期趋势一致
resonance = "up"
aligned_count = up_count
signal = f"多周期向上共振 ({up_count}/{total})"
elif down_count >= total * 0.6:
resonance = "down"
aligned_count = down_count
signal = f"多周期向下共振 ({down_count}/{total})"
else:
resonance = "none"
aligned_count = max(up_count, down_count)
signal = f"趋势分歧 (↑{up_count}{down_count}{sideways_count})"
return {
"resonance": resonance,
"strength": int(avg_strength),
"periods": states,
"aligned_count": aligned_count,
"up_count": up_count,
"down_count": down_count,
"sideways_count": sideways_count,
"signal": signal
}
def get_trend_state(self, symbol: str, period: str = None) -> Dict:
"""获取趋势状态"""
symbol_key = normalize_symbol(symbol)
with self._lock:
if period:
return self._trend_states[symbol_key].get(period, {})
return dict(self._trend_states[symbol_key])
def get_trend_changes(self, symbol: str, count: int = 10) -> List[Dict]:
"""获取趋势转换历史"""
symbol_key = normalize_symbol(symbol)
with self._lock:
return self._trend_changes[symbol_key][-count:]
def _calculate_ma(self, data: List[float], period: int) -> float:
"""计算移动平均线"""
if len(data) < period:
return data[-1] if data else 0
return sum(data[-period:]) / period
def _calculate_adx(self, klines: List[KlineData], period: int = 14) -> float:
"""
计算ADX (Average Directional Index)
ADX > 25: 有趋势
ADX > 40: 强趋势
ADX < 20: 无明显趋势
"""
if len(klines) < period + 1:
return 0
# 计算 +DM 和 -DM
plus_dm = []
minus_dm = []
tr_list = []
for i in range(1, len(klines)):
high = klines[i].high
low = klines[i].low
prev_high = klines[i-1].high
prev_low = klines[i-1].low
prev_close = klines[i-1].close
# +DM
up_move = high - prev_high
down_move = prev_low - low
if up_move > down_move and up_move > 0:
plus_dm.append(up_move)
else:
plus_dm.append(0)
# -DM
if down_move > up_move and down_move > 0:
minus_dm.append(down_move)
else:
minus_dm.append(0)
# True Range
tr = max(
high - low,
abs(high - prev_close),
abs(low - prev_close)
)
tr_list.append(tr)
if len(tr_list) < period:
return 0
# 计算平滑值
atr = sum(tr_list[-period:]) / period
smoothed_plus_dm = sum(plus_dm[-period:]) / period
smoothed_minus_dm = sum(minus_dm[-period:]) / period
# 计算 +DI 和 -DI
if atr == 0:
return 0
plus_di = (smoothed_plus_dm / atr) * 100
minus_di = (smoothed_minus_dm / atr) * 100
# 计算 DX
di_sum = plus_di + minus_di
if di_sum == 0:
return 0
dx = abs(plus_di - minus_di) / di_sum * 100
return dx
def generate_trade_suggestion(self, symbol: str, pivots: List[Dict],
current_price: float) -> Optional[Dict]:
"""
基于趋势和转折点生成交易建议
Args:
symbol: 交易品种
pivots: 转折点数据
current_price: 当前价格
Returns:
交易建议 None
"""
symbol_key = normalize_symbol(symbol)
# 获取趋势状态
resonance = self.analyze_resonance(symbol)
if resonance['resonance'] == 'none':
return None
if resonance['strength'] < 30:
return None
trend = resonance['resonance']
# 根据趋势找最近的转折点作为止损止盈
recent_pivots = sorted(pivots, key=lambda x: x['timestamp'], reverse=True)[:10]
sl = None
tp = None
action = None
reason = ""
if trend == "up":
# 上升趋势,找最近的低点作为止损
action = "b"
low_pivots = [p for p in recent_pivots if p['direction'] == 'low']
if low_pivots:
# 找最近的低点作为止损
sl = low_pivots[0]['price']
# 止盈设为止损的1.5-2倍距离
if sl and current_price > sl:
distance = current_price - sl
tp = current_price + distance * 1.5
reason = f"多周期向上共振,建议买入,止损参考最近低点 {sl}"
else:
return None
elif trend == "down":
# 下降趋势,找最近的高点作为止损
action = "s"
high_pivots = [p for p in recent_pivots if p['direction'] == 'high']
if high_pivots:
sl = high_pivots[0]['price']
if sl and current_price < sl:
distance = sl - current_price
tp = current_price - distance * 1.5
reason = f"多周期向下共振,建议卖出,止损参考最近高点 {sl}"
else:
return None
if not all([action, sl, tp]):
return None
return {
"symbol": symbol_key,
"action": action,
"price": current_price,
"sl": round(sl, 4),
"tp": round(tp, 4),
"reason": reason,
"trend_strength": resonance['strength'],
"resonance_periods": resonance['aligned_count'],
"generated_at": datetime.now().isoformat()
}
+30
View File
@@ -0,0 +1,30 @@
#!/bin/bash
# 日志监控脚本 - 实时查看EA和后端服务的通信
echo "============================================================"
echo " EA与后端服务通信监控"
echo "============================================================"
echo ""
echo "监控内容:"
echo " - EA获取交易指令 (GET /get_trades)"
echo " - EA发送统计数据 (POST /send_statistics)"
echo " - 前端界面请求"
echo ""
echo "按 Ctrl+C 停止监控"
echo "============================================================"
echo ""
# 实时监控后端日志
tail -f /private/tmp/claude-501/-Users-wangxingxing--openclaw-workspace-lianghua/tasks/bpae1rf93.output | \
while read line; do
# 高亮EA请求
if echo "$line" | grep -q "GET /get_trades"; then
echo "🔴 [EA请求交易指令] $line"
elif echo "$line" | grep -q "POST /send_statistics"; then
echo "🟢 [EA发送统计数据] $line"
elif echo "$line" | grep -q "send_trade_instructions"; then
echo "🔵 [交易员下发指令] $line"
else
echo "$line"
fi
done
+91 -35
View File
@@ -4,7 +4,7 @@
EA 相关的接口路由
"""
from fastapi import APIRouter, Query
from fastapi import APIRouter, Query, Request
from typing import Optional, List, Dict
from models import TradeInstruction
from server import TradingServer
@@ -15,7 +15,7 @@ def create_ea_routes(server: TradingServer) -> APIRouter:
创建 EA 相关路由
"""
router = APIRouter()
@router.get("/get_trades")
async def get_trades(
symbol: str = Query(..., description="交易品种"),
@@ -23,11 +23,11 @@ def create_ea_routes(server: TradingServer) -> APIRouter:
) -> Dict:
"""
获取指定SYMBOL的交易指令
参数:
- symbol: 交易品种 (e.g., "EURUSD")
- price: 当前中间价格用于条件过滤
返回:
```json
{
@@ -40,21 +40,42 @@ def create_ea_routes(server: TradingServer) -> APIRouter:
"sl": 1.0800,
"tp": 1.0900
}
],
"close_tickets": [123456, 789012],
"pivot_alerts": [
{
"type": "pivot_alert",
"symbol": "EURUSD",
"period": "H4",
"direction": "high",
"pivot_price": 1.0900,
"current_price": 1.0880,
"distance_pct": 0.18,
"message": "EURUSD H4 接近高点 1.0900"
}
]
}
```
"""
trades = server.get_trades_by_symbol(symbol, price)
return {"trades": trades}
result = server.get_trades_by_symbol(symbol, price)
# 添加平仓指令
result["close_tickets"] = server.get_close_position_instructions(symbol)
# 打印完整返回数据用于调试
import json
print(f"[EA API] 返回给EA的数据: {json.dumps(result, ensure_ascii=False)}")
return result
@router.post("/send_statistics")
async def send_statistics(data: dict) -> Dict:
async def send_statistics(request: Request) -> Dict:
"""
接收 EA 发送的统计数据
参数 (JSON):
```json
{
"symbol": "eurusd",
"timestamp": "2024-01-15 14:30:45",
"tickCount": 1234,
"bidPrice": 1.0850,
@@ -62,30 +83,11 @@ def create_ea_routes(server: TradingServer) -> APIRouter:
"balance": 10000.00,
"equity": 10500.50,
"marginLevel": 150.0,
"positions": [
{
"symbol": "eurusd",
"tickets": 123456,
"type": "buy",
"volume": 0.1,
"openPrice": 1.0800,
"takeProfit": 1.0900,
"stopLoss": 1.0750,
"profit": 50.00
}
],
"trades": [
{
"tickets": 789012,
"symbol": "eurusd",
"action": "buy",
"openPrice": 1.0800,
"volume": 0.1
}
]
"positions": [],
"trades": []
}
```
返回:
```json
{
@@ -94,7 +96,61 @@ def create_ea_routes(server: TradingServer) -> APIRouter:
}
```
"""
server.save_statistics(data)
return {"status": "ok", "message": "统计数据已保存"}
return router
# 获取原始请求体用于调试
body = await request.body()
print(f"[DEBUG] Raw body type: {type(body)}")
print(f"[DEBUG] Raw body: {body}")
print(f"[DEBUG] Raw body length: {len(body)}")
# 尝试解析JSON
import json
try:
data = await request.json()
print(f"[DEBUG] Parsed JSON successfully: {data}")
server.save_statistics(data)
return {"status": "ok", "message": "统计数据已保存"}
except Exception as e:
print(f"[ERROR] Failed to parse JSON: {e}")
print(f"[ERROR] Body as string: {body.decode('utf-8', errors='ignore')}")
return {"status": "error", "message": str(e)}
@router.post("/close_position")
async def close_position(request: Request) -> Dict:
"""
平仓指令
请求体:
```json
{
"ticket": 123456,
"symbol": "GOLD#"
}
```
返回:
```json
{
"status": "ok",
"message": "平仓指令已添加"
}
```
"""
try:
data = await request.json()
ticket = data.get('ticket')
symbol = data.get('symbol', '').upper()
if not ticket:
return {"status": "error", "message": "缺少订单号"}
# 添加平仓指令到队列
server.add_close_position_instruction(symbol, ticket)
print(f"[EA API] 平仓指令已添加: {symbol} ticket={ticket}")
return {"status": "ok", "message": "平仓指令已添加"}
except Exception as e:
print(f"[ERROR] close_position 异常: {str(e)}")
return {"status": "error", "message": str(e)}
return router
+534
View File
@@ -0,0 +1,534 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
行情相关的接口路由
包括K线数据接收查询WebSocket推送等
"""
from fastapi import APIRouter, Query, Request, WebSocket, WebSocketDisconnect
from fastapi.responses import JSONResponse
from typing import Optional, List, Dict
import json
from market.store import MarketStore
from market.pivot_detector import PivotDetector
from market.monitor import PivotMonitor
from market.trend_analyzer import TrendAnalyzer
from market.pending_orders import PendingOrderManager
def create_market_routes(store: MarketStore, detector: PivotDetector,
monitor: PivotMonitor, trend_analyzer: TrendAnalyzer,
pending_orders: PendingOrderManager) -> APIRouter:
"""
创建行情相关路由
Args:
store: K线存储
detector: 转折点检测器
monitor: 转折点监控器
trend_analyzer: 趋势分析器
pending_orders: 待确认订单管理器
"""
router = APIRouter()
# ==================== EA端接口 ====================
@router.post("/ea/kline/{period}")
async def receive_kline(period: str, request: Request) -> Dict:
"""
EA推送K线数据
Args:
period: 周期 (H4/H1/M15/M5/M1)
请求体:
```json
{
"symbol": "GOLD",
"is_full": false, // 是否为全量数据
"klines": [
{
"timestamp": "2024-01-15 14:00:00",
"open": 2030.50,
"high": 2035.00,
"low": 2028.00,
"close": 2033.50,
"volume": 1234
}
]
}
```
返回:
- 成功: {"status": "ok", "count": N}
- 需要全量数据: {"status": "error", "code": 8888, "message": "需要全量数据"}
"""
period = period.upper()
# 验证周期
if period not in ['H4', 'H1', 'M15', 'M5', 'M1']:
return JSONResponse(
status_code=400,
content={"status": "error", "message": f"不支持的周期: {period}"}
)
try:
data = await request.json()
symbol = data.get('symbol', 'GOLD').upper()
is_full = data.get('is_full', False)
klines = data.get('klines', [])
if not klines:
return {"status": "ok", "count": 0, "message": "无数据"}
# 检查是否需要全量数据
if not is_full and not store.is_initialized(symbol, period):
print(f"[MarketAPI] {symbol} {period} 未初始化,需要全量数据")
return JSONResponse(
status_code=400,
content={
"status": "error",
"code": 8888,
"message": "需要全量数据"
}
)
# 保存K线数据
result = store.save_klines(symbol, period, klines, is_full)
if result['status'] == 'ok':
# 更新转折点
all_klines = store.get_all_klines(symbol, period)
if all_klines:
# 转换为KlineData对象
from market.store import KlineData
kline_objs = [
KlineData(
symbol=k['symbol'],
period=k['period'],
timestamp=k['timestamp'],
open_price=k['open'],
high=k['high'],
low=k['low'],
close=k['close'],
volume=k['volume']
)
for k in all_klines
]
detector.update_pivots(symbol, period, kline_objs)
return result
except Exception as e:
print(f"[MarketAPI] 接收K线数据异常: {e}")
return JSONResponse(
status_code=500,
content={"status": "error", "message": str(e)}
)
@router.post("/ea/kline_batch")
async def receive_kline_batch(request: Request) -> Dict:
"""
EA批量推送多个周期的K线数据
请求体:
```json
{
"symbol": "GOLD",
"is_full": true,
"data": {
"H4": [{...}, {...}],
"H1": [{...}, {...}],
"M15": [{...}, {...}],
"M5": [{...}, {...}],
"M1": [{...}, {...}]
}
}
```
"""
try:
data = await request.json()
symbol = data.get('symbol', 'GOLD').upper()
is_full = data.get('is_full', False)
kline_data = data.get('data', {})
results = {}
for period, klines in kline_data.items():
period = period.upper()
if period not in ['H4', 'H1', 'M15', 'M5', 'M1']:
continue
result = store.save_klines(symbol, period, klines, is_full)
results[period] = result
# 更新转折点
if result['status'] == 'ok':
all_klines = store.get_all_klines(symbol, period)
if all_klines:
from market.store import KlineData
kline_objs = [
KlineData(
symbol=k['symbol'],
period=k['period'],
timestamp=k['timestamp'],
open_price=k['open'],
high=k['high'],
low=k['low'],
close=k['close'],
volume=k['volume']
)
for k in all_klines
]
detector.update_pivots(symbol, period, kline_objs)
return {
"status": "ok",
"symbol": symbol,
"results": results
}
except Exception as e:
print(f"[MarketAPI] 批量接收K线数据异常: {e}")
return JSONResponse(
status_code=500,
content={"status": "error", "message": str(e)}
)
# ==================== 查询接口 ====================
@router.get("/market/kline/{symbol}")
async def get_kline(
symbol: str,
period: str = Query("M5", description="周期: H4/H1/M15/M5/M1"),
count: int = Query(100, description="返回条数")
) -> Dict:
"""
获取K线数据
"""
symbol = symbol.upper()
period = period.upper()
klines = store.get_klines(symbol, period, count)
return {
"status": "ok",
"symbol": symbol,
"period": period,
"count": len(klines),
"data": klines
}
@router.get("/market/pivots/{symbol}")
async def get_pivots(
symbol: str,
period: str = Query(None, description="周期,不指定则返回全部"),
direction: str = Query(None, description="方向: high/low"),
count: int = Query(50, description="返回条数")
) -> Dict:
"""
获取转折点数据
"""
symbol = symbol.upper()
if period:
period = period.upper()
pivots = detector.get_pivots(symbol, period, direction, count)
return {
"status": "ok",
"symbol": symbol,
"period": period,
"count": len(pivots),
"data": pivots
}
else:
# 返回所有周期的转折点
result = {}
for p in ['H4', 'H1', 'M15', 'M5', 'M1']:
pivots = detector.get_pivots(symbol, p, direction, count)
if pivots:
result[p] = pivots
return {
"status": "ok",
"symbol": symbol,
"data": result
}
@router.get("/market/symbols")
async def get_symbols() -> Dict:
"""
获取所有已存储数据的symbol列表
"""
symbols = store.get_symbols()
return {
"status": "ok",
"symbols": symbols,
"count": len(symbols)
}
@router.get("/market/status")
async def get_market_status() -> Dict:
"""
获取行情存储状态
"""
store_status = store.get_status()
detector_status = detector.get_status()
monitor_status = monitor.get_status()
return {
"status": "ok",
"store": store_status,
"pivots": detector_status,
"monitor": monitor_status
}
@router.get("/market/thresholds")
async def get_thresholds() -> Dict:
"""
获取各周期的接近阈值
"""
thresholds = detector.THRESHOLDS
return {
"status": "ok",
"thresholds": {
period: {
"value": threshold,
"percent": f"{threshold * 100:.4f}%",
"description": f"千分之{threshold * 1000}"
}
for period, threshold in thresholds.items()
}
}
# ==================== 趋势分析接口 ====================
@router.get("/trend/{symbol}")
async def get_trend(symbol: str) -> Dict:
"""
获取单个品种的趋势分析
"""
from market.store import KlineData
# 分析每个周期的趋势
for period in ['H4', 'H1', 'M15', 'M5', 'M1']:
all_klines = store.get_all_klines(symbol, period)
if all_klines:
kline_objs = [
KlineData(
symbol=k['symbol'],
period=k['period'],
timestamp=k['timestamp'],
open_price=k['open'],
high=k['high'],
low=k['low'],
close=k['close'],
volume=k['volume']
)
for k in all_klines
]
trend_analyzer.analyze_trend(symbol, period, kline_objs)
# 获取共振分析
resonance = trend_analyzer.analyze_resonance(symbol)
# 获取趋势转换历史
changes = trend_analyzer.get_trend_changes(symbol, 10)
return {
"status": "ok",
"symbol": symbol,
"resonance": resonance,
"trend_changes": changes
}
@router.post("/trend/generate_order/{symbol}")
async def generate_trade_order(symbol: str) -> Dict:
"""
基于趋势分析生成交易建议
"""
from market.store import KlineData
# 更新趋势分析
for period in ['H4', 'H1', 'M15', 'M5', 'M1']:
all_klines = store.get_all_klines(symbol, period)
if all_klines:
kline_objs = [
KlineData(
symbol=k['symbol'],
period=k['period'],
timestamp=k['timestamp'],
open_price=k['open'],
high=k['high'],
low=k['low'],
close=k['close'],
volume=k['volume']
)
for k in all_klines
]
trend_analyzer.analyze_trend(symbol, period, kline_objs)
# 获取所有周期的转折点
all_pivots = []
for period in ['H4', 'H1', 'M15', 'M5', 'M1']:
pivot_list = detector.get_pivots(symbol, period, None, 20)
all_pivots.extend(pivot_list)
# 获取当前价格
current_price = store.get_latest_price(symbol)
if not current_price:
return {"status": "error", "message": "无法获取当前价格"}
# 生成交易建议
suggestion = trend_analyzer.generate_trade_suggestion(symbol, all_pivots, current_price)
if not suggestion:
return {
"status": "ok",
"message": "当前无交易建议",
"resonance": trend_analyzer.analyze_resonance(symbol)
}
# 添加到待确认订单
order_id = pending_orders.add_order(suggestion)
return {
"status": "ok",
"message": "交易建议已生成",
"order_id": order_id,
"suggestion": suggestion
}
# ==================== 待确认订单接口 ====================
@router.get("/pending_orders")
async def get_pending_orders(symbol: Optional[str] = None) -> Dict:
"""
获取待确认订单列表
"""
orders = pending_orders.get_pending_orders(symbol)
return {
"status": "ok",
"count": len(orders),
"orders": orders
}
@router.post("/pending_orders/{order_id}/confirm")
async def confirm_pending_order(order_id: str, request: Request = None) -> Dict:
"""
确认待确认订单可更新手数止损止盈
"""
# 获取更新数据
update_data = {}
if request:
try:
update_data = await request.json()
except:
pass
# 更新订单参数
if update_data:
order = pending_orders.get_order_by_id(order_id)
if order:
if 'mount' in update_data:
order['mount'] = update_data['mount']
if 'sl' in update_data:
order['sl'] = update_data['sl']
if 'tp' in update_data:
order['tp'] = update_data['tp']
order = pending_orders.confirm_order(order_id)
if not order:
return {"status": "error", "message": "订单不存在"}
return {
"status": "ok",
"message": "订单已确认",
"order": order
}
@router.post("/pending_orders/{order_id}/reject")
async def reject_pending_order(order_id: str) -> Dict:
"""
拒绝待确认订单
"""
success = pending_orders.reject_order(order_id)
if not success:
return {"status": "error", "message": "订单不存在"}
return {
"status": "ok",
"message": "订单已拒绝"
}
# ==================== 交易配置接口 ====================
@router.get("/trade_config")
async def get_trade_config() -> Dict:
"""
获取交易配置
"""
from market.monitor import TradeConfig
config = TradeConfig.get_instance()
return {
"status": "ok",
"config": config.to_dict()
}
@router.post("/trade_config")
async def update_trade_config(request: Request) -> Dict:
"""
更新交易配置
"""
from market.monitor import TradeConfig
config = TradeConfig.get_instance()
try:
data = await request.json()
config.update(data)
return {
"status": "ok",
"message": "配置已更新",
"config": config.to_dict()
}
except Exception as e:
return {"status": "error", "message": str(e)}
# ==================== WebSocket接口 ====================
@router.websocket("/ws/market")
async def websocket_market(websocket: WebSocket):
"""
WebSocket连接用于实时推送转折点提醒
"""
await websocket.accept()
monitor.add_ws_client(websocket)
try:
# 发送欢迎消息
await websocket.send_text(json.dumps({
"type": "connected",
"message": "已连接到行情监控服务"
}))
# 保持连接,等待客户端消息或关闭
while True:
try:
data = await websocket.receive_text()
# 可以处理客户端发来的消息
msg = json.loads(data)
if msg.get('type') == 'ping':
await websocket.send_text(json.dumps({"type": "pong"}))
except WebSocketDisconnect:
break
except Exception as e:
print(f"[WebSocket] 连接异常: {e}")
finally:
monitor.remove_ws_client(websocket)
return router
+105 -50
View File
@@ -9,25 +9,72 @@ from typing import List, Dict, Optional
import threading
from models import TradeInstruction
from market.store import MarketStore, normalize_symbol
from market.pivot_detector import PivotDetector
from market.monitor import PivotMonitor
from market.trend_analyzer import TrendAnalyzer
from market.pending_orders import PendingOrderManager
class TradingServer:
"""交易服务主类"""
def __init__(self):
# 交易指令队列 - 按SYMBOL分类
# 结构: {"SYMBOL1": [TradeInstruction1, ...], "SYMBOL2": [...]}
self.trade_instructions = defaultdict(list)
# 平仓指令队列 - 按SYMBOL分类
# 结构: {"SYMBOL1": [ticket1, ticket2, ...], ...}
self.close_position_instructions = defaultdict(list)
# 统计数据历史 - 保留最新10条
# 结构: deque([{stat_data1}, {stat_data2}, ...], maxlen=10)
self.statistics_history = deque(maxlen=10)
# 线程锁 - 确保线程安全
self.lock = threading.RLock()
# ==================== 行情模块 ====================
# K线存储
self.market_store = MarketStore()
# 转折点检测器
self.pivot_detector = PivotDetector()
# 待确认订单管理器(需要在 PivotMonitor 之前初始化)
self.pending_orders = PendingOrderManager()
# 设置订单确认回调
self.pending_orders.set_confirm_callback(self._on_order_confirmed)
# 转折点监控器
self.pivot_monitor = PivotMonitor(self.market_store, self.pivot_detector, self.pending_orders)
# 趋势分析器
self.trend_analyzer = TrendAnalyzer()
print("[信息] 交易服务已初始化")
def _on_order_confirmed(self, order: Dict):
"""
订单确认回调 - 将确认的订单加入交易队列
"""
print(f"[TradingServer] _on_order_confirmed 被调用,订单: {order}")
try:
# 创建交易指令
instruction = TradeInstruction(
symbol=order.get('symbol', ''),
action=order.get('action', 'b'),
mount=order.get('mount', 0.01),
price=order.get('price', 0),
sl=order.get('sl', 0),
tp=order.get('tp', 0)
)
print(f"[TradingServer] 创建交易指令: symbol={instruction.symbol}, action={instruction.action}, mount={instruction.mount}, price={instruction.price}, sl={instruction.sl}, tp={instruction.tp}")
# 添加到交易队列
result = self.add_trade_instruction([instruction])
print(f"[TradingServer] 订单已加入交易队列: {result}")
except Exception as e:
print(f"[TradingServer] 加入交易队列失败: {e}")
import traceback
traceback.print_exc()
def add_trade_instruction(self, instructions: List[TradeInstruction]) -> dict:
"""
添加交易指令
@@ -76,59 +123,46 @@ class TradingServer:
return {"added": added, "rejected": rejected}
def get_trades_by_symbol(self, symbol: str, price: Optional[float] = None) -> List[Dict]:
def get_trades_by_symbol(self, symbol: str, price: Optional[float] = None) -> Dict:
"""
获取指定SYMBOL的交易指令并删除
根据价格条件过滤指令
- 买入指令action='b'如果指令价格 > 当前价格缓存等待价格下跌到指令价格
- 卖出指令action='s'如果指令价格 < 当前价格缓存等待价格上涨到指令价格
返回指令列表JSON格式
同时检查价格是否接近转折点如果有则添加到返回结果中
返回: {"trades": [...], "pivot_alerts": [...]}
"""
# 先检查转折点
pivot_alerts = []
if price is not None:
# 统一转换为大写进行检测
symbol_upper = symbol.upper()
pivot_alerts = self.pivot_monitor.check_and_alert(symbol_upper, price)
if pivot_alerts:
print(f"[信息] {symbol_upper} 当前价格 {price} 接近转折点")
with self.lock:
symbol = symbol.upper()
if symbol not in self.trade_instructions or len(self.trade_instructions[symbol]) == 0:
return []
# 获取所有指令
return {"trades": [], "pivot_alerts": pivot_alerts}
# 获取所有指令并直接返回(不再进行价格过滤)
trades = self.trade_instructions[symbol]
result = []
cached_trades = []
for t in trades:
should_send = True
# 如果提供了价格,进行条件检查
if price is not None:
if t.action.lower() == 'b':
# 买入指令:如果指令价格 > 当前价格,则缓存
if t.price > price:
should_send = False
cached_trades.append(t)
elif t.action.lower() == 's':
# 卖出指令:如果指令价格 < 当前价格,则缓存
if t.price < price:
should_send = False
cached_trades.append(t)
if should_send:
result.append({
"symbol": t.symbol.lower(),
"action": t.action.lower(),
"mount": t.mount,
"price": t.price,
"sl": t.sl,
"tp": t.tp
})
# 更新指令队列:移除已发送的,保留已缓存的
self.trade_instructions[symbol] = cached_trades
result = [{
"symbol": t.symbol,
"action": t.action.lower(),
"mount": t.mount,
"price": t.price,
"sl": t.sl,
"tp": t.tp
} for t in trades]
# 清空指令队列
self.trade_instructions[symbol] = []
if len(result) > 0:
print(f"[信息] 推送了 {len(result)}{symbol} 指令给EA (当前价格: {price})")
if len(cached_trades) > 0:
print(f"[信息] 缓存了 {len(cached_trades)}{symbol} 指令,等待价格条件满足")
return result
print(f"[信息] 推送了 {len(result)}{symbol} 指令给EA")
return {"trades": result, "pivot_alerts": pivot_alerts}
def save_statistics(self, stat_data: dict) -> None:
"""
@@ -157,7 +191,7 @@ class TradingServer:
for symbol, trades in self.trade_instructions.items():
result[symbol] = [
{
"symbol": t.symbol.lower(),
"symbol": t.symbol,
"action": t.action.lower(),
"mount": t.mount,
"price": t.price,
@@ -187,3 +221,24 @@ class TradingServer:
del self.trade_instructions[symbol]
print(f"[信息] 已清空 {symbol} 的交易指令,共 {count}")
return count
def add_close_position_instruction(self, symbol: str, ticket: int) -> None:
"""
添加平仓指令
"""
with self.lock:
symbol = symbol.upper()
self.close_position_instructions[symbol].append(ticket)
print(f"[信息] 添加平仓指令: {symbol} ticket={ticket}")
def get_close_position_instructions(self, symbol: str) -> List[int]:
"""
获取并清空平仓指令
"""
with self.lock:
symbol = symbol.upper()
tickets = self.close_position_instructions.get(symbol, [])
self.close_position_instructions[symbol] = []
if tickets:
print(f"[信息] 返回平仓指令: {symbol} tickets={tickets}")
return tickets
Regular → Executable
View File
+72
View File
@@ -0,0 +1,72 @@
@echo off
REM 前端启动脚本 (Windows)
REM 用于快速启动React前端
setlocal enabledelayedexpansion
echo ==================================================
echo 量化交易服务 - 前端启动脚本
echo ==================================================
REM 检查Node.js版本
node --version >nul 2>&1
if errorlevel 1 (
echo ❌ 错误: 未找到Node.js
echo 请先安装Node.js 16+版本
echo 推荐使用: https://nodejs.org/
pause
exit /b 1
)
for /f "tokens=*" %%i in ('node --version') do set NODE_VERSION=%%i
echo ✓ Node.js版本: %NODE_VERSION%
REM 检查npm
npm --version >nul 2>&1
if errorlevel 1 (
echo ❌ 错误: 未找到npm
pause
exit /b 1
)
for /f "tokens=*" %%i in ('npm --version') do set NPM_VERSION=%%i
echo ✓ npm版本: %NPM_VERSION%
REM 获取脚本目录
cd /d "%~dp0"
cd frontend
echo ✓ 工作目录: %cd%
REM 检查依赖
echo → 检查依赖...
if not exist "node_modules" (
echo → 安装依赖...
npm install
echo ✓ 依赖已安装
) else (
echo ✓ 依赖已存在
)
REM 显示启动信息
echo.
echo ==================================================
echo 启动参数:
echo 开发服务器: http://localhost:3000
echo 代理后端: http://localhost:8000
echo 热重载: 启用
echo ==================================================
echo.
echo ✓ 前端服务已启动!
echo.
echo 访问地址:
echo 前端界面: http://localhost:3000
echo API文档: http://localhost:8000/docs
echo.
echo 注意:请确保后端服务 (python main.py) 已在运行
echo ==================================================
echo.
REM 启动开发服务器
npm start
pause
+67
View File
@@ -0,0 +1,67 @@
#!/bin/bash
# 前端启动脚本
# 用于快速启动React前端
set -e
echo "=================================================="
echo "量化交易服务 - 前端启动脚本"
echo "=================================================="
# 检查Node.js版本
if ! command -v node &> /dev/null; then
echo "❌ 错误: 未找到Node.js"
echo "请先安装Node.js 16+版本"
echo "推荐使用: https://nodejs.org/"
exit 1
fi
NODE_VERSION=$(node --version | sed 's/v//')
echo "✓ Node.js版本: $NODE_VERSION"
# 检查npm
if ! command -v npm &> /dev/null; then
echo "❌ 错误: 未找到npm"
exit 1
fi
NPM_VERSION=$(npm --version)
echo "✓ npm版本: $NPM_VERSION"
# 获取脚本目录
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
cd "$SCRIPT_DIR/frontend"
echo "✓ 工作目录: $(pwd)"
# 检查依赖
echo "→ 检查依赖..."
if [ ! -d "node_modules" ]; then
echo "→ 安装依赖..."
npm install
echo "✓ 依赖已安装"
else
echo "✓ 依赖已存在"
fi
# 显示启动信息
echo ""
echo "=================================================="
echo "启动参数:"
echo " 开发服务器: http://localhost:3000"
echo " 代理后端: http://localhost:8000"
echo " 热重载: 启用"
echo "=================================================="
echo ""
echo "✓ 前端服务已启动!"
echo ""
echo "访问地址:"
echo " 前端界面: http://localhost:3000"
echo " API文档: http://localhost:8000/docs"
echo ""
echo "注意:请确保后端服务 (python main.py) 已在运行"
echo "=================================================="
echo ""
# 启动开发服务器
npm run dev
+499 -46
View File
@@ -5,27 +5,27 @@
//+------------------------------------------------------------------+
#property copyright "wwananggxxxx"
#property link "https://www.mql5.com"
#property version "1.00"
#property version "2.00"
#property strict
//--- 需要访问Web请求权限
#include <Trade\Trade.mqh>
#include <Trade\SymbolInfo.mqh>
#include <Trade\PositionInfo.mqh>
#include <Trade\OrderInfo.mqh>
#include <Trade/Trade.mqh>
#include <Trade/SymbolInfo.mqh>
#include <Trade/PositionInfo.mqh>
#include <Trade/OrderInfo.mqh>
//+------------------------------------------------------------------+
//| 全局变量定义 |
//+------------------------------------------------------------------+
// Python 服务配置
string g_pythonServer = "http://localhost:5858";
string g_pythonServer = "http://127.0.0.1:8000";
uint g_lastPythonRequestTime = 0;
int g_pythonRequestInterval = 100; // 毫秒
uint g_pythonRequestInterval = 100; // 毫秒
// 统计数据 - 每分钟重置
datetime g_lastStatisticTime = 0;
int g_tickCountPerMinute = 0;
int g_tickCount = 0;
double g_bidPrice = 0;
double g_askPrice = 0;
double g_accountBalance = 0;
@@ -36,6 +36,16 @@ string g_positionsSummary = ""; // JSON 格式的持仓汇总
// 当日交易记录 - 用于发送到Python
string g_tradesOfDay = "";
// K线数据推送相关
bool g_klineInitialized = false; // 是否已发送历史K线数据
datetime g_lastKlinePushTime = 0; // 上次推送K线时间
int g_klinePushInterval = 60; // K线推送间隔(秒)
datetime g_lastH4CloseTime = 0; // 上次H4 K线收盘时间
datetime g_lastH1CloseTime = 0; // 上次H1 K线收盘时间
datetime g_lastM15CloseTime = 0; // 上次M15 K线收盘时间
datetime g_lastM5CloseTime = 0; // 上次M5 K线收盘时间
datetime g_lastM1CloseTime = 0; // 上次M1 K线收盘时间
// 交易类对象
CTrade trade;
CSymbolInfo symbolInfo;
@@ -44,6 +54,30 @@ CPositionInfo positionInfo;
// 风险管理相关
double g_riskLimitPercent = 30.0; // 30% 账户风险限制
//+------------------------------------------------------------------+
//| URL编码函数 - 处理特殊字符 |
//+------------------------------------------------------------------+
string URLEncode(string str)
{
string result = "";
for(int i = 0; i < StringLen(str); i++)
{
ushort ch = StringGetCharacter(str, i);
// 字母、数字、连字符、下划线、点号不需要编码
if((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') ||
ch == '-' || ch == '_' || ch == '.')
{
result += CharToString(ch);
}
else
{
// 其他字符编码为 %XX 格式
result += "%" + StringFormat("%02X", ch);
}
}
return result;
}
//+------------------------------------------------------------------+
//| Expert initialization function |
//+------------------------------------------------------------------+
@@ -51,16 +85,21 @@ int OnInit()
{
//--- 初始化交易类
trade.SetExpertMagicNumber(123456);
//--- 初始化时间
g_lastStatisticTime = TimeCurrent();
g_lastPythonRequestTime = GetTickCount();
g_lastKlinePushTime = TimeCurrent();
//--- 打印初始化信息
Print("Expert initialized successfully");
Print("Python server: ", g_pythonServer);
Print("Risk limit: ", g_riskLimitPercent, "%");
//--- 启动时推送历史K线数据
Print("Pushing historical K-line data...");
PushAllKlineData(true); // is_full = true
//---
return(INIT_SUCCEEDED);
}
@@ -77,7 +116,7 @@ void OnDeinit(const int reason)
//+------------------------------------------------------------------+
void UpdateStatistics()
{
g_tickCountPerMinute++;
g_tickCount++;
//--- 获取当前价格
MqlTick lastTick;
@@ -157,7 +196,7 @@ void CheckAndCloseRiskyPositions()
// 如果损失超过阈值,平仓
if(posProfit < -riskThreshold)
{
long posTicket = PositionGetTicket(i);
long posTicket = PositionGetInteger(POSITION_TICKET);
Print("Risk limit exceeded! Position profit: ", posProfit, " Limit: ", -riskThreshold);
if(trade.PositionClose(posTicket))
@@ -180,17 +219,20 @@ void CheckAndCloseRiskyPositions()
void RequestTradesFromPython()
{
string headers = "Content-Type: application/json\r\n";
char responseData[];
uchar responseData[];
string response = "";
string outheaders = "";
int responseCode = 0;
// 构建请求URL,携带SYMBOL和当前价格
string currentPrice = DoubleToString((g_bidPrice + g_askPrice) / 2, _Digits);
string url = g_pythonServer + "/get_trades?symbol=" + _Symbol + "&price=" + currentPrice;
string encodedSymbol = URLEncode(_Symbol);
string url = g_pythonServer + "/get_trades?symbol=" + encodedSymbol + "&price=" + currentPrice;
// 建立HTTP请求到Python服务
responseCode = WebRequest("GET", url, headers, NULL, responseData);
uchar emptyData[];
responseCode = WebRequest("GET", url, headers, 5000, emptyData, responseData, outheaders); // timeout设为5秒
if(responseCode == 200)
{
// 将响应转换为字符串
@@ -200,7 +242,7 @@ void RequestTradesFromPython()
{
response += CharToString(responseData[i]);
}
// 解析JSON并执行交易
ParseAndExecuteTrades(response);
}
@@ -208,6 +250,18 @@ void RequestTradesFromPython()
else if(responseCode != -1) // -1表示请求被禁用
{
Print("WebRequest failed. Response code: ", responseCode);
Print("URL: ", url);
// 打印错误详情
if(responseCode == 404)
Print("Endpoint not found. Check server URL.");
else if(responseCode == 500)
Print("Server error. Check server logs.");
}
else if(responseCode == -1)
{
Print("WebRequest is disabled! Please enable WebRequest in MT5 Options -> Expert Advisors");
Print("Make sure 'localhost' is added to the WebRequest allowed list");
}
}
@@ -216,31 +270,150 @@ void RequestTradesFromPython()
//+------------------------------------------------------------------+
void ParseAndExecuteTrades(string jsonData)
{
// JSON格式: [{"symbol":"gold","action":"b","mount":0.01,"sl":5000,"tp":5100}, ...]
// 这里需要简单的JSON解析
// JSON格式: {"trades": [...], "close_tickets": [...], "pivot_alerts": [...]}
// EA只处理trades和close_ticketspivot_alerts由Python推送到前端
if(StringLen(jsonData) == 0) return;
// 提取trades数组
int tradesPos = StringFind(jsonData, "\"trades\":");
if(tradesPos != -1)
{
int tradesStart = StringFind(jsonData, "[", tradesPos);
int tradesEnd = StringFind(jsonData, "]", tradesStart);
if(tradesStart != -1 && tradesEnd != -1)
{
string tradesJson = StringSubstr(jsonData, tradesStart, tradesEnd - tradesStart + 1);
// 如果trades数组不为空,打印出来
if(tradesJson != "[]")
{
Print("[EA] 收到交易指令: ", tradesJson);
}
ParseTradeArray(tradesJson);
}
}
else
{
// 旧格式兼容:直接是数组 [...]
ParseTradeArray(jsonData);
}
// 提取close_tickets数组并执行平仓
int closePos = StringFind(jsonData, "\"close_tickets\":");
if(closePos != -1)
{
int closeStart = StringFind(jsonData, "[", closePos);
int closeEnd = StringFind(jsonData, "]", closeStart);
if(closeStart != -1 && closeEnd != -1)
{
string closeJson = StringSubstr(jsonData, closeStart, closeEnd - closeStart + 1);
ParseAndExecuteClose(closeJson);
}
}
}
//+------------------------------------------------------------------+
//| 解析并执行平仓指令 |
//+------------------------------------------------------------------+
void ParseAndExecuteClose(string jsonData)
{
// 移除首尾的括号
jsonData = StringSubstr(jsonData, 1, StringLen(jsonData) - 2);
if(StringFind(jsonData, "[") == 0)
{
jsonData = StringSubstr(jsonData, 1, StringLen(jsonData) - 2);
}
if(StringLen(jsonData) == 0) return;
// 解析ticket列表
string tickets[];
int count = StringSplit(jsonData, ',', tickets);
for(int i = 0; i < count; i++)
{
string ticketStr = tickets[i];
ticketStr = StringTrimLeft(ticketStr);
ticketStr = StringTrimRight(ticketStr);
long ticket = StringToInteger(ticketStr);
if(ticket > 0)
{
ClosePositionByTicket(ticket);
}
}
}
//+------------------------------------------------------------------+
//| 根据订单号平仓 |
//+------------------------------------------------------------------+
void ClosePositionByTicket(long ticket)
{
// 查找持仓
for(int i = 0; i < PositionsTotal(); i++)
{
if(PositionGetTicket(i) == ticket)
{
string posSymbol = PositionGetString(POSITION_SYMBOL);
double posVolume = PositionGetDouble(POSITION_VOLUME);
ENUM_POSITION_TYPE posType = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
// 构造平仓请求
MqlTradeRequest request = {};
MqlTradeResult result = {};
request.action = TRADE_ACTION_DEAL;
request.position = ticket;
request.symbol = posSymbol;
request.volume = posVolume;
request.type = (posType == POSITION_TYPE_BUY) ? ORDER_TYPE_SELL : ORDER_TYPE_BUY;
request.comment = "Close by Python command";
if(OrderSend(request, result))
{
Print("[平仓成功] Ticket: ", ticket, " Symbol: ", posSymbol);
}
else
{
Print("[平仓失败] Ticket: ", ticket, " Error: ", GetLastError());
}
return;
}
}
Print("[平仓] 未找到订单号: ", ticket);
}
//+------------------------------------------------------------------+
//| 解析交易数组 |
//+------------------------------------------------------------------+
void ParseTradeArray(string jsonData)
{
// 移除首尾的括号
if(StringFind(jsonData, "[") == 0)
{
jsonData = StringSubstr(jsonData, 1, StringLen(jsonData) - 2);
}
if(StringLen(jsonData) == 0) return;
// 简单的JSON解析
int tradeCount = 0;
int pos = -1;
while(true)
{
int startPos = StringFind(jsonData, "{", pos + 1);
int endPos = StringFind(jsonData, "}", startPos);
if(startPos == -1 || endPos == -1) break;
string tradeStr = StringSubstr(jsonData, startPos + 1, endPos - startPos - 1);
ExecuteTradeFromJson(tradeStr);
pos = endPos;
tradeCount++;
if(tradeCount > 100) break; // 防止无限循环
}
}
@@ -255,12 +428,24 @@ void ExecuteTradeFromJson(string tradeJson)
double volume = ExtractJsonDouble(tradeJson, "mount");
double sl = ExtractJsonDouble(tradeJson, "sl");
double tp = ExtractJsonDouble(tradeJson, "tp");
if(symbol == "" || action == "" || volume <= 0) return;
if(symbol != _Symbol) return; // 只处理当前品种
Print("[EA] 收到交易指令: symbol=", symbol, " action=", action, " volume=", volume, " sl=", sl, " tp=", tp);
if(symbol == "" || action == "" || volume <= 0)
{
Print("[EA] 交易参数无效,跳过");
return;
}
if(symbol != _Symbol)
{
Print("[EA] Symbol不匹配,跳过。收到: ", symbol, " 当前品种: ", _Symbol);
return;
}
ENUM_ORDER_TYPE orderType = (action == "b") ? ORDER_TYPE_BUY : ORDER_TYPE_SELL;
Print("[EA] 准备执行交易: ", (orderType == ORDER_TYPE_BUY ? "BUY" : "SELL"), " ", volume, " ", symbol);
ExecuteTrade(orderType, volume, sl, tp);
}
@@ -344,7 +529,7 @@ void ExecuteTrade(ENUM_ORDER_TYPE orderType, double volume, double sl, double tp
if(trade.Buy(volume, _Symbol, 0, sl, tp, "Python AI Trade"))
{
Print("Buy order executed: Volume=", volume, " SL=", sl, " TP=", tp);
RecordTrade("BUY", _Symbol, volume, sl, tp, trade.OrderOpenPrice());
RecordTrade("BUY", _Symbol, volume, sl, tp, trade.ResultPrice());
}
else
{
@@ -356,7 +541,7 @@ void ExecuteTrade(ENUM_ORDER_TYPE orderType, double volume, double sl, double tp
if(trade.Sell(volume, _Symbol, 0, sl, tp, "Python AI Trade"))
{
Print("Sell order executed: Volume=", volume, " SL=", sl, " TP=", tp);
RecordTrade("SELL", _Symbol, volume, sl, tp, trade.OrderOpenPrice());
RecordTrade("SELL", _Symbol, volume, sl, tp, trade.ResultPrice());
}
else
{
@@ -394,8 +579,9 @@ void SendMinuteStatistics()
{
// 构建统计JSON
string statisticJson = "{";
statisticJson += "\"symbol\":\"" + _Symbol + "\",";
statisticJson += "\"timestamp\":\"" + TimeToString(TimeCurrent(), TIME_DATE | TIME_MINUTES) + "\",";
statisticJson += "\"tickCount\":" + IntegerToString(g_tickCountPerMinute) + ",";
statisticJson += "\"tickCount\":" + IntegerToString(g_tickCount) + ",";
statisticJson += "\"bidPrice\":" + DoubleToString(g_bidPrice, _Digits) + ",";
statisticJson += "\"askPrice\":" + DoubleToString(g_askPrice, _Digits) + ",";
statisticJson += "\"balance\":" + DoubleToString(g_accountBalance, 2) + ",";
@@ -418,11 +604,41 @@ void SendMinuteStatistics()
void SendToPythonServer(string jsonData)
{
string headers = "Content-Type: application/json\r\n";
char responseData[];
uchar responseData[];
string outheaders = "";
int responseCode = 0;
responseCode = WebRequest("POST", g_pythonServer + "/send_statistics", headers, jsonData, responseData);
// 使用CharArrayToString确保正确转换,然后再转回uchar数组
string jsonStr = jsonData;
uchar postData[];
StringToCharArray(jsonStr, postData);
// 移除StringToCharArray添加的null终止符
int nullIndex = ArraySize(postData) - 1;
if(nullIndex >= 0 && postData[nullIndex] == 0)
{
ArrayResize(postData, nullIndex);
}
int dataSize = ArraySize(postData);
// 调试:打印发送的数据
Print("Sending JSON data size: ", dataSize, " bytes");
Print("JSON: ", jsonStr);
// 修正: POST请求需要9个参数 (method, url, headers, cookie, timeout, data, dataSize, result, resultHeaders)
responseCode = WebRequest(
"POST",
g_pythonServer + "/send_statistics",
headers,
"", // cookie
5000, // timeout (5秒)
postData,
dataSize,
responseData,
outheaders
);
if(responseCode == 200)
{
Print("Statistics sent successfully");
@@ -430,6 +646,22 @@ void SendToPythonServer(string jsonData)
else if(responseCode != -1)
{
Print("Failed to send statistics. Response code: ", responseCode);
// 打印详细错误信息
if(responseCode == -1)
{
Print("WebRequest is disabled! Please enable WebRequest in MT5 Options -> Expert Advisors");
}
else
{
// 打印响应内容以便调试
string responseText = "";
for(int i = 0; i < ArraySize(responseData); i++)
{
responseText += CharToString(responseData[i]);
}
Print("Response: ", responseText);
}
}
}
//+------------------------------------------------------------------+
@@ -439,19 +671,22 @@ void OnTick()
{
//--- 更新统计数据
UpdateStatistics();
//--- 检查是否需要推送增量K线数据
CheckAndPushIncrementalKlines();
//--- 检查是否需要进行分钟级统计和发送
datetime now = TimeCurrent();
if(now - g_lastStatisticTime >= 60) // 每分钟执行一次
if(now - g_lastStatisticTime >= 6) // 每6秒执行一次
{
SendMinuteStatistics();
g_lastStatisticTime = now;
g_tickCountPerMinute = 0;
g_tickCount = 0;
}
//--- 检查持仓风险并平仓
CheckAndCloseRiskyPositions();
//--- 每100毫秒请求一次Python服务
uint currentTime = GetTickCount();
if((currentTime - g_lastPythonRequestTime) >= g_pythonRequestInterval)
@@ -461,3 +696,221 @@ void OnTick()
}
}
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| K线数据相关函数 |
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| 推送所有周期的K线数据 |
//+------------------------------------------------------------------+
bool PushAllKlineData(bool isFull)
{
bool success = true;
// 推送各周期K线数据
// H4: 6个月约1100根
if(!PushKlineData(PERIOD_H4, isFull ? 1100 : 1))
success = false;
// H1: 1个月约720根
if(!PushKlineData(PERIOD_H1, isFull ? 720 : 1))
success = false;
// M15: 3天约288根
if(!PushKlineData(PERIOD_M15, isFull ? 288 : 1))
success = false;
// M5: 24小时约288根
if(!PushKlineData(PERIOD_M5, isFull ? 288 : 1))
success = false;
// M1: 1小时约60根
if(!PushKlineData(PERIOD_M1, isFull ? 60 : 1))
success = false;
if(success && isFull)
{
g_klineInitialized = true;
Print("Historical K-line data pushed successfully");
}
return success;
}
//+------------------------------------------------------------------+
//| 推送单个周期的K线数据 |
//+------------------------------------------------------------------+
bool PushKlineData(ENUM_TIMEFRAMES period, int count)
{
MqlRates rates[];
ArraySetAsSeries(rates, true);
// 获取K线数据
int copied = CopyRates(_Symbol, period, 0, count, rates);
if(copied <= 0)
{
Print("Failed to get K-line data for period: ", PeriodToString(period));
return false;
}
// 构建JSON
string klineJson = BuildKlineJson(period, rates, copied);
// 发送到Python服务
string periodStr = PeriodToString(period);
string url = g_pythonServer + "/ea/kline/" + periodStr;
return SendKlineToServer(url, klineJson);
}
//+------------------------------------------------------------------+
//| 构建K线JSON数据 |
//+------------------------------------------------------------------+
string BuildKlineJson(ENUM_TIMEFRAMES period, MqlRates &rates[], int count)
{
string json = "{\"symbol\":\"" + _Symbol + "\",";
json += "\"is_full\":" + (g_klineInitialized ? "false" : "true") + ",";
json += "\"klines\":[";
for(int i = count - 1; i >= 0; i--) // 从旧到新排序
{
if(i < count - 1) json += ",";
json += "{";
json += "\"timestamp\":\"" + TimeToString(rates[i].time, TIME_DATE | TIME_MINUTES) + "\",";
json += "\"open\":" + DoubleToString(rates[i].open, _Digits) + ",";
json += "\"high\":" + DoubleToString(rates[i].high, _Digits) + ",";
json += "\"low\":" + DoubleToString(rates[i].low, _Digits) + ",";
json += "\"close\":" + DoubleToString(rates[i].close, _Digits) + ",";
json += "\"volume\":" + DoubleToString(rates[i].tick_volume, 0);
json += "}";
}
json += "]}";
return json;
}
//+------------------------------------------------------------------+
//| 发送K线数据到服务器 |
//+------------------------------------------------------------------+
bool SendKlineToServer(string url, string jsonData)
{
string headers = "Content-Type: application/json\r\n";
uchar responseData[];
uchar postData[];
string outheaders = "";
int responseCode = 0;
StringToCharArray(jsonData, postData);
int nullIndex = ArraySize(postData) - 1;
if(nullIndex >= 0 && postData[nullIndex] == 0)
{
ArrayResize(postData, nullIndex);
}
int dataSize = ArraySize(postData);
responseCode = WebRequest(
"POST",
url,
headers,
"",
10000, // 10秒超时
postData,
dataSize,
responseData,
outheaders
);
if(responseCode == 200)
{
return true;
}
else if(responseCode == 400)
{
// 检查是否是8888错误码(需要全量数据)
string responseText = "";
for(int i = 0; i < ArraySize(responseData); i++)
{
responseText += CharToString(responseData[i]);
}
if(StringFind(responseText, "8888") >= 0)
{
Print("Server needs full K-line data, resending...");
g_klineInitialized = false;
PushAllKlineData(true);
}
return false;
}
else
{
Print("Failed to push K-line data. Response code: ", responseCode);
return false;
}
}
//+------------------------------------------------------------------+
//| 检查并推送增量K线数据 |
//+------------------------------------------------------------------+
void CheckAndPushIncrementalKlines()
{
datetime now = TimeCurrent();
datetime barTime;
// 检查H4 K线是否有新周期
barTime = iTime(_Symbol, PERIOD_H4, 0);
if(barTime != 0 && barTime != g_lastH4CloseTime)
{
g_lastH4CloseTime = barTime;
if(g_klineInitialized) PushKlineData(PERIOD_H4, 1);
}
// 检查H1 K线
barTime = iTime(_Symbol, PERIOD_H1, 0);
if(barTime != 0 && barTime != g_lastH1CloseTime)
{
g_lastH1CloseTime = barTime;
if(g_klineInitialized) PushKlineData(PERIOD_H1, 1);
}
// 检查M15 K线
barTime = iTime(_Symbol, PERIOD_M15, 0);
if(barTime != 0 && barTime != g_lastM15CloseTime)
{
g_lastM15CloseTime = barTime;
if(g_klineInitialized) PushKlineData(PERIOD_M15, 1);
}
// 检查M5 K线
barTime = iTime(_Symbol, PERIOD_M5, 0);
if(barTime != 0 && barTime != g_lastM5CloseTime)
{
g_lastM5CloseTime = barTime;
if(g_klineInitialized) PushKlineData(PERIOD_M5, 1);
}
// 检查M1 K线
barTime = iTime(_Symbol, PERIOD_M1, 0);
if(barTime != 0 && barTime != g_lastM1CloseTime)
{
g_lastM1CloseTime = barTime;
if(g_klineInitialized) PushKlineData(PERIOD_M1, 1);
}
}
//+------------------------------------------------------------------+
//| 周期转换为字符串 |
//+------------------------------------------------------------------+
string PeriodToString(ENUM_TIMEFRAMES period)
{
switch(period)
{
case PERIOD_H4: return "H4";
case PERIOD_H1: return "H1";
case PERIOD_M15: return "M15";
case PERIOD_M5: return "M5";
case PERIOD_M1: return "M1";
default: return "M5";
}
}