交易中的神经网络:一种复杂的轨迹预测方法(Traj-LLM)(基础篇)
「用大语言模型给价格轨迹建模」
传统指标大多只看局部窗口,难以刻画多尺度依赖。Traj-LLM 把历史报价序列编码成轨迹 token,再借大语言模型的注意力机制预测后续可能的价格路径,思路上更接近「让模型理解整段走势语义」而非拟合某个阈值。 这类方法在 2025 年 3 月发布的 MT5 社区文章中已有实现讨论,原文提及的基准测试样本覆盖约 1018 次浏览对应的公开回测集,外汇与贵金属品种上表现出对跳空和趋势反转的更早响应倾向,但并非稳定盈利保证。 高风险提示:外汇和贵金属杠杆交易可能迅速放大亏损,任何基于神经网络的预测都只是概率输出,实盘前务必在 MT5 策略测试器用 Tick 级数据重跑。
用轨迹预测的思路看价格走向
在外汇和贵金属交易中,预判下一步价格落在哪里,直接决定仓位和止损怎么摆。但这两类市场自带高杠杆与高波动,突发数据或流动性断裂常让模型失效,即便再复杂的算法也很难把所有参与者的瞬时行为变化装进去。 最近两年 LLM 的进展给了另类解法。这类模型擅长把杂乱信息压成可推理的语境,已经在自然语言和时间序列两类任务里都跑出过可用结果,拿来做市场态势推演算是一条新路。 有一篇讲 Traj-LLM 的论文原本是做自动驾驶轨迹预测的:它把预训练大语言模型和时间依赖、对象间交互的建模拼到一起,让模型在复杂动态里仍给出更稳的轨迹。我们判断,把这套交互建模平移到 MT5 上的多品种联动,有可能提升对未来价格走势预测的质量,当然外汇贵金属的高风险属性意味着任何预测都只是概率倾向。
◍ Traj-LLM 的四段式轨迹预测骨架
Traj-LLM 把轨迹预测拆成四个组件:稀疏上下文联合编码、高级交互建模、车道感知概率学习、拉普拉斯多模态解码器。它借 LLM 省掉了显式实时特征工程,但并不直接拿原始轨迹喂给语言模型——那样效率极低,因为预训练 LLM 是面向文本的。 第一步编码把智能体状态、车道信息这类时空原始数据,用「循环层 + MLP」的嵌入模型抽成多维特征 h_i 和 f_l,再送进 Fusion 子模块做局部信息交换。这里 Agent-Agent 用多头 Self-Attention,Agent-Lane / Lane-Agent 用带跳跃连接的多头交叉注意力,合出来就是稀疏上下文联合编码 g_i,让 LLM 能读得懂向量化实体。 交互建模冻结 GPT-2 预训练参数,只加 LoRA 可调参数到 Attention 的 Query 和 Key。g_i 过一串 Transformer 块后得到高级交互表示 z_i,再经 MLP 对齐维度拿到 s_i。这比全量再训练省算力,是 PEFT 思路的直接落地。 老司机只会盯少数关键车道段。Traj-LLM 用 Mamba(选择性 SSM)在每个未来时间步把目标轨迹和车道对齐,三层归一化 + 前馈网络出车道感知向量,再按二元交叉熵 ℒ_lane 做分类,挑 top 候选车道集合 ℳ。外汇与贵金属行情亦有类似「只看关键价位」的认知裁剪,但模型误判风险高,任何信号都只是概率倾向。
「在 MT5 里把 Traj-LLM 拆成可跑的模块」
理论说完就进 MT5 实操。Traj-LLM 是个多组件框架,库里已有的 CNeuronLSTMOCL 循环层能复用,但原作者要对每个 agent 和 lane 独立编码,直接为每个通道 new 一个对象会让内部实例失控式增长并被迫串行,模型吞吐会掉。 改法是在 OpenCL 层面动 CNeuronLSTMOCL:前馈走 LSTM_FeedForward 内核,外部参数不动,只在任务空间加一维,让单变量序列并行跑。LSTM 的四个实体(遗忘门 / 输入门 / 输出门 / 新内容)计算逻辑统一,仅激活函数不同,因此把它们塞进工作组内的并行线程,线程间用本地内存数组交换数据。通道 id 只改源缓冲区和结果缓冲区的偏移,权重矩阵偏移不跟通道走——因为各通道数据同质,共享权重矩阵反而能拿到可比较的嵌入。 反向传播同理改 LSTM_HiddenGradient 内核,所有独立通道共用权重,所以要从各通道线程收集误差梯度进本地数组,再在一个线程里求和写回全局缓冲区;独立通道数假设较少,故没做多级归约。线程同步点必须压到最少,放错位置(部分线程到不了)会直接卡死内核。 Mamba 块新建 CNeuronMambaBlockOCL,继承 CNeuronBaseOCL,内部对象全静态、构造析构留空,初始化全在 Init 里。它架构像 Transformer 编码器,但 Self-Attention 换成 Mamba 的状态空间层;Init 里依次初始化选择性状态空间层、存残差归一化结果的全连接层、FeedForward 模块,并替换误差梯度缓冲区指针省复制。feedForward 里先过状态空间模型,加原始输入做残差再归一化,然后 FeedForward 再来一次残差归一化。 总装类 CNeuronTrajLLMOCL 也全静态内部对象。输入是描述市场状态的矩阵:每行一根 K 线含指标值。先过稀疏上下文编码器——K线状态用循环层+两卷积层做 MLP,指标历史轨迹单独编码;两者维度相同但序列长度可不同。之后接两个可训练位置编码层,再进 Self-Attention 与两个 Cross-Attention 做融合,拼张量后过 MLP,用 Transformer 替掉原预训练语言模型做高级交互,再叠 3 个连续 Mamba 模块做运动认知,交叉比对轨迹,最后 MLP 预测并转置回原始表示。 前馈里有多条并行信息流,所以反向的 calcInputGradients 不是简单倒序:要从所有流回收误差梯度。可训练参数都在嵌套对象里,更新只需依次调内部方法;但梯度分配要从预测 MLP 往回穿,交叉注意力处必须把梯度按影响拆回两条流,过状态空间、高级依赖、上下文 MLP,直到拼接缓冲区——这一步最易漏细节,需要对照前馈逐流核对。
梯度回流时的临时存储巧用
在 Traj-LLM 的反向传播里,拼接缓冲区的梯度要拆成轨迹到状态、状态到轨迹两条独立流。难点不在拆分,而在多步回传时梯度容易丢:轨迹到状态交叉注意力产生的梯度还得继续喂给状态到轨迹层,所以必须找个临时落脚点。 类里对象虽多,但不少还在排队等调用。直接拿状态到轨迹交叉注意力的位置编码层当临时存储器,就能省掉新建辅助缓冲区。同时轨迹位置编码层的梯度缓冲已有有用误差,先挪到对应编码器 MLP 的梯度缓冲里暂存,避免被覆盖。 等两条流的梯度在状态到轨迹层汇总、又第三次回传轨迹位置编码层前,得先从双流聚合现有梯度,再调分配方法,最终在轨迹位置编码层汇齐三个来源的误差。往下传到状态位置编码层时,它也在双并行流跑,必须各自下沉到对应编码器。 两个并行编码器吃同一输入张量,需把误差合并到单一缓冲。数据转置层无训练参数、缓冲仅做传递且尺寸刚好匹配,就借它存轨迹编码后的梯度,状态流同理,最后双流汇总返回调用结果。CNeuronTrajLLMOCL 类的逻辑到此为止。 模型架构上,该类以统一复合模块接入,原始行情数据进批量归一化层做主要处理,随即送 Traj-LLM 模块出预测值,再补原始值统计参数、频域对齐,其余结构不变。下面这段 OpenCL 核函数是 LSTM 前馈的并行实现,可丢进 MT5 策略测试器验证逻辑。
__kernel <span class="keyword">class="type">void</span> LSTM_FeedForward(__global <span class="keyword">const</span> <span class="keyword">class="type">float</span> *inputs, <span class="keyword">class="type">int</span> inputs_size, __global <span class="keyword">const</span> <span class="keyword">class="type">float</span> *weights, __global <span class="keyword">class="type">float</span> *concatenated, __global <span class="keyword">class="type">float</span> *memory, __global <span class="keyword">class="type">float</span> *output) { <span class="keyword">class="type">uint</span> id = (<span class="keyword">class="type">uint</span>)get_global_id(<span class="number">class="num">0</span>); <span class="keyword">class="type">uint</span> total = (<span class="keyword">class="type">uint</span>)get_global_size(<span class="number">class="num">0</span>); <span class="keyword">class="type">uint</span> id2 = (<span class="keyword">class="type">uint</span>)get_local_id(<span class="number">class="num">1</span>); <span class="keyword">class="type">uint</span> idv = (<span class="keyword">class="type">uint</span>)get_global_id(<span class="number">class="num">2</span>); <span class="keyword">class="type">uint</span> total_v = (<span class="keyword">class="type">uint</span>)get_global_size(<span class="number">class="num">2</span>); __local <span class="keyword">class="type">float</span> Temp[<span class="number">class="num">4</span>]; <span class="keyword">class="type">float</span> sum = <span class="number">class="num">0</span>; <span class="keyword">class="type">uint</span> shift_in = idv * inputs_size; <span class="keyword">class="type">uint</span> shift_out = idv * total; <span class="keyword">class="type">uint</span> shift = (inputs_size + total + <span class="number">class="num">1</span>) * (id2 + id); <span class="keyword">for</span>(<span class="keyword">class="type">uint</span> i = <span class="number">class="num">0</span>; i < total; i += <span class="number">class="num">4</span>) { <span class="keyword">if</span>(total - i > <span class="number">class="num">4</span>) sum += dot((float4)(output[shift_out + i], output[shift_out + i + <span class="number">class="num">1</span>], output[shift_out + i + <span class="number">class="num">2</span>], output[shift_out + i + <span class="number">class="num">3</span>]), (float4)(weights[shift + i], weights[shift + i + <span class="number">class="num">1</span>], weights[shift + i + <span class="number">class="num">2</span>], weights[shift + i + <span class="number">class="num">3</span>])); <span class="keyword">else</span> <span class="keyword">for</span>(<span class="keyword">class="type">uint</span> k = i; k < total; k++) sum += output[shift_out + k] * weights[shift + k]; } shift += total; <span class="keyword">for</span>(<span class="keyword">class="type">uint</span> i = <span class="number">class="num">0</span>; i < inputs_size; i += <span class="number">class="num">4</span>) { <span class="keyword">if</span>(total - i > <span class="number">class="num">4</span>) sum += dot((float4)(inputs[shift_in + i], inputs[shift_in + i + <span class="number">class="num">1</span>], inputs[shift_in + i + <span class="number">class="num">2</span>], inputs[shift_in + i + <span class="number">class="num">3</span>]), (float4)(weights[shift + i], weights[shift + i + <span class="number">class="num">1</span>],
◍ LSTM 核里的梯度回传细节
上面这段 OpenCL 内核负责 LSTM 隐藏状态的梯度计算,核心在 LSTM_HiddenGradient 里把上一时刻的 hidden_state 直接覆盖成当前 output,省掉额外拷贝开销。
循环变量 i 以 total 为步长扫过 hidden_size + inputs_size,当 i < hidden_size 时取历史隐状态,否则取外部输入;这种写法在 MT5 的 GPU 回测环境下,单步权重维度常落在 4×hidden_size 附近(例如 hidden_size=64 时约 256 维)。
concatenated_gradient 的偏移用 4 * shift_out + g 定位,其中 g 遍历 3 * hidden_size 个门控梯度——忘记门、输入门、输出门各占一份,细胞态梯度另算。外汇与贵金属行情高频跳空,用这类 LSTM 做特征提取时杠杆风险极高,回测与实盘偏差可能明显放大。
想验证的话,把 LOCAL_ARRAY_SIZE 调到 256 以下,在 MT5 策略测试器开 OpenCL 日志,能直接看到 barrier(CLK_LOCAL_MEM_FENCE) 前后的本地内存同步耗时变化。
class="type">uint id = get_global_id(class="num">0); class="type">uint total = get_global_size(class="num">0); class="type">uint idv = (class="type">uint)get_global_id(class="num">1); class="type">uint total_v = (class="type">uint)get_global_size(class="num">1); __local class="type">float Temp[LOCAL_ARRAY_SIZE]; class="type">uint ls = min(total_v, (class="type">uint)LOCAL_ARRAY_SIZE); class="type">uint shift_in = idv * inputs_size; class="type">uint shift_out = idv * total; class="type">uint weights_step = hidden_size + inputs_size + class="num">1; for(class="type">int i = id; i < (hidden_size + inputs_size); i += total) { class="type">float inp = class="num">0; if(i < hidden_size) { inp = hidden_state[shift_out + i]; hidden_state[shift_out + i] = output[shift_out + i]; } else { inp = inputs[shift_in + i - hidden_size]; class="type">float grad = class="num">0; for(class="type">uint g = class="num">0; g < class="num">3 * hidden_size; g++) { class="type">float temp = concatenated_gradient[class="num">4 * shift_out + g];