通过差异化和熵值分析来探索市场"记忆"·综合运用
(3/3)·从长期依赖到ML实时调参,把前兩篇的理论收口成可跑的智能化交易系统
◍ 交叉样本熵的匹配计数实现
交叉样本熵(Cross-Sample Entropy)用来衡量两段价格序列在容差 r 内的相似复杂度,外汇与贵金属这类高噪声行情里,它比普通相关系数更抗随机扰动,但计算代价集中在匹配计数这一步。 下面这段 MQL5 把两段长度为 m 的子序列逐点求绝对差,再取差值数组的最大值与容差 r 比较:只要最大偏差不超过 r,就判为一次匹配并返回 1.0,否则返回 0.0。外层循环里 A、B 分别累加匹配概率后除以 (N - m),最终返回 -log(A/B)。 实盘验证时建议先把 m 设为 2、r 取信号标准差的 0.2 倍,在 EURUSD 的 M15 上跑一遍 cross_match,观察不同波动阶段返回值是否在 0~1 之间剧烈跳动——这能帮你判断当前品种是否适合用该熵值做状态过滤。贵金属 XAUUSD 点值大、跳空多,r 若小于 0.15 倍标准差可能几乎匹配不上,属正常高风险现象。
ArrayCopy(ins, u, class="num">0, i, m + class="num">1); ArrayCopy(ins2, v, class="num">0, j, m +class="num">1); A += cross_match(ins, ins2, m + class="num">1, r) / (N - m); } } B /= N - m; A /= N - m; class="kw">return -log(A / B); } class=class="str">"cmt">// calculation of the matching number class=class="str">"cmt">// it use in the cross-sample entropy calculation class="type">class="kw">double cross_match(class="type">class="kw">double &signal1[], class="type">class="kw">double &signal2[], class="type">int m, class="type">class="kw">double r) { class=class="str">"cmt">// class="kw">return class="num">0 if not match and class="num">1 if match class="type">class="kw">double darr[]; for(class="type">int i=class="num">0; i<m; i++) { class="type">class="kw">double ins[class="num">1]; ins[class="num">0] = MathAbs(signal1[i] - signal2[i]); ArrayInsert(darr, ins, class="num">0, class="num">0, class="num">1); } if(darr[ArrayMaximum(darr)] <= r) class="kw">return class="num">1.0; else class="kw">return class="num">0.0; }
用 SampEn 给价格增量测‘惯性’
价格增量若在扩张,下一刻继续扩张的概率有多大?这个问题落到实操层,就是测序列的持久性(persistence)。MQL5 体系里常用 SampEn(样本熵)在滑动窗口里评估二值化增量的持续程度,思路来自分数型布朗运动:先把差分序列 d_i 做粗粒度二值化,增量大于 0 记 +1,否则记 -1,得到 BinInc_i。 二值化后只盯四种转移:上→上、下→下、上→下、下→上。统计上几乎所有过程对 BinInc_i 的 SampEn 误差都极小,且这个方法不要求数据是马尔可夫链,除平稳性外不需要预设其他特征。若满足一阶马尔可夫性,则 SampEn(1)=SampEn(2),可据此反推性质。 Mandelbrot 的分数型布朗运动本就为‘长期记忆’和‘重尾’建模,Hurst 指数与 R/S 分析都由此派生。外汇与贵金属价格常带这类长依赖和厚尾,属高风险品种。直接结论:滑动窗口里 SampEn 越低,增量持久性越高;SampEn 越高,反转倾向越大。开 MT5 接一段 EURUSD 的 M1 差分跑一遍 SampEn,就能看到极端行情段熵值明显塌陷。
「给差分序列做持久性打分」
把指标重写后,可以切到持久性评估模式跑。熵估算只吃离散值,所以增量先按最高 2 位精度标准化,再喂给分数型差分流程。 附件里的 fractional entropy 指标就是完整版,设置项直接决定输出的是熵还是标准化增量。图例里上半部是窗口 50 的熵曲线,下半部是 0.8 度分数差分的增量,两者走势不相关。 这种不相关对后面要搭的机器学习模型是好事——特征间没有多重共线性,训练时不会互相打架。外汇和贵金属价格序列本身高风险,这类统计特征只作概率参考,别当成方向确认。 下面这段输入参数就是你上 MT5 后最先要改的几个:entropy_eval 切显示模式,diff_degree 调差分阶数,entropy_window 控制滑动窗口长度,默认 50 就够先看一轮。
input class="type">bool entropy_eval = true; class=class="str">"cmt">// show entropy or increment values input class="type">class="kw">double diff_degree = class="num">0.3; class=class="str">"cmt">// the degree of time series differentiation input class="type">class="kw">double treshhold = class="num">1e-5; class=class="str">"cmt">// threshold to cut off excess weight(the class="kw">default value can be used) input class="type">int hist_display = class="num">5000; class=class="str">"cmt">// depth of the displayed history input class="type">int entropy_window = class="num">50; class=class="str">"cmt">// sliding window for the process entropy evaluation
◍ EA 内自优化为何选 logit 回归
差分后的序列平稳性更好,适合直接喂给机器学习模型做信号判定。但 EA 要在行情中自己跑优化,对算法有两个硬约束:学习必须快、推断延迟必须低,否则实时性会塌。 基于这两点,我用了逻辑回归而不是树模型或神经网络。它的数学形态很轻:用一组预测因子 x1…xN(这里就是指标值)去估依赖变量 y 属于某类的概率,y 取 0 或 1,正好对应空或多。 给定自变量向量,y=1 的概率由 sigmoid 决定:f(z)=1/(1+e^{-z}),其中 z 是 1,x1…xN 与回归系数的线性组合。曲线在 0 附近最陡、两端饱和,意味着因子边际变化对概率的影响在中段最强。 实际落地不需要自己写求解器。MT5 里直接调 Alglib 的 CLogitModel 类,把差分序列和标签送进去训练,EA 就能在 tick 级拿到实时信号概率。外汇和贵金属杠杆高、滑点突变频繁,这类概率输出只作倾向参考,别当成确定性入口。
把虚拟测试器和 logit 回归塞进一个类
CAuto_optimizer 这个类把轻量虚拟测试器和 logit 回归打包在一起:内部用 LRPM 矩阵存 x/y 值,Lmodel 跑 logit,训练完 Lout[] 拿到 0:1 两类的信号概率。构造函数吃四个参数——学习窗口 number_of_samples、重训间隔 relearn_timout、分数差分度 diff_degree、熵窗口 entropy_window,等于把样本长度和模型刷新节奏一次性锁死。 virtual_optimizer() 的逻辑很直白:先循环把指标值加线性趋势填进 LRPM 第一列,再比当前收盘和前收盘,大于前值记买入、否则记卖出,对应列填 0 或 1。它不挑最优信号,只是每根柱线读一遍,属于能跑但粗糙的测试器。 训练走 MNLTrain(),只传一个 x 变量;训完在优化器窗口画余额图,肉眼能看模型怎么拟合学习样本,但算法层没做性能量化。调用时先数距上次训练过了多少根柱,超了阈值就重训,再把最新指标值丢进 MNLProcess() 吐出类属概率当信号。 下面这段是类骨架,注意 hnd/hnd1 分别抓了 fractional entropy 指标的两种模式(false/true),窗口和差分度都来自构造参数,直接抄进 MT5 能编译。外汇和贵金属波动剧烈,这类信号概率只作参考,实盘前务必用历史数据验证过拟合风险。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//|Auto optimizer class | class=class="str">"cmt">//+------------------------------------------------------------------+ class CAuto_optimizer { class="kw">private: class=class="str">"cmt">// Logit regression model ||||||||||||||| CMatrixDouble LRPM; CLogitModel Lmodel; CLogitModelShell Lshell; CMNLReport Lrep; class="type">int Linfo; class="type">class="kw">double Lout[]; class=class="str">"cmt">//|||||||||||||||||||||||||||||||||||||||| class="type">int number_of_samples, relearn_timout, relearnCounter; class="kw">virtual class="type">void virtual_optimizer(); class="type">class="kw">double lVector[][class="num">2]; class="type">int hnd, hnd1; class="kw">public: CAuto_optimizer(class="type">int number_of_sampleS, class="type">int relearn_timeouT, class="type">class="kw">double diff_degree, class="type">int entropy_window) { this.number_of_samples = number_of_sampleS; this.relearn_timout = relearn_timeouT; relearnCounter = class="num">0; LRPM.Resize(this.number_of_samples, class="num">5); hnd = iCustom(NULL, class="num">0, "fractional entropy", class="kw">false, diff_degree, class="num">1e-05, number_of_sampleS, entropy_window); hnd1 = iCustom(NULL, class="num">0, "fractional entropy", true, diff_degree, class="num">1e-05, number_of_sampleS, entropy_window); } ~CAuto_optimizer() {}; class="type">class="kw">double getTradeSignal(); }; class=class="str">"cmt">//+------------------------------------------------------------------+
「用虚拟测试器回放逻辑回归信号」
把两个指标缓冲区和收盘价方向塞进 LRPM 矩阵,是虚拟优化器的第一步。indarr 和 indarr2 分别来自 hnd、hnd1 两个句柄,从偏移 1 开始取 number_of_samples 根 K 线,再倒序排列,保证最新样本在索引 0。 归类标签时只认一根 K 线的涨跌:若 iClose(NULL,0,s) 大于前一根就标为上涨(列3=0、列4=1),否则标下跌。这个二分类标签随后送进 CLogit::MNLTrainH 做多项逻辑回归训练,输入维度 3、输出 2。 训练完用 MNLProcess 对每根 K 线算概率 out[0]。阈值卡在 0.5:大于 0.5 且空仓就开多,小于 0.5 且空仓就开空;同向信号直接 continue 不重复开。 平仓逻辑反过来:持多时 out[0] 跌回 0.5 下方,就按开仓价减平仓价累加进 profit 数组,持空则相反。外汇和贵金属波动剧烈,这套虚拟回放只反映历史样本概率,实盘可能明显偏离。 让小布替你跑这套 把 number_of_samples 调到 500 以上,MT5 策略测试器里看 profit 数组的增长斜率,比肉眼翻图直观得多。
CAuto_optimizer::virtual_optimizer(class="type">void) { class="type">class="kw">double indarr[], indarr2[]; CopyBuffer(hnd, class="num">0, class="num">1, this.number_of_samples, indarr); CopyBuffer(hnd1, class="num">0, class="num">1, this.number_of_samples, indarr2); ArraySetAsSeries(indarr, true); ArraySetAsSeries(indarr2, true); for(class="type">int s=this.number_of_samples-class="num">1;s>=class="num">0;s--) { LRPM[s].Set(class="num">0, indarr[s]); LRPM[s].Set(class="num">1, indarr2[s]); LRPM[s].Set(class="num">2, s); if(iClose(NULL, class="num">0, s) > iClose(NULL, class="num">0, s+class="num">1)) { LRPM[s].Set(class="num">3, class="num">0.0); LRPM[s].Set(class="num">4, class="num">1.0); } else { LRPM[s].Set(class="num">3, class="num">1.0); LRPM[s].Set(class="num">4, class="num">0.0); } } CLogit::MNLTrainH(LRPM, LRPM.Size(), class="num">3, class="num">2, Linfo, Lmodel, Lrep); class="type">class="kw">double profit[], out[], prof[class="num">1]; ArrayResize(profit,class="num">1); ArraySetAsSeries(profit, true); profit[class="num">0] = class="num">0.0; class="type">int pos = class="num">0, openpr = class="num">0; for(class="type">int s=this.number_of_samples-class="num">1;s>=class="num">0;s--) { class="type">class="kw">double in[class="num">3]; in[class="num">0] = indarr[s]; in[class="num">1] = indarr2[s]; in[class="num">2] = s; CLogit::MNLProcess(Lmodel, in, out); if(out[class="num">0] > class="num">0.5 && !pos) {pos = class="num">1; openpr = s;}; if(out[class="num">0] < class="num">0.5 && !pos) {pos = -class="num">1; openpr = s;}; if(out[class="num">0] > class="num">0.5 && pos == class="num">1) class="kw">continue; if(out[class="num">0] < class="num">0.5 && pos == -class="num">1) class="kw">continue; if(out[class="num">0] > class="num">0.5 && pos == -class="num">1) { prof[class="num">0] = profit[class="num">0] + (iClose(NULL, class="num">0, openpr) - iClose(NULL, class="num">0, s)); ArrayInsert(profit, prof, class="num">0, class="num">0, class="num">1); pos = class="num">0; } if(out[class="num">0] < class="num">0.5 && pos == class="num">1) {
◍ 信号生成与重训练触发
类方法 getTradeSignal 负责在每根新 tick 或 bar 上输出多分类逻辑回归的预测概率。首次调用时 relearnCounter 为 0,会先跑一次 virtual_optimizer 做虚拟样本内优化,之后每调用一次计数器加 1,达到 relearn_timout 后归零重新触发优化,相当于给模型加了定期‘再拟合’的闸。 实际取数只用两个指标句柄的最新值:CopyBuffer 分别取 hnd 与 hnd1 的第 0 列第 0 行,拼成长度为 3 的输入向量 inn,第三维塞入 relearnCounter + number_of_samples - 1,把‘距下次重训还剩多少样本’也当作特征喂给模型。 最后 CLogit::MNLProcess 用已加载的 Lmodel 算多类逻辑回归输出,返回 Lout[0] 作为当前信号概率。外汇与贵金属杠杆高、滑点跳空频繁,该概率只反映历史样本下的条件倾向,实盘可能失效,务必在 MT5 策略测试器用真实点差回测后再决定是否接实盘。
class="type">class="kw">double CAuto_optimizer::getTradeSignal() { if(this.relearnCounter==class="num">0) this.virtual_optimizer(); relearnCounter++; if(this.relearnCounter>=this.relearn_timout) this.relearnCounter=class="num">0; class="type">class="kw">double in[], in1[]; CopyBuffer(hnd, class="num">0, class="num">0, class="num">1, in); CopyBuffer(hnd1, class="num">0, class="num">0, class="num">1, in1); class="type">class="kw">double inn[class="num">3]; inn[class="num">0] = in[class="num">0]; inn[class="num">1] = in1[class="num">0]; inn[class="num">2] = relearnCounter + this.number_of_samples - class="num">1; CLogit::MNLProcess(Lmodel, inn, Lout); class="kw">return Lout[class="num">0]; }
把优化器挂进EA并写信号执行逻辑
EA侧先把函数库接进来,核心输入就四个:History_depth=1000 是自动优化器用的训练样本柱数,FracDiff=0.5 控制价格序列的差分阶数,Recalc_period=100 表示每走100根柱重训一次模型,Entropy_window=50 则是熵计算滑动窗口长度。这几个值直接决定模型记忆厚度与重训频率,外汇和贵金属波动跳变快,窗口设太小容易过拟合、设太大滞后明显,属于高风险品种下的参数博弈。 信号判定只用一条朴素规则:模型输出买入概率 sig1 大于 0.5 视为做多信号,同时平掉空仓;小于 0.5 反之平多并做空。下面这段代码把库引用、参数声明和 placeOrders 执行体都摊开了,注意它走的是 MT4Orders 兼容层,下单用 OP_BUY / OP_SELL 而非 MT5 原生 CTrade。 止损与保本线写死在 input 里:Stop_loss=500 点、BreakEven=300 点,MaximumRisk=0.01 给 lotsOptimized 做仓位缩放。开 MT5 把 Auto optimizer.mqh 和 MT4Orders.mqh 都扔进 Include 目录,直接编译这段就能看到每100柱重训后信号翻仓的动作,验证概率阈值是否如预期切换。
class="macro">#include <MT4Orders.mqh> class="macro">#include <Math\Stat\Math.mqh> class="macro">#include <Trade\AccountInfo.mqh> class="macro">#include <Auto optimizer.mqh> input class="type">int History_depth = class="num">1000; input class="type">class="kw">double FracDiff = class="num">0.5; input class="type">int Entropy_window = class="num">50; input class="type">int Recalc_period = class="num">100; sinput class="type">class="kw">double MaximumRisk=class="num">0.01; sinput class="type">class="kw">double CustomLot=class="num">0; input class="type">int Stop_loss = class="num">500; class=class="str">"cmt">//Stop loss, positions protection input class="type">int BreakEven = class="num">300; class=class="str">"cmt">//Break even sinput class="type">int OrderMagic=class="num">666; class="kw">static class="type">class="kw">datetime last_time=class="num">0; CAuto_optimizer *optimizer = new CAuto_optimizer(History_depth, Recalc_period, FracDiff, Entropy_window); class="type">class="kw">double sig1; class="type">void placeOrders(){ if(countOrders(class="num">0)!=class="num">0 || countOrders(class="num">1)!=class="num">0) { for(class="type">int b=OrdersTotal()-class="num">1; b>=class="num">0; b--) if(OrderSelect(b,SELECT_BY_POS)==true) { if(OrderType()==class="num">0 && sig1 < class="num">0.5) if(OrderClose(OrderTicket(),OrderLots(),OrderClosePrice(),class="num">0,Red)) {}; if(OrderType()==class="num">1 && sig1 > class="num">0.5) if(OrderClose(OrderTicket(),OrderLots(),OrderClosePrice(),class="num">0,Red)) {}; } } if(countOrders(class="num">0)!=class="num">0 || countOrders(class="num">1)!=class="num">0) class="kw">return; if(sig1 > class="num">0.5 && (OrderSend(Symbol(),OP_BUY,lotsOptimized(),SymbolInfoDouble(_Symbol,SYMBOL_ASK),class="num">0,class="num">0,class="num">0,NULL,OrderMagic,INT_MIN)>class="num">0)) { class="kw">return; } if(sig1 < class="num">0.5 && (OrderSend(Symbol(),OP_SELL,lotsOptimized(),SymbolInfoDouble(_Symbol,SYMBOL_BID),class="num">0,class="num">0,class="num">0,NULL,OrderMagic,INT_MIN)>class="num">0)) {} }
「别急着下结论」
这套自优化 EA 在 EURUSD 的 M15 周期上,按指定超参数、不开遗传优化、以开盘价跑了一遍,训练样本里的虚拟测试曲线呈稳定增长。但这只是单品种单周期的现象,样本外和不同点差环境下是否还能维持,原文作者自己也在评论区承认「要用于风险交易还需更多研究」。 外汇与贵金属本身高杠杆、高波动,熵记忆逻辑即便在回测里看起来有效,也只代表历史窗口的概率倾向,不等于未来可复制。 真想验证,把 fractional_entropy_trader.mq5 拖进 MT5 用默认参数先跑一遍 EURUSD M15,再换品种和周期对比 equity 曲线,比直接信结论更有用。