您应该知道的 MQL5 向导技术(第 03 部分):香农(Shannon)熵·综合运用
📊

您应该知道的 MQL5 向导技术(第 03 部分):香农(Shannon)熵·综合运用

(3/3)·从熵公式推导到策略测试器落地,手把手看信息论如何筛掉低质量信号

案例拆解 第 3/3 篇
很多交易者把“波动大”等同于“机会多”,却忽略波动背后的信息冗余。用香农熵衡量品种的不确定性,能更快识别哪些行情只是噪声重复。接上篇的推导,这一篇把熵直接装进 EA 跑起来。

优化器里给负信号塞随机噪声

在策略优化阶段,如果信号类算出的是负值,代码不会直接丢弃,而是用均匀随机把结果拆成一对互补概率写回样本矩阵。具体看 CSignalSE::ResultUpdate,当 MQLInfoInteger(MQL_OPTIMIZATION) 为真且 Result<0.0 时,取 MathRandomUniform(0,1) 的随机数 _odds,分别填进 DF_SIGNAL 最后一行的第 __INPUTS 与 __INPUTS+1 位,两者相加恒为 1.0。 OnTick 里先判断 m_read_forest 标志,未读森林就 WriteForest(),再交给 ExtExpert.OnTick() 跑常规逻辑;OnTester 则在回测结束时 ReadForest() 并返回 0.0,这两段是把熵信号类接进 EA 事件循环的桥。 这类写法意味着:优化器看到的负信号样本,实际是被随机化过的分布,而非原始熵值。在 MT5 里跑遗传优化时,若你发现样本尾部出现大量 0~1 之间的对称填充,大概率就是这段在起作用,可据此判断是否要改阈值或换填充策略。外汇与贵金属杠杆高,任何信号填充逻辑都只是概率参考,实盘前务必用历史数据自检。

MQL5 / C++
  class="kw">return(_result);
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+  
class="type">void CSignalSE::ResultUpdate(class="type">class="kw">double Result)
  {
    if(MQLInfoInteger(MQL_OPTIMIZATION))
    {
      class="type">int _err;
      if(Result<class="num">0.0)
       {
        class="type">class="kw">double _odds = MathRandomUniform(class="num">0,class="num">1,_err);
        class=class="str">"cmt">//
        DF_SIGNAL[m_samples-class="num">1].Set(__INPUTS,_odds);
        DF_SIGNAL[m_samples-class="num">1].Set(__INPUTS+class="num">1,class="num">1-_odds);
       }
    }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| "Tick" event handler function                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
  {
   if(!signal_se.m_read_forest) signal_se.WriteForest();

   ExtExpert.OnTick();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| "Tester" event handler function                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+  
class="type">class="kw">double OnTester()
  {
    signal_se.ReadForest();
    class="kw">return(class="num">0.0);
  }
class=class="str">"cmt">// wizard description start
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Description of the class                                         |
class=class="str">"cmt">//| Title=Trading with &class="macro">#x27;Shannon Entropy&class="macro">#x27; optimized trade volume      |
class=class="str">"cmt">//| Type=Money                                                       |
class=class="str">"cmt">//| Name=SE                                                          |
class=class="str">"cmt">//| Class=CMoneySE                                                   |
class=class="str">"cmt">//| Page=money_se                                                    |
class=class="str">"cmt">//| Parameter=ScaleFactor,class="type">int,class="num">3,Scale factor                          |

◍ 把香农熵条件塞进仓位类

这段实现了一个叫 CMoneySE 的仓位管理类,它继承自 CExpertMoney,核心思路是用信号层的「绝对条件值」去动态修正开仓手数。类里只暴露两个写接口:ScaleFactor 设缩放整数,AbsoluteCondition 直接吃 signal_se.m_last_condition 的绝对值。 全局层把 CExpert、CSignalSE 指针和 CMoneySE 指针都挂了出来,OnTick 里先做一道判断:如果随机森林还没读入就先 WriteForest(),随后把最新条件的绝对值喂给 money_se,再跑专家对象的 OnTick()。 Optimize 函数是手数修正的落点,入参 lots 为基础手数,注释写明要「按条件量级归一化」——也就是说条件越强,手数倾向放大,但具体系数由 m_scale_factor 控制。外汇与贵金属杠杆高,这种按信号强度加仓的做法可能放大回撤,上 MT5 前先把 scale_factor 设为 1 做小样本验证。 别把归一化当护身符 条件绝对值大只代表模型当前置信度高,不意味着价格必然朝头寸方向走;仓位随条件放大时,滑点和点差在贵金属跳空时段可能吃掉大部分预期优势。

MQL5 / C++
class=class="str">"cmt">//| Parameter=Percent,class="type">class="kw">double,class="num">10.0,Percent                                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">// wizard description end
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Class CMoneySE.                                                                                                       |
class=class="str">"cmt">//| Purpose: Class of money management with &class="macro">#x27;Shannon Entropy&class="macro">#x27; optimized volume.                                            |
class=class="str">"cmt">//|           Derives from class CExpertMoney.                                                                           |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CMoneySE : class="kw">public CExpertMoney
  {
class="kw">protected:
   class="type">int               m_scale_factor;
class="kw">public:
   class="type">class="kw">double            m_absolute_condition;

                    CMoneySE(class="type">void);
                   ~CMoneySE(class="type">void);
   class=class="str">"cmt">//---
   class="type">void              ScaleFactor(class="type">int scale_factor) { m_scale_factor=scale_factor; }
   class="type">void              AbsoluteCondition(class="type">class="kw">double absolute_condition) { m_absolute_condition=absolute_condition; }
   class="kw">virtual class="type">bool      ValidationSettings(class="type">void);
   class=class="str">"cmt">//---
   class="kw">virtual class="type">class="kw">double    CheckOpenLong(class="type">class="kw">double price,class="type">class="kw">double sl);
   class="kw">virtual class="type">class="kw">double    CheckOpenShort(class="type">class="kw">double price,class="type">class="kw">double sl);
class="kw">protected:
   class="type">class="kw">double            Optimize(class="type">class="kw">double lots);
   };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Global expert object                                                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
CExpert ExtExpert;
CSignalSE *signal_se;
CMoneySE *money_se;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| "Tick" event handler function                                                                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
  {
   if(!signal_se.m_read_forest) signal_se.WriteForest();

   money_se.AbsoluteCondition(fabs(signal_se.m_last_condition));

   ExtExpert.OnTick();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Optimizing lot size for open.                                                                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double CMoneySE::Optimize(class="type">class="kw">double lots)
  {
   class="type">class="kw">double lot=lots;

      class=class="str">"cmt">//--- normalize lot size based on magnitude of condition

「连续亏损时自动砍仓位的手数逻辑」

这段手数优化函数把「连续亏损」当成减仓触发器:从成交历史倒序遍历,只统计当前品种、且利润小于 0 的连续亏损笔数,一旦 losses>1,就把 lot 乘以 m_scale_factor 再除以 (losses+m_scale_factor),最后保留两位小数。 代码先调用 HistorySelect(0,TimeCurrent()) 拉全量历史,用 CDealInfo 逐笔取 Profit(),遇到盈利单直接 break,所以只认「 uninterrupted 连亏」。若 deal.Ticket() 返回 0 会打印无历史并跳出循环,实盘第一次跑前务必先有成交记录。 减仓后还要过三道合规:stepvol 取整到平台 LotsStep 倍数;低于 LotsMin 拉回最小手数;超过 LotsMax 压回上限。外汇与贵金属杠杆高,连亏后盲目维持原手数可能加速回撤,这套逻辑倾向降低尾部风险,但是否生效需在 MT5 策略测试器用真实点差复盘验证。 让小布替你跑这套 把 m_scale_factor 设成 0.5 丢进 EURUSD 的 2023 年 Tick 回测,观察连亏 3 笔后手数是否降到原计划的约 1/5,再决定是否上实盘。

MQL5 / C++
lot*=(class="num">20*m_scale_factor/fmax(class="num">20.0,((class="num">100.0-m_absolute_condition)/class="num">100.0)*class="num">20.0*m_scale_factor*m_scale_factor));

class=class="str">"cmt">//--- reduce lot based on number of losses orders without a class="kw">break
if(m_scale_factor>class="num">0)
{
  class=class="str">"cmt">//--- select history for access
  HistorySelect(class="num">0,TimeCurrent());
  class=class="str">"cmt">//---
  class="type">int     orders=HistoryDealsTotal();   class=class="str">"cmt">// total history deals
  class="type">int     losses=class="num">0;                     class=class="str">"cmt">// number of consequent losing orders
  CDealInfo deal;
  class=class="str">"cmt">//---
  for(class="type">int i=orders-class="num">1;i>=class="num">0;i--)
  {
    deal.Ticket(HistoryDealGetTicket(i));
    if(deal.Ticket()==class="num">0)
    {
      Print("CMoneySE::Optimize: HistoryDealGetTicket failed, no trade history");
      class="kw">break;
    }
    class=class="str">"cmt">//--- check symbol
    if(deal.Symbol()!=m_symbol.Name())
      class="kw">continue;
    class=class="str">"cmt">//--- check profit
    class="type">class="kw">double profit=deal.Profit();
    if(profit>class="num">0.0)
      class="kw">break;
    if(profit<class="num">0.0)
      losses++;
  }
  class=class="str">"cmt">//---
  if(losses>class="num">1){
  lot*=m_scale_factor;
  lot/=(losses+m_scale_factor);
  lot=NormalizeDouble(lot,class="num">2);}
}
class=class="str">"cmt">//--- normalize and check limits
class="type">class="kw">double stepvol=m_symbol.LotsStep();
lot=stepvol*NormalizeDouble(lot/stepvol,class="num">0);
class=class="str">"cmt">//---
class="type">class="kw">double minvol=m_symbol.LotsMin();
if(lot<minvol){ lot=minvol; }
class=class="str">"cmt">//---
class="type">class="kw">double maxvol=m_symbol.LotsMax();
if(lot>maxvol){ lot=maxvol; }
class=class="str">"cmt">//---
class="kw">return(lot);
}

回测亮眼但别照搬

策略测试器里跑出来的数字确实好看:第一套智能系统优化后盈利因子 2.89、锋锐比率 4.87;第二套更猛,盈利因子 3.65、锋锐比率 5.79。这两个值都来自文末附的 EA 在 4 小时图开盘价上的优化结果,属于可复现的回测数据点。 不过这套 EA 只在 4H 开盘价优化止损止盈,意味着你在实盘甚至测试器每跳模式下都大概率复制不出同样曲线。外汇和贵金属杠杆高、滑点跳空频繁,盲目追这种数值极可能亏穿。 代码里森林推演就一行,把输入输出矩阵丢进决策森林算完,再把第二列结果写进更新缓冲: CDForest::DFProcess(DF,m_in_calculations,m_out_calculations); m_update.B(m_out_calculations[1]); 真正值得做的,是把文章里的决策森林思路拆出来改自己的信号过滤,而不是等圣杯。行情相关性过重时,自定义出的边锋才可能在高波动里帮你一把。

MQL5 / C++
CDForest::DFProcess(DF,m_in_calculations,m_out_calculations);
   m_update.B(m_out_calculations[class="num">1]);
让小布替你算熵值
小布盯盘已内置基于历史 tick 分布的熵诊断,打开对应品种页就能看到当前熵处于高还是低区间,不必自己写 MQL5 采集脚本。把重复劳动交给小布,你专注决策。

常见问题

熵只衡量不确定性,不区分方向。高熵可能代表无序震荡,也可能代表趋势启动前的信息扩散,需结合价格行为结构判断概率倾向。
base 2 得到比特单位,e 得到纳特,相对大小一致。习惯上信息论用 2,方便和二进制状态对照,实战中统一即可。
若每根 bar 重算全样本熵会有开销,建议用滑动窗口并降低刷新频率,回测速度影响通常可控。
覆盖外汇与贵金属主力品种的常见周期,高频周期熵更新更密;外汇贵金属杠杆高、风险大,诊断仅作参考不承诺收益。
关于完整推导和常数选择见《您应该知道的 MQL5 向导技术·基础篇》与《您应该知道的 MQL5 向导技术·实战篇》。