交易中的神经网络:统一轨迹生成模型(UniTraj)(基础篇)
「用统一轨迹模型给行情做条件生成」
传统指标只给单一输出,而 UniTraj 把行情当成一条可条件生成的轨迹:给定上下文窗口,模型同时拟合多条可能路径,而不是硬猜下一个点位。这思路对 MT5 上的外汇与贵金属尤其有用,因为这两类品种的高杠杆与跳空特性,单点预测极易被噪声击穿。 在 MQL5 里可以先把历史序列规整成模型输入。下面这段把最近 100 根收盘价拉成数组,作为轨迹上下文的雏形;注意外汇与贵金属波动率高,实盘前务必用小资金验证,策略失效概率不低。 真正落地时,建议先在策略测试器用 2020–2024 年 XAUUSD 的 M5 数据回测,观察模型在重大数据日的轨迹发散宽度,再决定是否接实盘信号。
class="type">class="kw">double close_array[class="num">100]; CopyClose(_Symbol, PERIOD_CURRENT, class="num">0, class="num">100, close_array); class=class="str">"cmt">// 将最近100根收盘价读入数组,作为轨迹上下文输入 for(class="type">int i=class="num">0;i<class="num">100;i++) close_array[i]=close_array[i]/_Point; class=class="str">"cmt">// 按最小报价单位归一化,降低不同品种量纲差异
把残缺轨迹喂给统一模型
多体轨迹分析里,跟踪、识别、轨迹建模、动作识别是四道基础工序,其中轨迹建模直接决定后续走势判断的质量。现有方法大多绑定单一任务:预测模型常忽略后向时空依赖,条件计算算法能算历史却常在推演未来时失准,导致只能复刻过去、难支撑实盘规划。外汇与贵金属多周期联立下,这种局限会让策略回测和实时推演脱节,属典型高风险场景。 UniTraj 把任意不完整的个体轨迹加一张可见性蒙版,统一成「蒙版轨迹」输入,用同一个制程处理预测、补数、建模。它内嵌 Ghost Spatial Mask 进 Transformer 编码器,并把 Mamba 改成双向时态编码器管长期依赖,再配 Bidirectional Temporal Scaling 扫轨迹保时序。论文实验显示该框架在多项任务上稳健性优于专用模型,读者可据此在 MT5 用历史 tick 构造蒙版轨迹,验证长周期重建偏差。
◍ 把任意残缺轨迹塞进同一个生成框架
UniTraj 的核心做法,是把任意输入都看成一条带蒙版的轨迹序列:看得见的位置当约束,看不见的段就是生成目标。具体定义里,完整轨迹 X 形状为 [N, T, D],N 是个体数、T 是步长、D 是状态维度;二元蒙版 M[N,T] 标记 m_i,t=1 为已知、0 为缺失,于是轨迹被切成 X_v=X⊙M 与 X_m=X⊙(1−M) 两段,模型要吐出 Y'={X'_v, X'_m},其中 X'_v 是重造、X'_m 是新生成。 相对速度 v_i,t 由相邻时间步坐标相减得到,缺失位填零再乘蒙版;单类别向量 c_i,t 用来承载进攻/防御这类策略身份,个体特征经投影函数 φ_x(⋅)(作者用带 W_x 的 MLP 实现)融位置、速度、可见性、类别为高维 f_i,xt,⊕ 表示级联、⊙ 是逐元素乘。 和侧重密集社交互动的注意力模型不同,UniTraj 面对的是任意不完整输入,所以它加了「幽灵空间蒙版(GSM)」模块抽象缺失的空间结构,无缝嵌进变换器且不增复杂度。编码器用多头自注意力在每个时间步对 N 个个体做顺序无关的空间建模,并用可训练编码替掉正弦位置编码,输出 F_s,xt 再沿时间级联成整条轨迹的空间表示。 时态侧用双向时态曼巴补长依赖:蒙版 M 沿时间不折叠成 M',经双向时态缩放(BTS)得伸缩矩阵 S 与逆 S'。s_i,t 按「距上次观测的步数」用负指数加 ReLU 算,约束影响单调衰在 0~1,量化缺失缝隙的随时间淡化。后验高斯均值 μ_q、标准差 σ_q 算出后从 N(0,I) 采潜变量 Z,和解码函数 φ_dec(MLP)合起来生成整轨;训练时同时算可见区重造误差与蒙版区恢复误差。外汇/贵金属相关的轨迹预测本就高风险,这类生成误差只是概率性约束,不能直接当方向依据。
「把 UniTraj 塞进 MT5:OpenCL 与类的实战落地」
原论文的 UniTraj 在结构上有两处和之前方法不同:一是额外蒙版步骤拖慢决策,二是全轨迹传输在实盘拿不到、还吃内存。折中方案是把数据拆成历史与未来两部分,未来轨迹只在训练阶段喂给模型提取时空依赖,实时跑预测模式时直接省掉这张张量。 OpenCL 端先写了 UniTrajPrepare 内核,把历史与已知未来轨迹级联并套蒙版。它吃 4 个输入缓冲、吐 1 个输出,二维任务空间第一维取历史深度和规划范围的较大值,第二维是变量数。配套的反向内核 UniTrajPrepareGrad 复用前馈结果缓冲,不回传蒙版梯度;伸缩矩阵内核 UniTrajBTS 只用 2 个线程分别算正逆矩阵,且因只配蒙版工作、无反向内核。 主程序里算法收进 CNeuronUniTraj 类,全部对象 static、内存交系统管。Init 里按作者实验结论只放 1 个 Transformer 编码层加 4 个 Mamba 层,输入窗口大小硬设为 3(数值/速度/蒙版),序列长由变量数定。feedForward 时若没预测值就用全零蒙版,训练期随机蒙盖 50% 输入逼模型学复原。 模型集成到环境状态编码器,CreateEncoderDescriptions 中先全连接接原始价、批量归一化去量纲,再进 UniTraj 模块(蒙版系数 50%),输出补回归一化删掉的统计变量并做频域校准。训练 EA 改了 StudyEncoder.mq5:Train 里按回报概率选轨迹,Mamba 递归性质要求单条轨迹内嵌循环迭代状态,50% 概率只投历史、50% 连真实未来一起投,输出合并张量跑反向把恢复+预测误差压到最小。外汇/贵金属这类高波动品种上,该结构对显存和速度的影响需要在你本机 MT5 实测才知边界。 下面这段是 OpenCL 前馈内核的头部声明,看清指针布局就能照着改自己的数据缓冲:
__kernel <span class="keyword">class="type">void</span> UniTrajPrepare(__global <span class="keyword">const</span> <span class="keyword">class="type">class="kw">float</span> *history, __global <span class="keyword">const</span> <span class="keyword">class="type">class="kw">float</span> *h_mask, __global <span class="keyword">const</span> <span class="keyword">class="type">class="kw">float</span> *future, __global <span class="keyword">const</span> <span class="keyword">class="type">class="kw">float</span> *f_mask,
GPU 核函数里的轨迹张量排布
在 MT5 的 OpenCL 管线里,历史与未来样本被压成一段连续 float 缓冲,每个变量在每个时间步占 3 个槽位:原始值、差分增量、缺失掩码。核函数 UniTrajPrepare 用 get_global_id(0) 取时间索引 i、get_global_id(1) 取变量索引 v,再靠 shift_out = 3 * (i * variables + v) 定位写入位置。 历史段处理时,若 i 不在末位且掩码非 0,差分 v 取相邻步 history[shift_in + variables] - h 再乘 mask;否则置 0。遇到 isnan / isinf 直接把 v、h、mask 全清 0,避免梯度爆炸污染后续训练。 未来段逻辑对称,但偏移要叠加上 shift_f_out = 3 * (h_total * variables + v),把未来数据接在历史缓冲之后。下面这段是核函数头部与历史写入的核心片段,可直接拷进 .cl 文件用 clGetKernelInfo 验证局部尺寸。 别在 CPU 模拟器里跑这套 MT5 的 OpenCL 模拟模式会把 get_global_size(1) 退化成 1,variables 维度塌缩后 shift_out 计算虽不出错,但并行优势全无;真要测吞吐得在带独显的终端开『使用 OpenCL』勾选,否则回测时 GPU 占用可能长期为 0。
__global class="type">class="kw">float *output, const class="type">int h_total, const class="type">int f_total ) { const class="type">size_t i = get_global_id(class="num">0); const class="type">size_t v = get_global_id(class="num">1); const class="type">size_t variables = get_global_size(class="num">1); const class="type">int shift_in = i * variables + v; const class="type">int shift_out = class="num">3 * shift_in; const class="type">int shift_f_out = class="num">3 * (h_total * variables + v); class=class="str">"cmt">//--- history if(i < h_total) { class="type">class="kw">float mask = h_mask[shift_in]; class="type">class="kw">float h = history[shift_in]; class="type">class="kw">float v = (i < (h_total - class="num">1) && mask != class="num">0 ? (history[shift_in + variables] - h) * mask : class="num">0); if(isnan(v) || isinf(v)) v = h = mask = class="num">0; output[shift_out] = h * mask; output[shift_out + class="num">1] = v; output[shift_out + class="num">2] = mask; } class=class="str">"cmt">//--- future if(i < f_total) { class="type">class="kw">float mask = f_mask[shift_in]; class="type">class="kw">float f = future[shift_in]; class="type">class="kw">float v = (i < (f_total - class="num">1) && mask != class="num">0 ? (future[shift_in + variables] - f) * mask : class="num">0); if(isnan(v) || isinf(v)) v = f = mask = class="num">0; output[shift_f_out + shift_out] = f * mask; output[shift_f_out + shift_out + class="num">1] = v; output[shift_f_out + shift_out + class="num">2] = mask; } }
◍ 历史与未来梯度的掩码归零逻辑
这段 OpenCL 内核把历史段与未来段的梯度分别写回 history_gr 与 future_gr,核心靠 mask 通道(偏移 +2)做门控。mask 不大于 0 时梯度直接记 0,意味着该变量在当前步被模型判为无效,不反向传播。 历史段梯度由 output_gr[shift_out] 乘 mask 起手,再减去相邻步梯度乘 mask,并加上前一步输出与掩码的乘积项;若算出 NaN 或 Inf 则强制归零,避免 GPU 上数值爆炸。未来段完全对称,只是索引叠了 shift_f_out 偏移。 UniTrajBTS 内核另算前向累积系数:当 i==0 时按 concat_inp 的掩码通道做 last = 1 + (1-m)*last 的递推,total 步循环下 d_forw 会随 mask 接近 1 而收敛、接近 0 而指数增长。外汇与贵金属行情跳空频繁,mask 异常偏高时这套系数可能放大噪声,实盘前建议在 MT5 策略测试器打印 d_forw 峰值验证。
const class="type">int shift_f_out = class="num">3 * (h_total * variables + v); class=class="str">"cmt">//--- history if(i < h_total) { class="type">class="kw">float mask = output[shift_out + class="num">2]; class="type">class="kw">float grad = class="num">0; if(mask > class="num">0) { grad = output_gr[shift_out] * mask; grad -= (i < (h_total - class="num">1) && mask != class="num">0 ? (output_gr[shift_out + class="num">1]) * mask : class="num">0); grad += (i > class="num">0 ? output[shift_out + class="num">1 - class="num">3 * variables] * output[shift_out + class="num">2 - class="num">3 * variables] : class="num">0); if(isnan(grad) || isinf(grad)) grad = class="num">0; class=class="str">"cmt">//--- } history_gr[shift_in] = grad; } class=class="str">"cmt">//--- future if(i < f_total) { class="type">class="kw">float mask = output[shift_f_out + shift_out + class="num">2]; class="type">class="kw">float grad = class="num">0; if(mask > class="num">0) { grad = output_gr[shift_f_out + shift_out] * mask; grad -= (i < (h_total - class="num">1) && mask != class="num">0 ? (output_gr[shift_f_out + shift_out + class="num">1]) * mask : class="num">0); grad += (i > class="num">0 ? output[shift_f_out + shift_out + class="num">1 - class="num">3 * variables] * output[shift_f_out + shift_out + class="num">2 - class="num">3 * variables] : class="num">0); if(isnan(grad) || isinf(grad)) grad = class="num">0; class=class="str">"cmt">//--- } future_gr[shift_in] = grad; } } __kernel class="type">void UniTrajBTS(__global const class="type">class="kw">float * concat_inp, __global class="type">class="kw">float * d_forw, __global class="type">class="kw">float * d_bakw, const class="type">int total ) { const class="type">size_t i = get_global_id(class="num">0); const class="type">size_t v = get_global_id(class="num">1); const class="type">size_t variables = get_global_size(class="num">1); if(i == class="num">0) { const class="type">int step = variables * class="num">3; const class="type">int start = v * class="num">3 + class="num">2; class="type">class="kw">float last = class="num">0; d_forw[v] = class="num">0; for(class="type">int p = class="num">1; p < total; p++) { class="type">class="kw">float m = concat_inp[start + p * step]; d_forw[p * variables + v] = last = class="num">1 + (class="num">1 - m) * last; } } else {