diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..6072279
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,20 @@
+# macOS
+.DS_Store
+
+# Python
+venv/
+__pycache__/
+*.pyc
+*.pyo
+
+# Logs
+*.log
+
+# Backups
+*.backup
+
+# IDE
+.claude/
+
+# Node.js
+node_modules/
\ No newline at end of file
diff --git a/FRONTEND_INTEGRATION.md b/FRONTEND_INTEGRATION.md
new file mode 100644
index 0000000..bf8c8fc
--- /dev/null
+++ b/FRONTEND_INTEGRATION.md
@@ -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
\ No newline at end of file
diff --git a/MT5_WEBREQUEST_CHECKLIST.md b/MT5_WEBREQUEST_CHECKLIST.md
new file mode 100644
index 0000000..c90a5f9
--- /dev/null
+++ b/MT5_WEBREQUEST_CHECKLIST.md
@@ -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
\ No newline at end of file
diff --git a/QUICKSTART.md b/QUICKSTART.md
index f0dce33..22240f4 100644
--- a/QUICKSTART.md
+++ b/QUICKSTART.md
@@ -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 集成
+
+**开始您的量化交易之旅吧!** 🚀
diff --git a/README.md b/README.md
index 5babb86..92f8573 100644
--- a/README.md
+++ b/README.md
@@ -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推送实时提醒到前端页面。
## 安装和运行
diff --git a/api_market_examples.py b/api_market_examples.py
new file mode 100644
index 0000000..3643131
--- /dev/null
+++ b/api_market_examples.py
@@ -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()
\ No newline at end of file
diff --git a/frontend/README.md b/frontend/README.md
new file mode 100644
index 0000000..1b82882
--- /dev/null
+++ b/frontend/README.md
@@ -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
+
+
+ 我的组件
+
+
+
+
+
+
+
+```
+
+## 🔄 与 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 开发的乐趣!**
\ No newline at end of file
diff --git a/frontend/index.html b/frontend/index.html
new file mode 100644
index 0000000..fca5f89
--- /dev/null
+++ b/frontend/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+ 量化交易系统
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
new file mode 100644
index 0000000..b857764
--- /dev/null
+++ b/frontend/package-lock.json
@@ -0,0 +1,1628 @@
+{
+ "name": "lianghua-trading-frontend-vue",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "lianghua-trading-frontend-vue",
+ "version": "1.0.0",
+ "dependencies": {
+ "@mdi/font": "^7.4.0",
+ "axios": "^1.6.0",
+ "echarts": "^5.4.0",
+ "vue": "^3.4.0",
+ "vue-echarts": "^6.6.0",
+ "vue-router": "^4.2.0",
+ "vuetify": "^3.5.0"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-vue": "^5.0.0",
+ "vite": "^5.0.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz",
+ "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.0"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "license": "MIT"
+ },
+ "node_modules/@mdi/font": {
+ "version": "7.4.47",
+ "resolved": "https://registry.npmjs.org/@mdi/font/-/font-7.4.47.tgz",
+ "integrity": "sha512-43MtGpd585SNzHZPcYowu/84Vz2a2g31TvPMTm9uTiCSWzaheQySUcSyUH/46fPnuPQWof2yd0pGBtzee/IQWw==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
+ "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz",
+ "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz",
+ "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz",
+ "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz",
+ "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz",
+ "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz",
+ "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz",
+ "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz",
+ "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz",
+ "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz",
+ "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz",
+ "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz",
+ "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz",
+ "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz",
+ "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz",
+ "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz",
+ "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz",
+ "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz",
+ "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz",
+ "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz",
+ "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz",
+ "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz",
+ "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz",
+ "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz",
+ "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@vitejs/plugin-vue": {
+ "version": "5.2.4",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz",
+ "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^5.0.0 || ^6.0.0",
+ "vue": "^3.2.25"
+ }
+ },
+ "node_modules/@vue/compiler-core": {
+ "version": "3.5.29",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.29.tgz",
+ "integrity": "sha512-cuzPhD8fwRHk8IGfmYaR4eEe4cAyJEL66Ove/WZL7yWNL134nqLddSLwNRIsFlnnW1kK+p8Ck3viFnC0chXCXw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.0",
+ "@vue/shared": "3.5.29",
+ "entities": "^7.0.1",
+ "estree-walker": "^2.0.2",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/@vue/compiler-dom": {
+ "version": "3.5.29",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.29.tgz",
+ "integrity": "sha512-n0G5o7R3uBVmVxjTIYcz7ovr8sy7QObFG8OQJ3xGCDNhbG60biP/P5KnyY8NLd81OuT1WJflG7N4KWYHaeeaIg==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-core": "3.5.29",
+ "@vue/shared": "3.5.29"
+ }
+ },
+ "node_modules/@vue/compiler-sfc": {
+ "version": "3.5.29",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.29.tgz",
+ "integrity": "sha512-oJZhN5XJs35Gzr50E82jg2cYdZQ78wEwvRO6Y63TvLVTc+6xICzJHP1UIecdSPPYIbkautNBanDiWYa64QSFIA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.0",
+ "@vue/compiler-core": "3.5.29",
+ "@vue/compiler-dom": "3.5.29",
+ "@vue/compiler-ssr": "3.5.29",
+ "@vue/shared": "3.5.29",
+ "estree-walker": "^2.0.2",
+ "magic-string": "^0.30.21",
+ "postcss": "^8.5.6",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/@vue/compiler-ssr": {
+ "version": "3.5.29",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.29.tgz",
+ "integrity": "sha512-Y/ARJZE6fpjzL5GH/phJmsFwx3g6t2KmHKHx5q+MLl2kencADKIrhH5MLF6HHpRMmlRAYBRSvv347Mepf1zVNw==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.29",
+ "@vue/shared": "3.5.29"
+ }
+ },
+ "node_modules/@vue/devtools-api": {
+ "version": "6.6.4",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
+ "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
+ "license": "MIT"
+ },
+ "node_modules/@vue/reactivity": {
+ "version": "3.5.29",
+ "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.29.tgz",
+ "integrity": "sha512-zcrANcrRdcLtmGZETBxWqIkoQei8HaFpZWx/GHKxx79JZsiZ8j1du0VUJtu4eJjgFvU/iKL5lRXFXksVmI+5DA==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/shared": "3.5.29"
+ }
+ },
+ "node_modules/@vue/runtime-core": {
+ "version": "3.5.29",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.29.tgz",
+ "integrity": "sha512-8DpW2QfdwIWOLqtsNcds4s+QgwSaHSJY/SUe04LptianUQ/0xi6KVsu/pYVh+HO3NTVvVJjIPL2t6GdeKbS4Lg==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/reactivity": "3.5.29",
+ "@vue/shared": "3.5.29"
+ }
+ },
+ "node_modules/@vue/runtime-dom": {
+ "version": "3.5.29",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.29.tgz",
+ "integrity": "sha512-AHvvJEtcY9tw/uk+s/YRLSlxxQnqnAkjqvK25ZiM4CllCZWzElRAoQnCM42m9AHRLNJ6oe2kC5DCgD4AUdlvXg==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/reactivity": "3.5.29",
+ "@vue/runtime-core": "3.5.29",
+ "@vue/shared": "3.5.29",
+ "csstype": "^3.2.3"
+ }
+ },
+ "node_modules/@vue/server-renderer": {
+ "version": "3.5.29",
+ "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.29.tgz",
+ "integrity": "sha512-G/1k6WK5MusLlbxSE2YTcqAAezS+VuwHhOvLx2KnQU7G2zCH6KIb+5Wyt6UjMq7a3qPzNEjJXs1hvAxDclQH+g==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-ssr": "3.5.29",
+ "@vue/shared": "3.5.29"
+ },
+ "peerDependencies": {
+ "vue": "3.5.29"
+ }
+ },
+ "node_modules/@vue/shared": {
+ "version": "3.5.29",
+ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.29.tgz",
+ "integrity": "sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg==",
+ "license": "MIT"
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
+ "node_modules/axios": {
+ "version": "1.13.6",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz",
+ "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.11",
+ "form-data": "^4.0.5",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "license": "MIT"
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/echarts": {
+ "version": "5.6.0",
+ "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz",
+ "integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "2.3.0",
+ "zrender": "5.6.1"
+ }
+ },
+ "node_modules/entities": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
+ "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.21.5",
+ "@esbuild/android-arm": "0.21.5",
+ "@esbuild/android-arm64": "0.21.5",
+ "@esbuild/android-x64": "0.21.5",
+ "@esbuild/darwin-arm64": "0.21.5",
+ "@esbuild/darwin-x64": "0.21.5",
+ "@esbuild/freebsd-arm64": "0.21.5",
+ "@esbuild/freebsd-x64": "0.21.5",
+ "@esbuild/linux-arm": "0.21.5",
+ "@esbuild/linux-arm64": "0.21.5",
+ "@esbuild/linux-ia32": "0.21.5",
+ "@esbuild/linux-loong64": "0.21.5",
+ "@esbuild/linux-mips64el": "0.21.5",
+ "@esbuild/linux-ppc64": "0.21.5",
+ "@esbuild/linux-riscv64": "0.21.5",
+ "@esbuild/linux-s390x": "0.21.5",
+ "@esbuild/linux-x64": "0.21.5",
+ "@esbuild/netbsd-x64": "0.21.5",
+ "@esbuild/openbsd-x64": "0.21.5",
+ "@esbuild/sunos-x64": "0.21.5",
+ "@esbuild/win32-arm64": "0.21.5",
+ "@esbuild/win32-ia32": "0.21.5",
+ "@esbuild/win32-x64": "0.21.5"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+ "license": "MIT"
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.15.11",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
+ "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/postcss": {
+ "version": "8.5.8",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
+ "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "license": "MIT"
+ },
+ "node_modules/resize-detector": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/resize-detector/-/resize-detector-0.3.0.tgz",
+ "integrity": "sha512-R/tCuvuOHQ8o2boRP6vgx8hXCCy87H1eY9V5imBYeVNyNVpuL9ciReSccLj2gDcax9+2weXy3bc8Vv+NRXeEvQ==",
+ "license": "MIT"
+ },
+ "node_modules/rollup": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
+ "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.59.0",
+ "@rollup/rollup-android-arm64": "4.59.0",
+ "@rollup/rollup-darwin-arm64": "4.59.0",
+ "@rollup/rollup-darwin-x64": "4.59.0",
+ "@rollup/rollup-freebsd-arm64": "4.59.0",
+ "@rollup/rollup-freebsd-x64": "4.59.0",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.59.0",
+ "@rollup/rollup-linux-arm-musleabihf": "4.59.0",
+ "@rollup/rollup-linux-arm64-gnu": "4.59.0",
+ "@rollup/rollup-linux-arm64-musl": "4.59.0",
+ "@rollup/rollup-linux-loong64-gnu": "4.59.0",
+ "@rollup/rollup-linux-loong64-musl": "4.59.0",
+ "@rollup/rollup-linux-ppc64-gnu": "4.59.0",
+ "@rollup/rollup-linux-ppc64-musl": "4.59.0",
+ "@rollup/rollup-linux-riscv64-gnu": "4.59.0",
+ "@rollup/rollup-linux-riscv64-musl": "4.59.0",
+ "@rollup/rollup-linux-s390x-gnu": "4.59.0",
+ "@rollup/rollup-linux-x64-gnu": "4.59.0",
+ "@rollup/rollup-linux-x64-musl": "4.59.0",
+ "@rollup/rollup-openbsd-x64": "4.59.0",
+ "@rollup/rollup-openharmony-arm64": "4.59.0",
+ "@rollup/rollup-win32-arm64-msvc": "4.59.0",
+ "@rollup/rollup-win32-ia32-msvc": "4.59.0",
+ "@rollup/rollup-win32-x64-gnu": "4.59.0",
+ "@rollup/rollup-win32-x64-msvc": "4.59.0",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
+ "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
+ "license": "0BSD"
+ },
+ "node_modules/vite": {
+ "version": "5.4.21",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
+ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.21.3",
+ "postcss": "^8.4.43",
+ "rollup": "^4.20.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vue": {
+ "version": "3.5.29",
+ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.29.tgz",
+ "integrity": "sha512-BZqN4Ze6mDQVNAni0IHeMJ5mwr8VAJ3MQC9FmprRhcBYENw+wOAAjRj8jfmN6FLl0j96OXbR+CjWhmAmM+QGnA==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.29",
+ "@vue/compiler-sfc": "3.5.29",
+ "@vue/runtime-dom": "3.5.29",
+ "@vue/server-renderer": "3.5.29",
+ "@vue/shared": "3.5.29"
+ },
+ "peerDependencies": {
+ "typescript": "*"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vue-echarts": {
+ "version": "6.7.3",
+ "resolved": "https://registry.npmjs.org/vue-echarts/-/vue-echarts-6.7.3.tgz",
+ "integrity": "sha512-vXLKpALFjbPphW9IfQPOVfb1KjGZ/f8qa/FZHi9lZIWzAnQC1DgnmEK3pJgEkyo6EP7UnX6Bv/V3Ke7p+qCNXA==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "resize-detector": "^0.3.0",
+ "vue-demi": "^0.13.11"
+ },
+ "peerDependencies": {
+ "@vue/composition-api": "^1.0.5",
+ "@vue/runtime-core": "^3.0.0",
+ "echarts": "^5.4.1",
+ "vue": "^2.6.12 || ^3.1.1"
+ },
+ "peerDependenciesMeta": {
+ "@vue/composition-api": {
+ "optional": true
+ },
+ "@vue/runtime-core": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vue-echarts/node_modules/vue-demi": {
+ "version": "0.13.11",
+ "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.11.tgz",
+ "integrity": "sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "vue-demi-fix": "bin/vue-demi-fix.js",
+ "vue-demi-switch": "bin/vue-demi-switch.js"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "@vue/composition-api": "^1.0.0-rc.1",
+ "vue": "^3.0.0-0 || ^2.6.0"
+ },
+ "peerDependenciesMeta": {
+ "@vue/composition-api": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vue-router": {
+ "version": "4.6.4",
+ "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz",
+ "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/devtools-api": "^6.6.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/posva"
+ },
+ "peerDependencies": {
+ "vue": "^3.5.0"
+ }
+ },
+ "node_modules/vuetify": {
+ "version": "3.12.2",
+ "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-3.12.2.tgz",
+ "integrity": "sha512-cVQa4+5iQpDs00ToMUnWRHlMdv1d5tEH2wcZIthqSCmBipQAG4rQKE55zFwZFYlPyiDhUVY1RcAFtXCuHNcCww==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/johnleider"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.7",
+ "vite-plugin-vuetify": ">=2.1.0",
+ "vue": "^3.5.0",
+ "webpack-plugin-vuetify": ">=3.1.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ },
+ "vite-plugin-vuetify": {
+ "optional": true
+ },
+ "webpack-plugin-vuetify": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/zrender": {
+ "version": "5.6.1",
+ "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz",
+ "integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "tslib": "2.3.0"
+ }
+ }
+ }
+}
diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 0000000..9f1f952
--- /dev/null
+++ b/frontend/package.json
@@ -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"
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/App.vue b/frontend/src/App.vue
new file mode 100644
index 0000000..ea5f7e8
--- /dev/null
+++ b/frontend/src/App.vue
@@ -0,0 +1,63 @@
+
+
+
+
+ 量化交易系统
+
+
+ mdi-refresh
+
+
+
+
+
+
+
+ {{ item.icon }}
+
+
+ {{ item.title }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/api/market.js b/frontend/src/api/market.js
new file mode 100644
index 0000000..862e993
--- /dev/null
+++ b/frontend/src/api/market.js
@@ -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
\ No newline at end of file
diff --git a/frontend/src/api/trading.js b/frontend/src/api/trading.js
new file mode 100644
index 0000000..93cf8a1
--- /dev/null
+++ b/frontend/src/api/trading.js
@@ -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
\ No newline at end of file
diff --git a/frontend/src/main.js b/frontend/src/main.js
new file mode 100644
index 0000000..cdb54dd
--- /dev/null
+++ b/frontend/src/main.js
@@ -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')
\ No newline at end of file
diff --git a/frontend/src/plugins/vuetify.js b/frontend/src/plugins/vuetify.js
new file mode 100644
index 0000000..fda6791
--- /dev/null
+++ b/frontend/src/plugins/vuetify.js
@@ -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',
+ },
+ },
+ },
+ },
+})
\ No newline at end of file
diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js
new file mode 100644
index 0000000..43b1f24
--- /dev/null
+++ b/frontend/src/router/index.js
@@ -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
\ No newline at end of file
diff --git a/frontend/src/style.css b/frontend/src/style.css
new file mode 100644
index 0000000..25299a1
--- /dev/null
+++ b/frontend/src/style.css
@@ -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;
+}
\ No newline at end of file
diff --git a/frontend/src/views/Dashboard.vue b/frontend/src/views/Dashboard.vue
new file mode 100644
index 0000000..dc452a6
--- /dev/null
+++ b/frontend/src/views/Dashboard.vue
@@ -0,0 +1,136 @@
+
+
+
+
+ 仪表板
+
+
+
+
+
+
+
+
+ mdi-heart
+ 服务状态
+
+
+
+ {{ status.ok ? '正常' : '异常' }}
+
+
+
+
+
+
+
+
+ mdi-format-list-bulleted
+ 待执行指令
+
+
+ {{ pendingTradesCount }}
+
+
+
+
+
+
+
+ mdi-chart-line
+ 统计记录
+
+
+ {{ statisticsCount }}
+
+
+
+
+
+
+
+ mdi-currency-usd
+ 活跃品种
+
+
+ {{ activeSymbolsCount }}
+
+
+
+
+
+
+
+
+
+ {{ error }}
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/views/Market.vue b/frontend/src/views/Market.vue
new file mode 100644
index 0000000..2679427
--- /dev/null
+++ b/frontend/src/views/Market.vue
@@ -0,0 +1,1141 @@
+
+
+
+
+ 行情分析
+
+
+
+
+
+
+
+
+ {{ alert.symbol }} {{ alert.period }}
+
+ {{ alert.is_breakthrough ? '已突破' : '接近' }}{{ alert.direction === 'high' ? '高点' : '低点' }}
+
+ {{ alert.pivot_price }}
+ 当前价格: {{ alert.current_price }}
+ 距离: {{ alert.distance_pct }}%
+
+
+
+
+
+ mdi-file-document-edit
+ 自动生成交易指令
+
+
+
+ {{ alert.pending_order.action === 'b' ? '买入' : '卖出' }}
+
+ 价格: {{ alert.pending_order.price?.toFixed(2) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ mdi-check
+ 确认
+
+
+ 放弃
+
+
+
+
+ {{ alert.pending_order.reason }}
+
+
+ mdi-clock-outline
+ 3分钟内未操作将自动移除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ mdi-briefcase
+ 当前持仓
+
+
+
+
+
+
+
+ | 品种 |
+ 订单号 |
+ 方向 |
+ 手数 |
+ 开仓价 |
+ 当前价 |
+ 盈亏 |
+ 止损距离 |
+ 止盈距离 |
+ 操作 |
+
+
+
+
+ | {{ pos.symbol }} |
+ {{ pos.ticket }} |
+
+
+ {{ pos.type === 'BUY' ? '买入' : '卖出' }}
+
+ |
+ {{ pos.volume }} |
+ {{ pos.priceOpen?.toFixed(2) }} |
+ {{ pos.currentPrice?.toFixed(2) }} |
+
+ {{ pos.profit >= 0 ? '+' : '' }}{{ pos.profit?.toFixed(2) }}
+ |
+ {{ pos.distanceSL?.toFixed(2) || '-' }} |
+ {{ pos.distanceTP?.toFixed(2) || '-' }} |
+
+
+ 平仓
+
+ |
+
+
+
+
+
+
+ 当前无持仓
+
+
+
+
+
+
+
+
+
+
+ mdi-lan-connect
+ {{ wsConnected ? 'WebSocket 已连接' : 'WebSocket 断开' }}
+
+
+
+
+
+
+
+
+
+ mdi-trending-up
+ 趋势分析
+
+
+
+
+
+
+
+ 多周期共振
+
+ {{ trendData.resonance.signal }}
+
+
+ 趋势强度: {{ trendData.resonance.strength }}%
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ period }}
+
+ {{ getTrendLabel(state.trend) }}
+
+
+
+ 强度: {{ state.strength }}%
+ ADX: {{ state.adx }}
+
+
+ MA10: {{ state.ma_fast?.toFixed(2) }}
+ MA20: {{ state.ma_slow?.toFixed(2) }}
+
+
+ mdi-information-outline
+ {{ state.reason || '分析中...' }}
+
+
+
+
+
+
+
+
+ 暂无趋势分析数据
+
+
+
+
+
+
+
+
+
+
+
+ mdi-cog
+ 自动交易配置
+
+
+
+
+
+
+
+
+
+ 品种配置
+
+
+
+
+ | 品种 |
+ 手数 |
+ 止损偏移(点) |
+ 操作 |
+
+
+
+
+ |
+ {{ symbol }}
+ |
+
+
+ |
+
+
+ |
+
+ 保存
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 添加
+
+
+
+
+ mdi-information
+ M1周期接近转折点时自动生成交易指令。止损偏移为固定点数,如GOLD设0.5表示止损在转折点±0.5点。
+
+
+
+
+
+
+
+
+
+
+
+ mdi-chart-candlestick
+ K线图表 - {{ selectedSymbol }} {{ selectedPeriod }}
+
+
+
+
mdi-chart-box-outline
+
暂无K线数据,请等待EA推送数据
+
+
+
+
+
+
+
+
+
+
+
+
+ mdi-information
+ 数据状态
+
+
+
+
+
+
+ | 品种 |
+ 周期 |
+ K线数量 |
+ 已初始化 |
+ 转折点数量 |
+
+
+
+
+
+ | {{ symbol }} |
+ {{ period }} |
+ {{ periodData.count }} |
+
+
+ {{ periodData.initialized ? '是' : '否' }}
+
+ |
+
+ {{ (marketStatus.pivots && marketStatus.pivots[symbol] && marketStatus.pivots[symbol][period]) ? marketStatus.pivots[symbol][period].pivot_count : 0 }}
+ |
+
+
+
+
+
+
+ 暂无状态数据
+
+
+
+
+
+
+
+
+ {{ errorMessage }}
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/views/Statistics.vue b/frontend/src/views/Statistics.vue
new file mode 100644
index 0000000..27477e6
--- /dev/null
+++ b/frontend/src/views/Statistics.vue
@@ -0,0 +1,248 @@
+
+
+
+
+ 统计数据分析
+
+
+
+
+
+
+
+
+ mdi-counter
+ 总记录数
+
+
+ {{ totalRecords }}
+
+
+
+
+
+
+
+ mdi-trending-up
+ 平均价格
+
+
+ {{ averagePrice.toFixed(5) }}
+
+
+
+
+
+
+
+ mdi-chart-line
+ 最高价格
+
+
+ {{ maxPrice.toFixed(5) }}
+
+
+
+
+
+
+
+ mdi-chart-line-variant
+ 最低价格
+
+
+ {{ minPrice.toFixed(5) }}
+
+
+
+
+
+
+
+
+
+ 价格趋势图
+
+
+
+
+
+
+
+
+
+
+
+ 详细数据
+
+
+
+ {{ item.bidPrice.toFixed(2) }}
+
+
+
+ {{ item.askPrice.toFixed(2) }}
+
+
+
+ {{ item.balance.toFixed(2) }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ error }}
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/views/Status.vue b/frontend/src/views/Status.vue
new file mode 100644
index 0000000..ac12a9f
--- /dev/null
+++ b/frontend/src/views/Status.vue
@@ -0,0 +1,250 @@
+
+
+
+
+ 服务状态监控
+
+
+
+
+
+
+
+
+
+ mdi-heart
+
+ 服务健康状态
+
+
+
+ {{ healthStatus.ok ? '服务正常' : '服务异常' }}
+
+
+ 最后检查: {{ lastCheckTime }}
+
+
+
+
+
+
+
+
+ mdi-information
+ 系统信息
+
+
+
+
版本: {{ systemInfo.version || '未知' }}
+
运行时间: {{ formatUptime(systemInfo.uptime) }}
+
内存使用: {{ formatMemory(systemInfo.memory) }}
+
+
+
+
+
+
+
+
+
+
+ 详细服务指标
+
+
+
+
+
+ {{ serviceMetrics.pendingTrades }}
+ 待执行指令
+
+
+
+
+
+
+
+ {{ serviceMetrics.totalTrades }}
+ 总交易次数
+
+
+
+
+
+
+
+ {{ serviceMetrics.activeSymbols }}
+ 活跃品种
+
+
+
+
+
+
+
+ {{ serviceMetrics.successRate }}%
+ 成功率
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 连接状态
+
+
+
+
+
+
+ mdi-server
+
+
+
后端服务
+
+ {{ connectionStatus.backend ? '已连接' : '未连接' }}
+
+
+
+
+
+
+
+
+
+
+ mdi-chart-line
+
+
+
MT5 连接
+
+ {{ connectionStatus.mt5 ? '已连接' : '未连接' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ error }}
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/views/TradeOrders.vue b/frontend/src/views/TradeOrders.vue
new file mode 100644
index 0000000..5922f30
--- /dev/null
+++ b/frontend/src/views/TradeOrders.vue
@@ -0,0 +1,308 @@
+
+
+
+
+ 交易指令管理
+
+
+
+
+
+
+
+ 发送交易指令
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 发送交易指令
+
+
+
+
+
+
+
+
+
+
+ 待执行指令
+
+ 清空全部
+
+
+
+
+
+
+ {{ item.direction === 'BUY' ? '买入' : '卖出' }}
+
+
+
+
+ {{ formatTime(item.timestamp) }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ error }}
+
+
+
+
+
+
+
+
+ {{ success }}
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/start_vue.sh b/frontend/start_vue.sh
new file mode 100755
index 0000000..0839df7
--- /dev/null
+++ b/frontend/start_vue.sh
@@ -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
\ No newline at end of file
diff --git a/frontend/vite.config.js b/frontend/vite.config.js
new file mode 100644
index 0000000..b1c0e95
--- /dev/null
+++ b/frontend/vite.config.js
@@ -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/, '')
+ }
+ }
+ }
+})
\ No newline at end of file
diff --git a/main.py b/main.py
index d5d7185..276b58b 100644
--- a/main.py
+++ b/main.py
@@ -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
diff --git a/market/__init__.py b/market/__init__.py
new file mode 100644
index 0000000..925dba2
--- /dev/null
+++ b/market/__init__.py
@@ -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']
\ No newline at end of file
diff --git a/market/merger.py b/market/merger.py
new file mode 100644
index 0000000..54c5dd0
--- /dev/null
+++ b/market/merger.py
@@ -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
\ No newline at end of file
diff --git a/market/monitor.py b/market/monitor.py
new file mode 100644
index 0000000..a09c1ec
--- /dev/null
+++ b/market/monitor.py
@@ -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
+ }
\ No newline at end of file
diff --git a/market/pending_orders.py b/market/pending_orders.py
new file mode 100644
index 0000000..f7dc00b
--- /dev/null
+++ b/market/pending_orders.py
@@ -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
\ No newline at end of file
diff --git a/market/pivot_detector.py b/market/pivot_detector.py
new file mode 100644
index 0000000..ed0fb09
--- /dev/null
+++ b/market/pivot_detector.py
@@ -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
\ No newline at end of file
diff --git a/market/store.py b/market/store.py
new file mode 100644
index 0000000..f11ebeb
--- /dev/null
+++ b/market/store.py
@@ -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)
\ No newline at end of file
diff --git a/market/trend_analyzer.py b/market/trend_analyzer.py
new file mode 100644
index 0000000..8535c70
--- /dev/null
+++ b/market/trend_analyzer.py
@@ -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()
+ }
\ No newline at end of file
diff --git a/monitor_logs.sh b/monitor_logs.sh
new file mode 100755
index 0000000..635b760
--- /dev/null
+++ b/monitor_logs.sh
@@ -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
\ No newline at end of file
diff --git a/routes_ea.py b/routes_ea.py
index 8af60b9..0121372 100644
--- a/routes_ea.py
+++ b/routes_ea.py
@@ -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
\ No newline at end of file
diff --git a/routes_market.py b/routes_market.py
new file mode 100644
index 0000000..1668997
--- /dev/null
+++ b/routes_market.py
@@ -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
\ No newline at end of file
diff --git a/server.py b/server.py
index c2b53a6..7271ed3 100644
--- a/server.py
+++ b/server.py
@@ -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
diff --git a/start.sh b/start.sh
old mode 100644
new mode 100755
diff --git a/start_frontend.bat b/start_frontend.bat
new file mode 100644
index 0000000..11f55d4
--- /dev/null
+++ b/start_frontend.bat
@@ -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
\ No newline at end of file
diff --git a/start_frontend.sh b/start_frontend.sh
new file mode 100755
index 0000000..9af7662
--- /dev/null
+++ b/start_frontend.sh
@@ -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
\ No newline at end of file
diff --git a/wangxxGold.mq5 b/wangxxGold.mq5
index 694f9d6..d17edcb 100644
--- a/wangxxGold.mq5
+++ b/wangxxGold.mq5
@@ -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
-#include
-#include
-#include
+#include
+#include
+#include
+#include
//+------------------------------------------------------------------+
//| 全局变量定义 |
//+------------------------------------------------------------------+
// 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";
+ }
+ }