神经网络在交易中的实际应用 Python (第一部分)·综合运用
把指标塞进神经网络输入数组的写法
做 MT5 上的 AIGC 行情判定,第一步是把多周期指标摊平进一个 double 数组。下面这段就是把第 41~59 号特征位填满,注意 High 与 Open 两套取值分开存,方便后续网络区分波动来源与开盘惯性。 WPR 取了 i+1 偏移,BullsPower 也偏移一根,说明输入刻意混入了「上一根」动量,而非纯当前柱。MACD_High 乘 10000、OsMA_High 乘 100000,是把小数指标放大到与价格量级接近,避免浮点权重被网络忽略。 StdDev 同样乘 10000,Stochastic 原值不动(本身 0~100),ADX_PLUSDI 也是原值。最后用 FileWrite 把 inS[0] 到 inS[52] 一次性落盘到 InputNet2OutNet1Max 文件,这就是给外部推理端喂样本的原始通道。外汇与贵金属波动剧烈,这种放大系数在不同品种上可能要重调,否则输入分布会偏掉。
inS[class="num">41]=WPR[i+class="num">1]; inS[class="num">42]=MACD_High[i]*class="num">10000; inS[class="num">43]=OsMA_High[i]*class="num">100000; inS[class="num">44]=TriX_High[i]*class="num">100000;; inS[class="num">45]=BullsPower[i+class="num">1]*class="num">1000; inS[class="num">46]=ADX_PLUSDI[i+class="num">1]; inS[class="num">47]=StdDev_High[i]*class="num">10000; inS[class="num">48]=Stochastic0[i]; inS[class="num">49]=Stochastic1[i]; inS[class="num">50]=CCI_Open[i]; inS[class="num">51]=Momentum_Open[i]; inS[class="num">52]=RSI_Open[i];; inS[class="num">53]=WPR[i]; inS[class="num">54]=MACD_Open[i]*class="num">10000; inS[class="num">55]=OsMA_Open[i]*class="num">100000; inS[class="num">56]=TriX_Open[i]*class="num">100000;; inS[class="num">57]=BullsPower[i]*class="num">1000; inS[class="num">58]=ADX_PLUSDI[i]; inS[class="num">59]=StdDev_Open[i]*class="num">10000; FileWrite(HandleInputNet2OutNet1Max, inS[class="num">0],inS[class="num">1],inS[class="num">2],inS[class="num">3],inS[class="num">4],inS[class="num">5],inS[class="num">6],inS[class="num">7],inS[class="num">8],inS[class="num">9],inS[class="num">10],inS[class="num">11],inS[class="num">12],inS[class="num">13], inS[class="num">14],inS[class="num">15],inS[class="num">16],inS[class="num">17],inS[class="num">18],inS[class="num">19],inS[class="num">20],inS[class="num">21],inS[class="num">22],inS[class="num">23],inS[class="num">24],inS[class="num">25],inS[class="num">26], inS[class="num">27],inS[class="num">28],inS[class="num">29],inS[class="num">30],inS[class="num">31],inS[class="num">32],inS[class="num">33],inS[class="num">34],inS[class="num">35],inS[class="num">36],inS[class="num">37],inS[class="num">38],inS[class="num">39], inS[class="num">40],inS[class="num">41],inS[class="num">42],inS[class="num">43],inS[class="num">44],inS[class="num">45],inS[class="num">46],inS[class="num">47],inS[class="num">48],inS[class="num">49],inS[class="num">50],inS[class="num">51],inS[class="num">52],
◍ 把 H1 与 D1 开盘差写进训练文件的实操片段
这段脚本在做一件事:把小时图开盘价和日线开盘价的偏离度,按点值放大后落盘,供后续神经网络训练用。计算式是 (iOpen(NULL,PERIOD_H1,i) - iOpen(NULL,PERIOD_D1,iBarShift(NULL,PERIOD_D1,iTime(NULL,PERIOD_H1,i)))) * 10000,也就是把价差换算成标准点(1 点 = 0.0001),EURUSD 上 0.0050 的落差会记成 50。 紧随其后的指标 DibMin1-1 是配套观测件:独立子窗口、单线绘制、值域锁在 -2 到 2,初始画 500 根历史。它不预测方向,只把跨周期开盘错位可视化,帮你判断样本里噪声有多大。 外汇与贵金属跨周期价差受跳空影响,实盘复制时大概率遇到周末缺口导致的异常值,建议先跑模拟盘验证。
FileWrite(HandleOutNet2Max, (iOpen(NULL,PERIOD_H1,i)-iOpen(NULL,PERIOD_D1,iBarShift(NULL,PERIOD_D1,iTime(NULL,PERIOD_H1,i))))*class="num">10000); class="macro">#class="kw">property indicator_separate_window class="macro">#class="kw">property indicator_buffers class="num">1 class="macro">#class="kw">property indicator_plots class="num">1 class="macro">#class="kw">property indicator_type1 DRAW_LINE class="macro">#class="kw">property indicator_minimum -class="num">2 class="macro">#class="kw">property indicator_maximum class="num">2 class="macro">#class="kw">property indicator_color1 Red class="kw">input class="type">int History=class="num">500; class="type">class="kw">double Buf[]; class="type">int OnInit() { SetIndexBuffer(class="num">0,Buf,INDICATOR_DATA); class="kw">return(INIT_SUCCEEDED); } class="type">int OnCalculate(class="kw">const class="type">int rates_total, class="kw">const class="type">int prev_calculated,
「用 H1 与 D1 低点比对标记日内方向」
这段自定义指标的核心逻辑,是把当前 H1 收盘价序列和日线低点做比对,给每个小时 K 线打上方向标记。它先取 H1 总柱数减 1 作为循环起点,若超过 History 上限就截断,History 为 0 时则直接用全部 H1 柱数,避免越界。 循环里用 time[i]%86400/3600 算出当天已过小时数 Calc,再取对应日线的低点 min1。随后从当前柱往前推 Calc 根 H1 柱,只要任一根 H1 低点高于日线低点,就标记 min=1,否则立刻置 -1 并跳出。最终 Buf[i] 只可能是 1 或 -1,反映截至该小时日内低点是否被 H1 结构刷新。 实盘验证时把 History 参数设小(如 500),加载到 EURUSD 的 H1 图,能直观看到 Buf 在亚盘、美盘切换时的翻转频率差异。外汇与贵金属杠杆高,这类标记仅作结构参考,不代表后续概率倾向。
class="kw">const class="type">class="kw">datetime &time[], class="kw">const class="type">class="kw">double &open[], class="kw">const class="type">class="kw">double &high[], class="kw">const class="type">class="kw">double &low[], class="kw">const class="type">class="kw">double &close[], class="kw">const class="type">long &tick_volume[], class="kw">const class="type">long &volume[], class="kw">const class="type">int &spread[]) { class=class="str">"cmt">//--- class="type">int i,z,Calc; class="type">class="kw">double price; i=iBars(NULL,PERIOD_H1)-class="num">1; if(i>History-class="num">1) i=History-class="num">1; if(History==class="num">0) i=iBars(NULL,PERIOD_H1)-class="num">1; ArraySetAsSeries(Buf,true); ArraySetAsSeries(time,true); while(i>=class="num">0) { class="type">int min=class="num">0; Calc=(class="type">int)time[i]%class="num">86400/class="num">3600; class="type">class="kw">double min1=iLow(NULL,PERIOD_D1,iBarShift(NULL,PERIOD_D1,iTime(NULL,PERIOD_H1,i))); for(z=class="num">0;z<=Calc;z++) { price=iLow(NULL,PERIOD_H1,i+z); if(min1<price) { min=class="num">1; }else { min=-class="num">1; class="kw">break; } } Buf[i]=min; i--; } class="kw">return(rates_total); }
给 H1 波动切一个独立窗口的裸指标
下面这段 MT5 自定义指标把输出塞进副图,纵向范围锁在 -2 到 2,画线颜色取 LightSeaGreen,标签写死成 DibMax1-1。它只声明 1 个缓冲区和 1 个 plot,本质是给后续差值计算留好画布,并不在 OnInit 里做任何复杂初始化。 核心参数只有一个 input int History=500,控制回看根数;若设成 0,则直接取当前品种 H1 周期的全部 BAR 数。外汇与贵金属杠杆高、跳空频繁,这种硬编码周期与根数的写法在换品种时可能漏算远端 K 线,实盘前务必手调。 OnCalculate 里先用 iBars(NULL,PERIOD_H1)-1 拿到 H1 最后一根下标,再与 History-1 取小,保证不越界;随后把 Buf 和 time 都设成时间序列(最新在 0 号位)。主循环从 i 往下扫,用 time[i]%86400/3600 算出该 BAR 所属的 UTC 小时数 Calc,为按小时切片统计做准备——你打开 MT5 把这段贴进编辑器,改 History 参数就能直接看副图是否按预期出线。
class="macro">#class="kw">property version "class="num">1.00" class="macro">#class="kw">property indicator_separate_window class="macro">#class="kw">property indicator_buffers class="num">1 class="macro">#class="kw">property indicator_plots class="num">1 class="macro">#class="kw">property indicator_type1 DRAW_LINE class="macro">#class="kw">property indicator_minimum -class="num">2 class="macro">#class="kw">property indicator_maximum class="num">2 class="macro">#class="kw">property indicator_color1 LightSeaGreen class="macro">#class="kw">property indicator_label1 "DibMax1-class="num">1" class=class="str">"cmt">//---- class="kw">input parameters class="kw">input class="type">int History=class="num">500; class="type">class="kw">double Buf[]; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Custom indicator initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- indicator buffers mapping SetIndexBuffer(class="num">0,Buf,INDICATOR_DATA); class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Custom indicator iteration function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnCalculate(class="kw">const class="type">int rates_total, class="kw">const class="type">int prev_calculated, class="kw">const class="type">class="kw">datetime &time[], class="kw">const class="type">class="kw">double &open[], class="kw">const class="type">class="kw">double &high[], class="kw">const class="type">class="kw">double &low[], class="kw">const class="type">class="kw">double &close[], class="kw">const class="type">long &tick_volume[], class="kw">const class="type">long &volume[], class="kw">const class="type">int &spread[]) { class=class="str">"cmt">//--- class="type">int i,z,Calc; class="type">class="kw">double price; i=iBars(NULL,PERIOD_H1)-class="num">1; if (i>History-class="num">1)i=History-class="num">1; if (History==class="num">0) i=iBars(NULL,PERIOD_H1)-class="num">1; ArraySetAsSeries(Buf,true); ArraySetAsSeries(time,true); while(i>=class="num">0) { class="type">int max=class="num">0; Calc=(class="type">int)time[i]%class="num">86400/class="num">3600;
◍ 用日线高点框定小时线回撤
这段逻辑解决一个具体动作:拿日线最高价当标尺,去扫一遍当前小时 K 线及之后若干根,看价格有没有向上刺破它。 先取日线最高价 max1,用 iBarShift 把小时 K 线时间映射到日线 Bar 序号,再读该日最高。若小时线上任一根 high 不低于 max1,max 记 1(未破);一旦某根 high 超过 max1,立刻 max=-1 并 break,说明回撤已被日高压制失败。 Buf[i] 存的就是这个判定结果,逐根向左推进。外汇与贵金属波动受杠杆放大,此类刺破信号仅代表概率倾向,实盘须结合止损。 代码逐行拆解: double max1=iHigh(NULL,PERIOD_D1,iBarShift(NULL,PERIOD_D1,iTime(NULL,PERIOD_H1,i))); // 取日线周期上、与当前小时 Bar 时间对应的那根日 K 的最高价 for(z=0;z<=Calc;z++) // 从当前小时 Bar 开始,向右数 Calc 根做循环 { price=iHigh(NULL,PERIOD_H1,i+z);//+1-1 // 取小时周期第 i+z 根的最高价 if(max1>price) // 若日线高点仍高于这根小时高点 { max=1; // 标记未上破 }else { max=-1; // 标记已上破 break; // 跳出循环,不再检查后续 } } Buf[i]=max; // 把判定写进缓冲区对应位置 i--; // 索引左移一根 } return(rates_total); // 返回处理的 Bar 总数 }
class="type">class="kw">double max1=iHigh(NULL,PERIOD_D1,iBarShift(NULL,PERIOD_D1,iTime(NULL,PERIOD_H1,i))); for(z=class="num">0;z<=Calc;z++) { price=iHigh(NULL,PERIOD_H1,i+z);class=class="str">"cmt">//+class="num">1-class="num">1 if(max1>price) { max=class="num">1; }else { max=-class="num">1; class="kw">break; } } Buf[i]=max; i--; } class="kw">return(rates_total); }
「用 Python 把 EURUSD 双网络跑通」
在 MT5 的集成环境里装好 Python 3.8 并接上 TensorFlow、Keras、Numpy、Pandas 后,训练脚本 EURUSDPyTren.py 要丢进 \Common\Files 目录。脚本核心是建两个序贯网络:Net1 输入层 22 神经元、输出层 60 神经元,Net2 接收 Net1 的输出再压成 1 个值。 超参数先按保守设定走:Net1 训练 10 个 epoch、batch_size 10、30% 数据做验证;Net2 则跑到 100 个 epoch。这些数只是起点,隐藏层加减、epoch 多少都可能在回测里改变曲线形状,外汇与贵金属波动大,实盘前务必自测。 跑完会在 \Common\Files 落下 net1Max.h5、net1Min.h5、net2Max.h5、net2Min.h5 四个权重,以及 IndicatorMax、IndicatorMin、Indicator 三个结果文件。EA 端用 PythonTestExpert 在 H1 仅开盘价模式、2011 年至今区间去模拟接收,产生 EURUSDTest 与 EURUSDDate 供脚本喂数据。 下面这段是脚本前半的精简版,注意标准化用 mean/std 原地减除,训练完直接 save 成 h5:
class="kw">import numpy as np class="kw">import pandas as pd class="kw">import tensorflow as tf from tensorflow.keras.models class="kw">import Sequential from tensorflow.keras.layers class="kw">import Dense from tensorflow.keras.models class="kw">import load_model # 从csv读入三个数据框:Net1输入、Net2输入(即Net1输出)、Net2输出 InputNet1=pd.read_csv(&class="macro">#x27;EURUSDInputNet1Min.csv&class="macro">#x27;, delimiter=&class="macro">#x27;;&class="macro">#x27;,header=None) InputNet2OutNet1=pd.read_csv(&class="macro">#x27;EURUSDInputNet2OutNet1Min.csv&class="macro">#x27;, delimiter=&class="macro">#x27;;&class="macro">#x27;,header=None) OutNet2=pd.read_csv(&class="macro">#x27;EURUSDOutNet2Min.csv&class="macro">#x27;, delimiter=&class="macro">#x27;;&class="macro">#x27;,header=None) # 对InputNet1做零均值单位方差标准化 mean = InputNet1.mean(axis=class="num">0) std = InputNet1.std(axis=class="num">0) InputNet1 -= mean InputNet1 /= std # 对InputNet2OutNet1同样标准化 mean = InputNet2OutNet1.mean(axis=class="num">0) std = InputNet2OutNet1.std(axis=class="num">0) InputNet2OutNet1 -= mean InputNet2OutNet1 /= std # 建Net1:22输入relu,60输出线性 Net1Min = Sequential() Net1Min.add(Dense(class="num">22, activation=&class="macro">#x27;relu&class="macro">#x27;, input_shape=(InputNet1.shape[class="num">1],))) Net1Min.add(Dense(class="num">60)) Net1Min.compile(optimizer=&class="macro">#x27;adam&class="macro">#x27;, loss=&class="macro">#x27;mse&class="macro">#x27;, metrics=[&class="macro">#x27;mse&class="macro">#x27;]) print(Net1Min.summary()) # 训练10轮,批大小10,class="num">30%验证 Net1Min.fit(InputNet1, InputNet2OutNet1, epochs=class="num">10, batch_size=class="num">10,verbose=class="num">2,validation_split=class="num">0.3) Net1Min.save(&class="macro">#x27;net1Min.h5&class="macro">#x27;) # 对OutNet2标准化 mean = OutNet2.mean(axis=class="num">0) std = OutNet2.std(axis=class="num">0) OutNet2 -= mean OutNet2 /= std # 建Net2:60输入relu,1输出 Net2Min = Sequential() Net2Min.add(Dense(class="num">60, activation=&class="macro">#x27;relu&class="macro">#x27;, input_shape=(InputNet2OutNet1.shape[class="num">1],))) Net2Min.add(Dense(class="num">1)) Net2Min.compile(optimizer=&class="macro">#x27;adam&class="macro">#x27;, loss=&class="macro">#x27;mse&class="macro">#x27;, metrics=[&class="macro">#x27;mae&class="macro">#x27;]) print(Net2Min.summary()) # 训练100轮 Net2Min.fit(InputNet2OutNet1, OutNet2, epochs=class="num">100, batch_size=class="num">10,verbose=class="num">2,validation_split=class="num">0.3) Net2Min.save(&class="macro">#x27;net2Min.h5&class="macro">#x27;) # 后面Max分支同构,略;最后载入模型做测试 NetTest=pd.read_csv(&class="macro">#x27;EURUSDTest.csv&class="macro">#x27;, delimiter=&class="macro">#x27;;&class="macro">#x27;,header=None) Date=pd.read_csv(&class="macro">#x27;EURUSDDate.csv&class="macro">#x27;, delimiter=&class="macro">#x27;;&class="macro">#x27;,header=None) Net1Min = load_model(&class="macro">#x27;net1Min.h5&class="macro">#x27;)
用 Python 堆叠网络算出 EURUSD 的极值指标
这段脚本把两层 Keras 网络串起来,先由 Net1 把 22 维标准化输入压成中间表征,再交 Net2 吐出 Min/Max 两套极值预测。训练时 Net1Min 用 10 个 epoch、batch_size=10、validation_split=0.3 拟合,输入按列做了减均值除标准差的归一化(mean/std 来自 InputNet1 的 axis=0)。 预测阶段直接 load_model 读回 net1Min.h5 / net2Min.h5 / net1Max.h5 / net2Max.h5,对 EURUSDTest.csv 跑前向,把 Net2Min、Net2Max 按日期拼进 Date 帧,分别写出 IndicatorMin.csv、IndicatorMax.csv 和双列的 Indicator.csv,分隔符统一用分号、不写表头。 MT5 侧只留了接口骨架:handleInput、HandleDate 两个 int 句柄和 double in[22] 输入数组,还没接文件读取。外汇与贵金属属高风险品种,这类离线模型输出只作概率参考,实盘前务必在策略测试器回测验证。
Net2Min= load_model(&class="macro">#x27;net2Min.h5&class="macro">#x27;) Net1Max = load_model(&class="macro">#x27;net1Max.h5&class="macro">#x27;) Net2Max= load_model(&class="macro">#x27;net2Max.h5&class="macro">#x27;) Net1Min = Net1Min.predict(NetTest) Net2Min = Net2Min.predict(Net1Min) Net1Max = Net1Max.predict(NetTest) Net2Max = Net2Max.predict(Net1Max) Date=pd.DataFrame(Date) Date[&class="macro">#x27;class="num">0&class="macro">#x27;] = Net2Min Date.to_csv(&class="macro">#x27;IndicatorMin.csv&class="macro">#x27;,index=False, header=False,sep=&class="macro">#x27;;&class="macro">#x27;) Date[&class="macro">#x27;class="num">0&class="macro">#x27;] = Net2Max Date.to_csv(&class="macro">#x27;IndicatorMax.csv&class="macro">#x27;,index=False, header=False,sep=&class="macro">#x27;;&class="macro">#x27;) Date[&class="macro">#x27;class="num">0&class="macro">#x27;] = Net2Min Date[&class="macro">#x27;class="num">1&class="macro">#x27;] = Net2Max Date.to_csv(&class="macro">#x27;Indicator.csv&class="macro">#x27;,index=False, header=False,sep=&class="macro">#x27;;&class="macro">#x27;) class="kw">input(&class="macro">#x27;Press ENTER to exit&class="macro">#x27;) class="kw">import numpy as np class="kw">import pandas as pd class="kw">import tensorflow as tf from tensorflow.keras.models class="kw">import Sequential from tensorflow.keras.layers class="kw">import Dense from tensorflow.keras.models class="kw">import load_model InputNet1=pd.read_csv(&class="macro">#x27;EURUSDInputNet1Min.csv&class="macro">#x27;, delimiter=&class="macro">#x27;;&class="macro">#x27;,header=None) InputNet2OutNet1=pd.read_csv(&class="macro">#x27;EURUSDInputNet2OutNet1Min.csv&class="macro">#x27;, delimiter=&class="macro">#x27;;&class="macro">#x27;,header=None) OutNet2=pd.read_csv(&class="macro">#x27;EURUSDOutNet2Min.csv&class="macro">#x27;, delimiter=&class="macro">#x27;;&class="macro">#x27;,header=None) mean = InputNet1.mean(axis=class="num">0) std = InputNet1.std(axis=class="num">0) InputNet1 -= mean InputNet1 /= std Net1Min = Sequential() Net1Min.add(Dense(class="num">22, activation=&class="macro">#x27;relu&class="macro">#x27;, input_shape=(InputNet1.shape[class="num">1],))) Net1Min.add(Dense(class="num">60)) Net1Min = Sequential() Net1Min.add(Dense(class="num">22, activation=&class="macro">#x27;relu&class="macro">#x27;, input_shape=(InputNet1.shape[class="num">1],))) Net1Min.add(Dense(class="num">11)) Net1Min.add(Dense(class="num">60)) Net1Max.compile(optimizer=&class="macro">#x27;adam&class="macro">#x27;, loss=&class="macro">#x27;mse&class="macro">#x27;, metrics=[&class="macro">#x27;mse&class="macro">#x27;]) print(Net1Max.summary()) Net1Min.fit(InputNet1, InputNet2OutNet1, epochs=class="num">10, batch_size=class="num">10,verbose=class="num">2,validation_split=class="num">0.3) Net1Min.save(&class="macro">#x27;net1Min.h5&class="macro">#x27;) NetTest=pd.read_csv(&class="macro">#x27;EURUSDTest.csv&class="macro">#x27;, delimiter=&class="macro">#x27;;&class="macro">#x27;,header=None) Date=pd.read_csv(&class="macro">#x27;EURUSDDate.csv&class="macro">#x27;, delimiter=&class="macro">#x27;;&class="macro">#x27;,header=None) Net1Min = load_model(&class="macro">#x27;net1Min.h5&class="macro">#x27;) Net2Min= load_model(&class="macro">#x27;net2Min.h5&class="macro">#x27;) Net1Max = load_model(&class="macro">#x27;net1Max.h5&class="macro">#x27;) Net2Max= load_model(&class="macro">#x27;net2Max.h5&class="macro">#x27;) Net1Min = Net1Min.predict(NetTest) Net2Min = Net2Min.predict(Net1Min) Net1Max = Net1Max.predict(NetTest) Net2Max = Net2Max.predict(Net1Max) Date=pd.DataFrame(Date) Date[&class="macro">#x27;class="num">0&class="macro">#x27;] = Net2Min Date.to_csv(&class="macro">#x27;IndicatorMin.csv&class="macro">#x27;,index=False, header=False,sep=&class="macro">#x27;;&class="macro">#x27;) Date[&class="macro">#x27;class="num">0&class="macro">#x27;] = Net2Max Date.to_csv(&class="macro">#x27;IndicatorMax.csv&class="macro">#x27;,index=False, header=False,sep=&class="macro">#x27;;&class="macro">#x27;) Date[&class="macro">#x27;class="num">0&class="macro">#x27;] = Net2Min Date[&class="macro">#x27;class="num">1&class="macro">#x27;] = Net2Max Date.to_csv(&class="macro">#x27;Indicator.csv&class="macro">#x27;,index=False, header=False,sep=&class="macro">#x27;;&class="macro">#x27;) class="kw">input(&class="macro">#x27;Press ENTER to exit&class="macro">#x27;)
◍ EA 初始化与日线价差特征的实时采样
这段 MT5 专家顾问代码把跨周期价差压成整型数组,方便后续喂给模型或写盘。初始化时开两个共享 CSV:一个写当前品种 Test.csv,一个读写 Date.csv,都走 FILE_COMMON 公共目录,多终端能同时读。 OnTick 里先做一个 15 长度的滑动:把 in[0..14] 整体左移 5 位(in[i]=in[i+5]),腾出尾部存新特征。接着用日线 D1 和小时线 H1 的 open/high/low 算 7 个价差,乘 100000 或 10000 转成整数点值——比如 in[15] 是 (日线开盘-日线最低)*100000,in[18] 是 (日线最高-1小时前开盘)*10000。 外汇和贵金属点差放大后整数化会丢小数精度,回测和实盘可能倾向出现细微偏差;高杠杆下这类特征若直接驱动下单,风险显著。开 MT5 把这段贴进 EA,改 Symbol() 前缀就能看自己的品种写出什么数列。 别把正态当圣经 价差乘 10 万后分布常被误认高斯,黄金跳空日里 in[17] 尾部分位数会明显厚尾,过滤信号时最好按分位截断而不是硬套标准差。
class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- handleInput=FileOpen(Symbol()+"Test.csv",FILE_CSV|FILE_WRITE|FILE_SHARE_READ|FILE_ANSI|FILE_COMMON,";"); HandleDate=FileOpen(Symbol()+"Date.csv",FILE_CSV|FILE_READ|FILE_WRITE|FILE_ANSI|FILE_COMMON,";"); class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(class="kw">const class="type">int reason) { class=class="str">"cmt">//--- FileClose(handleInput); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert tick function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { for(class="type">int i=class="num">0; i<=class="num">14; i++) { in[i]=in[i+class="num">5]; } in[class="num">15]=((iOpen(NULL,PERIOD_D1,class="num">0)-iLow(NULL,PERIOD_D1,class="num">0))*class="num">100000); in[class="num">16]=((iHigh(NULL,PERIOD_D1,class="num">0)-iOpen(NULL,PERIOD_D1,class="num">0))*class="num">100000); in[class="num">17]=((iHigh(NULL,PERIOD_D1,class="num">0)-iLow(NULL,PERIOD_D1,class="num">0))*class="num">100000); in[class="num">18]=((iHigh(NULL,PERIOD_D1,class="num">0)-iOpen(NULL,PERIOD_H1,class="num">1))*class="num">10000); in[class="num">19]=((iOpen(NULL,PERIOD_H1,class="num">1)-iLow(NULL,PERIOD_D1,class="num">0))*class="num">10000); in[class="num">20]=((iHigh(NULL,PERIOD_D1,class="num">0)-iOpen(NULL,PERIOD_H1,class="num">0))*class="num">10000); in[class="num">21]=((iOpen(NULL,PERIOD_H1,class="num">0)-iLow(NULL,PERIOD_D1,class="num">0))*class="num">10000); FileWrite(handleInput,
「把指标输出落盘到共享 CSV」
这段 MT5 指标代码把计算后的数值和当前时间写进一个公共目录下的 CSV,方便外部程序或另一段 EA 直接读。 初始化时仅做两件事:把两个绘图缓冲绑定到指标线,并把小数位锁死为 5 位——做外汇或贵金属时这对应常见的 5 位报价精度,改了可能让跨文件比对错位。 OnCalculate 里先以 FILE_COMMON 标志打开 Indicator.csv,用分号做分隔符;再读首行字符串转成时间 Date0 并立即关句柄。注意它没做文件存在性判断,首次运行若文件缺失会返回 INVALID_HANDLE,Date0 变成 1970 年起点。 外汇与贵金属杠杆高、滑点大,任何依赖盘外文件同步的逻辑都可能因写入延迟产生信号漂移,上机前务必在策略测试器用 2019 年以后的样本跑一遍确认时间戳对齐。
in[class="num">0],in[class="num">1],in[class="num">2],in[class="num">3],in[class="num">4],in[class="num">5],in[class="num">6],in[class="num">7],in[class="num">8],in[class="num">9],in[class="num">10],in[class="num">11],in[class="num">12],in[class="num">13],in[class="num">14],in[class="num">15], in[class="num">16],in[class="num">17],in[class="num">18],in[class="num">19],in[class="num">20],in[class="num">21]); FileWrite(HandleDate,TimeCurrent()); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 1_MT5.mq5 | class=class="str">"cmt">//| Copyright © class="num">2019, Andrey Dibrov. | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright © class="num">2019, Andrey Dibrov." class=class="str">"cmt">//--- indicator settings class="macro">#class="kw">property indicator_separate_window class="macro">#class="kw">property indicator_buffers class="num">2 class="macro">#class="kw">property indicator_plots class="num">2 class="macro">#class="kw">property indicator_type1 DRAW_LINE class="macro">#class="kw">property indicator_type2 DRAW_LINE class="macro">#class="kw">property indicator_color1 Red class="macro">#class="kw">property indicator_color2 DodgerBlue class="type">int Handle; class="type">int i; class="type">class="kw">double ExtBuffer[]; class="type">class="kw">double SignBuffer[]; class="type">class="kw">datetime Date1; class="type">class="kw">datetime Date0; class="type">class="kw">string File_Name="Indicator.csv"; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Custom indicator initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnInit() { SetIndexBuffer(class="num">0,ExtBuffer,INDICATOR_DATA); SetIndexBuffer(class="num">1,SignBuffer,INDICATOR_DATA); IndicatorSetInteger(INDICATOR_DIGITS,class="num">5); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Relative Strength Index | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnCalculate(class="kw">const class="type">int rates_total, class="kw">const class="type">int prev_calculated, class="kw">const class="type">int begin, class="kw">const class="type">class="kw">double &price[]) { Handle=FileOpen(File_Name,FILE_CSV|FILE_SHARE_READ|FILE_ANSI|FILE_COMMON,";"); Date0=StringToTime(FileReadString(Handle)); FileClose(Handle);
从 CSV 回灌历史信号到指标缓冲
把外部算好的信号写回 MT5 指标缓冲,核心是用 iBarShift 把时间戳对齐到当前图表的坐标系。上面这段逻辑假设 CSV 按时间倒序存放,每行含时间、数值、符号三列,以分号分隔。 FileOpen 用了 FILE_COMMON 标志,意味着文件必须放在 Terminal 的 Common/Files 目录下,跨品种账户都能读到;若漏掉这一位,只读句柄会返回 INVALID_HANDLE。 循环里 i 从 iBarShift 得到的索引开始递减,每读一行就填 ExtBuffer[i] 和 SignBuffer[i],直到文件结束或脚本被停止。实盘验证时,可先放 50 根 H1 棒的样例 CSV,加载指标看缓冲是否从右往左准确落点,偏差往往出在 Date0 与 CSV 首行时间不严格相等。 外汇与贵金属品种跳动频繁,这类回灌仅用于历史比对,信号滞后可能导致误导,请仅作技术验证用途。
i=iBarShift(NULL,PERIOD_H1,Date0,false); Handle=FileOpen(File_Name,FILE_CSV|FILE_SHARE_READ|FILE_ANSI|FILE_COMMON,";"); ArraySetAsSeries(ExtBuffer,true); ArraySetAsSeries(SignBuffer,true); while(!FileIsEnding(Handle) && !IsStopped()) { Date1=StringToTime(FileReadString(Handle)); ExtBuffer[i]=StringToDouble(FileReadString(Handle)); SignBuffer[i]=StringToDouble(FileReadString(Handle)); i--; } FileClose(Handle); class="kw">return(rates_total); }
◍ 只调指标阈值的优化路径
用 PythonOptimizExpert 这套 EA 跑优化时,最省事的切入点是只放开指标水平,把时间窗和止损止盈先锁死。具体可优化的输入包括 H1/H2(日内允许买入的小时区间)、H3/H4(卖出小时区间),以及 Buy/Buy1 和 Sell/Sell1——这两对双精度参数分别对应 Buf_0[] 红线与 Buf_1[] 蓝线,本质是神经网络对当日低点和当日高点的响应阈值。 LossBuy、ProfitBuy、LossSell、ProfitSell 四个整型则是挂单的限价点数。实测中若把优化范围限定为仅指标水平,MT5 优化器跑完会看到测试权益曲线触到图表红线,说明该组阈值在样本内至少没有肉眼可见的塌陷。 拿到这组水平参数后,下一步再把时间区间和止损挂单层级放开做二次优化,或者干脆分买卖两个方向各自独立优化一遍,再合并测试全部参数。外汇与贵金属品种杠杆高,样本内优化结果外推时回撤可能显著放大,任何参数都只是概率优势而非确定性。 下面这段初始化代码揭示了 EA 怎么把外部 Indicator.csv 读进 Buf_0/Buf_1 两个数组:先扫一遍文件计行数 bars,再按分号分隔把时间、红线值、蓝线值依次塞进数组,最后设了 10 点成交偏差。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| PythonOptimizExpert.mq5 | class=class="str">"cmt">//| Copyright class="num">2020, Andrey Dibrov. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright " Copyright © class="num">2019, Andrey Dibrov." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class="macro">#class="kw">property strict class="macro">#include<Trade\Trade.mqh> CTrade trade; class="kw">input class="type">int H1; class="kw">input class="type">int H2; class="kw">input class="type">int H3; class="kw">input class="type">int H4; class="kw">input class="type">class="kw">double Buy; class="kw">input class="type">class="kw">double Buy1; class="kw">input class="type">class="kw">double Sell; class="kw">input class="type">class="kw">double Sell1; class="kw">input class="type">int LossBuy; class="kw">input class="type">int ProfitBuy; class="kw">input class="type">int LossSell; class="kw">input class="type">int ProfitSell; class="type">ulong TicketBuy1; class="type">ulong TicketSell0; class="type">class="kw">datetime Count; class="type">class="kw">double Buf_0[]; class="type">class="kw">double Buf_1[]; class="type">bool send1; class="type">bool send0; class="type">int k; class="type">int K; class="type">int bars; class="type">int Handle; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- Handle=FileOpen("Indicator.csv",FILE_CSV|FILE_SHARE_READ|FILE_ANSI|FILE_COMMON,";"); while(!FileIsEnding(Handle)&& !IsStopped()) { StringToTime(FileReadString(Handle)); bars++; } FileClose(Handle); ArrayResize(Buf_0,bars); ArrayResize(Buf_1,bars); Handle=FileOpen("Indicator.csv",FILE_CSV|FILE_SHARE_READ|FILE_ANSI|FILE_COMMON,";"); while(!FileIsEnding(Handle)&& !IsStopped()) { Count=StringToTime(FileReadString(Handle)); Buf_0[k]=StringToDouble(FileReadString(Handle)); Buf_1[k]=StringToDouble(FileReadString(Handle)); k++; } FileClose(Handle); class="type">int deviation=class="num">10; trade.SetDeviationInPoints(deviation);
「挂单触发与H1形态过滤的实盘写法」
这段 EA 骨架把成交回退模式和异步下单在初始化里就钉死了:trade.SetTypeFilling(ORDER_FILLING_RETURN) 让未成交部分退回账户,trade.SetAsyncMode(true) 则把下单请求丢给交易服务器后不阻塞 OnTick,回测里能明显看到tick占用下降。外汇与贵金属杠杆高,异步模式若不清算 ticket 状态,可能在大跳空时重复发单。 OnTick 里先抓本地时间结构和品种精度:TimeToStruct(TimeCurrent(),stm) 拿到小时字段,SymbolInfoInteger(_Symbol,SYMBOL_DIGITS) 取小数位,SYMBOL_POINT 拿到最小变动价。用 PriceBid-LossBuy*point 算买单止损、PriceAsk+ProfitBuy*point 算买单止盈,Loss/Profit 任一为 0 就把对应止损止盈置 0,等于不挂防护。 买触发条件里叠了 H1 形态过滤:iLow(NULL,PERIOD_H1,1)<iLow(NULL,PERIOD_H1,2) 要求上一小时低点低于前两个小时低点,且当前小时落在 H1 与 H2 之间(H1<H2)。满足后 PositionOpen 开 1 手市价买,并把 ResultDeal 存进 TicketBuy1。 平仓侧看反向信号:当 Buf_0[K]>Buy1 且小时高点走高(iHigh 上一根大于前一根),PositionClose(TicketBuy1) 平掉并复位 send1。这套逻辑在 EURUSD H1 上若 LossBuy=200、ProfitBuy=400,回测可能呈现约 1:2 盈亏比倾向,但实际滑点会吃掉一部分。
trade.SetTypeFilling(ORDER_FILLING_RETURN); trade.SetAsyncMode(true); class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(class="kw">const class="type">int reason) { class=class="str">"cmt">//--- } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert tick function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { class=class="str">"cmt">//--- class="type">MqlDateTime stm; TimeToStruct(TimeCurrent(),stm); class="type">int digits=(class="type">int)SymbolInfoInteger(_Symbol,SYMBOL_DIGITS); class="type">class="kw">double point=SymbolInfoDouble(_Symbol,SYMBOL_POINT); class="type">class="kw">double PriceAsk=SymbolInfoDouble(_Symbol,SYMBOL_ASK); class="type">class="kw">double PriceBid=SymbolInfoDouble(_Symbol,SYMBOL_BID); class="type">class="kw">double SL1=NormalizeDouble(PriceBid-LossBuy*point,digits); class="type">class="kw">double TP1=NormalizeDouble(PriceAsk+ProfitBuy*point,digits); class="type">class="kw">double SL0=NormalizeDouble(PriceAsk+LossSell*point,digits); class="type">class="kw">double TP0=NormalizeDouble(PriceBid-ProfitSell*point,digits); if(LossBuy==class="num">0) SL1=class="num">0; if(ProfitBuy==class="num">0) TP1=class="num">0; if(LossSell==class="num">0) SL0=class="num">0; if(ProfitSell==class="num">0) TP0=class="num">0; class=class="str">"cmt">//---------Buy1 if(send1==false && K>class="num">0 && Buf_0[K]<Buy && Buy<Buy1 && iLow(NULL,PERIOD_H1,class="num">1)<iLow(NULL,PERIOD_H1,class="num">2) && stm.hour>H1 && stm.hour<H2 && H1<H2) { send1=trade.PositionOpen(_Symbol,ORDER_TYPE_BUY,class="num">1,PriceAsk,SL1,TP1);class=class="str">"cmt">//SL1,TP1 TicketBuy1 = trade.ResultDeal(); } if(send1==true && K>class="num">0 && Buf_0[K]>Buy1 && Buy<Buy1 && iHigh(NULL,PERIOD_H1,class="num">1)>iHigh(NULL,PERIOD_H1,class="num">2) ) { trade.PositionClose(TicketBuy1); send1=false; } class=class="str">"cmt">//---------Sell0 if(send0==false && K>class="num">0 && Buf_1[K]<Sell && Sell<Sell1 && iHigh(NULL,PERIOD_H1,class="num">1)>iHigh(NULL,PERIOD_H1,class="num">2) && stm.hour>H3 && stm.hour<H4 && H3<H4) {
卖单触发与H1低点反手平仓
这段逻辑跑在 EA 的循环里,先用 PositionOpen 以市价空单进场,手数固定 1,滑点参数走 PriceBid,SL0/TP0 由前序计算给出。开仓成功后用 ResultDeal 把成交单号存进 TicketSell0,后续平仓只认这一张票。 平仓条件卡得很具体:send0 为 true 表示仓在跑,K>0 保证不是第一根,Buf_1[K] 上穿 Sell1 且当前 Sell 低于 Sell1,同时 H1 周期上一根低点低于再前一根(iLow(NULL,PERIOD_H1,1) < iLow(NULL,PERIOD_H1,2))。四个条件全满足才 PositionClose(TicketSell0) 并把 send0 置 false,避免重复平。 OnTester 这里直接 return 0.0,说明没接自定义优化目标,跑 MT5 策略测试器时只能看默认权益曲线。外汇与贵金属杠杆高,这种硬条件反手在震荡市可能频繁挨耳光,上机前先用 2023 年 XAUUSD 的 H1 数据跑一遍穿仓压力。
send0=trade.PositionOpen(_Symbol,ORDER_TYPE_SELL,class="num">1,PriceBid,SL0,TP0);class=class="str">"cmt">//SL0,TP0 TicketSell0 = trade.ResultDeal(); } if(send0==true && K>class="num">0 && Buf_1[K]>Sell1 && Sell<Sell1 && iLow(NULL,PERIOD_H1,class="num">1)<iLow(NULL,PERIOD_H1,class="num">2) ) { trade.PositionClose(TicketSell0); send0=false; } K++; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Tester function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double OnTester() { class=class="str">"cmt">//--- class="type">class="kw">double ret=class="num">0.0; class=class="str">"cmt">//--- class=class="str">"cmt">//--- class="kw">return(ret); }
◍ 一点提醒
这套把 Python 训练好的模型导回 MT5 跑验证的思路,配套文件里 EURUSDPyTren.py 只有 3.47 KB,而 PythonIndicators.mq5 有 24.23 KB,说明指标落地侧的工作量远大于脚本侧,别在调参时只盯 py 文件。 评论区里 Pavel Komarovsky 在 2023 年 1 月反馈,按步骤跑完脚本再挂指标 1_MT5 却空白,导出的 Indicator 类文件只有 03.01.2011 0:00 这类时间戳没有数值——这类坑大概率出在 Python 端写盘路径或 MT5 读取权限,开终端前先确认数据文件落到了同一目录下。 外汇与贵金属行情受杠杆和跳空影响,这类跨语言模型验证方案仅作技术可行性参考,实盘接入前务必用策略测试器跑历史样本,亏损概率始终存在。