feat: 添加前端界面和市场分析模块
- 新增 Vue 3 + Vuetify 前端界面 - 新增市场分析模块 (market/) - 更新主服务器和路由 - 更新 MT5 EA 文件 - 添加 .gitignore 排除临时文件
This commit is contained in:
+20
@@ -0,0 +1,20 @@
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
# Python
|
||||
venv/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# Backups
|
||||
*.backup
|
||||
|
||||
# IDE
|
||||
.claude/
|
||||
|
||||
# Node.js
|
||||
node_modules/
|
||||
@@ -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
|
||||
@@ -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
@@ -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 集成
|
||||
|
||||
**开始您的量化交易之旅吧!** 🚀
|
||||
|
||||
@@ -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推送实时提醒到前端页面。
|
||||
|
||||
## 安装和运行
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -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 开发的乐趣!**
|
||||
@@ -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>
|
||||
Generated
+1628
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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')
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Executable
+33
@@ -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
|
||||
@@ -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/, '')
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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']
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
@@ -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)
|
||||
@@ -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()
|
||||
}
|
||||
Executable
+30
@@ -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
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
Executable
+67
@@ -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
@@ -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_tickets,pivot_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";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user