矩阵分解:更实用的建模·进阶篇
◍ 用矩阵变换在图表上画旋转箭头
这段脚本演示了如何在 MT5 的 Canvas 位图上,借助 2x2 矩阵乘法把一个基准三角形旋转并填充成箭头。M_2 里存的是三个顶点偏移量:(1.5,-0.75)、(1.0,0.0)、(1.5,0.75),绕原点构成一个朝右的尖头;M_1 被设成对角为 size 的缩放矩阵,经过 MatrixA_x_MatrixB 两次相乘后,M_2 里的坐标即被旋转/缩放。 循环里把画布中心 (px/2, py/2) 作为锚点,给每个变换后的顶点加上 x、y 偏移,得到 dx/dy 像素数组,再用 FillPolygon 以纯紫(clrPurple, alpha 255)涂出箭头主体,并在中心用 FillCircle 画一个半径 5 像素的红色点。 OnInit 中先取图表宽高(ChartGetInteger 的 CHART_WIDTH_IN_PIXELS / CHART_HEIGHT_IN_PIXELS),建一个名为 "BL" 的位图标签铺满画布,Erase 成白底,调用 Arrow(px/2, py/2, 160) 即生成尺寸 160 的箭头,最后 canvas.Update(true) 刷新。外汇与贵金属图表叠加自定义图形属高风险辅助观察,参数 size 与锚点可改,开 MT5 挂上即可见效果。
class="num">29. class="num">1.5, -.class="num">75, class="num">30. class="num">1.0, class="num">0.0, class="num">31. class="num">1.5, .class="num">75 class="num">32. }, class="num">33. M_3[M_2.Size() / class="num">2][class="num">2]; class="num">34. class="num">35. class="type">int dx[M_2.Size() / class="num">2], dy[M_2.Size() / class="num">2]; class="num">36. class="num">37. MatrixA_x_MatrixB(M_1, M_2, M_3, class="num">2); class="num">38. ZeroMemory(M_1); class="num">39. M_1[class="num">0][class="num">0] = M_1[class="num">1][class="num">1] = size; class="num">40. MatrixA_x_MatrixB(M_1, M_3, M_2, class="num">2); class="num">41. class="num">42. for (class="type">int c = class="num">0; c < (class="type">int)M_2.Size() / class="num">2; c++) class="num">43. { class="num">44. dx[c] = x + (class="type">int) M_2[c][class="num">0]; class="num">45. dy[c] = y + (class="type">int) M_2[c][class="num">1]; class="num">46. } class="num">47. class="num">48. canvas.FillPolygon(dx, dy, ColorToARGB(clrPurple, class="num">255)); class="num">49. canvas.FillCircle(x, y, class="num">5, ColorToARGB(clrRed, class="num">255)); class="num">50. } class="num">51. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">52. class="type">int OnInit() class="num">53. { class="num">54. class="type">int px, py; class="num">55. class="num">56. px = (class="type">int)ChartGetInteger(class="num">0, CHART_WIDTH_IN_PIXELS, class="num">0); class="num">57. py = (class="type">int)ChartGetInteger(class="num">0, CHART_HEIGHT_IN_PIXELS, class="num">0); class="num">58. class="num">59. canvas.CreateBitmapLabel("BL", class="num">0, class="num">0, px, py, COLOR_FORMAT_ARGB_NORMALIZE); class="num">60. canvas.Erase(ColorToARGB(clrWhite, class="num">255)); class="num">61. class="num">62. Arrow(px / class="num">2, py / class="num">2, class="num">160); class="num">63. class="num">64. canvas.Update(true); class="num">65. class="num">66. class="kw">return INIT_SUCCEEDED; class="num">67. } class="num">68. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">69. class="type">int OnCalculate(const class="type">int rates_total, const class="type">int prev_calculated, const class="type">int begin, const class="type">class="kw">double &price[]) class="num">70. { class="num">71. class="kw">return rates_total; class="num">72. } class="num">73. class=class="str">"cmt">//+------------------------------------------------------------------+
「退出时清理画布对象」
指标或 EA 在终端卸载、图表关闭、品种切换时,系统会调用 OnDeinit 并传入 reason 参数标识退出原因。若之前用 CCanvas 在图表上绘制了自定义图形,必须在这里主动销毁,否则残留对象会留在图表里,下次加载可能叠加或报错。 上面这段 MQL5 代码只做了一件事:在 OnDeinit 内调用 canvas.Destroy() 释放资源。注意 canvas 是类实例,Destroy 会删除由它创建的所有 OBJ_BITMAP_LABEL 一类图形资源,不依赖终端自动回收。 实盘验证方式:在 MT5 加载一个用了 CCanvas 的自定义指标,切换周期或删除指标,用 Ctrl+B 打开对象列表看是否还有遗留位图标签。外汇与贵金属交易杠杆高,测试请先用模拟账户。
class="num">74. class="type">void OnDeinit(const class="type">int reason) class="num">75. { class="num">76. canvas.Destroy(); class="num">77. } class="num">78. class=class="str">"cmt">//+------------------------------------------------------------------+
行矩阵和列矩阵互换时的索引坑
把行矩阵转成列矩阵,多数时候只是旋转或改索引,不一定要重算元素。等价前提是 a11=b11、a21=b12,也就是原行向量第 1、2 个元素分别落进列向量第 1、2 位。对称方阵(如 M_1)最省事,行列读出来一致,不用特殊处理。 MQL5 里动态数组如 M_3 必须提前定义好分配长度,否则访问矩阵元素时直接抛运行时错误。上一节的 MatrixA_x_MatrixB 依赖固定索引解码,一旦改成显式行列结构,第 42 行的循环就解不出数据,计算也得重写。 乘法顺序在矩阵里不能乱换:2x3 乘 3x2 和反过来结果不同。代码里第 58 行先传列矩阵 M_1、再传行矩阵 M_2,若调换顺序数值就错;第二个参数指定的是第一个矩阵的列数。当前过程没做「左矩阵行数=右矩阵列数」的校验,调用方自己得保证结构正确。 绘图时给 Plot 传的是行类型矩阵,每行一个顶点,列顺序固定为 X 在第一列、Y 在第二列。哪怕画三维,也要先投影到 XY 平面再塞进矩阵。第 29 行的 for 循环用 c0 管 dx/dy 索引并按需扩内存,c1 在内部自增并决定终止;若某行不足两列,循环中会越界报错。 下面这段 Arrow 函数把旋转矩阵 M_1 和箭头形状 M_2 乘出 M_3,再交给绘图层。注意 M_2.Size() 决定 M_3 长度,_ToRadians 宏把角度转弧度,cos/sin 构成 2x2 旋转阵。外汇贵金属图表上跑这类绘制前,先在脚本里单步看 M_3 输出,高风险品种别让绘图循环卡主线程。
class="num">20. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">21. class="type">void Arrow(const class="type">int x, const class="type">int y, const class="type">class="kw">ushort angle, const class="type">uchar size = class="num">100) class="num">22. { class="num">23. class="type">class="kw">double M_1[]= { class="num">24. cos(_ToRadians(angle)), sin(_ToRadians(angle)), class="num">25. -sin(_ToRadians(angle)), cos(_ToRadians(angle)) class="num">26. }, class="num">27. M_2[]= { class="num">28. class="num">0.0, class="num">0.0, class="num">29. class="num">1.5, -.class="num">75, class="num">30. class="num">1.0, class="num">0.0, class="num">31. class="num">1.5, .class="num">75 class="num">32. }, class="num">33. M_3[M_2.Size()]; class="num">09. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">10. class="macro">#define _ToRadians(A) (A * (M_PI / class="num">180.0)) class="num">11. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">12. class="type">void MatrixA_x_MatrixB(const class="type">class="kw">double &A[], const class="type">class="kw">ushort Rows, const class="type">class="kw">double &B[], class="type">class="kw">double &R[]) class="num">13. { class="num">14. class="type">uint Lines = (class="type">uint)(A.Size() / Rows); class="num">15.
◍ 矩阵乘法与画布绘制的底层实现
这段逻辑干了两件事:用嵌套循环做矩阵乘,把结果落进 R 数组;再用 Canvas 接口把数组里的点画成多边形和圆心标记。 外层循环以 Rows 为步长遍历 B 的大小,内层先清零 R[car+cbl],再对 Lines 条线做累加:R[car+cbl] += A[(cal*Rows)+car] * B[cal+cbl]。这就是标准二维矩阵乘的展平写法,A 按列优先、B 按块偏移。 Plot 函数把一维数组 A 当成坐标对来读:每两次自增 c1 取一个 x、y 偏移,ArrayResize 按 A.Size() 预扩容避免反复分配。最后用 FillPolygon 画紫色实心多边形(clrPurple, 255 不透明),并在锚点画半径 5 的红色圆。 Arrow 用 2x2 旋转矩阵 M_1 乘局部形状 M_2(含 4 个顶点,最长边 1.5、尾翼 ±0.75),angle 经 _ToRadians 转弧度后驱动方向。默认 size=100 仅作入参占位,实际缩放靠外部坐标映射。 在 MT5 里把这段直接塞进自定义指标,改 clrPurple 为 clrDodgerBlue 就能立刻看出多边形区域变化;外汇与贵金属图表叠加此类图形时需注意高风险,图形仅作概率参考。
for (class="type">uint cbl = class="num">0; cbl < B.Size(); cbl += Rows) for (class="type">uint car = class="num">0; car < Rows; car++) { R[car + cbl] = class="num">0; for (class="type">uint cal = class="num">0; cal < Lines; cal++) R[car + cbl] += (A[(cal * Rows) + car] * B[cal + cbl]); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void Plot(const class="type">int x, const class="type">int y, const class="type">class="kw">double &A[]) { class="type">int dx[], dy[]; for (class="type">uint c0 = class="num">0, c1 = class="num">0; c1 < A.Size(); c0++) { ArrayResize(dx, c0 + class="num">1, A.Size()); ArrayResize(dy, c0 + class="num">1, A.Size()); dx[c0] = x + (class="type">int)(A[c1++]); dy[c0] = y + (class="type">int)(A[c1++]); } canvas.FillPolygon(dx, dy, ColorToARGB(clrPurple, class="num">255)); canvas.FillCircle(x, y, class="num">5, ColorToARGB(clrRed, class="num">255)); ArrayFree(dx); ArrayFree(dy); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void Arrow(const class="type">int x, const class="type">int y, const class="type">class="kw">ushort angle, const class="type">uchar size = class="num">100) { class="type">class="kw">double M_1[]={ cos(_ToRadians(angle)), sin(_ToRadians(angle)), -sin(_ToRadians(angle)), cos(_ToRadians(angle)) }, M_2[]={ class="num">0.0, class="num">0.0, class="num">1.5, -.class="num">75, class="num">1.0, class="num">0.0, class="num">1.5, .class="num">75 },
「矩阵链式变换画点的收尾写法」
上面这段是绘制流程的末尾:先把 M_2 的当前尺寸塞进 M_3 的末行,再调 MatrixA_x_MatrixB 用 2 行规模的 M_1 去左乘 M_3,结果回写 M_3。 接着清掉 M_1 并只给对角项 M_1[0]、M_1[3] 赋值为 size,相当于构造一个仅做缩放的 2x2 矩阵;第二次 MatrixA_x_MatrixB 把它左乘 M_3,输出到 M_2,完成坐标的缩放映射。 最后用 Plot(x, y, M_2) 把变换后的点阵送出。你在 MT5 里改 size 这个值,能直接看到绘制图形整体缩放比例变化,建议从 1.0 起以 0.1 步进验证。 外汇与贵金属图表叠加自定义绘制属高风险操作,参数误用可能导致图形错位,仅作技术验证用途。
class="num">56. M_3[M_2.Size()]; class="num">57. class="num">58. MatrixA_x_MatrixB(M_1, class="num">2, M_2, M_3); class="num">59. ZeroMemory(M_1); class="num">60. M_1[class="num">0] = M_1[class="num">3] = size; class="num">61. MatrixA_x_MatrixB(M_1, class="num">2, M_3, M_2); class="num">62. Plot(x, y, M_2); class="num">63. } class="num">64. class=class="str">"cmt">//+------------------------------------------------------------------+
矩阵乘法只是旋转问题的入口
前两篇里我们把矩阵分解拆到最薄:只留 CPU 上的 MatrixA_x_MatrixB 乘法,不碰 OpenCL 和 GPU。实际三维程序、光栅优化甚至游戏引擎,分解几乎都在 GPU 跑;本文刻意用 CPU 只是证明流程本身没那么玄,2.9 KB 的 Matrizes.mq5 就能跑通。 很多新手直接调库,不知道背后是哪种因子分解。这里说的模型只解决一个具体问题——旋转物体,和学校里通用分解不是一回事,指望它做通用求解会直接失效。 下个系列话题会以矩阵分解为底,现在把这两篇的代码自己改两行矩阵维度,在 MT5 里看输出,比背公式有用。外汇和贵金属杠杆高,这类数学工具只辅助验证,别当成信号源。