交易中的神经网络:时间序列的分段线性表示(基础篇)
「把行情切成线段:时间序列的PLR思路」
做价格行为分析时,把连续报价压成若干条直线段,往往比盯原始K线更容易抓结构。这种做法叫分段线性表示(Piecewise Linear Representation,PLR),本质是用最少折点还原走势的主干。 在 MT5 里可以用最简单的最小误差法做 PLR:从左端起连直线,当下一点偏离超阈值就断开,新段重头连。下面这段 MQL5 演示了基础逻辑,实盘前建议先用历史数据跑一遍看段数对噪声的敏感度。 外汇与贵金属杠杆高、跳空频繁,PLR 段数设得太少会吞掉关键反转,设太多又退化成裸价格。先用 EURUSD 的 M15 回看 2024 年数据,段误差阈值 0.0003 附近通常能保留主要摆动。
◍ 为什么要把行情序列压成线段
在 MT5 里盯盘,你面对的原始报价是一串按时间排列的点。数据量一大,逐点分析的复杂度直线上升,决策延迟本身就会放大外汇和贵金属的高风险——该动手时还在算,滑点和错判就吃掉了利润空间。 把时间序列做分段线性表示(PLR),本质是用一小段一小段的直线去逼近原始曲线,用更少参数保留走势骨架。本文要落地的 BPLR(双向分段线性表示)来自一篇讨论集体异常检测的论文,它专门解决「一群点一起反常、但单拎出来都不像异常」这种难啃的问题。 异常分三类:点异常是单个点跳变,多半是报价源或传感器出错;上下文异常得看所处区间才显怪;集体异常是子序列整体行为偏移,单独看每个点都正常。在金融场景里,集体异常常藏着群体性的资金异动,忽略它等于丢掉预警信号。 直接用原始高维序列跑异常检测很吃算力。常规路子分两步:先降维,再在低维子空间里量距离。BPLR 就是把序列压成低维表达,配套的分段积分(PI)相似性度量用较低开销算距离,开 MT5 接数据验证时,这套组合能明显省下回测时的等待。
BPLR 双向分段怎么把周期切成形状
BPLR 的异常检测分两步走:先把时间序列做表示,再衡量相似度。它默认你分析的外汇或贵金属报价带周期性——这个周期大小要么靠实验估,要么来自先验,然后把整条输入序列切成不重叠、长度等于预期周期的子段,靠比对子段找异常区。 表示单个子段时,先抓所有可能的趋势转折点(TTP)。子段首尾元素自动算 TTP,方法里共识别 6 种 TTP 变体。每个 TTP 的重要性用「偏离子段均值多少」来量,再按重要性排好序。 从最重要的 TTP 1 出发,往前后两个方向迭代找分段线:反向遍历元素,只要候选点和 TTP 1 的偏离不超超参 δß(序列点与分段线的最大允许偏差)就继续,一旦超阈就停、存下分区;若前面发现的 TTP 落进分区里就删掉。正向同理。因为极值前后都搜,才叫「双向」。 两个子段像不像,看它们分段后围成的形状区域。作者建了距离矩阵 M_dist,每个子段 i 的总偏差 D_i 就是矩阵第 i 行求和;D_i 明显偏离其他子段相关均值,就判异常。论文里合成与真实数据实验都显示这套可行,但外汇/贵金属行情受事件驱动,周期假设本身可能失效,实盘验证前先在小布盯盘里跑一遍回测。
「用 OpenCL 把行情切成动态分段」
金融市场的高动态性意味着任意两段不相交行情都可能出现明显偏差,所以没必要去搜异常子序列,更实用的做法是把整段历史改写成分段线性表示(PLR)。PLR 用动态分段长度替代固定窗口,既能提取不同尺度的特征,又始终保持固定数量的输出参数,顺带压缩了数据量、降低了后续模型复杂度。外汇与贵金属波动剧烈,这种表征对降低过拟合有帮助,但仍是高风险工具,效果只具概率性。 我们在 OpenCL 端写了一个 PLR 内核,输入三个缓冲区指针:inputs(原始序列)、outputs(分段结果)、isttp(趋势转折点标记),再加两个常量 transpose(是否转置)和 min_step(注册转折点的最小偏差)。内核在二维空间发起,一维是序列长度、一维是多元序列里的单变量个数;每个线程先定位自己负责的元素,再判断它是否落在极值走廊的端点,只有走廊内极值才打 TTP 标志。打标后做线程组同步,未标记的线程直接退出,标记的线程计算分段斜率、偏移和长度——长度用「分段元素数 / 单变量总元素数」的归一化分数存,避免整数运算陷阱。 反向传播时另起一个 PLRGradient 内核,把后续层的误差梯度按前馈公式回传。因 PLR 本身无可训练参数,梯度只流向输入数据,不更新权重。下面给出前馈内核的参数声明片段,注意 min_step 直接决定转折点密度,调大它分段数会骤减。 主程序里封装成 CNeuronPLROCL 类,继承神经层基类。feedForward 调 PLR 内核时按单变量序列建局部组做同步;calcInputGradients 则各线程独立跑。实测把 PLR 接在批量归一化前、用统一超参处理多模态序列最稳:所有单变量先归一化到同一分布,min_step 就能通用,否则不同量纲的金银油汇率混在一起几乎调不动。模型后端接 3 层 MLP 做条件独立预测,再转置回原结构、补回统计参数,用 FreDF 协调各单变量轨迹。
__kernel <span class="keyword">class="type">void</span> PLR(__global <span class="keyword">const</span> <span class="keyword">class="type">class="kw">float</span> *inputs, __global <span class="keyword">class="type">class="kw">float</span> *outputs, __global <span class="keyword">class="type">int</span> *isttp, <span class="keyword">const</span> <span class="keyword">class="type">int</span> transpose,
◍ 用最小波动阈值卡掉毛刺拐点
这段 OpenCL 内核做的是 ZigZag 式的拐点判定,但和 MT5 自带指标不同,它用 min_step 当噪声门限,而不是固定点数。核心思路是:从当前 bar 向左右两个方向回溯,只要相邻极值差小于 min_step,就继续吞掉,直到拉开足够距离才算确认拐点。 代码里 i==0 或 i==lenth-1 直接标成 bttp(边界点必算),中间点则进入双向 while 循环。prev_pos 向左缩、next_pos 向右扩,过程中动态维护 max_v/min_v 与对应位置;循环退出条件是两个方向任一极值差突破 min_step,或碰到序列头尾。 最后那组 if 判断很直白:当前值同时大于等于左邻且大于右邻(或等号组合),且 max_pos/min_pos 落在本 bar,才认定为拐点。把 min_step 设成 EURUSD 的 0.0003~0.0005 去跑,能明显滤掉 5 分钟图上的假摆动;外汇和贵金属杠杆高,参数过窄会频繁重绘,实盘前务必在策略测试器里用历史数据验一遍重绘率。
const class="type">class="kw">float min_step ) { const class="type">size_t i = get_global_id(class="num">0); const class="type">size_t lenth = get_global_size(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); class=class="str">"cmt">//--- constants const class="type">int shift_in = ((class="type">bool)transpose ? (i * variables + v) : (v * lenth + i)); const class="type">int step_in = ((class="type">bool)transpose ? variables : class="num">1); class="type">class="kw">float value = inputs[shift_in]; class="type">bool bttp = class="kw">false; if(i == class="num">0 || i == lenth - class="num">1) bttp = true; else { class="type">class="kw">float prev = value; class="type">int prev_pos = i; class="type">class="kw">float max_v = value; class="type">class="kw">float max_pos = i; class="type">class="kw">float min_v = value; class="type">class="kw">float min_pos = i; class="kw">while(fmax(fabs(prev - max_v), fabs(prev - min_v)) < min_step && prev_pos > class="num">0) { prev_pos--; prev = inputs[shift_in - (i - prev_pos) * step_in]; if(prev >= max_v && (prev - min_v) < min_step) { max_v = prev; max_pos = prev_pos; } if(prev <= min_v && (max_v - prev) < min_step) { min_v = prev; min_pos = prev_pos; } } class=class="str">"cmt">//--- class="type">class="kw">float next = value; class="type">int next_pos = i; class="kw">while(fmax(fabs(next - max_v), fabs(next - min_v)) < min_step && next_pos < (lenth - class="num">1)) { next_pos++; next = inputs[shift_in + (next_pos - i) * step_in]; if(next > max_v && (next - min_v) < min_step) { max_v = next; max_pos = next_pos; } if(next < min_v && (max_v - next) < min_step) { min_v = next; min_pos = next_pos; } } if( (value >= prev && value > next) || (value > prev && value == next) || (value <= prev && value < next) || (value < prev && value == next) ) if(max_pos == i || min_pos == i)