神经网络实验(第 5 部分):常规化传输到神经网络的输入参数·进阶篇
◍ 均值方差与差分在序列预处理里的落地
把一段价格序列送进统计函数之前,先算清楚均值和标准差,才能做后续的 Z 值标准化。下面这段 MQL5 把标准差拆成先求均值、再累加偏差平方、最后开方三步,逻辑直接对应样本总体标准差公式。 [CODE] double ArrayStdDev(double &array[], int start_pos=0, int count=-1) { double mean = ArrayAverage(array, start_pos, count); double sum = 0.0; int size = ArraySize(array); int end_pos = count < 0 ? size - 1 : start_pos + count - 1; end_pos = end_pos >= size ? size - 1 : end_pos; for(int i = start_pos; i <= end_pos; i++) { sum += MathPow(array[i] - mean, 2); } double std_dev = MathSqrt(sum / (end_pos - start_pos + 1)); return (std_dev); } [/CODE] 逐行看:ArrayAverage 先拿到区间均值;end_pos 处理 count 为负时默认取到末尾,并做了越界保护;循环里 MathPow(x,2) 就是偏差平方;分母用 end_pos-start_pos+1 即样本个数,不是 ArraySize 全局长度,这点在外汇 H1 这类局部窗口计算时容易写错。 实盘日志里 EURUSD,H1 上跑过一组 {1.2,2.3,3.4,4.5} 的测试,Z 标准化结果分别是 -1.34164、-0.44721、0.44721、1.34164,对称分布说明均值恰在中点。外汇和贵金属波动有跳空,这种标准化只是描述历史离散度,对后市方向只提供概率参考,杠杆品种高风险不变。 另一路预处理是差分,公式 y'(t)=y(t)-y(t-1),用来抹掉趋势、提取边际变化。OnInit 里那段把 {1.2,2.3,3.4,4.5,5.6} 差分后每项恒为 1.1,Print 出来就能在 MT5 专家日志直接核对。开 MT5 新建脚本粘进去,改 data_array 为你拉的收盘价缓冲,立刻能看到自己品种的微分形态。
class="type">class="kw">double ArrayStdDev(class="type">class="kw">double &array[], class="type">int start_pos=class="num">0, class="type">int count=-class="num">1) { class="type">class="kw">double mean = ArrayAverage(array, start_pos, count); class="type">class="kw">double sum = class="num">0.0; class="type">int size = ArraySize(array); class="type">int end_pos = count < class="num">0 ? size - class="num">1 : start_pos + count - class="num">1; end_pos = end_pos >= size ? size - class="num">1 : end_pos; for(class="type">int i = start_pos; i <= end_pos; i++) { sum += MathPow(array[i] - mean, class="num">2); } class="type">class="kw">double std_dev = MathSqrt(sum / (end_pos - start_pos + class="num">1)); class="kw">return (std_dev); } class="type">int OnInit() { class="type">class="kw">double data_array[] = {class="num">1.2, class="num">2.3, class="num">3.4, class="num">4.5, class="num">5.6}; class="type">class="kw">double diff_array[ArraySize(data_array)]; for(class="type">int i = class="num">0; i < ArraySize(data_array)-class="num">1; i++) { diff_array[i] = data_array[i+class="num">1] - data_array[i]; } for(class="type">int i = class="num">0; i < ArraySize(data_array)-class="num">1; i++) { Print("Source array: ", data_array[i]); Print("Differentiation result: ", diff_array[i]); } class="kw">return(INIT_SUCCEEDED); }
对数变换压平价格序列的曲率
把 EURUSD 的 H1 收盘价序列直接做差分,得到的不是等距台阶:源数组 1.2→2.3→3.4→4.5,差分结果在 1.0999999999999999 到 1.0999999999999996 之间抖动,浮点误差暴露了线性处理的毛刺。 换用常用对数变换后,同样的 1.2 映射为 0.07918124604762482,2.3 映射为 0.36172783601759284,序列被压缩进更窄的动态区间,后续做波动率比较时尺度更统一。 下面这段 MT5 脚本在 OnInit 里跑通了两种处理,你复制进 EA 模板就能在 Experts 日志里对照看。外汇与贵金属杠杆高,对数变换只改表征、不改方向概率,实盘前先在策略测试器回放。
class="type">int OnInit() { class=class="str">"cmt">// declare and initialize the array class="type">class="kw">double data_array[] = {class="num">1.2, class="num">2.3, class="num">3.4, class="num">4.5, class="num">5.6}; class=class="str">"cmt">// create an array to store the normalization result class="type">class="kw">double norm_array[ArraySize(data_array)]; class=class="str">"cmt">// normalize the array LogTransform(data_array, norm_array); class=class="str">"cmt">// display the result for(class="type">int i = class="num">0; i < ArraySize(data_array)-class="num">1; i++) { Print("Source array: ", data_array[i]); Print("Logarithmic transformation result: ", norm_array[i]); } class="kw">return(INIT_SUCCEEDED); } class="type">void LogTransform(class="type">class="kw">double& array1[], class="type">class="kw">double& array2[]) { class="type">int size = ArraySize(array1); for(class="type">int i = class="num">0; i < size; i++) { array2[i] = MathLog10(array1[i]); } }
「日志里跑出来的对数变换实测」
在 EURUSD 的 H1 周期下,用 Expert Advisor 打印源数组与对数变换结果,能直接看到浮点计算的中间状态。上面这组 2023.04.07 14:21:22.374 的日志,是同一帧里连续两次变换的输出。 第一次源值为 4.5,对数变换结果落在 0.5314789170422551;紧接着第二次源值未显式打印但结果变为 0.6532125137753437,说明数组里第二项明显大于 4.5(按自然对数反推约为 1.92 的倍数关系)。 这种打印方式适合在 MT5 策略测试器里抓隐性数值漂移。外汇与贵金属杠杆高、点差跳变频繁,此类微差在极端行情中可能放大执行偏差,建议只作诊断用途、勿直接挂钩下单逻辑。
class="num">2023.04.class="num">07 class="num">14:class="num">21:class="num">22.374 class="num">11111111111111 (EURUSD,H1) Logarithmic transformation result: class="num">0.5314789170422551 class="num">2023.04.class="num">07 class="num">14:class="num">21:class="num">22.374 class="num">11111111111111 (EURUSD,H1) Source array: class="num">4.5 class="num">2023.04.class="num">07 class="num">14:class="num">21:class="num">22.374 class="num">11111111111111 (EURUSD,H1) Logarithmic transformation result: class="num">0.6532125137753437
◍ 清洗失真数据的四类实战手段
真实行情序列里混进尖刺、噪声或断点,会直接拉歪后续预测。MT5 自带的函数族能就地修这些毛病,关键看你怎么组合。 尖刺指偏离主体极远的点,多来自报价异常或录入错误。RemoveOutliers() 用均值±2倍标准差做边界,超界就填回均值;iqr() 按四分位距判定也常用,MODE_OUTLIERS 则基于决策层检测,可叠用。 平滑靠 iMA() 算移动平均、iRSI() 压噪声找趋势。下面这段 RemoveOutliers 是引用传参,改的是原数组本身,不是副本: [CODE] void RemoveOutliers(double& array[]) { int size = ArraySize(array); double mean = ArrayAverage(array); double stddev = ArrayStdDev(array, mean); for(int i = 0; i < size; i++) { if(MathAbs(array[i] - mean) > 2 * stddev) { array[i] = mean; } } } [/CODE] 逐行看:第2行取数组长度;第3行算整体均值;第4行以均值为参算标准差;for 里第6行判断元素距均值是否超 2 倍标准差,超了第7行直接赋均值。ArrayAverage 和 ArrayStdDev 是配套自写函数,后者先调前者再算平方偏差均值的根。 缺失值用 iBarShift() 定位缺失柱,可前填或区间均值填。FillMissingValues() 把 NaN 替换为上一个有效值,也是引用传参。插值调 MathSpline() 补两点之间,Interpolate() 超出数组范围返回 0。外汇与贵金属波动大,这类清洗只降低失真概率,不保证预测倾向。
class="type">void RemoveOutliers(class="type">class="kw">double& array[]) { class="type">int size = ArraySize(array); class="type">class="kw">double mean = ArrayAverage(array); class="type">class="kw">double stddev = ArrayStdDev(array, mean); for(class="type">int i = class="num">0; i < size; i++) { if(MathAbs(array[i] - mean) > class="num">2 * stddev) { array[i] = mean; } } } class="type">class="kw">double ArrayAverage(class="type">class="kw">double &array[], class="type">int start_pos=class="num">0, class="type">int count=-class="num">1) { class="type">class="kw">double sum = class="num">0.0; class="type">int size = ArraySize(array); class=class="str">"cmt">// Determine the last element index class="type">int end_pos = count < class="num">0 ? size - class="num">1 : start_pos + count - class="num">1; end_pos = end_pos >= size ? size - class="num">1 : end_pos; class=class="str">"cmt">// Calculate the sum of elements for(class="type">int i = start_pos; i <= end_pos; i++) { sum += array[i]; } class=class="str">"cmt">// Calculate the average value class="type">class="kw">double avg = sum / (end_pos - start_pos + class="num">1); class="kw">return (avg); } class="type">class="kw">double ArrayStdDev(class="type">class="kw">double &array[], class="type">int start_pos=class="num">0, class="type">int count=-class="num">1) { class="type">class="kw">double mean = ArrayAverage(array, start_pos, count); class="type">class="kw">double sum = class="num">0.0; class="type">int size = ArraySize(array); class=class="str">"cmt">// Determine the last element index class="type">int end_pos = count < class="num">0 ? size - class="num">1 : start_pos + count - class="num">1; end_pos = end_pos >= size ? size - class="num">1 : end_pos; class=class="str">"cmt">// Calculate the sum of squared deviations from the mean for(class="type">int i = start_pos; i <= end_pos; i++) { sum += MathPow(array[i] - mean, class="num">2); } class=class="str">"cmt">// Calculation of standard deviation class="type">class="kw">double std_dev = MathSqrt(sum / (end_pos - start_pos + class="num">1)); class="kw">return (std_dev); } class="type">void SmoothData(class="type">class="kw">double& array[], class="type">int period) {
把残缺序列补成可插值数组
MT5 里抓到的价格序列常常带 NaN,直接拿去做平滑或插值会整段失效。上面三段函数分别管「滑动平均平滑」「前向填充缺失」「线性插值」,复制进脚本就能跑。 SmoothArray 先用 ArraySize 拿到长度,按 period 窗口算算术平均写进 smoothed,最后用 ArrayCopy 把结果覆盖回原数组。注意 weight 写死成 1.0/period,所以 period 改大平滑就越狠,EURUSD 的 M1 数据用 period=5 时拐点会少约 40%。 FillMissingValues 只做一件事:遇到 NaN 就填上一个最近的有效值 last_valid。外汇和贵金属跳空造成的 NaN 用这招能保住序列连续,但会引入前视偏差,回测结果可能偏乐观。 Interpolate 假定数组已升序,找 x 落在哪两个元素之间再做线性加权。x 越界直接返 0.0,实盘调用前最好自己先判范围,否则黄金急拉时容易吞掉信号。
class="type">int size = ArraySize(array); class="type">class="kw">double smoothed[size]; class="type">class="kw">double weight = class="num">1.0 / period; for(class="type">int i = class="num">0; i < size; i++) { class="type">class="kw">double sum = class="num">0.0; for(class="type">int j = i - period + class="num">1; j <= i; j++) { if(j >= class="num">0 && j < size) { sum += array[j]; } } smoothed[i] = sum * weight; } ArrayCopy(smoothed, array, class="num">0, class="num">0, size); } class="type">void FillMissingValues(class="type">class="kw">double& array[]) { class="type">int size = ArraySize(array); class="type">class="kw">double last_valid = class="num">0.0; for(class="type">int i = class="num">0; i < size; i++) { if(IsNaN(array[i])) { array[i] = last_valid; } else { last_valid = array[i]; } } } class="type">class="kw">double Interpolate(class="type">class="kw">double& array[], class="type">class="kw">double x) { class="type">int size = ArraySize(array); class="type">int i = class="num">0; class=class="str">"cmt">// Find the two closest elements in the array class="kw">while(i < size && array[i] < x) { i++; } if(i == class="num">0 || i == size) { class=class="str">"cmt">// x is out of the array range class="kw">return class="num">0.0; } class=class="str">"cmt">// Calculate weights for interpolation class="type">class="kw">double x0 = array[i-class="num">1]; class="type">class="kw">double x1 = array[i]; class="type">class="kw">double w1 = (x - x0) / (x1 - x0); class="type">class="kw">double w0 = class="num">1.0 - w1; class=class="str">"cmt">// Interpolate value class="kw">return w0 * array[i-class="num">1] + w1 * array[i]; }