神经网络实践:最小二乘法·进阶篇
◍ 用 Canvas 把回归线和点误差画出来
这段绘制逻辑把一条斜率为 -tan(θ) 的拟合线直接渲染到 MT5 的 Canvas 上,并在线下偏移 20 像素处用 8 位小数打印 f(x) 表达式。角度转弧度后取负正切,就是该线在屏幕坐标系里的视觉斜率,和后面点阵的残差方向一致。
- canvas.TextOut(global.x, global.y + _SizeLine + 20, StringFormat("f(x) = %.8fx", -MathTan(_ToRadians(global.Angle))), ColorToARGB(clrBlack));
// 在基准点下方 _SizeLine+20 像素处,用黑色写出拟合函数,斜率保留 8 位小数
- canvas.Line(
- global.x - (int)(_SizeLine * cos(_ToRadians(global.Angle))),
- global.y - (int)(_SizeLine * sin(_ToRadians(global.Angle))),
- global.x + (int)(_SizeLine * cos(_ToRadians(global.Angle))),
- global.y + (int)(_SizeLine * sin(_ToRadians(global.Angle))),
- ColorToARGB(clrForestGreen)
- ;
// 以 global 为中点、半长 _SizeLine 画绿色拟合线段,端点由角度余弦/正弦投影得到
- Func_01();
- canvas.Update(true);
// 调 Func_01 画点阵与残差,再强制刷新画布 Func_01 里硬编码了 4 组 (x,y) 偏移量:(-100,150)、(-80,50)、(30,-80)、(100,-120),循环里每点先画半径 5 的红圆,再向下引紫色竖线到拟合线,累加 abs(残差)。
- void Func_01(void)
- {
- int A[]={
- -100, 150,
- -80, 50,
- 30, -80,
- 100, -120
- };
// 8 个整数构成 4 个二维样本点,相对 global 的偏移
- canvas.LineVertical(global.x, global.y - _SizeLine, global.y + _SizeLine, ColorToARGB(clrRoyalBlue, 255));
- canvas.LineHorizontal(global.x - _SizeLine, global.x + _SizeLine, global.y, ColorToARGB(clrRoyalBlue, 255));
// 先画蓝白不透明的十字基准轴,便于肉眼对齐
- for (uint c0 = 0, c1 = 0; c1 < A.Size(); c0++)
- vx = A[c1++];
- vy = A[c1++];
- canvas.FillCircle(global.x + vx, global.y + vy, 5, ColorToARGB(clrRed, 255));
// 顺序取点对,画红色实心圆标记样本
- ly = (vx * MathTan(_ToRadians(global.Angle))) - vy;
- canvas.LineVertical(global.x + vx, global.y + vy, global.y + (int)(ly + vy), ColorToARGB(clrPurple));
- err += MathAbs(ly);
// 残差 ly 为拟合值减实际 y,紫色竖线长度即误差,err 求和供后续判定 把 Angle 从 0 调到 45,会看到绿线逆时针倒下、紫色残差线整体变长,err 很可能上升。外汇与贵金属图表叠加这类自定义层属高风险操作,参数误设可能误导进出场判断,实盘前务必在策略测试器里跑一遍。
class="num">54. canvas.TextOut(global.x, global.y + _SizeLine + class="num">20, StringFormat("f(x) = %.8fx", -MathTan(_ToRadians(global.Angle))), ColorToARGB(clrBlack)); class="num">55. canvas.Line( class="num">56. global.x - (class="type">int)(_SizeLine * cos(_ToRadians(global.Angle))), class="num">57. global.y - (class="type">int)(_SizeLine * sin(_ToRadians(global.Angle))), class="num">58. global.x + (class="type">int)(_SizeLine * cos(_ToRadians(global.Angle))), class="num">59. global.y + (class="type">int)(_SizeLine * sin(_ToRadians(global.Angle))), class="num">60. ColorToARGB(clrForestGreen) class="num">61. ); class="num">63. Func_01(); class="num">65. canvas.Update(true); class="num">66. } class="num">20. class="type">void Func_01(class="type">void) class="num">21. { class="num">22. class="type">int A[]={ class="num">23. -class="num">100, class="num">150, class="num">24. -class="num">80, class="num">50, class="num">25. class="num">30, -class="num">80, class="num">26. class="num">100, -class="num">120 class="num">27. }; class="num">29. class="type">int vx, vy; class="num">30. class="type">class="kw">double ly, err; class="num">32. canvas.LineVertical(global.x, global.y - _SizeLine, global.y + _SizeLine, ColorToARGB(clrRoyalBlue, class="num">255)); class="num">33. canvas.LineHorizontal(global.x - _SizeLine, global.x + _SizeLine, global.y, ColorToARGB(clrRoyalBlue, class="num">255)); class="num">35. err = class="num">0; class="num">36. for (class="type">uint c0 = class="num">0, c1 = class="num">0; c1 < A.Size(); c0++) class="num">38. vx = A[c1++]; class="num">39. vy = A[c1++]; class="num">40. canvas.FillCircle(global.x + vx, global.y + vy, class="num">5, ColorToARGB(clrRed, class="num">255)); class="num">41. ly = (vx * MathTan(_ToRadians(global.Angle))) - vy; class="num">42. canvas.LineVertical(global.x + vx, global.y + vy, global.y + (class="type">int)(ly + vy), ColorToARGB(clrPurple)); class="num">43. err += MathAbs(ly);
「把浮点误差直接画到画布上」
调试自定义指标时,最怕的是数值 quietly 错掉却没有任何提示。上面这段把误差值 err 以 8 位小数精度直接 TextOut 到 canvas 的指定坐标,颜色用 clrRed 的 ARGB 值,肉眼一眼就能看到异常。 坐标取的是 global.x - 200 与 global.y + _SizeLine + 20,也就是相对全局锚点左移 200 像素、并在水平参考线下方 20 像素处落字。StringFormat 的 "%.8f" 意味着误差分辨率到 1e-8,对点差换算或 Tick 级差值检查够用。 外汇与贵金属报价小数位多、滑点随机,这类浮点偏差在高杠杆下可能放大成错误信号;开 MT5 把这段塞进 OnDraw,跑一轮 EURUSD 的 M1 回看,若红字长期为 0.00000000 才算干净。
class="num">45. canvas.TextOut(global.x - class="num">200, global.y + _SizeLine + class="num">20, StringFormat("Error: %.8f", err), ColorToARGB(clrRed)); class="num">46. } class="num">47. class=class="str">"cmt">//+------------------------------------------------------------------+
屏幕第四象限带来的Y轴镜像坑
在 MT5 里画图,原点 (0,0) 默认落在左上角,这意味着我们始终在第四象限做绘制。物理参考点没变,只是把计算的表示平移到了另一个虚拟起点。新手常忽略这点,盲目把 MT5 矩阵值直接丢进 Excel、MatLab 或 SCILab 复绘,结果 Y 轴整条上下颠倒,手动核对计算时越看越懵。
这种镜像不代表程序算错,而是表示层错位。若你拿另一款绘图软件重现同样数值,会看到图形和 MT5 终端显示完全反着来——误差检查、斜率比对全变成无解谜题。外汇与贵金属行情推导属高风险活动,任何图形误读都可能放大仓位判断偏差。
修复很轻量:改矩阵取值符号,并把第 40–42 行计算翻转,让紫色连线与点位正确对应。下面这段代码就是修正后的绘制函数,注意第 40 行用 global.y - vy 而非直接加 vy,第 41 行斜率项前乘了负号做镜像补偿。
逐行拆解:22–27 行定义样本点阵(x,y 成对);32–33 画蓝色十字参考轴;36–44 遍历点,40 行以 global.y - vy 把屏幕坐标翻正,41 行用 -MathTan 修正切线方向,42 行画紫色误差竖线,43 行累加绝对误差;45 行输出 8 位精度误差值。误差变大说明调参方向偏了,缩小则倾向正确。
开 MT5 把这段贴进 canvas 工程,改 _Angle 看 Error 数值波动,就能直观验证象限翻转是否修干净。接下来才轮到让机器自己搜最优系数。
class="num">19. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">20. class="type">void Func_01(class="type">void) class="num">21. { class="num">22. class="type">int A[]={ class="num">23. -class="num">100, -class="num">150, class="num">24. -class="num">80, -class="num">50, class="num">25. class="num">30, class="num">80, class="num">26. class="num">100, class="num">120 class="num">27. }; class="num">28. class="num">29. class="type">int vx, vy; class="num">30. class="type">class="kw">double ly, err; class="num">31. class="num">32. canvas.LineVertical(global.x, global.y - _SizeLine, global.y + _SizeLine, ColorToARGB(clrRoyalBlue, class="num">255)); class="num">33. canvas.LineHorizontal(global.x - _SizeLine, global.x + _SizeLine, global.y, ColorToARGB(clrRoyalBlue, class="num">255)); class="num">34. class="num">35. err = class="num">0; class="num">36. for (class="type">uint c0 = class="num">0, c1 = class="num">0; c1 < A.Size(); c0++) class="num">37. { class="num">38. vx = A[c1++]; class="num">39. vy = A[c1++]; class="num">40. canvas.FillCircle(global.x + vx, global.y - vy, class="num">5, ColorToARGB(clrRed, class="num">255)); class="num">41. ly = vy - (vx * -MathTan(_ToRadians(global.Angle))); class="num">42. canvas.LineVertical(global.x + vx, global.y - vy, global.y - (class="type">int)(ly + vy), ColorToARGB(clrPurple)); class="num">43. err += MathAbs(ly); class="num">44. } class="num">45. canvas.TextOut(global.x - class="num">200, global.y + _SizeLine + class="num">20, StringFormat("Error: %.8f", err), ColorToARGB(clrRed)); class="num">46. } class="num">47. class=class="str">"cmt">//+------------------------------------------------------------------+
◍ 用平方和把切线斜率逼出来
想从一堆散点里抠出一条最贴的切线,直接拿点到直线的垂直距离求和会撞墙:这种绝对值累加的函数没法求导,常数一导就归零。原文里的做法是把紫色线段改成正方形面积——不追最小线长,改追最小面积,数学上就变成标准二次式,能顺出斜率的一阶导数。 二次式正是抛物线本源方程,对它求导得到的就是角系数 a 的逼近值。当误差项趋近零,算出的斜率会倾向无限靠近理想值,但实战中几乎不可能真到零,只能越近越好。 代码里第40行 d0 += (vx * vy) 是在累加 X·Y 乘积,第41行 d1 += MathPow(vx, 2) 是在累加 X 平方,这两项是后面解最小二乘斜率的核心输入。第43到45行用当前角度算每个点的纵向偏差 ly 并累加绝对值 err,纯粹是为了画面上画紫线,不参与求导。 只算 a 会默认直线过原点(0,0),这点在样本库里往往不存在,会暗中拽偏斜率。动画里绿线才是公式真值,和只求 a 返回的理想斜率有肉眼可见的偏差。要脱钩原点,得把 b 也解出来,下一段看怎么改。
class="num">19. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">20. class="type">void Func_01(class="type">void) class="num">21. { class="num">22. class="type">int A[]={ class="num">23. -class="num">100, -class="num">150, class="num">24. -class="num">80, -class="num">50, class="num">25. class="num">30, class="num">80, class="num">26. class="num">100, class="num">120 class="num">27. }; class="num">28. class="num">29. class="type">int vx, vy; class="num">30. class="type">class="kw">double ly, err, d0, d1; class="num">31. class="num">32. canvas.LineVertical(global.x, global.y - _SizeLine, global.y + _SizeLine, ColorToARGB(clrRoyalBlue, class="num">255)); class="num">33. canvas.LineHorizontal(global.x - _SizeLine, global.x + _SizeLine, global.y, ColorToARGB(clrRoyalBlue, class="num">255)); class="num">34. class="num">35. err = d0 = d1 = class="num">0; class="num">36. for (class="type">uint c0 = class="num">0, c1 = class="num">0; c1 < A.Size(); c0++) class="num">37. { class="num">38. vx = A[c1++]; class="num">39. vy = A[c1++]; class="num">40. d0 += (vx * vy); class="num">41. d1 += MathPow(vx, class="num">2); class="num">42. canvas.FillCircle(global.x + vx, global.y - vy, class="num">5, ColorToARGB(clrRed, class="num">255)); class="num">43. ly = vy - (vx * -MathTan(_ToRadians(global.Angle))); class="num">44. canvas.LineVertical(global.x + vx, global.y - vy, global.y - (class="type">int)(ly + vy), ColorToARGB(clrPurple)); class="num">45. err += MathAbs(ly); class="num">46. }
「把误差和导数比直接画到画布上」
这段收尾代码负责把计算过程中的诊断信息直接渲染到 MT5 的 Canvas 对象,方便在自定义指标或 EA 的可视化层当场核对数值。第 47 行在基准坐标左移 200 像素、下移 _SizeLine+20 的位置,用红色 8 位精度输出误差变量 err,任何大于 0.00000001 的偏离都肉眼可见。 第 48 行紧接着在再下移 20 像素处用森林绿输出 d0/d1 的比值,也就是两个导数(或差分)项的商,精度同样锁在 %.8f。外汇与贵金属价格序列在高波动时段差分易畸变,这个比值异常往往先于图形变形暴露计算bug,属于高频排查点。 整个绘制块以 49 行的花括号收口,50 行是 MQL5 惯用的分隔注释行。复制到你的 canvas 绘图函数末尾,改一下 global.x/global.y 锚点就能立即在图表上看到诊断叠层,省去反复 Print 到日志的麻烦。
class="num">47. canvas.TextOut(global.x - class="num">200, global.y + _SizeLine + class="num">20, StringFormat("Error: %.8f", err), ColorToARGB(clrRed)); class="num">48. canvas.TextOut(global.x - class="num">200, global.y + _SizeLine + class="num">40, StringFormat("(de/da) : %.8f", d0 / d1), ColorToARGB(clrForestGreen)); class="num">49. } class="num">50. class=class="str">"cmt">//+------------------------------------------------------------------+