通过差异化和熵值分析来探索市场
🧠

通过差异化和熵值分析来探索市场"记忆"·综合运用

(3/3)·从长期依赖到ML实时调参,把前兩篇的理论收口成可跑的智能化交易系统

含代码示例 第 3/3 篇
很多交易者把价格增量当万能平稳化手段,却不知不觉剪掉了预测最需要的残余记忆。分数差分能在保留长期依赖的同时压住非平稳噪声,这篇把整套流程落到EA里。接上篇的熵与差异化基础,我们直接进综合应用。

◍ 交叉样本熵的匹配计数实现

交叉样本熵(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 倍标准差可能几乎匹配不上,属正常高风险现象。

MQL5 / C++
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 就够先看一轮。

MQL5 / C++
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 能编译。外汇和贵金属波动剧烈,这类信号概率只作参考,实盘前务必用历史数据验证过拟合风险。

MQL5 / C++
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 数组的增长斜率,比肉眼翻图直观得多。

MQL5 / C++
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 策略测试器用真实点差回测后再决定是否接实盘。

MQL5 / C++
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柱重训后信号翻仓的动作,验证概率阈值是否如预期切换。

MQL5 / C++
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 曲线,比直接信结论更有用。

把重复劳动交给小布
这些分数差分的平稳性诊断与实时熵监控,小布盯盘已内置AIGC模块,打开对应品种页即可看到序列记忆衰减曲线,你只管判断参数窗口是否该重训。

常见问题

核心只差差分阶数d可为小数,代码需重写滞后权重循环,本篇第3节给出持久性评估的可直接套用片段。
可以,小布的品种页支持自定义d阶导出CSV,EA用iCustom或文件读取都能接,省去自己写指标缓冲。
概率上存在,建议用滚动窗口且熵值超阈才触发重训,贵金属与外汇杠杆高,过拟合会放大滑点风险。
意味历史价格对远期仍有统计牵引,均值回归类策略可能比突破类在高频噪声下更稳,但需配合平稳性检验。
倾向不少于三年多品种交叉,单品种样本易陷特异,具体见第9节测试函数库与结论里的参数稳定性表。