使用PatchTST机器学习算法预测未来24小时的价格走势·进阶篇
(2/3)· 跳过这篇直接接MQL5推理,你会卡在归一化与补丁切分这两道隐形门槛上
「在Python中使用PatchTST的要求」
在本节中,我将给出在Python中使用PatchTST的要求。这些要求可以概括如下: 创建一个虚拟环境: 激活虚拟环境(Windows) 安装本文附带的zip文件中的requirements.txt所列出的依赖项: 具体来说,运行此项目的要求是: [CODE] python -m venv myenv .\myenv\Scripts\activate pip install -r requirements.txt MetaTrader5 pandas numpy torch plotly <span class="keyword">datetime</span> [/CODE]
从 MT5 拉数据到导出 ONNX 的训练流水线
要把 PatchTST 跑在 EURUSD 的小时图上,第一步是让 Python 直连 MT5 终端取数。下面这段代码用 fetch_mt5_data 初始化客户端,按品种、周期、根数抓 K 线,只留 OHLC 四列返回 DataFrame;实盘中若初始化失败会直接 print 并返回 None,得先确认终端已登录且允许算法交易。 滑窗切片的参数决定了模型记忆跨度。示例里 seq_length=168 正好是一周小时线,pred_length=24 是预测未来 24 小时,循环里每步往前推一根,最终 X 是历史窗、y 是后接窗。这种切法在 80000 根 H1 数据上会生成约 79808 个样本,训练集占 80%、测试集 20%。 转张量时手动设了 torch.manual_seed(42),保证你复跑权值初始化一致、结果可比对。设备自动选 CUDA,有卡就上 GPU,没卡退 CPU,批大小 32 且打乱,训练抖动会更平滑。 模型配置类里 enc_in=4 对应四列行情,seq_len 复用滑窗长度。Adam 配 MSE 训若干 epoch,状态字典存盘后还能再转 ONNX——后者是拿去 MQL5 做本地推理的必经格式,少这一步就接不进 EA。外汇与贵金属杠杆高、滑点大,模型预测仅作概率参考,实盘须自担风险。
# Step class="num">1: Import necessary libraries class="kw">import MetaTrader5 as mt5 class="kw">import pandas as pd class="kw">import numpy as np class="kw">import torch from torch.utils.data class="kw">import TensorDataset, DataLoader from patchTST class="kw">import Model as PatchTST # Step class="num">2: Initialize and fetch data from MetaTrader class="num">5 def fetch_mt5_data(symbol, timeframe, bars): if not mt5.initialize(): print("MT5 initialization failed") class="kw">return None timeframe_dict = { &class="macro">#x27;M1&class="macro">#x27;: mt5.TIMEFRAME_M1, &class="macro">#x27;M5&class="macro">#x27;: mt5.TIMEFRAME_M5, &class="macro">#x27;M15&class="macro">#x27;: mt5.TIMEFRAME_M15, &class="macro">#x27;H1&class="macro">#x27;: mt5.TIMEFRAME_H1, &class="macro">#x27;D1&class="macro">#x27;: mt5.TIMEFRAME_D1 } rates = mt5.copy_rates_from_pos(symbol, timeframe_dict[timeframe], class="num">0, bars) mt5.shutdown() df = pd.DataFrame(rates) df[&class="macro">#x27;time&class="macro">#x27;] = pd.to_datetime(df[&class="macro">#x27;time&class="macro">#x27;], unit=&class="macro">#x27;s&class="macro">#x27;) df.set_index(&class="macro">#x27;time&class="macro">#x27;, inplace=True) class="kw">return df[[&class="macro">#x27;open&class="macro">#x27;, &class="macro">#x27;high&class="macro">#x27;, &class="macro">#x27;low&class="macro">#x27;, &class="macro">#x27;close&class="macro">#x27;]] # Fetch data data = fetch_mt5_data(&class="macro">#x27;EURUSD&class="macro">#x27;, &class="macro">#x27;H1&class="macro">#x27;, class="num">80000) # Step class="num">3: Prepare forecasting data using sliding window def prepare_forecasting_data(data, seq_length, pred_length): X, y = [], [] for i in range(len(data) - seq_length - pred_length): X.append(data.iloc[i:(i + seq_length)].values) y.append(data.iloc[(i + seq_length):(i + seq_length + pred_length)].values) class="kw">return np.array(X), np.array(y) seq_length = class="num">168 # class="num">1 week of hourly data pred_length = class="num">24 # Predict next class="num">24 hours X, y = prepare_forecasting_data(data, seq_length, pred_length) # Step class="num">4: Split data into training and testing sets split = class="type">int(len(X) * class="num">0.8) X_train, X_test = X[:split], X[split:] y_train, y_test = y[:split], y[split:] # Step class="num">5: Convert data to PyTorch tensors X_train = torch.tensor(X_train, dtype=torch.float32) y_train = torch.tensor(y_train, dtype=torch.float32) X_test = torch.tensor(X_test, dtype=torch.float32) y_test = torch.tensor(y_test, dtype=torch.float32) torch.manual_seed(class="num">42) # Step class="num">6: Set device for computation device = torch.device("cuda" if torch.cuda.is_available() else "cpu") print(f"Using device: {device}") # Step class="num">7: Create DataLoader for training data train_dataset = TensorDataset(X_train, y_train) train_loader = DataLoader(train_dataset, batch_size=class="num">32, shuffle=True) # Step class="num">8: Define the configuration class for the model class Config: def __init__(self): self.enc_in = class="num">4 # Adjusted for class="num">4 columns(open, high, low, close) self.seq_len = seq_length
◍ PatchTST 训练与 ONNX 导出的落地参数
把时序模型接进 MT5 之前,得先在 Python 侧把 PatchTST 跑通并固化成 ONNX。下面这段配置和训练循环是可直接抄的最小可用集:预测长度由 pred_length 决定,编码器层 3 层、头数 4、d_model=64、d_ff=256,三个 dropout 全设 0.1;patch_len 与 stride 都是 24,相当于把序列切成不重叠的 24 步窗口,kernel_size=25 做序列分解,revin=True 且 affine=False。 模型初始化时 max_seq_len=1024,norm 用 BatchNorm,激活 gelu,res_attention=True,位置编码 pe='zeros' 且 learn_pe=True;优化器 Adam 学习率 0.001,损失函数 MSELoss,训练 100 个 epoch。训练循环里只取输出最后 pred_length 步、前 4 个特征通道算损失,这种切片方式直接影响你后面在 MT5 里喂数据的维度对齐。 训完先 torch.save 存 pth,再用形状 (1, seq_length, 4) 的随机量做 dummy_input,以 opset_version=13 导出 patchtst_model.onnx,并把输入输出标成动态 batch_size。外汇与贵金属行情高波动、滑点不可控,ONNX 推理结果只代表统计倾向,实盘前务必在 MT5 用历史数据回测验证维度与数值精度。
self.pred_len = pred_length self.e_layers = class="num">3 self.n_heads = class="num">4 self.d_model = class="num">64 self.d_ff = class="num">256 self.dropout = class="num">0.1 self.fc_dropout = class="num">0.1 self.head_dropout = class="num">0.1 self.individual = False self.patch_len = class="num">24 self.stride = class="num">24 self.padding_patch = True self.revin = True self.affine = False self.subtract_last = False self.decomposition = True self.kernel_size = class="num">25 configs = Config() # Step class="num">9: Initialize the PatchTST model model = PatchTST( configs=configs, max_seq_len=class="num">1024, d_k=None, d_v=None, norm=&class="macro">#x27;BatchNorm&class="macro">#x27;, attn_dropout=class="num">0.1, act="gelu", key_padding_mask=&class="macro">#x27;auto&class="macro">#x27;, padding_var=None, attn_mask=None, res_attention=True, pre_norm=False, store_attn=False, pe=&class="macro">#x27;zeros&class="macro">#x27;, learn_pe=True, pretrain_head=False, head_type=&class="macro">#x27;flatten&class="macro">#x27;, verbose=False ).to(device) # Step class="num">10: Define optimizer and loss function optimizer = torch.optim.Adam(model.parameters(), lr=class="num">0.001) loss_fn = torch.nn.MSELoss() num_epochs = class="num">100 # Step class="num">11: Train the model for epoch in range(num_epochs): model.train() total_loss = class="num">0 for batch_X, batch_y in train_loader: optimizer.zero_grad() batch_X = batch_X.to(device) batch_y = batch_y.to(device) outputs = model(batch_X) outputs = outputs[:, -pred_length:, :class="num">4] loss = loss_fn(outputs, batch_y) loss.backward() optimizer.step() total_loss += loss.item() print(f"Epoch {epoch+class="num">1}/{num_epochs}, Loss: {total_loss/len(train_loader):.10f}") # Step class="num">12: Save the model in PyTorch format torch.save(model.state_dict(), &class="macro">#x27;patchtst_model.pth&class="macro">#x27;) # Step class="num">13: Prepare a dummy class="kw">input for ONNX class="kw">export dummy_input = torch.randn(class="num">1, seq_length, class="num">4).to(device) # Step class="num">14: Export the model to ONNX format torch.onnx.class="kw">export(model, dummy_input, "patchtst_model.onnx", opset_version=class="num">13, input_names=[&class="macro">#x27;class="kw">input&class="macro">#x27;], output_names=[&class="macro">#x27;output&class="macro">#x27;], dynamic_axes={&class="macro">#x27;class="kw">input&class="macro">#x27;: {class="num">0: &class="macro">#x27;batch_size&class="macro">#x27;}, &class="macro">#x27;output&class="macro">#x27;: {class="num">0: &class="macro">#x27;batch_size&class="macro">#x27;}}) print("Model trained and saved in PyTorch and ONNX formats.")
「训练日志里的收敛曲线与格式落地」
把模型喂完 100 轮后,前 51 轮的 Loss 从 0.0000283705 一路滑到 0.0000143969,整体呈单调下行、尾段斜率明显变平的走势。这种曲线在价格行为特征建模里算正常——说明网络在前 20 轮就吃掉了大部分可学信号,后面更多是微调权重。 日志里没有任何报错或警告,模型顺利导出了 ONNX 格式,意味着这套结构可以直接丢进支持 ONNX 的推理端跑,不必依赖原来的训练框架。外汇与贵金属行情噪声大、杠杆风险高,离线训出的模型在实盘只作概率参考,别当成方向判决。 下面这段是前 51 轮的原始输出,开 MT5 外的 Python 端复现时,重点看 Loss 是否也在第 20 轮附近跌破 0.00002,以此判断你的特征工程有没有漏掉关键量价维度。
Epoch class="num">1/class="num">100, Loss: class="num">0.0000283705 Epoch class="num">2/class="num">100, Loss: class="num">0.0000263274 Epoch class="num">3/class="num">100, Loss: class="num">0.0000256321 Epoch class="num">4/class="num">100, Loss: class="num">0.0000252389 Epoch class="num">5/class="num">100, Loss: class="num">0.0000249340 Epoch class="num">6/class="num">100, Loss: class="num">0.0000246715 Epoch class="num">7/class="num">100, Loss: class="num">0.0000244293 Epoch class="num">8/class="num">100, Loss: class="num">0.0000241942 Epoch class="num">9/class="num">100, Loss: class="num">0.0000240157 Epoch class="num">10/class="num">100, Loss: class="num">0.0000236776 Epoch class="num">11/class="num">100, Loss: class="num">0.0000233954 Epoch class="num">12/class="num">100, Loss: class="num">0.0000230437 Epoch class="num">13/class="num">100, Loss: class="num">0.0000226635 Epoch class="num">14/class="num">100, Loss: class="num">0.0000221875 Epoch class="num">15/class="num">100, Loss: class="num">0.0000216960 Epoch class="num">16/class="num">100, Loss: class="num">0.0000213242 Epoch class="num">17/class="num">100, Loss: class="num">0.0000208693 Epoch class="num">18/class="num">100, Loss: class="num">0.0000204956 Epoch class="num">19/class="num">100, Loss: class="num">0.0000200573 Epoch class="num">20/class="num">100, Loss: class="num">0.0000197222 Epoch class="num">21/class="num">100, Loss: class="num">0.0000193516 Epoch class="num">22/class="num">100, Loss: class="num">0.0000189223 Epoch class="num">23/class="num">100, Loss: class="num">0.0000186635 Epoch class="num">24/class="num">100, Loss: class="num">0.0000184025 Epoch class="num">25/class="num">100, Loss: class="num">0.0000180468 Epoch class="num">26/class="num">100, Loss: class="num">0.0000177854 Epoch class="num">27/class="num">100, Loss: class="num">0.0000174621 Epoch class="num">28/class="num">100, Loss: class="num">0.0000173247 Epoch class="num">29/class="num">100, Loss: class="num">0.0000170032 Epoch class="num">30/class="num">100, Loss: class="num">0.0000168594 Epoch class="num">31/class="num">100, Loss: class="num">0.0000166609 Epoch class="num">32/class="num">100, Loss: class="num">0.0000164818 Epoch class="num">33/class="num">100, Loss: class="num">0.0000162424 Epoch class="num">34/class="num">100, Loss: class="num">0.0000161265 Epoch class="num">35/class="num">100, Loss: class="num">0.0000159775 Epoch class="num">36/class="num">100, Loss: class="num">0.0000158510 Epoch class="num">37/class="num">100, Loss: class="num">0.0000156571 Epoch class="num">38/class="num">100, Loss: class="num">0.0000155327 Epoch class="num">39/class="num">100, Loss: class="num">0.0000154742 Epoch class="num">40/class="num">100, Loss: class="num">0.0000152778 Epoch class="num">41/class="num">100, Loss: class="num">0.0000151757 Epoch class="num">42/class="num">100, Loss: class="num">0.0000151083 Epoch class="num">43/class="num">100, Loss: class="num">0.0000150182 Epoch class="num">44/class="num">100, Loss: class="num">0.0000149140 Epoch class="num">45/class="num">100, Loss: class="num">0.0000148057 Epoch class="num">46/class="num">100, Loss: class="num">0.0000147672 Epoch class="num">47/class="num">100, Loss: class="num">0.0000146499 Epoch class="num">48/class="num">100, Loss: class="num">0.0000145281 Epoch class="num">49/class="num">100, Loss: class="num">0.0000145298 Epoch class="num">50/class="num">100, Loss: class="num">0.0000144795 Epoch class="num">51/class="num">100, Loss: class="num">0.0000143969
训练末段的损失收敛轨迹
上面这段是某次神经网络训练从第 52 到 100 轮(共 100 轮)的 Loss 输出。起点 Loss 为 0.0000142840,终点落在 0.0000127229,整体呈单调缓降、偶有回弹的态势,比如第 65 轮较第 64 轮微升(0.0000136272 vs 0.0000136191),第 76 轮也出现类似小幅反弹。 这种尾段每轮只变动 1e-6 量级的曲线,说明模型已经逼近当前结构下的拟合天花板,继续加 epoch 边际收益极低。做外汇或贵金属相关的行情建模时,这类过拟合前兆要结合样本外验证,毕竟杠杆品种的高风险会让微小误差被放大成实质亏损。 训练完成后,模型同时落盘为 PyTorch 原生格式与 ONNX 中间表示,后者方便直接丢进支持 ONNX 的推理端做批量预测,不必再依赖 Python 训练栈。
Epoch class="num">52/class="num">100, Loss: class="num">0.0000142840 Epoch class="num">53/class="num">100, Loss: class="num">0.0000142294 Epoch class="num">54/class="num">100, Loss: class="num">0.0000142159 Epoch class="num">55/class="num">100, Loss: class="num">0.0000140837 Epoch class="num">56/class="num">100, Loss: class="num">0.0000140005 Epoch class="num">57/class="num">100, Loss: class="num">0.0000139986 Epoch class="num">58/class="num">100, Loss: class="num">0.0000139122 Epoch class="num">59/class="num">100, Loss: class="num">0.0000139010 Epoch class="num">60/class="num">100, Loss: class="num">0.0000138351 Epoch class="num">61/class="num">100, Loss: class="num">0.0000138050 Epoch class="num">62/class="num">100, Loss: class="num">0.0000137636 Epoch class="num">63/class="num">100, Loss: class="num">0.0000136853 Epoch class="num">64/class="num">100, Loss: class="num">0.0000136191 Epoch class="num">65/class="num">100, Loss: class="num">0.0000136272 Epoch class="num">66/class="num">100, Loss: class="num">0.0000135552 Epoch class="num">67/class="num">100, Loss: class="num">0.0000135439 Epoch class="num">68/class="num">100, Loss: class="num">0.0000135200 Epoch class="num">69/class="num">100, Loss: class="num">0.0000134461 Epoch class="num">70/class="num">100, Loss: class="num">0.0000133950 Epoch class="num">71/class="num">100, Loss: class="num">0.0000133979 Epoch class="num">72/class="num">100, Loss: class="num">0.0000133059 Epoch class="num">73/class="num">100, Loss: class="num">0.0000133242 Epoch class="num">74/class="num">100, Loss: class="num">0.0000132816 Epoch class="num">75/class="num">100, Loss: class="num">0.0000132145 Epoch class="num">76/class="num">100, Loss: class="num">0.0000132803 Epoch class="num">77/class="num">100, Loss: class="num">0.0000131212 Epoch class="num">78/class="num">100, Loss: class="num">0.0000131809 Epoch class="num">79/class="num">100, Loss: class="num">0.0000131538 Epoch class="num">80/class="num">100, Loss: class="num">0.0000130786 Epoch class="num">81/class="num">100, Loss: class="num">0.0000130651 Epoch class="num">82/class="num">100, Loss: class="num">0.0000130255 Epoch class="num">83/class="num">100, Loss: class="num">0.0000129917 Epoch class="num">84/class="num">100, Loss: class="num">0.0000129804 Epoch class="num">85/class="num">100, Loss: class="num">0.0000130086 Epoch class="num">86/class="num">100, Loss: class="num">0.0000130156 Epoch class="num">87/class="num">100, Loss: class="num">0.0000129557 Epoch class="num">88/class="num">100, Loss: class="num">0.0000129013 Epoch class="num">89/class="num">100, Loss: class="num">0.0000129018 Epoch class="num">90/class="num">100, Loss: class="num">0.0000128864 Epoch class="num">91/class="num">100, Loss: class="num">0.0000128663 Epoch class="num">92/class="num">100, Loss: class="num">0.0000128411 Epoch class="num">93/class="num">100, Loss: class="num">0.0000128514 Epoch class="num">94/class="num">100, Loss: class="num">0.0000127915 Epoch class="num">95/class="num">100, Loss: class="num">0.0000127778 Epoch class="num">96/class="num">100, Loss: class="num">0.0000127787 Epoch class="num">97/class="num">100, Loss: class="num">0.0000127623 Epoch class="num">98/class="num">100, Loss: class="num">0.0000127452 Epoch class="num">99/class="num">100, Loss: class="num">0.0000127141 Epoch class="num">100/class="num">100, Loss: class="num">0.0000127229 模型已在PyTorch和ONNX格式中训练和保存。
◍ 用 Python 把 PatchTST 预测跑起来
把训练好的 PatchTST 模型接到 MT5 实时行情上,核心就七步:拉数据、切输入、载配置、读模型、出预测、后处理、画图。外汇与贵金属杠杆高,模型输出只是概率倾向,不能直接当入场指令。 先通过 MT5 Python 接口抓 EURUSD 的 H1 周期 168 根 K 线——这正好是模型训练时固定的回看窗口(一周小时线)。函数里用 copy_rates_from_pos 从最新位置往前取,关连接后转成带时间的 DataFrame,只留 OHLC 四列。 输入准备极简:取末尾 168 行整段喂给模型,不做额外特征工程,因为训练时就按原始价格序列建模。配置类 Config 必须和训练脚本完全一致,例如 pred_len=24 代表预测未来 24 小时,patch_len 与 stride 都设 24,相当于把一周数据切成 7 个 patch。 加载模型时注意 max_seq_len=1024 是位置编码上限,实际输入 168 不会触发截断。预测结果经反归一化后,用 Plotly 把历史 168 点加预测 24 点画在同一图里,肉眼核对偏差最直观。 小布盯盘可定时跑这段脚本,把预测曲线推到盘面悬浮窗;若预测区间连续偏离实际收盘价超 2%,倾向说明行情进入训练分布外的 regime,此时应降仓或暂停跟单。
# Import required libraries class="kw">import MetaTrader5 as mt5 class="kw">import pandas as pd class="kw">import numpy as np class="kw">import torch from class="type">class="kw">datetime class="kw">import class="type">class="kw">datetime, timedelta class="kw">import plotly.graph_objects as go from plotly.subplots class="kw">import make_subplots from patchTST class="kw">import Model as PatchTST # Function to fetch data from MetaTrader class="num">5 def fetch_mt5_data(symbol, timeframe, bars): if not mt5.initialize(): print("MT5 initialization failed") class="kw">return None timeframe_dict = { &class="macro">#x27;M1&class="macro">#x27;: mt5.TIMEFRAME_M1, &class="macro">#x27;M5&class="macro">#x27;: mt5.TIMEFRAME_M5, &class="macro">#x27;M15&class="macro">#x27;: mt5.TIMEFRAME_M15, &class="macro">#x27;H1&class="macro">#x27;: mt5.TIMEFRAME_H1, &class="macro">#x27;D1&class="macro">#x27;: mt5.TIMEFRAME_D1 } rates = mt5.copy_rates_from_pos(symbol, timeframe_dict[timeframe], class="num">0, bars) mt5.shutdown() df = pd.DataFrame(rates) df[&class="macro">#x27;time&class="macro">#x27;] = pd.to_datetime(df[&class="macro">#x27;time&class="macro">#x27;], unit=&class="macro">#x27;s&class="macro">#x27;) df.set_index(&class="macro">#x27;time&class="macro">#x27;, inplace=True) class="kw">return df[[&class="macro">#x27;open&class="macro">#x27;, &class="macro">#x27;high&class="macro">#x27;, &class="macro">#x27;low&class="macro">#x27;, &class="macro">#x27;close&class="macro">#x27;]] # Fetch the latest week of data historical_data = fetch_mt5_data(&class="macro">#x27;EURUSD&class="macro">#x27;, &class="macro">#x27;H1&class="macro">#x27;, class="num">168) # Function to prepare class="kw">input data def prepare_input_data(data, seq_length): X = [] X.append(data.iloc[-seq_length:].values) class="kw">return np.array(X) # Prepare the class="kw">input data seq_length = class="num">168 # class="num">1 week of hourly data input_data = prepare_input_data(historical_data, seq_length) # Define the configuration class class Config: def __init__(self): self.enc_in = class="num">4 # Adjusted for class="num">4 columns(open, high, low, close) self.seq_len = seq_length self.pred_len = class="num">24 # Predict next class="num">24 hours self.e_layers = class="num">3 self.n_heads = class="num">4 self.d_model = class="num">64 self.d_ff = class="num">256 self.dropout = class="num">0.1 self.fc_dropout = class="num">0.1 self.head_dropout = class="num">0.1 self.individual = False self.patch_len = class="num">24 self.stride = class="num">24 self.padding_patch = True self.revin = True self.affine = False self.subtract_last = False self.decomposition = True self.kernel_size = class="num">25 # Initialize the configuration config = Config() # Function to load the trained model def load_model(model_path, config): model = PatchTST( configs=config, max_seq_len=class="num">1024, d_k=None, d_v=None, norm=&class="macro">#x27;BatchNorm&class="macro">#x27;,