From 702586dcfd48f489c5f1bd7f54abeb374af71630 Mon Sep 17 00:00:00 2001 From: Shuo Cao <42436068+caoshuo594@users.noreply.github.com> Date: Mon, 27 Oct 2025 19:45:33 +0800 Subject: [PATCH] Add files via upload --- CHANGELOG.md | 153 +++++++++++++ LSTM_EA.ex5 | Bin 0 -> 34466 bytes LSTM_EA.mq5 | 119 ++++++++++ README.md | 477 ++++++++++++++++++++++++++++++++++++++++ lstm_model.onnx | Bin 0 -> 10512 bytes lstm_training.ipynb | 398 +++++++++++++++++++++++++++++++++ prompt_professional.txt | 383 ++++++++++++++++++++++++++++++++ prompt_simple.txt | 98 +++++++++ requirements.txt | 7 + scaler_params.npy | Bin 0 -> 501 bytes train_lstm.py | 465 +++++++++++++++++++++++++++++++++++++++ training_loss.png | Bin 0 -> 97763 bytes 12 files changed, 2100 insertions(+) create mode 100644 CHANGELOG.md create mode 100644 LSTM_EA.ex5 create mode 100644 LSTM_EA.mq5 create mode 100644 README.md create mode 100644 lstm_model.onnx create mode 100644 lstm_training.ipynb create mode 100644 prompt_professional.txt create mode 100644 prompt_simple.txt create mode 100644 requirements.txt create mode 100644 scaler_params.npy create mode 100644 train_lstm.py create mode 100644 training_loss.png diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..829f455 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,153 @@ +# 更新日志 (Changelog) + +## [2.0.0] - 2025-10-27 + +### ✨ 新增功能 + +#### 训练改进 +- **训练集/验证集划分**: 自动划分80%训练集和20%验证集 +- **双Loss监控**: 每个epoch同时显示训练Loss和验证Loss +- **早停机制(Early Stopping)**: 验证Loss连续20轮不降则自动停止训练 +- **最佳模型保存**: 自动保存验证Loss最低的模型,而非最后一轮 +- **智能训练**: 最多训练500轮,但通常会在150-200轮触发早停 + +#### 可视化功能 +- **训练曲线图**: 自动生成 `training_loss.png` + - 双曲线对比(训练Loss vs 验证Loss) + - 标记最佳模型的epoch位置 + - 提供完整曲线和后期收敛细节两个子图 + - 支持中文字体显示 + +#### 诊断功能 +- **训练总结报告**: 训练结束后自动输出 + - 总训练轮数 + - 最佳模型epoch + - 最佳验证Loss + - 最终训练Loss和验证Loss + - 训练/验证Loss差异分析 +- **智能诊断**: 自动判断模型状态 + - ✓ 良好:没有明显过拟合 + - ⚠ 尚可:有轻微过拟合倾向 + - ✗ 警告:可能存在过拟合 + +### 📝 文档更新 + +#### README.md +- 更新训练流程说明,详细说明新增功能 +- 新增"如何判断模型训练效果好坏"FAQ +- 新增"验证Loss远高于训练Loss"问题解答 +- 更新训练参数表格,包含早停和验证集信息 +- 更新性能基准数据 +- 标注版本号为2.0.0并列出更新内容 + +#### 提示词文件 +- **prompt_simple.txt**: 更新训练脚本要求,包含验证集和早停 +- **prompt_professional.txt**: 详细补充所有新功能的实现要求 + - 验证集监控 + - 早停机制 + - 最佳模型保存 + - 训练可视化 + - 智能诊断 + - 更新成功标准和重要提示 + +### 🔧 代码优化 + +#### train_lstm.py +- 代码从150行扩展到~330行(增加了大量功能但保持可读性) +- 所有新增代码均有详细中文注释 +- 改进的训练循环: + ```python + for epoch in range(500): # 最多500轮 + # 训练阶段 + model.train() + # ... 训练代码 ... + + # 验证阶段 + model.eval() + with torch.no_grad(): + # ... 验证代码 ... + + # 早停检查 + if val_loss < best_val_loss: + best_model_state = model.state_dict() + else: + patience_counter += 1 + + if patience_counter >= patience: + break # 触发早停 + ``` + +### 📊 训练效果对比 + +#### v1.0.0(旧版本) +- 固定训练100轮 +- 只有训练Loss +- 无法判断过拟合 +- 可能保存次优模型 +- 训练时间:~15秒 + +#### v2.0.0(新版本) +- 智能训练150-200轮(早停) +- 训练Loss + 验证Loss +- 自动检测过拟合 +- 保证保存最佳模型 +- 训练时间:~30秒 +- 新增训练曲线图 +- 新增诊断报告 + +### 🎯 教学价值提升 + +1. **理解过拟合**: 通过双Loss对比直观理解 +2. **学习早停**: 了解工业界常用的训练技巧 +3. **模型评估**: 学会判断模型质量的方法 +4. **可视化思维**: 培养通过图表分析问题的能力 +5. **最佳实践**: 掌握训练集/验证集划分的重要性 + +### 📦 新增文件 + +- `training_loss.png` - 训练曲线图(自动生成) +- `CHANGELOG.md` - 本更新日志 + +### ⚙️ 依赖变化 + +需要安装matplotlib用于绘图(可选): +```bash +pip install matplotlib +``` + +如果未安装matplotlib,程序会跳过绘图但不影响训练。 + +### 🐛 问题修复 + +- 修复了可能保存次优模型的问题(现在保存验证Loss最低的模型) +- 优化了文件路径处理,确保所有输出文件保存到脚本目录 + +### 🔮 未来计划 + +- [ ] 添加TensorBoard支持 +- [ ] 支持学习率动态调整 +- [ ] 添加多模型对比功能 +- [ ] 支持分布式训练 +- [ ] 增加更多技术指标特征 + +--- + +## [1.0.0] - 2025-10-27(初始版本) + +### 基础功能 +- LSTM模型训练 +- CUDA GPU加速 +- MT5数据获取 +- ONNX模型导出 +- MQL5 EA交易 +- 基础文档 + +--- + +**版本说明**: +- 主版本号:重大功能变更或架构调整 +- 次版本号:新增功能或重要改进 +- 修订号:Bug修复或小优化 + +**当前版本**: 2.0.0 +**维护状态**: 活跃开发中 diff --git a/LSTM_EA.ex5 b/LSTM_EA.ex5 new file mode 100644 index 0000000000000000000000000000000000000000..7fb6eed68b17b8dfd048c3dde2e7cdcba7d9957d GIT binary patch literal 34466 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 D + +input double LotSize = 0.01; // 手数 +input int StopLoss = 100; // 止损点数 +input int TakeProfit = 150; // 止盈点数 + +long model_handle; +CTrade trade; + +// 归一化参数(从训练脚本自动生成) +double data_min[5] = {0.95383, 0.95586, 0.95351, 0.95383, 9.0}; +double data_max[5] = {1.23394, 1.23485, 1.23327, 1.23396, 24004.0}; + +//+------------------------------------------------------------------+ +int OnInit() +{ + model_handle = OnnxCreateFromBuffer(lstm_model, ArraySize(lstm_model)); + if(model_handle == INVALID_HANDLE) + { + Print("ONNX模型加载失败"); + return INIT_FAILED; + } + + // 设置输入形状: [batch_size=1, seq_len=10, features=5] + ulong input_shape[] = {1, 10, 5}; + if(!OnnxSetInputShape(model_handle, 0, input_shape)) + { + Print("设置ONNX输入形状失败"); + return INIT_FAILED; + } + + // 设置输出形状: [batch_size=1, output_size=1] + ulong output_shape[] = {1, 1}; + if(!OnnxSetOutputShape(model_handle, 0, output_shape)) + { + Print("设置ONNX输出形状失败"); + return INIT_FAILED; + } + + Print("ONNX模型加载成功,准备交易EURUSD H1"); + return INIT_SUCCEEDED; +} + +//+------------------------------------------------------------------+ +void OnDeinit(const int reason) +{ + OnnxRelease(model_handle); +} + +//+------------------------------------------------------------------+ +void OnTick() +{ + // 仅在新K线时执行 + static datetime last_bar = 0; + datetime current_bar = iTime(_Symbol, PERIOD_H1, 0); + if(current_bar == last_bar) return; + last_bar = current_bar; + + // 准备输入数据: [1, 10, 5] + // 使用double避免在传参或计算时发生不必要的类型转换警告 + double inputs[50]; + for(int i = 0; i < 10; i++) + { + double bar_open = iOpen(_Symbol, PERIOD_H1, i); + double bar_high = iHigh(_Symbol, PERIOD_H1, i); + double bar_low = iLow(_Symbol, PERIOD_H1, i); + double bar_close = iClose(_Symbol, PERIOD_H1, i); + // 使用double存储成交量,避免精度或类型转换警告 + double bar_volume = (double)iVolume(_Symbol, PERIOD_H1, i); + + // 归一化(结果仍为double) + inputs[i*5 + 0] = (bar_open - data_min[0]) / (data_max[0] - data_min[0]); + inputs[i*5 + 1] = (bar_high - data_min[1]) / (data_max[1] - data_min[1]); + inputs[i*5 + 2] = (bar_low - data_min[2]) / (data_max[2] - data_min[2]); + inputs[i*5 + 3] = (bar_close - data_min[3]) / (data_max[3] - data_min[3]); + inputs[i*5 + 4] = (bar_volume - data_min[4]) / (data_max[4] - data_min[4]); + } + + // 运行推理 + // 输出也使用double,确保与输入数组类型一致 + double output[1]; + if(!OnnxRun(model_handle, ONNX_DEFAULT, inputs, output)) + { + Print("ONNX推理失败"); + return; + } + + double current_price = iClose(_Symbol, PERIOD_H1, 0); + double predicted_price = output[0] * (data_max[3] - data_min[3]) + data_min[3]; + + // 交易逻辑: 预测价格上涨买入,下跌卖出 + if(PositionsTotal() == 0) + { + if(predicted_price > current_price * 1.0001) + { + double sl = current_price - StopLoss * _Point; + double tp = current_price + TakeProfit * _Point; + trade.Buy(LotSize, _Symbol, 0, sl, tp); + Print("买入 | 当前:", current_price, " 预测:", predicted_price); + } + else if(predicted_price < current_price * 0.9999) + { + double sl = current_price + StopLoss * _Point; + double tp = current_price - TakeProfit * _Point; + trade.Sell(LotSize, _Symbol, 0, sl, tp); + Print("卖出 | 当前:", current_price, " 预测:", predicted_price); + } + } +} +//+------------------------------------------------------------------+ diff --git a/README.md b/README.md new file mode 100644 index 0000000..946c12f --- /dev/null +++ b/README.md @@ -0,0 +1,477 @@ +# LSTM量化交易教学项目 - EURUSD H1策略 + +## 项目简介 + +基于深度学习LSTM网络的外汇量化交易完整解决方案,使用真实EURUSD历史数据训练模型,通过ONNX格式在MT5平台实现实盘交易。本项目涵盖从数据获取、模型训练、导出到回测的完整工作流,是学习AI量化交易的理想教学案例。 + +## 技术栈 + +- **深度学习框架**: PyTorch 2.5+ (支持CUDA GPU加速) +- **模型架构**: LSTM (Long Short-Term Memory) +- **模型格式**: ONNX (跨平台部署) +- **交易平台**: MetaTrader 5 +- **编程语言**: Python 3.11 + MQL5 +- **数据源**: MT5实时行情数据 + +## LSTM在量化交易中的优势 + +**LSTM (长短期记忆网络)** 是循环神经网络(RNN)的变体,特别适合时间序列预测: + +1. **长期依赖捕捉**: 通过门控机制(遗忘门、输入门、输出门)记住长期价格趋势 +2. **自动特征提取**: 无需手动构造技术指标,直接从原始OHLCV数据学习 +3. **非线性建模**: 可拟合复杂的市场动态,优于传统线性模型 +4. **梯度稳定性**: 解决传统RNN的梯度消失问题,支持深层网络训练 + +## 项目文件结构 + +``` +LSTM/ +├── requirements.txt # Python依赖包清单 +├── train_lstm.py # 训练脚本(支持GPU加速) +├── lstm_model.onnx # 训练生成的ONNX模型(~11KB) +├── scaler_params.npy # MinMax归一化参数 +├── training_loss.png # 训练曲线图(自动生成) +├── LSTM_EA.mq5 # MQL5交易EA代码 +├── prompt_simple.txt # 简单版提示词模板 +├── prompt_professional.txt # 专业版提示词模板 +└── README.md # 项目文档 +``` + +## 快速开始 + +### 环境准备 + +**硬件要求**: +- CPU: 任意现代处理器 +- GPU: NVIDIA显卡(可选,推荐RTX系列) +- 内存: 8GB+ +- 硬盘: 1GB可用空间 + +**软件要求**: +- Python 3.11+ +- MetaTrader 5 (已登录账户) +- CUDA 12.1+ (GPU训练需要) + +### 1. 安装依赖 + +```bash +pip install -r requirements.txt +``` + +**依赖包说明**: +- `torch`: PyTorch深度学习框架 +- `onnx`: ONNX模型格式支持 +- `MetaTrader5`: MT5 Python API +- `pandas`: 数据处理 +- `scikit-learn`: 数据归一化 +- `numpy`: 数值计算 + +**安装GPU版PyTorch** (如有NVIDIA显卡): +```bash +pip uninstall torch torchvision torchaudio -y +pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121 +``` + +### 2. 训练模型 + +```bash +python train_lstm.py +``` + +**训练流程**: +1. 自动检测CUDA并使用GPU加速(如可用) +2. 连接MT5终端获取EURUSD历史数据 +3. 使用MinMaxScaler归一化OHLCV数据 +4. **数据集划分**: 80%训练集 + 20%验证集 +5. **智能训练**: 最多500个epoch,支持早停机制 +6. **验证监控**: 实时追踪训练Loss和验证Loss +7. **最佳模型保存**: 自动保存验证Loss最低的模型 +8. **可视化**: 生成训练曲线图(training_loss.png) +9. 导出ONNX格式模型 +10. 保存归一化参数供推理使用 + +**训练输出示例**: +``` +使用设备: cuda +GPU型号: NVIDIA GeForce RTX 4090 +获取到 31,053 条EURUSD H1数据 + +训练集形状: torch.Size([24833, 10, 5]) +验证集形状: torch.Size([6209, 10, 5]) + +开始训练... +Epoch 10 | 训练Loss: 0.307397 | 验证Loss: 0.301387 +Epoch 20 | 训练Loss: 0.198500 | 验证Loss: 0.189706 +... +Epoch 164 | 训练Loss: 0.000424 | 验证Loss: 0.000152 ← 最佳模型 + +早停触发! 验证损失已经20个epoch没有改善 +已恢复最佳模型 (Epoch 164) + +训练总结: +总训练轮数: 184 epochs +最佳模型: Epoch 164 +最佳验证Loss: 0.000152 +✓ 模型状态: 良好,没有明显的过拟合 + +训练曲线已保存: training_loss.png +ONNX模型已保存: lstm_model.onnx (10.27 KB) +``` + +### 3. 部署到MT5 + +#### 步骤A: 复制ONNX模型 +``` +将 lstm_model.onnx 复制到: +MT5安装目录/MQL5/Files/lstm_model.onnx +``` + +#### 步骤B: 验证归一化参数 +训练脚本会输出归一化参数,确保与 `LSTM_EA.mq5` 第20-21行一致: +```cpp +double data_min[5] = {0.95383, 0.95586, 0.95351, 0.95383, 9.00000}; +double data_max[5] = {1.23394, 1.23485, 1.23327, 1.23396, 24004.00000}; +``` + +#### 步骤C: 编译EA +1. 将 `LSTM_EA.mq5` 复制到 `MT5/MQL5/Experts/` +2. 在MetaEditor中打开并编译(F7) + +#### 步骤D: 策略回测 +1. 打开策略测试器(Ctrl+R) +2. 配置参数: + - EA: LSTM_EA + - 品种: EURUSD + - 周期: H1 + - 模式: 每笔成交 + - 日期: 选择样本外数据(如最近3个月) + - 初始资金: 10,000 USD +3. 点击"开始"运行回测 + +### 4. EA参数说明 + +| 参数 | 默认值 | 说明 | +|------|--------|------| +| LotSize | 0.01 | 每笔交易手数 | +| StopLoss | 100 | 止损点数 | +| TakeProfit | 150 | 止盈点数 | + +## 模型架构详解 + +### 网络结构 + +``` +输入层: [batch_size, 10, 5] + ├─ 10: 时间步长(10根历史K线) + └─ 5: 特征维度(OHLCV) + ↓ +LSTM层: input_size=5, hidden_size=20, num_layers=1 + ├─ 遗忘门: 决定丢弃哪些历史信息 + ├─ 输入门: 决定存储哪些新信息 + └─ 输出门: 决定输出哪些信息 + ↓ +全连接层: Linear(20 → 1) + ↓ +输出层: [batch_size, 1] + └─ 预测下一根K线收盘价(归一化值) +``` + +**模型参数**: +- 总参数量: ~2,040个 +- ONNX文件大小: 10.27 KB +- 推理速度: <1ms/次 (GPU) + +### 数据预处理 + +**归一化公式**: +```python +normalized = (value - min) / (max - min) # 映射到[0,1] +``` + +**反归一化公式**: +```python +real_value = normalized * (max - min) + min +``` + +### 训练参数 + +| 参数 | 值 | 说明 | +|------|-----|------| +| 数据划分 | 80%/20% | 训练集/验证集 | +| 优化器 | Adam | 自适应学习率 | +| 学习率 | 0.001 | 初始学习率 | +| 损失函数 | MSE | 均方误差 | +| 批大小 | 全量 | 使用所有训练数据 | +| 最大轮数 | 500 | 最多训练500个epoch | +| 早停耐心 | 20 | 验证Loss 20轮不降则停止 | +| 最佳模型 | 自动保存 | 保存验证Loss最低的模型 | + +## 交易逻辑 + +```python +每小时新K线形成时: +1. 获取最近10根H1 K线 [Open, High, Low, Close, Volume] +2. 使用训练时的参数归一化数据 +3. 输入LSTM模型进行推理 +4. 反归一化得到预测收盘价 +5. 交易决策: + - 预测价 > 当前价 * 1.0001 → 买入 + - 预测价 < 当前价 * 0.9999 → 卖出 + - 其他情况 → 不交易 +6. 设置止损止盈并提交订单 +``` + +## 性能优化 + +### 提升模型效果 + +1. **增加模型容量**: + ```python + self.lstm = nn.LSTM(input_size=5, hidden_size=50, num_layers=2) + ``` + +2. **扩展时间窗口**: + ```python + lookback = 20 # 使用20根K线预测 + ``` + +3. **调整训练参数**: + ```python + patience = 30 # 增加早停耐心值 + optimizer = torch.optim.Adam(model.parameters(), lr=0.0005) # 降低学习率 + ``` + +4. **添加技术指标**: + ```python + features = ['open', 'high', 'low', 'close', 'volume', 'rsi', 'macd'] + ``` + +5. **数据增强**: + - 添加噪声 + - 时间窗口滑动 + - 多时间周期融合 + +### 防止过拟合 + +✅ **已实现的功能**: + +1. **训练集/验证集分割**: + ```python + train_size = int(len(X) * 0.8) # 80%训练,20%验证 + X_train = X[:train_size] + X_val = X[train_size:] + ``` + +2. **早停法(Early Stopping)**: + ```python + patience = 20 # 验证Loss 20轮不降则停止 + if val_loss < best_val_loss: + best_model_state = model.state_dict() # 保存最佳模型 + ``` + +3. **验证Loss监控**: + ```python + # 每10轮显示训练和验证Loss对比 + print(f"训练Loss: {train_loss:.6f} | 验证Loss: {val_loss:.6f}") + ``` + +4. **训练曲线可视化**: + - 自动生成 `training_loss.png` + - 双曲线对比(训练vs验证) + - 标记最佳模型位置 + +⚙️ **可选的进阶方法**: + +5. **添加Dropout**: + ```python + self.dropout = nn.Dropout(0.2) + ``` + +6. **L2正则化**: + ```python + optimizer = Adam(model.parameters(), lr=0.001, weight_decay=1e-5) + ``` + +7. **学习率衰减**: + ```python + scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, patience=5) + ``` + +## 常见问题排查 + +### 训练相关 + +**Q: 训练时CUDA不可用?** +```bash +# 检查PyTorch版本 +python -c "import torch; print(torch.__version__)" + +# 应显示 2.5.1+cu121 (带+cu121后缀) +# 如果是 2.5.1+cpu,需重装GPU版本: +pip install torch --index-url https://download.pytorch.org/whl/cu121 +``` + +**Q: MT5连接失败?** +``` +解决方案: +1. 确保MT5已启动并登录 +2. 检查工具→选项→EA交易→允许DLL导入 +3. 尝试手动在MT5图表上加载EURUSD H1 +``` + +**Q: 训练Loss不下降?** +``` +可能原因: +1. 学习率过大 → 降低到0.0001 +2. 数据未归一化 → 检查scaler +3. 数据质量差 → 检查MT5历史数据完整性 +4. 模型过于简单 → 增加hidden_size或num_layers +``` + +**Q: 验证Loss远高于训练Loss?** +``` +原因: 模型过拟合训练数据 +解决方案: +1. 查看训练曲线图(training_loss.png)确认 +2. 减少训练轮数或降低模型复杂度 +3. 增加训练数据量 +4. 添加Dropout层或L2正则化 +5. 使用早停机制(已自动启用) +``` + +**Q: 如何判断模型训练效果好坏?** +``` +关键指标: +1. ✅ 验证Loss越小越好 (如<0.001) +2. ✅ 训练Loss和验证Loss接近 (差异<10%) +3. ✅ Loss曲线平稳收敛不震荡 +4. ✅ 早停在合理位置触发 + +查看方法: +- 打开 training_loss.png 查看曲线 +- 阅读终端输出的"训练总结" +- 关注"模型状态"的诊断结果 +``` + +### 回测相关 + +**Q: ONNX推理失败 "wrong dimension"?** +```cpp +// 确保在OnInit()中设置输入输出形状 +ulong input_shape[] = {1, 10, 5}; +OnnxSetInputShape(model_handle, 0, input_shape); + +ulong output_shape[] = {1, 1}; +OnnxSetOutputShape(model_handle, 0, output_shape); +``` + +**Q: 回测没有交易信号?** +``` +检查清单: +1. 归一化参数是否一致 +2. ONNX模型是否正确加载 +3. 时间周期是否为H1 +4. 查看Expert日志的预测值 +``` + +**Q: 回测收益为负?** +``` +这是正常的,原因: +1. LSTM难以预测随机市场 +2. 模型过拟合训练数据 +3. 交易成本(点差/滑点)未考虑 +4. 需要优化止损止盈参数 + +建议: +- 仅用于学习,不要实盘 +- 尝试不同市场和时间周期 +- 结合其他技术指标 +``` + +## 进阶扩展方向 + +### 1. 模型改进 +- **双向LSTM**: 同时考虑过去和未来信息 +- **GRU**: 更简单的门控单元,训练更快 +- **Transformer**: 注意力机制,捕捉长期依赖 +- **CNN-LSTM**: 卷积提取局部特征+LSTM捕捉时序 + +### 2. 特征工程 +- **技术指标**: RSI, MACD, 布林带, KDJ +- **市场情绪**: VIX指数, 恐慌指数 +- **基本面**: 经济数据, 新闻情绪分析 +- **多周期**: M5/M15/H4数据融合 + +### 3. 策略优化 +- **强化学习**: PPO/DQN优化交易决策 +- **仓位管理**: Kelly公式动态调整手数 +- **风控系统**: 最大回撤限制, 连续亏损止损 +- **集成学习**: 多模型投票/加权平均 + +### 4. 工程化部署 +- **实时推理服务**: FastAPI + Docker +- **模型监控**: MLflow跟踪模型性能 +- **在线学习**: 定期用新数据微调模型 +- **多品种交易**: 扩展到黄金/原油/股指 + +## 性能基准 + +### 训练性能 (RTX 4090) + +| 指标 | 数值 | +|------|------| +| 数据量 | 31,053条 | +| 训练集 | 24,833条 (80%) | +| 验证集 | 6,209条 (20%) | +| 训练时间 | ~30秒 (含早停) | +| 实际轮数 | 164-184 (早停触发) | +| 最佳验证Loss | ~0.0001-0.0002 | +| 显存占用 | ~500MB | + +### 推理性能 (MT5) + +| 指标 | 数值 | +|------|------| +| 单次推理 | <1ms | +| H1周期延迟 | 可忽略 | +| CPU占用 | <1% | + +## 免责声明 + +**重要提示**: + +1. 本项目仅用于教学和研究目的,不构成任何投资建议 +2. 金融市场存在不可预测性,过去表现不代表未来收益 +3. LSTM模型可能过拟合历史数据,实盘表现可能与回测不符 +4. 实盘交易前务必在模拟账户充分测试 +5. 交易有风险,投资需谨慎,请勿使用无法承受损失的资金 + +## 参考资料 + +- [PyTorch官方文档](https://pytorch.org/docs/stable/index.html) +- [ONNX格式规范](https://onnx.ai/) +- [MQL5语言参考](https://www.mql5.com/zh/docs) +- [LSTM原理论文](https://www.bioinf.jku.at/publications/older/2604.pdf) +- [量化交易策略](https://www.quantstart.com/) + +## 许可证 + +本项目采用 MIT 许可证开源,允许自由使用、修改和分发。 + +## 作者与贡献 + +本项目由AI辅助生成,用于教学演示。欢迎提出改进建议和Bug反馈。 + +--- + +**最后更新**: 2025-10-27 +**版本**: 2.0.0 +**更新内容**: +- ✅ 新增训练集/验证集划分 (80%/20%) +- ✅ 新增早停机制 (Early Stopping) +- ✅ 新增最佳模型自动保存 +- ✅ 新增训练曲线可视化 +- ✅ 新增训练状态智能诊断 +- ✅ 优化训练流程和文档说明 + +**适用人群**: 量化交易初学者、深度学习实践者、MQL5开发者 diff --git a/lstm_model.onnx b/lstm_model.onnx new file mode 100644 index 0000000000000000000000000000000000000000..5e2a58b65cd2966e60a0b52ab3b03d6f7d3cf646 GIT binary patch literal 10512 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 Kq`hR+mHr<>8d9ME literal 0 HcmV?d00001 diff --git a/lstm_training.ipynb b/lstm_training.ipynb new file mode 100644 index 0000000..aac14a9 --- /dev/null +++ b/lstm_training.ipynb @@ -0,0 +1,398 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "de76d792", + "metadata": {}, + "source": [ + "# LSTM外汇价格预测 - 精简版\n", + "\n", + "本Notebook包含LSTM模型训练的核心代码,用于预测EURUSD价格。\n", + "\n", + "## 主要步骤\n", + "1. 导入库和设置设备\n", + "2. 从MT5获取历史数据\n", + "3. 数据归一化\n", + "4. 构造训练/验证数据集\n", + "5. 定义LSTM模型\n", + "6. 训练模型(早停机制)\n", + "7. 可视化训练过程\n", + "8. 导出ONNX模型" + ] + }, + { + "cell_type": "markdown", + "id": "063db750", + "metadata": {}, + "source": [ + "## 1. 导入库和设置设备" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fee98c7a", + "metadata": {}, + "outputs": [], + "source": [ + "import torch\n", + "import torch.nn as nn\n", + "import numpy as np\n", + "import pandas as pd\n", + "import MetaTrader5 as mt5\n", + "from sklearn.preprocessing import MinMaxScaler\n", + "from datetime import datetime, timedelta\n", + "import matplotlib.pyplot as plt\n", + "import os\n", + "\n", + "# 设置中文字体\n", + "plt.rcParams['font.sans-serif'] = ['SimHei']\n", + "plt.rcParams['axes.unicode_minus'] = False\n", + "\n", + "# 选择计算设备(GPU或CPU)\n", + "device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n", + "print(f\"使用设备: {device}\")" + ] + }, + { + "cell_type": "markdown", + "id": "19c6b9d0", + "metadata": {}, + "source": [ + "## 2. 从MT5获取EURUSD历史数据" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "eee75337", + "metadata": {}, + "outputs": [], + "source": [ + "# 连接MT5\n", + "mt5.initialize()\n", + "\n", + "# 设置时间范围(最近5年)\n", + "end_date = datetime.now()\n", + "start_date = end_date - timedelta(days=5*365)\n", + "\n", + "# 获取EURUSD H1数据\n", + "rates = mt5.copy_rates_range(\"EURUSD\", mt5.TIMEFRAME_H1, start_date, end_date)\n", + "mt5.shutdown()\n", + "\n", + "# 转换为DataFrame\n", + "df = pd.DataFrame(rates)\n", + "print(f\"获取到 {len(df):,} 条数据\")\n", + "df.head()" + ] + }, + { + "cell_type": "markdown", + "id": "4fda9d49", + "metadata": {}, + "source": [ + "## 3. 数据归一化" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c5dccb3f", + "metadata": {}, + "outputs": [], + "source": [ + "# 创建归一化器,将数据缩放到[0,1]范围\n", + "scaler = MinMaxScaler()\n", + "\n", + "# 对开、高、低、收、成交量进行归一化\n", + "df[['open', 'high', 'low', 'close', 'tick_volume']] = scaler.fit_transform(\n", + " df[['open', 'high', 'low', 'close', 'tick_volume']]\n", + ")\n", + "\n", + "print(\"归一化完成\")\n", + "print(f\"最小值: {scaler.data_min_}\")\n", + "print(f\"最大值: {scaler.data_max_}\")" + ] + }, + { + "cell_type": "markdown", + "id": "15ddac0b", + "metadata": {}, + "source": [ + "## 4. 构造时间序列数据集" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4887abce", + "metadata": {}, + "outputs": [], + "source": [ + "def create_dataset(data, lookback=10):\n", + " \"\"\"将时间序列转换为监督学习数据集\"\"\"\n", + " X, y = [], []\n", + " for i in range(len(data) - lookback - 1):\n", + " X.append(data[i:i+lookback]) # 输入:前10根K线\n", + " y.append(data[i+lookback, 3]) # 输出:第11根K线的收盘价\n", + " return np.array(X, dtype=np.float32), np.array(y, dtype=np.float32).reshape(-1, 1)\n", + "\n", + "# 提取特征\n", + "features = df[['open', 'high', 'low', 'close', 'tick_volume']].values\n", + "X, y = create_dataset(features)\n", + "\n", + "# 划分训练集(80%)和验证集(20%)\n", + "train_size = int(len(X) * 0.8)\n", + "X_train = torch.from_numpy(X[:train_size]).to(device)\n", + "y_train = torch.from_numpy(y[:train_size]).to(device)\n", + "X_val = torch.from_numpy(X[train_size:]).to(device)\n", + "y_val = torch.from_numpy(y[train_size:]).to(device)\n", + "\n", + "print(f\"训练集: {X_train.shape}\")\n", + "print(f\"验证集: {X_val.shape}\")" + ] + }, + { + "cell_type": "markdown", + "id": "208c234b", + "metadata": {}, + "source": [ + "## 5. 定义LSTM模型" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c2916d2d", + "metadata": {}, + "outputs": [], + "source": [ + "class SimpleLSTM(nn.Module):\n", + " \"\"\"简单的LSTM神经网络\"\"\"\n", + " def __init__(self):\n", + " super().__init__()\n", + " # LSTM层:输入5个特征,隐藏层20个神经元\n", + " self.lstm = nn.LSTM(input_size=5, hidden_size=20, num_layers=1, batch_first=True)\n", + " # 全连接层:将20维输出映射到1维(预测值)\n", + " self.fc = nn.Linear(20, 1)\n", + "\n", + " def forward(self, x):\n", + " lstm_out, _ = self.lstm(x)\n", + " return self.fc(lstm_out[:, -1, :]) # 取最后一个时间步的输出\n", + "\n", + "# 创建模型\n", + "model = SimpleLSTM().to(device)\n", + "optimizer = torch.optim.Adam(model.parameters(), lr=0.001)\n", + "criterion = nn.MSELoss()\n", + "\n", + "print(\"模型创建完成\")" + ] + }, + { + "cell_type": "markdown", + "id": "9e6f4eeb", + "metadata": {}, + "source": [ + "## 6. 训练模型(带早停机制)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "04481e99", + "metadata": {}, + "outputs": [], + "source": [ + "# 训练参数\n", + "max_epochs = 500\n", + "patience = 20 # 验证Loss不降20轮则停止\n", + "\n", + "# 记录训练过程\n", + "train_losses = []\n", + "val_losses = []\n", + "best_val_loss = float('inf')\n", + "best_epoch = 0\n", + "patience_counter = 0\n", + "best_model_state = None\n", + "\n", + "print(\"开始训练...\")\n", + "for epoch in range(max_epochs):\n", + " # 训练阶段\n", + " model.train()\n", + " optimizer.zero_grad()\n", + " train_output = model(X_train)\n", + " train_loss = criterion(train_output, y_train)\n", + " train_loss.backward()\n", + " optimizer.step()\n", + " \n", + " # 验证阶段\n", + " model.eval()\n", + " with torch.no_grad():\n", + " val_output = model(X_val)\n", + " val_loss = criterion(val_output, y_val)\n", + " \n", + " # 记录Loss\n", + " train_losses.append(train_loss.item())\n", + " val_losses.append(val_loss.item())\n", + " \n", + " # 每10轮打印一次\n", + " if (epoch + 1) % 10 == 0:\n", + " print(f\"Epoch {epoch+1:3d} | 训练Loss: {train_loss.item():.6f} | 验证Loss: {val_loss.item():.6f}\")\n", + " \n", + " # 早停检查\n", + " if val_loss.item() < best_val_loss:\n", + " best_val_loss = val_loss.item()\n", + " best_epoch = epoch + 1\n", + " patience_counter = 0\n", + " best_model_state = {k: v.cpu().clone() for k, v in model.state_dict().items()}\n", + " else:\n", + " patience_counter += 1\n", + " \n", + " if patience_counter >= patience:\n", + " print(f\"\\n早停触发!最佳模型在Epoch {best_epoch}\")\n", + " break\n", + "\n", + "# 恢复最佳模型\n", + "if best_model_state:\n", + " model.load_state_dict(best_model_state)\n", + " model = model.to(device)\n", + " print(f\"已恢复最佳模型 (Epoch {best_epoch}, 验证Loss: {best_val_loss:.6f})\")" + ] + }, + { + "cell_type": "markdown", + "id": "1669bc75", + "metadata": {}, + "source": [ + "## 7. 可视化训练过程" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cd1011f3", + "metadata": {}, + "outputs": [], + "source": [ + "# 绘制训练和验证Loss曲线\n", + "plt.figure(figsize=(12, 5))\n", + "\n", + "# 完整曲线\n", + "plt.subplot(1, 2, 1)\n", + "plt.plot(train_losses, label='训练Loss', color='blue', linewidth=2)\n", + "plt.plot(val_losses, label='验证Loss', color='orange', linewidth=2)\n", + "plt.axvline(x=best_epoch-1, color='red', linestyle='--', label=f'最佳模型 (Epoch {best_epoch})')\n", + "plt.xlabel('Epoch')\n", + "plt.ylabel('Loss')\n", + "plt.title('训练和验证损失曲线')\n", + "plt.legend()\n", + "plt.grid(True, alpha=0.3)\n", + "\n", + "# 后80%放大曲线\n", + "plt.subplot(1, 2, 2)\n", + "start_idx = int(len(train_losses) * 0.2)\n", + "plt.plot(range(start_idx, len(train_losses)), train_losses[start_idx:], label='训练Loss', color='blue', linewidth=2)\n", + "plt.plot(range(start_idx, len(val_losses)), val_losses[start_idx:], label='验证Loss', color='orange', linewidth=2)\n", + "plt.axvline(x=best_epoch-1, color='red', linestyle='--', label=f'最佳模型')\n", + "plt.xlabel('Epoch')\n", + "plt.ylabel('Loss')\n", + "plt.title('后期收敛细节')\n", + "plt.legend()\n", + "plt.grid(True, alpha=0.3)\n", + "\n", + "plt.tight_layout()\n", + "plt.savefig('training_loss.png', dpi=150)\n", + "plt.show()\n", + "\n", + "print(\"训练曲线已保存为 training_loss.png\")" + ] + }, + { + "cell_type": "markdown", + "id": "80b9c1c2", + "metadata": {}, + "source": [ + "## 8. 导出ONNX模型和归一化参数" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c5b82ee2", + "metadata": {}, + "outputs": [], + "source": [ + "# 导出ONNX模型\n", + "model.eval()\n", + "model_cpu = model.cpu()\n", + "dummy_input = torch.randn(1, 10, 5)\n", + "\n", + "torch.onnx.export(\n", + " model_cpu,\n", + " dummy_input,\n", + " \"lstm_model.onnx\",\n", + " export_params=True,\n", + " opset_version=11,\n", + " input_names=['input'],\n", + " output_names=['output']\n", + ")\n", + "\n", + "print(\"✓ ONNX模型已导出: lstm_model.onnx\")\n", + "\n", + "# 保存归一化参数\n", + "np.save('scaler_params.npy', {'min': scaler.data_min_, 'max': scaler.data_max_})\n", + "print(\"✓ 归一化参数已保存: scaler_params.npy\")\n", + "\n", + "# 输出MQL5格式的归一化参数\n", + "print(\"\\n=\" * 60)\n", + "print(\"归一化参数 (复制到LSTM_EA.mq5):\")\n", + "print(\"=\" * 60)\n", + "print(f\"double data_min[5] = {{{', '.join([f'{x:.5f}' for x in scaler.data_min_])}}};\")\n", + "print(f\"double data_max[5] = {{{', '.join([f'{x:.5f}' for x in scaler.data_max_])}}};\")\n", + "print(\"=\" * 60)" + ] + }, + { + "cell_type": "markdown", + "id": "cb21d2d4", + "metadata": {}, + "source": [ + "## 9. 训练总结" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b6ac5bc9", + "metadata": {}, + "outputs": [], + "source": [ + "print(\"=\" * 60)\n", + "print(\"训练总结\")\n", + "print(\"=\" * 60)\n", + "print(f\"总训练轮数: {len(train_losses)} epochs\")\n", + "print(f\"最佳模型: Epoch {best_epoch}\")\n", + "print(f\"最佳验证Loss: {best_val_loss:.6f}\")\n", + "print(f\"最终训练Loss: {train_losses[-1]:.6f}\")\n", + "print(f\"最终验证Loss: {val_losses[-1]:.6f}\")\n", + "\n", + "loss_diff = abs(train_losses[-1] - val_losses[-1])\n", + "print(f\"\\nLoss差异: {loss_diff:.6f}\")\n", + "if loss_diff < 0.0001:\n", + " print(\"✓ 模型状态: 良好\")\n", + "elif loss_diff < 0.001:\n", + " print(\"⚠ 模型状态: 尚可,有轻微过拟合\")\n", + "else:\n", + " print(\"✗ 模型状态: 可能过拟合\")\n", + "print(\"=\" * 60)" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/prompt_professional.txt b/prompt_professional.txt new file mode 100644 index 0000000..b7a5063 --- /dev/null +++ b/prompt_professional.txt @@ -0,0 +1,383 @@ +# AI量化交易LSTM+ONNX+MQL5完整项目生成提示词 + +## 项目背景 + +我需要一个用于教学的量化交易完整解决方案,使用深度学习LSTM模型预测价格并在MT5平台自动交易。 + +## 核心需求 + +### 1. 技术栈要求 +- **深度学习框架**: PyTorch (支持CUDA GPU加速) +- **模型类型**: LSTM (Long Short-Term Memory 长短期记忆网络) +- **模型格式**: ONNX (用于跨平台部署到MT5) +- **交易平台**: MetaTrader 5 (MQL5语言) +- **数据源**: MT5实时历史数据 (使用MetaTrader5 Python库) +- **编程语言**: Python 3.11+ 和 MQL5 + +### 2. 硬件环境 +- GPU: NVIDIA RTX 4090 +- CUDA: 12.1+ +- 已安装: Python, CUDA, cuDNN +- 需要: 自动检测CUDA并使用GPU训练 + +### 3. 项目目标 +这是一个**教学项目**,需要满足: +- ✅ 代码简洁易懂 (能少一句绝不多一句) +- ✅ 文件数量最少 (Python训练脚本1个 + ONNX模型1个 + MQL5 EA 1个) +- ✅ 完整工作流 (数据获取→训练→导出→回测) +- ✅ 可直接运行 (提供requirements.txt) +- ✅ 中文注释和文档 + +## 详细要求 + +### 一、Python训练脚本 (train_lstm.py) + +**功能清单**: +1. **CUDA检测与GPU训练** + - 自动检测CUDA是否可用 + - 如果CUDA不可用,提示安装GPU版PyTorch的命令 + - 显示GPU型号、CUDA版本、可用显存 + - 优先使用GPU训练,CPU作为备选 + +2. **数据获取** + - 使用`MetaTrader5`库连接MT5终端 + - 获取指定交易品种(如EURUSD)最近N年的H1数据 + - 自动检查MT5连接状态并给出清晰的错误提示 + - 显示获取的数据量和时间范围 + +3. **数据预处理** + - 使用`sklearn.MinMaxScaler`归一化OHLCV数据到[0,1] + - 构造时间序列训练集: 用前N根K线预测下一根收盘价 + - **数据集划分**: 80%训练集 + 20%验证集 (时间序列顺序划分) + - 保存归一化参数(最小值/最大值)供MQL5使用 + +4. **LSTM模型定义** + - 简洁的PyTorch LSTM模型 (input→LSTM→FC→output) + - 输入: [batch_size, seq_len, features] + - 输出: [batch_size, 1] (预测收盘价) + - 模型参数可配置 (隐藏层大小、层数等) + +5. **模型训练** + - 使用Adam优化器 + - MSE损失函数 + - **验证集监控**: 每个epoch计算训练Loss和验证Loss + - **早停机制(Early Stopping)**: 验证Loss连续N轮不降则停止 + - **最佳模型保存**: 自动保存验证Loss最低的模型状态 + - 训练过程显示进度和双Loss对比 + - 支持GPU加速训练 + +6. **ONNX导出** + - 训练完成后导出为ONNX格式 + - **使用最佳模型**: 导出验证Loss最低的模型,而非最后一轮 + - 文件保存到**脚本所在目录** (使用`__file__`获取路径) + - 验证ONNX模型正确性 + - 显示文件大小和完整保存路径 + +7. **训练可视化** + - 使用matplotlib绘制训练曲线图 + - 双曲线对比: 训练Loss vs 验证Loss + - 标记最佳模型的epoch位置 + - 生成两个子图: 完整曲线 + 后期收敛细节 + - 保存为training_loss.png + +8. **归一化参数输出** + - 保存为.npy文件 + - 同时在终端输出C++数组格式,方便直接复制到MQL5代码 + - 示例: `double data_min[5] = {0.95, 0.96, ...};` + +9. **训练总结报告** + - 显示总训练轮数、最佳epoch + - 显示最佳验证Loss和最终Loss + - 计算训练/验证Loss差异 + - **智能诊断**: 自动判断是否过拟合并给出建议 + +**代码风格**: +- 代码行数控制在250行以内 (含可视化和诊断功能) +- 每个关键步骤都有中文注释 +- 函数尽量简洁,避免过度封装 +- 输出信息美观清晰,便于教学演示 +- 训练过程有清晰的阶段划分注释 + +--- + +### 二、MQL5交易EA (LSTM_EA.mq5) + +**功能清单**: +1. **ONNX模型加载** + - 使用`#resource`方式嵌入ONNX文件 + - 在`OnInit()`中加载模型 + - 使用`OnnxSetInputShape`和`OnnxSetOutputShape`设置张量维度 + - 模型加载失败时给出清晰提示 + +2. **数据归一化** + - 硬编码训练时的归一化参数(从Python脚本输出复制) + - 归一化公式: `(value - min) / (max - min)` + - 对OHLCV五个特征分别归一化 + +3. **推理预测** + - 每个新K线形成时触发(使用静态变量判断) + - 获取最近N根K线数据 + - 归一化后输入ONNX模型 + - 反归一化得到真实预测价格 + +4. **交易逻辑** + - 预测价格 > 当前价格 → 买入 + - 预测价格 < 当前价格 → 卖出 + - 设置止损止盈 + - 每次只持有一个仓位 + +5. **可调参数** + - 手数 (LotSize) + - 止损点数 (StopLoss) + - 止盈点数 (TakeProfit) + +**代码风格**: +- 代码行数控制在100行以内 +- MQL5标准注释风格 +- 核心逻辑清晰,适合教学 + +--- + +### 三、依赖文件 (requirements.txt) + +必须包含: +``` +torch>=2.0.0 +numpy>=1.24.0 +onnx>=1.14.0 +onnxruntime>=1.15.0 +pandas>=2.0.0 +MetaTrader5>=5.0.0 +scikit-learn>=1.3.0 +``` + +并在README中说明如何安装GPU版PyTorch: +```bash +pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121 +``` + +--- + +### 四、README说明文档 + +**必须包含的章节**: + +1. **项目简介** + - 一句话概括项目功能 + - 技术栈列表 + +2. **LSTM原理说明** (教学重点) + - LSTM是什么 + - 为什么适合时间序列预测 + - 门控机制简介 (遗忘门、输入门、输出门) + - 在量化交易中的优势 + +3. **文件结构** + - 每个文件的作用 + - 文件之间的关系 + - 包含训练生成的文件(ONNX、参数、曲线图) + +4. **快速开始** + - 环境准备 (硬件、软件要求) + - 依赖安装 + - 训练模型 (详细步骤) + - **查看训练效果**: 如何解读曲线图和诊断报告 + - 部署到MT5 (详细步骤) + - 回测运行 + +5. **模型架构详解** + - 网络结构图 (文字ASCII图) + - 输入输出格式 + - 参数量统计 + +6. **训练参数说明** + - 数据划分比例 + - 优化器、学习率、损失函数 + - **早停机制参数** (耐心值、触发条件) + - **最佳模型保存策略** + +7. **使用示例** + - 训练脚本运行示例 + - 预期输出示例 (含验证Loss和诊断信息) + - 训练曲线图解读 + - 回测结果解读 + +8. **常见问题** + - CUDA不可用怎么办 + - MT5连接失败 + - ONNX推理错误 + - 回测无信号 + - 模型不收敛 + - **验证Loss远高于训练Loss (过拟合)** + - **如何判断模型训练效果好坏** + +9. **进阶方向** (启发学生思考) + - 如何改进模型 (增加层数、添加技术指标等) + - **如何防止过拟合** (重点说明已实现的功能) + - 如何优化交易策略 + +10. **免责声明** + - 教学用途,不构成投资建议 + - 实盘风险提示 + +--- + +## 特殊要求 + +### 代码简洁性 +- Python训练脚本: ≤250行 (含验证、早停、可视化) +- MQL5 EA: ≤100行 +- 避免过度封装和抽象 +- 每行代码都有存在的必要 + +### 教学友好性 +- 中文注释覆盖率 >50% +- 关键步骤输出详细日志 +- **训练过程可视化**: 生成易读的曲线图 +- **智能诊断**: 自动评估模型状态 +- 错误提示清晰具体 +- README配图或ASCII图辅助理解 + +### 可移植性 +- 文件路径使用`os.path`处理 +- ONNX和参数文件保存到脚本同目录 +- 不依赖绝对路径或特定文件夹结构 + +### 实用性 +- 数据来源真实 (MT5历史数据) +- 模型可真实回测 +- 参数可调节优化 +- 支持GPU加速训练 + +--- + +## 交付清单 + +请生成以下文件: + +1. ✅ `train_lstm.py` - Python训练脚本 +2. ✅ `LSTM_EA.mq5` - MQL5交易EA +3. ✅ `requirements.txt` - Python依赖清单 +4. ✅ `README.md` - 完整中文文档 +5. ✅ `prompt.txt` - 本提示词文件(用于复现) + +训练脚本运行后自动生成: +6. ✅ `lstm_model.onnx` - 训练好的ONNX模型 +7. ✅ `scaler_params.npy` - 归一化参数文件 + +--- + +## 示例配置 + +**默认参数建议**: +- 交易品种: EURUSD +- 时间周期: H1 +- 历史数据: 最近5年 +- 数据划分: 80%训练 / 20%验证 +- 时间窗口: 10根K线 +- LSTM隐藏层: 20个单元 +- 最大训练轮数: 500 epochs +- 早停耐心值: 20 epochs +- 学习率: 0.001 +- 批大小: 全量(小数据集) + +--- + +## 成功标准 + +生成的项目应该满足: +1. ✅ 新手可在10分钟内完成训练 +2. ✅ 代码总行数 <400行 (Python≤250行 + MQL5≤100行) +3. ✅ GPU自动检测并加速 +4. ✅ **训练集/验证集自动划分** +5. ✅ **早停机制自动运行** +6. ✅ **最佳模型自动保存** +7. ✅ **训练曲线自动生成** +8. ✅ **模型状态智能诊断** +9. ✅ 模型可成功导出为ONNX +10. ✅ EA可在MT5回测器运行 +11. ✅ 所有步骤有详细中文说明 +12. ✅ 文件保存路径正确 (脚本同目录) + +--- + +## 输出格式 + +请按以下顺序生成: +1. 先生成`requirements.txt` +2. 再生成`train_lstm.py` (包含详细注释) +3. 再生成`LSTM_EA.mq5` +4. 最后生成`README.md` (完整文档) + +每个文件生成后立即告知文件路径和关键信息。 + +--- + +## 风格示例 + +**Python代码风格**: +```python +# 检查CUDA +device = torch.device("cuda" if torch.cuda.is_available() else "cpu") +print(f"使用设备: {device}") +``` + +**MQL5代码风格**: +```cpp +// 归一化参数(从训练脚本复制) +double data_min[5] = {0.95, 0.96, 0.95, 0.95, 9.0}; +double data_max[5] = {1.23, 1.24, 1.23, 1.23, 24000.0}; +``` + +**文档风格**: +```markdown +## 快速开始 + +### 1. 安装依赖 +\`\`\`bash +pip install -r requirements.txt +\`\`\` + +### 2. 训练模型 +\`\`\`bash +python train_lstm.py +\`\`\` +``` + +--- + +## 重要提示 + +1. **GPU训练必须自动检测**: 不要假设用户已安装GPU版PyTorch +2. **文件路径必须正确**: 使用`__file__`确保保存到脚本目录 +3. **归一化参数必须输出**: 终端显示可直接复制的C++数组格式 +4. **ONNX张量维度必须设置**: MQL5中必须调用`OnnxSetInputShape`和`OnnxSetOutputShape` +5. **代码必须极简**: 优先考虑教学清晰度,而非工程复杂度 +6. **验证集必须包含**: 实现训练/验证划分和双Loss监控 +7. **早停必须实现**: 防止过拟合和浪费训练时间 +8. **最佳模型必须保存**: 不能只保存最后一轮的模型 +9. **可视化必须自动生成**: 便于学生直观理解训练过程 +10. **诊断信息必须输出**: 帮助学生判断模型质量 + +--- + +## 可选扩展 (如果有时间) + +✅ **已实现的核心功能**: +- 训练/验证集分割 +- 早停机制 +- 最佳模型保存 +- 训练曲线绘制 +- 智能诊断报告 + +⚙️ **可选的进阶功能**: +- 学习率衰减调度器 +- Dropout正则化 +- L2权重衰减 +- 支持多品种/多周期 +- 添加更多技术指标作为特征 +- 批量训练(DataLoader) + +--- + +**开始生成项目文件,使用中文输出,代码注释也用中文。** diff --git a/prompt_simple.txt b/prompt_simple.txt new file mode 100644 index 0000000..4624caf --- /dev/null +++ b/prompt_simple.txt @@ -0,0 +1,98 @@ +# LSTM量化交易项目生成 - 简单版提示词 + +## 我的需求 + +帮我生成一个**LSTM量化交易教学项目**,要求代码简洁,适合初学者。 + +## 项目要求 + +### 基本信息 +- **模型**: LSTM神经网络 +- **用途**: 预测EURUSD价格并自动交易 +- **平台**: MetaTrader 5 +- **语言**: Python + MQL5 +- **硬件**: 支持NVIDIA GPU训练 (我有RTX 4090) + +### 核心功能 +1. 从MT5获取EURUSD最近5年H1数据 +2. 用PyTorch训练LSTM模型 +3. 导出为ONNX格式 +4. 在MT5中自动交易 +5. 支持策略回测 + +### 文件要求 +只需要3个核心文件: +- `train_lstm.py` - Python训练脚本 +- `LSTM_EA.mq5` - MQL5交易EA +- `requirements.txt` - 依赖包清单 + +另外需要: +- `README.md` - 使用说明文档 + +### 代码风格 +- ✅ 极简代码 (能少一句绝不多一句) +- ✅ Python脚本 <150行 +- ✅ MQL5代码 <100行 +- ✅ 中文注释 +- ✅ 新手可看懂 + +## 技术细节 + +### Python训练脚本要做什么? +1. 检测CUDA,自动用GPU训练 +2. 连接MT5获取历史数据 +3. 归一化OHLCV数据 +4. **数据集划分**: 80%训练集 + 20%验证集 +5. 训练LSTM模型 (10根K线→预测下一根收盘价) +6. **早停机制**: 验证Loss不降则自动停止 +7. **最佳模型保存**: 自动保存验证Loss最低的模型 +8. **可视化**: 生成训练曲线图(training_loss.png) +9. 导出ONNX模型到**脚本同目录** +10. 输出归一化参数 (供MQL5使用) + +### MQL5 EA要做什么? +1. 加载ONNX模型 +2. 每小时获取最近10根K线 +3. 归一化后输入模型预测 +4. 预测涨→买入,预测跌→卖出 +5. 设置止损止盈 + +### README要写什么? +1. LSTM是什么 (简单解释) +2. 如何安装依赖 +3. 如何训练模型 +4. 如何查看训练曲线和效果评估 +5. 如何在MT5回测 +6. 常见问题 +7. 训练优化建议 (如何判断模型好坏) + +## 默认参数 +- 交易品种: EURUSD +- 时间周期: H1 +- 历史数据: 5年 +- 数据划分: 80%训练 / 20%验证 +- 时间窗口: 10根K线 +- LSTM隐藏层: 20 +- 最大训练轮数: 500 +- 早停耐心值: 20 +- 学习率: 0.001 + +## 重要提示 + +1. **GPU训练**: 自动检测CUDA,如果没有提示如何安装 +2. **文件路径**: 使用`__file__`确保ONNX保存到脚本目录 +3. **归一化参数**: 终端输出C++数组格式,可直接复制到MQL5 +4. **ONNX设置**: MQL5必须调用`OnnxSetInputShape`和`OnnxSetOutputShape` +5. **代码简洁**: 教学优先,不要过度工程化 +6. **智能训练**: 支持早停、验证集监控、最佳模型保存 +7. **可视化**: 自动生成训练曲线图便于效果评估 + +## 开始生成 + +请按顺序生成: +1. `requirements.txt` +2. `train_lstm.py` +3. `LSTM_EA.mq5` +4. `README.md` + +所有输出使用中文,代码注释也用中文。 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..699520d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +torch>=2.0.0 +numpy>=1.24.0 +onnx>=1.14.0 +onnxruntime>=1.15.0 +pandas>=2.0.0 +MetaTrader5>=5.0.0 +scikit-learn>=1.3.0 diff --git a/scaler_params.npy b/scaler_params.npy new file mode 100644 index 0000000000000000000000000000000000000000..f489709ca4bafdc786f015b6b552eab817db2963 GIT binary patch literal 501 zcmbR27wQ`j$;eQ~P_3SlTAW;@Zl$1Jt-^H=`z0QA$Z=K`K`vTLcpW1B1UsA$w;> zdm%?qA*Y5na|9z$tfr95&(F{6KM;TkZ~Kx$?xfDxLY~?}UX2K5ppx9oyh6SVh77Ph z{24$?1cC|$ftGl)wiXIyK+I0FC=>>nEfQ2H3NzcLPz=>u_`1-igP-l+m|O~~-2K^pqPpjGAD}qc(8B8uh4LkZ3Q47jC54Jf FdH`fipE>{l literal 0 HcmV?d00001 diff --git a/train_lstm.py b/train_lstm.py new file mode 100644 index 0000000..5a9181c --- /dev/null +++ b/train_lstm.py @@ -0,0 +1,465 @@ +# 导入PyTorch核心库,用于深度学习模型构建和训练 +import torch +# 导入PyTorch神经网络模块,包含各种网络层的定义 +import torch.nn as nn +# 导入NumPy,用于数值计算和数组操作 +import numpy as np +# 导入Pandas,用于数据处理和分析 +import pandas as pd +# 导入MetaTrader5接口,用于获取外汇交易数据 +import MetaTrader5 as mt5 +# 从scikit-learn导入数据归一化工具 +# 从scikit-learn导入数据归一化工具 +from sklearn.preprocessing import MinMaxScaler + +# ==================== CUDA设备检查 ==================== +# 强制检查CUDA是否可用,如果不可用则给出警告和安装指引 +if not torch.cuda.is_available(): + # 打印分隔线,使警告信息更醒目 + print("=" * 60) + # 提示用户CUDA不可用 + print("警告: CUDA不可用!") + # 显示当前安装的PyTorch版本号 + print(f"当前PyTorch版本: {torch.__version__}") + # 说明安装的是CPU版本 + print("您安装的是CPU版本的PyTorch") + # 提供换行,使信息更清晰 + print("\n请卸载当前PyTorch并安装CUDA版本:") + # 提供卸载当前版本的命令 + print("pip uninstall torch torchvision torchaudio") + # 提供安装CUDA版本的命令(针对CUDA 12.1) + print("pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121") + # 打印分隔线 + print("=" * 60) + # 询问用户是否继续使用CPU进行训练 + response = input("\n是否继续使用CPU训练? (y/n): ") + # 如果用户不输入'y',则退出程序 + if response.lower() != 'y': + quit() + +# 根据CUDA是否可用来选择计算设备(GPU或CPU) +device = torch.device("cuda" if torch.cuda.is_available() else "cpu") +# 打印分隔线 +print(f"\n{'='*60}") +# 显示当前使用的计算设备 +print(f"使用设备: {device}") +# 如果CUDA可用,显示GPU详细信息 +if torch.cuda.is_available(): + # 显示GPU型号名称 + print(f"GPU型号: {torch.cuda.get_device_name(0)}") + # 显示CUDA版本 + print(f"CUDA版本: {torch.version.cuda}") + # 显示GPU可用显存大小(转换为GB单位) + print(f"可用显存: {torch.cuda.get_device_properties(0).total_memory / 1024**3:.2f} GB") +# 打印分隔线 +# 打印分隔线 +print(f"{'='*60}\n") + +# ==================== 连接MT5并获取历史数据 ==================== +# 连接MT5获取数据 +print("正在连接MT5...") +# 初始化MT5连接,如果失败则退出程序 +if not mt5.initialize(): + # 提示用户MT5初始化失败的原因 + print("MT5初始化失败,请确保MT5已启动并登录") + # 退出程序 + quit() + +# 导入datetime模块用于处理日期和时间 +from datetime import datetime, timedelta +# 获取当前日期时间作为数据结束时间 +end_date = datetime.now() +# 计算开始时间为当前时间往前推5年(5*365天) +start_date = end_date - timedelta(days=5*365) + +# 提示用户正在获取数据,显示数据范围 +print(f"正在获取EURUSD H1数据 ({start_date.date()} 至 {end_date.date()})...") +# 从MT5获取EURUSD货币对的H1(1小时)时间框架的历史数据 +rates = mt5.copy_rates_range("EURUSD", mt5.TIMEFRAME_H1, start_date, end_date) +# 关闭MT5连接,释放资源 +mt5.shutdown() + +# 检查是否成功获取数据,如果没有数据则退出 +if rates is None or len(rates) == 0: + # 提示用户获取数据失败的可能原因 + print("获取数据失败,请检查MT5中是否有EURUSD历史数据") + # 退出程序 + quit() + +# 将获取的数据转换为Pandas DataFrame格式,便于后续处理 +df = pd.DataFrame(rates) +# 显示获取到的数据条数(使用千位分隔符格式化) +# 显示获取到的数据条数(使用千位分隔符格式化) +print(f"获取到 {len(df):,} 条EURUSD H1数据\n") + +# ==================== 数据预处理 ==================== +# 数据归一化 - 创建MinMaxScaler对象,用于将数据缩放到0-1范围 +scaler = MinMaxScaler() +# 对开盘价、最高价、最低价、收盘价和成交量进行归一化处理 +# fit_transform会先学习数据的最小值和最大值,然后进行归一化转换 +df[['open', 'high', 'low', 'close', 'tick_volume']] = scaler.fit_transform( + df[['open', 'high', 'low', 'close', 'tick_volume']]) + +# ==================== 构造训练数据集 ==================== +# 构造训练数据 - 定义函数将时间序列数据转换为监督学习格式 +def create_dataset(data, lookback=10): + """ + 将时间序列数据转换为监督学习数据集 + 参数: + data: 输入的时间序列数据 + lookback: 回溯窗口大小,表示用过去多少个时间步预测下一个时间步 + 返回: + X: 特征数据(过去lookback个时间步的数据) + y: 标签数据(下一个时间步的收盘价) + """ + # 初始化特征列表和标签列表 + X, y = [], [] + # 遍历数据,构造训练样本 + # 减去lookback+1是为了确保每个样本都有对应的标签 + for i in range(len(data) - lookback - 1): + # 提取从i到i+lookback的数据作为特征(输入) + X.append(data[i:i+lookback]) + # 提取i+lookback位置的收盘价(索引3)作为标签(输出) + y.append(data[i+lookback, 3]) # 3代表收盘价列 + # 将列表转换为NumPy数组,并指定数据类型为float32(节省内存) + return np.array(X, dtype=np.float32), np.array(y, dtype=np.float32).reshape(-1, 1) + +# 从DataFrame中提取需要的特征列(开、高、低、收、成交量) +features = df[['open', 'high', 'low', 'close', 'tick_volume']].values +# 调用create_dataset函数创建训练数据集,默认使用10个时间步作为回溯窗口 +X, y = create_dataset(features) + +# ==================== 划分训练集和验证集 ==================== +# 计算训练集大小(80%用于训练) +train_size = int(len(X) * 0.8) +# 划分训练集特征数据(前80%) +X_train_np = X[:train_size] +# 划分训练集标签数据(前80%) +y_train_np = y[:train_size] +# 划分验证集特征数据(后20%) +X_val_np = X[train_size:] +# 划分验证集标签数据(后20%) +y_val_np = y[train_size:] + +# 将训练集NumPy数组转换为PyTorch张量,并移动到指定的计算设备(GPU或CPU) +X_train = torch.from_numpy(X_train_np).to(device) +# 将训练集标签数据也转换为PyTorch张量并移动到设备 +y_train = torch.from_numpy(y_train_np).to(device) +# 将验证集特征数据转换为PyTorch张量并移动到设备 +X_val = torch.from_numpy(X_val_np).to(device) +# 将验证集标签数据转换为PyTorch张量并移动到设备 +y_val = torch.from_numpy(y_val_np).to(device) + +# 打印训练集和验证集的形状,便于确认数据维度 +print(f"训练集形状: {X_train.shape}") # 应该是 (训练样本数, 10, 5) +print(f"训练标签形状: {y_train.shape}") # 应该是 (训练样本数, 1) +print(f"验证集形状: {X_val.shape}") # 应该是 (验证样本数, 10, 5) +print(f"验证标签形状: {y_val.shape}\n") # 应该是 (验证样本数, 1) + +# ==================== 定义LSTM神经网络模型 ==================== +# LSTM模型 - 定义一个简单的LSTM神经网络类 +class SimpleLSTM(nn.Module): + """ + 简单的LSTM神经网络模型 + 用于预测外汇价格的下一个收盘价 + """ + def __init__(self): + """初始化模型结构""" + # 调用父类的初始化方法 + super().__init__() + # 定义LSTM层: + # input_size=5: 输入特征数(开、高、低、收、成交量) + # hidden_size=20: LSTM隐藏层的神经元数量 + # num_layers=1: LSTM层数 + # batch_first=True: 输入数据的第一个维度是batch_size + self.lstm = nn.LSTM(input_size=5, hidden_size=20, num_layers=1, batch_first=True) + # 定义全连接层,将LSTM的输出(20维)映射到1维(预测的收盘价) + self.fc = nn.Linear(20, 1) + + def forward(self, x): + """ + 前向传播函数 + 参数: + x: 输入数据,形状为 (batch_size, sequence_length, input_size) + 返回: + 预测值,形状为 (batch_size, 1) + """ + # 将输入数据传入LSTM层,获取输出 + # lstm_out形状: (batch_size, sequence_length, hidden_size) + # _表示隐藏状态和细胞状态,这里不需要使用 + lstm_out, _ = self.lstm(x) + # 取LSTM输出的最后一个时间步([:, -1, :]),传入全连接层得到最终预测 + return self.fc(lstm_out[:, -1, :]) + +# 创建模型实例并移动到指定设备(GPU或CPU) +model = SimpleLSTM().to(device) +# 定义优化器,使用Adam算法,学习率设为0.001 +optimizer = torch.optim.Adam(model.parameters(), lr=0.001) +# 定义损失函数,使用均方误差(MSE)损失 +# 定义损失函数,使用均方误差(MSE)损失 +criterion = nn.MSELoss() + +# ==================== 训练模型 ==================== +# 训练 +print("开始训练...") +# 打印分隔线,使训练过程更清晰 +print(f"{'='*60}") + +# 初始化记录训练过程的列表 +train_losses = [] # 记录每个epoch的训练损失 +val_losses = [] # 记录每个epoch的验证损失 +best_val_loss = float('inf') # 初始化最佳验证损失为无穷大 +best_epoch = 0 # 记录最佳模型对应的epoch +patience = 20 # 早停的耐心值(验证损失20个epoch不下降就停止) +patience_counter = 0 # 早停计数器 +best_model_state = None # 保存最佳模型的状态 + +# 训练最多500个epoch(但可能会提前停止) +for epoch in range(500): + # ========== 训练阶段 ========== + model.train() # 设置模型为训练模式 + # 清空之前的梯度,防止梯度累积 + optimizer.zero_grad() + # 前向传播:将训练数据输入模型,得到预测输出 + output = model(X_train) + # 计算训练损失:比较预测值和真实值之间的差异 + train_loss = criterion(output, y_train) + # 反向传播:计算损失函数对模型参数的梯度 + train_loss.backward() + # 更新参数:根据梯度和学习率更新模型参数 + optimizer.step() + + # ========== 验证阶段 ========== + model.eval() # 设置模型为评估模式 + with torch.no_grad(): # 验证时不计算梯度,节省内存 + # 在验证集上进行前向传播 + val_output = model(X_val) + # 计算验证损失 + val_loss = criterion(val_output, y_val) + + # 记录本epoch的损失值 + train_losses.append(train_loss.item()) + val_losses.append(val_loss.item()) + + # 每10个epoch打印一次训练进度 + if (epoch + 1) % 10 == 0: + # 显示当前epoch、训练损失和验证损失 + print(f"Epoch {epoch+1:3d} | 训练Loss: {train_loss.item():.6f} | 验证Loss: {val_loss.item():.6f}") + + # ========== 早停机制 ========== + # 如果当前验证损失是最好的 + if val_loss.item() < best_val_loss: + best_val_loss = val_loss.item() # 更新最佳验证损失 + best_epoch = epoch + 1 # 记录最佳epoch + patience_counter = 0 # 重置耐心计数器 + # 保存当前最佳模型的状态(深拷贝) + best_model_state = {k: v.cpu().clone() for k, v in model.state_dict().items()} + else: + # 如果验证损失没有改善,增加计数器 + patience_counter += 1 + + # 如果连续patience个epoch验证损失都没有改善,提前停止训练 + if patience_counter >= patience: + print(f"\n早停触发! 验证损失已经{patience}个epoch没有改善") + print(f"最佳模型出现在Epoch {best_epoch},验证Loss: {best_val_loss:.6f}") + break + +# 恢复最佳模型的参数 +if best_model_state is not None: + model.load_state_dict(best_model_state) + model = model.to(device) # 确保模型在正确的设备上 + print(f"\n已恢复最佳模型 (Epoch {best_epoch})") + +# 打印分隔线,表示训练结束 +# 打印分隔线,表示训练结束 +print(f"{'='*60}\n") + +# ==================== 绘制训练曲线 ==================== +# 绘制Loss曲线,可视化训练过程 +print("正在生成训练曲线图...") +try: + # 导入matplotlib绘图库 + import matplotlib.pyplot as plt + # 导入os模块(如果之前没有导入) + import os + # 设置中文字体支持 + plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签 + plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号 + + # 创建图形和坐标轴 + plt.figure(figsize=(12, 5)) + + # 第一个子图:训练损失和验证损失 + plt.subplot(1, 2, 1) + # 绘制训练损失曲线(蓝色) + plt.plot(train_losses, label='训练Loss', color='blue', linewidth=2) + # 绘制验证损失曲线(橙色) + plt.plot(val_losses, label='验证Loss', color='orange', linewidth=2) + # 标记最佳模型的位置(红色虚线) + plt.axvline(x=best_epoch-1, color='red', linestyle='--', label=f'最佳模型 (Epoch {best_epoch})') + # 设置x轴标签 + plt.xlabel('Epoch') + # 设置y轴标签 + plt.ylabel('Loss') + # 设置图表标题 + plt.title('训练和验证损失曲线') + # 显示图例 + plt.legend() + # 显示网格 + plt.grid(True, alpha=0.3) + + # 第二个子图:放大后期的损失曲线(更清楚地看收敛情况) + plt.subplot(1, 2, 2) + # 只显示后80%的epoch,方便观察收敛细节 + start_idx = int(len(train_losses) * 0.2) + plt.plot(range(start_idx, len(train_losses)), train_losses[start_idx:], + label='训练Loss', color='blue', linewidth=2) + plt.plot(range(start_idx, len(val_losses)), val_losses[start_idx:], + label='验证Loss', color='orange', linewidth=2) + plt.axvline(x=best_epoch-1, color='red', linestyle='--', label=f'最佳模型 (Epoch {best_epoch})') + plt.xlabel('Epoch') + plt.ylabel('Loss') + plt.title('训练和验证损失曲线(后80%)') + plt.legend() + plt.grid(True, alpha=0.3) + + # 调整子图之间的间距 + plt.tight_layout() + # 保存图表到文件(获取当前脚本所在目录) + current_dir = os.path.dirname(os.path.abspath(__file__)) + loss_plot_path = os.path.join(current_dir, "training_loss.png") + plt.savefig(loss_plot_path, dpi=150, bbox_inches='tight') + print(f"训练曲线已保存: {loss_plot_path}") + # 显示图表 + plt.show() +except ImportError: + # 如果没有安装matplotlib,跳过绘图 + print("未安装matplotlib库,跳过绘图。可运行: pip install matplotlib") +except Exception as e: + # 捕获其他绘图错误 + print(f"绘图时出错: {e}") + +print(f"{'='*60}\n") + +# ==================== 导出ONNX模型 ==================== +# 导出ONNX (先移到CPU) - ONNX是一种跨平台的模型格式 +print("正在导出ONNX模型...") +# 将模型设置为评估模式(关闭dropout等训练特有的层) +model.eval() +# 将模型从GPU移动到CPU,因为ONNX导出在CPU上更稳定 +model_cpu = model.cpu() +# 创建一个虚拟输入张量,用于ONNX导出时追踪模型结构 +# 形状为 (1, 10, 5):batch_size=1, sequence_length=10, input_size=5 +dummy_input = torch.randn(1, 10, 5) + +# 导入os模块用于文件路径操作 +import os +# 获取脚本所在目录的绝对路径 +script_dir = os.path.dirname(os.path.abspath(__file__)) +# 构造ONNX模型的完整保存路径 +onnx_path = os.path.join(script_dir, "lstm_model.onnx") +# 构造归一化参数的完整保存路径 +scaler_path = os.path.join(script_dir, "scaler_params.npy") + +# 显示脚本所在目录 +print(f"脚本所在目录: {script_dir}") +# 显示模型将要保存的路径 +print(f"模型将保存到: {onnx_path}\n") + +# 使用try-except捕获可能的导出错误 +try: + # 导出模型为ONNX格式 + torch.onnx.export( + model_cpu, # 要导出的模型 + dummy_input, # 模型的示例输入 + onnx_path, # 保存路径 + export_params=True, # 导出模型参数 + opset_version=11, # ONNX算子集版本(11是较为通用的版本) + do_constant_folding=True, # 是否执行常量折叠优化 + input_names=['input'], # 输入节点的名称 + output_names=['output'], # 输出节点的名称 + dynamic_axes={'input': {0: 'batch_size'}} # 动态维度设置(batch_size可变) + ) + + # 验证文件是否真的生成 + if os.path.exists(onnx_path): + # 获取生成的ONNX文件大小(转换为KB) + file_size = os.path.getsize(onnx_path) / 1024 + # 显示保存成功信息 + print(f"ONNX模型已保存: {onnx_path}") + # 显示文件大小 + print(f"文件大小: {file_size:.2f} KB") + + # 验证ONNX模型的正确性 + import onnx + # 加载刚才保存的ONNX模型 + onnx_model = onnx.load(onnx_path) + # 检查模型格式是否正确 + onnx.checker.check_model(onnx_model) + # 显示验证通过信息 + print("ONNX模型验证通过!") + else: + # 如果文件未生成,显示错误信息 + print(f"错误: ONNX文件未生成在 {onnx_path}") + +except Exception as e: + # 捕获并显示导出过程中的任何异常 + print(f"导出ONNX失败: {e}") + # 导入traceback模块 + import traceback + # 导入traceback模块 + import traceback + # 打印详细的错误堆栈信息,便于调试 + traceback.print_exc() + +# ==================== 保存归一化参数 ==================== +# 保存归一化参数 - 将归一化时使用的最小值和最大值保存下来 +# 保存为字典格式,包含'min'和'max'两个键 +np.save(scaler_path, {'min': scaler.data_min_, 'max': scaler.data_max_}) +# 显示归一化参数保存成功信息 +print(f"归一化参数已保存: {scaler_path}") + +# ==================== 输出归一化参数供MQL5使用 ==================== +# 输出归一化参数供MQL5使用 - 生成可以直接复制到MQL5代码中的数组声明 +# 打印分隔线 +print(f"\n{'='*60}") +# 提示这些参数需要复制到EA代码中 +print("归一化参数 (复制到LSTM_EA.mq5):") +# 打印分隔线 +print(f"{'='*60}") +# 生成data_min数组的声明语句,包含5个特征的最小值(保留5位小数) +print(f"double data_min[5] = {{{', '.join([f'{x:.5f}' for x in scaler.data_min_])}}};") +# 生成data_max数组的声明语句,包含5个特征的最大值(保留5位小数) +print(f"double data_max[5] = {{{', '.join([f'{x:.5f}' for x in scaler.data_max_])}}};") +# 打印分隔线 +print(f"{'='*60}\n") + +# ==================== 输出训练总结 ==================== +print(f"{'='*60}") +print("训练总结:") +print(f"{'='*60}") +print(f"总训练轮数: {len(train_losses)} epochs") +print(f"最佳模型: Epoch {best_epoch}") +print(f"最佳验证Loss: {best_val_loss:.6f}") +print(f"最终训练Loss: {train_losses[-1]:.6f}") +print(f"最终验证Loss: {val_losses[-1]:.6f}") +# 计算训练集和验证集的Loss差异,用于判断是否过拟合 +loss_diff = abs(train_losses[-1] - val_losses[-1]) +print(f"训练/验证Loss差异: {loss_diff:.6f}") +if loss_diff < 0.0001: + print("✓ 模型状态: 良好,没有明显的过拟合") +elif loss_diff < 0.001: + print("⚠ 模型状态: 尚可,有轻微的过拟合倾向") +else: + print("✗ 模型状态: 可能存在过拟合,建议调整模型复杂度或增加数据") +print(f"{'='*60}\n") + +# ==================== 输出完成信息和后续步骤 ==================== +# 显示训练完成信息 +print("训练完成! 下一步:") +# 提示步骤1:将ONNX模型文件复制到MT5的指定目录 +print("1. 将 lstm_model.onnx 复制到 MT5/MQL5/Files/") +# 提示步骤2:将归一化参数复制到EA代码中 +print("2. 将上面的归一化参数复制到 LSTM_EA.mq5") +# 提示步骤3:编译EA并运行回测 +print("3. 编译并运行EA进行回测") diff --git a/training_loss.png b/training_loss.png new file mode 100644 index 0000000000000000000000000000000000000000..4ae7c589f44464e96eeccd1568d861d0f04ab2ad GIT binary patch literal 97763 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∋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 z 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 W{f{e;F@u&u*}Gdy?b%Kf+W!M70Of7~ literal 0 HcmV?d00001