矩阵分解:更实用的建模·进阶篇
📘

矩阵分解:更实用的建模·进阶篇

第 2/2 篇

◍ 用矩阵变换在图表上画旋转箭头

这段脚本演示了如何在 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 挂上即可见效果。

MQL5 / C++
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 打开对象列表看是否还有遗留位图标签。外汇与贵金属交易杠杆高,测试请先用模拟账户。

MQL5 / C++
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 输出,高风险品种别让绘图循环卡主线程。

MQL5 / C++
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 就能立刻看出多边形区域变化;外汇与贵金属图表叠加此类图形时需注意高风险,图形仅作概率参考。

MQL5 / C++
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 步进验证。 外汇与贵金属图表叠加自定义绘制属高风险操作,参数误用可能导致图形错位,仅作技术验证用途。

MQL5 / C++
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 里看输出,比背公式有用。外汇和贵金属杠杆高,这类数学工具只辅助验证,别当成信号源。

常见问题

把箭头方向向量写成行矩阵,乘上旋转矩阵得到新向量,再加到起点坐标上就是末端位置,注意行列顺序别搞反。
在 OnDeinit 里用 ObjectsDeleteAll(0, 前缀名) 按对象前缀批量删除,避免残留对象堆在图表上。
可以,把代码贴给小布,它会标出行列矩阵互换时的索引坑,并提醒你在退出时加清理逻辑。
因为存储时[i][j]和[j][i]含义颠倒,遍历赋值要手动反转下标,否则画出来的点会整体错位。
按变换顺序左乘矩阵得到终矩阵,统一乘点集后重绘,结束前确保对象命名有规律方便一键清除。