神经网络实践:最小二乘法·综合运用
◍ 给回归直线加一个可调根偏移
为后续数学推导铺路,这一节先在指标里塞进一个机制:让线性回归直线的根(截距基准点)可以上下平移。改动量极小,却能让拟合直线在各种样本分布下都倾向给出最小残差面积,比固定过零点的直线更贴数据库。 核心就一句:在第 18 行结构体里加一个 Const_B 变量,再用上下箭头在运行期改第 65 行附近它的值。紫线长度跟着这个变量重算即可,逻辑不绕。 下面对贴出的 MQL5 片段做逐行拆解,方便你直接抄进 MT5 自定义指标验证。 001-005:文件头与版权、指标附着在图表窗口、声明 0 个 plot(纯 Canvas 绘图)。 006:引入 Canvas.mqh,所有线条文字靠它画。 008-009:宏定义角度转弧度、线段半长 _SizeLine=200 像素。 011:声明 CCanvas 对象。 013-019:结构体 st_00 存锚点 x,y、角度 Angle、新增的 Const_B(根偏移量)。 021-027:PlotText 按锚点下方排布文字,偏移量参与 y 坐标计算。 029-045:Func_01 内置 8 个样本点数组 A,画蓝十字坐标轴,err 累加残差从 0 起算——这就是后面比面积的入口。 外汇与贵金属波动剧烈,这类拟合仅作概率参考,实盘前务必在 MT5 策略测试器跑历史段验证稳定性。
class="num">001. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">002. class="macro">#class="kw">property copyright "Daniel Jose" class="num">003. class="macro">#class="kw">property indicator_chart_window class="num">004. class="macro">#class="kw">property indicator_plots class="num">0 class="num">005. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">006. class="macro">#include <Canvas\Canvas.mqh> class="num">007. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">008. class="macro">#define _ToRadians(A) (A * (M_PI / class="num">180.0)) class="num">009. class="macro">#define _SizeLine class="num">200 class="num">010. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">011. CCanvas canvas; class="num">012. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">013. class="kw">struct st_00 class="num">014. { class="num">015. class="type">int x, class="num">016. y; class="num">017. class="type">class="kw">double Angle, class="num">018. Const_B; class="num">019. }global; class="num">020. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">021. class="type">void PlotText(class="kw">const class="type">uchar line, class="kw">const class="type">class="kw">string sz0) class="num">022. { class="num">023. class="type">uint w, h; class="num">024. class="num">025. TextGetSize(sz0, w, h); class="num">026. canvas.TextOut(global.x - (w / class="num">2), global.y + _SizeLine + (line * h) + class="num">5, sz0, ColorToARGB(clrBlack)); class="num">027. } class="num">028. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">029. class="type">void Func_01(class="type">void) class="num">030. { class="num">031. class="type">int A[]={ class="num">032. -class="num">100, -class="num">150, class="num">033. -class="num">80, -class="num">50, class="num">034. class="num">30, class="num">80, class="num">035. class="num">100, class="num">120 class="num">036. }; class="num">037. class="num">038. class="type">int vx, vy; class="num">039. class="type">class="kw">double ly, err; class="num">040. class="type">class="kw">string s0 = ""; class="num">041. class="num">042. canvas.LineVertical(global.x, global.y - _SizeLine, global.y + _SizeLine, ColorToARGB(clrRoyalBlue, class="num">255)); class="num">043. canvas.LineHorizontal(global.x - _SizeLine, global.x + _SizeLine, global.y, ColorToARGB(clrRoyalBlue, class="num">255)); class="num">044. class="num">045. err = class="num">0;
角度微调与残差绘制的闭环
上面这段把「拟合直线」和「手动调参」连成了一个可交互闭环:Func_01 负责按当前角度和截距把每个样本点画成红圆,并用紫线标出它到拟合线的纵向偏差 ly,同时把偏差绝对值拼进字符串、把 ly 平方累加进 err。 err 就是普通最小二乘里的残差平方和,PlotText(3) 以 %.8f 精度把 Error 打在画布上——你拖动角度时能看到这个数字实时跳动,值越小代表当前直线与点云的贴合倾向越好。 NewAngle 是调参入口:direct 控制角度按 step=0.1 度加减,updow 控制截距 Const_B 同步加减;第 064 行用三元式把角度锁在 ±90 度以内,避免切线计算爆掉。每次调用先 Erase 白底,重算并画绿色抗锯齿拟合线,再跑一遍 Func_01 刷新圆点和误差。 外汇与贵金属品种上用这类可视化拟合做手动回看时,注意历史点云不代表未来概率,实盘仍属高风险。把 step 改成 0.01 可以得到更细的扫描粒度,但 OnTick 里频繁调会拖慢 MT5 图表响应。
for (class="type">uint c0 = class="num">0, c1 = class="num">0; c1 < A.Size(); c0++) { vx = A[c1++]; vy = A[c1++]; canvas.FillCircle(global.x + vx, global.y - vy, class="num">5, ColorToARGB(clrRed, class="num">255)); ly = vy - (vx * -MathTan(_ToRadians(global.Angle))) - global.Const_B; s0 += StringFormat("%.4f || ", MathAbs(ly)); canvas.LineVertical(global.x + vx, global.y - vy, global.y + (class="type">int)(ly - vy), ColorToARGB(clrPurple)); err += MathPow(ly, class="num">2); } PlotText(class="num">3, StringFormat("Error: %.8f", err)); PlotText(class="num">4, s0); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void NewAngle(class="kw">const class="type">char direct, class="kw">const class="type">char updow, class="kw">const class="type">class="kw">double step = class="num">0.1) { canvas.Erase(ColorToARGB(clrWhite, class="num">255)); global.Angle = (MathAbs(global.Angle + (step * direct)) < class="num">90 ? global.Angle + (step * direct) : global.Angle); global.Const_B += (step * updow); PlotText(class="num">1, StringFormat("Angle in graus => %.2f", MathAbs(global.Angle))); PlotText(class="num">2, StringFormat("f(x) = %.4fx %c %.4f", -MathTan(_ToRadians(global.Angle)), (global.Const_B < class="num">0 ? &class="macro">#x27;-&class="macro">#x27; : &class="macro">#x27;+&class="macro">#x27;), MathAbs(global.Const_B))); canvas.LineAA( global.x - (class="type">int)(_SizeLine * cos(_ToRadians(global.Angle))), (global.y - (class="type">int)global.Const_B) - (class="type">int)(_SizeLine * sin(_ToRadians(global.Angle))), global.x + (class="type">int)(_SizeLine * cos(_ToRadians(global.Angle))), (global.y - (class="type">int)global.Const_B) + (class="type">int)(_SizeLine * sin(_ToRadians(global.Angle))), ColorToARGB(clrForestGreen) ); Func_01(); canvas.Update(true); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { global.Angle = class="num">0;
「用方向键驱动画布重绘」
在指标初始化阶段,先抓到主图表的像素宽高:ChartGetInteger(0, CHART_WIDTH_IN_PIXELS, 0) 和 CHART_HEIGHT_IN_PIXELS 分别写入 global.x / global.y,随后用 CreateBitmapLabel 以全图尺寸建一块名为 BL 的位图标签,再把坐标各除以 2 落到屏幕中心,调用 NewAngle(0,0) 与 canvas.Update(true) 完成首帧,返回 INIT_SUCCEEDED。 OnCalculate 这里直接 return rates_total,不碰价格数组——本例的视觉逻辑全在图表事件里跑,不参与常规指标计算,所以 prev_calculated、begin、price[] 形同占位。 键盘交互才是核心:OnChartEvent 捕获 CHARTEVENT_KEYDOWN 后,用 TerminalInfoInteger 查 TERMINAL_KEYSTATE_LEFT/RIGHT/UP/DOWN 四个状态位,分别给 NewAngle 传 (-1,0)/(1,0)/(0,1)/(0,-1)。也就是说,按住方向键时画布角度增量会按轴向改变,松手即停。外汇与贵金属图表叠加此类交互层波动剧烈,实盘挂此脚本须留意高风险。 退出时 OnDeinit 只做 canvas.Destroy(),释放位图资源避免残留标签。想验证就复制下面整段到 MT5 指标工程,编译后开 EURUSD 图表按方向键看中心图层是否响应。
class="num">084. global.x = (class="type">int)ChartGetInteger(class="num">0, CHART_WIDTH_IN_PIXELS, class="num">0); class="num">085. global.y = (class="type">int)ChartGetInteger(class="num">0, CHART_HEIGHT_IN_PIXELS, class="num">0); class="num">086. class="num">087. canvas.CreateBitmapLabel("BL", class="num">0, class="num">0, global.x, global.y, COLOR_FORMAT_ARGB_NORMALIZE); class="num">088. global.x /= class="num">2; class="num">089. global.y /= class="num">2; class="num">090. class="num">091. NewAngle(class="num">0, class="num">0); class="num">092. class="num">093. canvas.Update(true); class="num">094. class="num">095. class="kw">return INIT_SUCCEEDED; class="num">096. } class="num">097. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">098. 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[]) class="num">099. { class="num">100. class="kw">return rates_total; class="num">101. } class="num">102. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">103. class="type">void OnChartEvent(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam) class="num">104. { class="num">105. class="kw">switch (id) class="num">106. { class="num">107. case CHARTEVENT_KEYDOWN: class="num">108. if (TerminalInfoInteger(TERMINAL_KEYSTATE_LEFT)) class="num">109. NewAngle(-class="num">1, class="num">0); class="num">110. if (TerminalInfoInteger(TERMINAL_KEYSTATE_RIGHT)) class="num">111. NewAngle(class="num">1, class="num">0); class="num">112. if (TerminalInfoInteger(TERMINAL_KEYSTATE_UP)) class="num">113. NewAngle(class="num">0, class="num">1); class="num">114. if (TerminalInfoInteger(TERMINAL_KEYSTATE_DOWN)) class="num">115. NewAngle(class="num">0, -class="num">1); class="num">116. class="kw">break; class="num">117. } class="num">118. } class="num">119. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">120. class="type">void OnDeinit(class="kw">const class="type">int reason) class="num">121. { class="num">122. canvas.Destroy(); class="num">123. }
◍ 别急着下结论
直线方程在 MQL5 里落地,比数学课本上的符号友好得多,但别以为循环搜参就是终点。我们目前只实现了一部分,更合适的直线写法还没定型,而搜索变量每多一个,暴力枚举的代价就指数级放大——设想一百万个变量,蛮力基本不可能跑通。 真正省力的路是把数学结构用对,而不是堆计算。文末那段生成图示的脚本就是让你亲手卡在「找方程」这一步,体会搜索空间为什么难啃。 外汇与贵金属波动受杠杆与跳空影响,这类几何拟合仅作辅助参考,实盘高风险。下一篇我们再换写法接着拆。