用于一组指标信号的朴素贝叶斯分类器·进阶篇
📊

用于一组指标信号的朴素贝叶斯分类器·进阶篇

(2/3)· 接上篇理论铺垫,本篇直接落地代码框架与信号统计,让指标委员会跑起来

实战向进阶 第 2/3 篇
很多交易者把多个指标信号简单“投票”就开仓,却从没验证过这些信号在历史里到底有多大独立预测力。把相关性当独立性用, committee 越大反而越容易集体翻车。先算清每个信号的条件概率,再谈组合。

「用枚举把指标信号框架搭起来」

在 MT5 里做信号框架,第一步是把指标类型收进一个枚举。内置指标和 iCustom 都进表,名字后缀带提示:i 是一个缓冲区、d 是两个、t 是三个,纯给用户防手滑;填错缓冲区索引或参数数量,框架直接往日志吐错误,不静默崩。 每个指标除了在输入参数里挑类型,还要给实际参数串、缓冲区编号、起始柱线号。信号条件靠另一套枚举收敛:值非空、带正负号、穿越另一指标或某价位、相对另一指标或等级的位置关系。第一个元素 '禁用' 用来关掉整组条件,默认每组信号都是禁用的,省得误触发。 方向检查另起一个枚举 EqualOrNone / UpSideOrAboveOrPositve / DownSideOrBelowOrNegative / NotEqual。比如 UpSide 那组能查上穿、数值为正、同指标连续柱线递增、异指标 X 高于 Y、位置高于某级;DownSide 镜像反过来。触发后由 Execution 枚举决定动作:发消息、买、卖、平掉所有仓、多空翻转、改止损止盈、或跳去查下一个条件——最后这项让信号链成为可能,例如先判主线穿信号线,再判是否发生在某级之上。 指标属性组最多 6 个,信号最多 6 个,单指标参数上限 20 个,都在 IndicatN.mqh 里可改。Indicator1Name 在选 iCustom 时填自定义指标名;Parameter1List 用逗号串自动识型:11.0 算 double、11 算 int、2015.01.01 20:00 算 datetime、true/false 布尔、带引号算 string;均线类型也能写 sma/ema 不带引号。缓冲区名认 main/signal/upper/jaw/tenkan 等。直接写 iMA@0(1,0,sma,high)[1] 这种原型,可以绕开属性组,在信号参数里现用现配。 EA 端用 #include <IndicatN.mqh> 编完,设置框就长出指标属性和信号定义两坨输入组。任何数值参数能写成 =var4 这种,绑到框架的 var1~var9 优化输入上,比如 iMACD@main(=var4,=var5,=var6,open)[0],关优化时也从同名输入读值,方便单一回测调参。

MQL5 / C++
enum IndicatorType
{
  iCustom,
  iAC,
  iAD,
  tADX_period_price,
  tAlligator_jawP_jawS_teethP_teethS_lipsP_lipsS_method_price,
  iAO,
  iATR_period,
  tBands_period_deviation_shift_price,
  iBearsPower_period_price,
  iBullsPower_period_price,
  iBWMFI,
  iCCI_period_price,
  iDeMarker_period,
  tEnvelopes_period_method_shift_price_deviation,

信号条件与指标枚举的骨架

这段声明把 EA 可调用的指标类型先锁死在枚举里:从 iForce、iMomentum、iRSI 到 dStochastic、dMACD 共 17 种,覆盖了趋势、动量、波动率三类常用工具。想接自定义指标就走 iCustom,但必须手动填名称、参数列表和缓存区序号,MT5 不会替你猜。 SignalCondition 枚举定义了 7 种判定逻辑:最常用的是 IndicatorXcrossesIndicatorY(两指标金叉死叉)和 IndicatorXcrossesLevelX(触线)。UpZeroDown 用 EqualOrNone / UpSide / DownSide / NotEqual 四个值描述方向或正负号,相当于把「上穿零轴」「低于基准」这类口语翻成机器能比的大小关系。 SignalType 决定了条件命中后干什么:除了 Alert、Buy、Sell,还有 CloseBuy、ModifyStopLoss、ProceedToNextCondition 等 11 种。ProceedToNextCondition 很关键——它让多个信号串行校验,而不是一触发就下单,实盘里能挡掉不少毛刺。 TradeSignals 类把上述结果收口:alert/buy/sell 等 bool 字段初始全 false,value 给 EMPTY_VALUE。外部输入 ConsistentSignalNumber 默认 1,意思是连续几根 K 线同信号才认;外汇与贵金属波动剧烈、滑点频繁,建议先在策略测试器里把这个数调到 2~3 看回撤变化再上真仓。

MQL5 / C++
enum IndicatorType
{
  iForce_period_method_price,
  dFractals,
  dGator_jawP_jawS_teethP_teethS_lipsP_lipsS_method_price,
  fIchimoku_tenkan_kijun_senkou,
  iMomentum_period_price,
  iMFI_period,
  iMA_period_shift_method_price,
  dMACD_fast_slow_signal_price,
  iOBV_price,
  iOsMA_fast_slow_signal_price,
  iRSI_period_price,
  dRVI_period,
  iSAR_step_maximum,
  iStdDev_period_shift_method_price,
  dStochastic_K_D_slowing_method_price,
  iWPR_period
};
enum SignalCondition
{
  Disabled,
  NotEmptyIndicatorX,
  SignOfValueIndicatorX,
  IndicatorXcrossesIndicatorY,
  IndicatorXcrossesLevelX,
  IndicatorXrelatesToIndicatorY,
  IndicatorXrelatesToLevelX
};
enum UpZeroDown
{
  EqualOrNone,
  UpSideOrAboveOrPositve,
  DownSideOrBelowOrNegative,
  NotEqual
};
enum SignalType
{
  Alert,
  Buy,
  Sell,
  CloseBuy,
  CloseSell,
  CloseAll,
  BuyAndCloseSell,
  SellAndCloseBuy,
  ModifyStopLoss,
  ModifyTakeProfit,
  ProceedToNextCondition
};
class="kw">input IndicatorType Indicator1Selector = iCustom;
class="kw">input class="type">class="kw">string Indicator1Name = "";
class="kw">input class="type">class="kw">string Parameter1List = "" class=class="str">"cmt">/*class="num">1.0,value:t,value:t*/;
class="kw">input class="type">class="kw">string Indicator1Buffer = "";
class="kw">input class="type">int Indicator1Bar = class="num">1;
class="kw">input class="type">class="kw">string __SIGNAL_A = "";
class="kw">input SignalCondition ConditionA = Disabled;
class="kw">input class="type">class="kw">string IndicatorA1 = "";
class="kw">input class="type">class="kw">string IndicatorA2 = "";
class="kw">input class="type">class="kw">double LevelA1 = class="num">0;
class="kw">input class="type">class="kw">double LevelA2 = class="num">0;
class="kw">input UpZeroDown DirectionA = EqualOrNone;
class="kw">input SignalType ExecutionA = Alert;
class TradeSignals
{
  class="kw">public:
    class="type">bool alert;
    class="type">bool buy;
    class="type">bool sell;
    class="type">bool buyExit;
    class="type">bool sellExit;
    class="type">bool ModifySL;
    class="type">bool ModifyTP;
    class="type">int index;
    class="type">class="kw">double value;
    class="type">class="kw">string message;
    TradeSignals(): alert(class="kw">false), buy(class="kw">false), sell(class="kw">false), buyExit(class="kw">false), sellExit(class="kw">false), ModifySL(class="kw">false), ModifyTP(class="kw">false), value(EMPTY_VALUE), message(""){}
};
class="macro">#include <IndicatN.mqh>
class="macro">#include <Expert0.mqh>
class="macro">#ifdef __MQL5__
  class="macro">#include <MarketMQL4.mqh>
  class="macro">#include <ind4to5.mqh>
  class="macro">#include <mt4orders.mqh>
class="macro">#endif
class="kw">input class="type">int ConsistentSignalNumber = class="num">1;
class="kw">input class="type">int Magic = class="num">0;

◍ 信号统计的柱线级埋点

在 EA 初始化阶段,先用 input 把 Lot 设成 0.01、TradeDuration 设成 1,再交给 Expert 对象托管下单逻辑;OnInit 直接回传指标命名空间的 handleInit 结果,不在这里写任何业务。 OnTick 里用 static datetime lastBar 卡住「每根新柱才跑一次」的节奏:只有 Time[0] 变了才调用 IndicatN::handleStart 拿信号数组,随后更新 lastBar。这样回测或实盘里都不会在同一根柱内重复统计,外汇与贵金属品种的高波动环境下能少算很多噪声。 统计块本身是一组裸计数器:bars 累计抽样柱数,bull/bear 分别记多头/空头事件柱,buy[]/sell[] 是无条件信号命中数,buyOnBull[]/sellOnBear[] 只在大方向已确立(up/down 标记为真)时才累加,相当于「条件过滤后的信号」。 遍历 ts 数组时,先靠 H_BULL / H_BEAR 翻转 up / down,之后凡是以 S_BUY 开头且 up 为真的,才进 buyOnBull;S_SELL 同理。PrintDetails 打开时会向日志吐出 buyOk / sellOk 加信号名,方便你直接在 MT5 终端对照哪类信号在趋势段里真正生效。

MQL5 / C++
class="kw">input class="type">class="kw">float Lot = class="num">0.01f;
class="kw">input class="type">int TradeDuration = class="num">1;
Expert e(Magic, Lot);
class="type">int OnInit()
{
  class="kw">return IndicatN::handleInit();
}
class="type">void OnTick()
{
  class="kw">static class="type">class="kw">datetime lastBar;
  
  if(lastBar != Time[class="num">0])
  {
    class="kw">const RubbArray<TradeSignals> *ts = IndicatN::handleStart();
    ...
    lastBar = Time[class="num">0];
  }
}
class="type">int bars = class="num">0;                                         class=class="str">"cmt">// 柱线/抽样的总计数
class="type">int bull = class="num">0, bear = class="num">0;                               class=class="str">"cmt">// 每笔交易类型的柱线/抽样数量
class="type">int buy[MAX_SIGNAL_NUM] = {class="num">0}, sell[MAX_SIGNAL_NUM] = {class="num">0};                     class=class="str">"cmt">// 无条件信号数组
class="type">int buyOnBull[MAX_SIGNAL_NUM] = {class="num">0}, sellOnBear[MAX_SIGNAL_NUM] = {class="num">0};          class=class="str">"cmt">// 条件 (成功) 信号数组
  class="kw">const RubbArray<TradeSignals> *ts = IndicatN::handleStart();
  class="type">bool up = class="kw">false, down = class="kw">false;
  class="type">int buySignalCount = class="num">0, sellSignalCount = class="num">0;
  
  for(class="type">int i = class="num">0; i < ts.size(); i++)
  {
    class=class="str">"cmt">// 用于收集统计信息d的警报
    if(ts[i].alert)
    {
      class=class="str">"cmt">// 在设置事件时, 按照 i 进行列举,
      class=class="str">"cmt">// 假设 H_xxx 应先至, 在信号 S_xxx 之前,
      class=class="str">"cmt">// 因为我们在此分配向上或向下的标记
      if(IndicatN::GetSignal(ts[i].index) == "H_BULL")
      {
        bull++;
        buy[ts[i].index]++;
        up = true;
      }
      else if(IndicatN::GetSignal(ts[i].index) == "H_BEAR")
      {
        bear++;
        sell[ts[i].index]++;
        down = true;
      }
      else if(StringFind(IndicatN::GetSignal(ts[i].index), "S_BUY") == class="num">0)
      {
        buy[ts[i].index]++;
        if(up)
        {
          if(PrintDetails) Print("buyOk ", IndicatN::GetSignal(ts[i].index));
          buyOnBull[ts[i].index]++;
        }
      }
      else if(StringFind(IndicatN::GetSignal(ts[i].index), "S_SELL") == class="num">0)
      {
        sell[ts[i].index]++;
        if(down)
        {
          if(PrintDetails) Print("sellOk ", IndicatN::GetSignal(ts[i].index));
          sellOnBear[ts[i].index]++;
        }
      }
    }
  }

「信号计数与冲突时的下单逻辑」

这段逻辑跑在每根 K 线收尾,先按 PrintDetails 开关决定是否把各策略消息打印到日志,再分别累加 buySignalCount 与 sellSignalCount。 若 e.getLastOrderBar() 返回的持仓 K 线数大于等于 TradeDuration,就直接平掉所有市价单,相当于给单子设了一个硬止损周期。 当买、卖信号计数都达到 ConsistentSignalNumber 时,代码只打印“信号冲突”而不下单,避免双向拉扯。 否则,若买信号达标就平掉卖单、且在无买单时补一个市价买;卖信号达标则对称处理。外汇与贵金属杠杆高,这种一致性阈值若设太小,可能在震荡里频繁开平。

MQL5 / C++
if(PrintDetails) Print(ts[i].message);
      }
    }
      if(ts[i].buy)
      {
        buySignalCount++;
      }
      else
      if(ts[i].sell)
      {
        sellSignalCount++;
      }
    if(e.getLastOrderBar() >= TradeDuration)
    {
      e.closeMarketOrders();
    }
    if(buySignalCount >= ConsistentSignalNumber
    && sellSignalCount >= ConsistentSignalNumber)
    {
      Print("信号冲突");
    }
    else
    if(buySignalCount >= ConsistentSignalNumber)
    {
      e.closeMarketOrders(e.mask(OP_SELL));
      
      if(e.getOrderCount(e.mask(OP_BUY)) == class="num">0)
      {
        e.placeMarketOrder(OP_BUY);
      }
    }
    else
    if(sellSignalCount >= ConsistentSignalNumber)
    {
      e.closeMarketOrders(e.mask(OP_BUY));
      
      if(e.getOrderCount(e.mask(OP_SELL)) == class="num">0)
      {
        e.placeMarketOrder(OP_SELL);
      }
    }
让小布替你跑这套回测
这些指标信号的概率统计与框架调试,小布盯盘的 AIGC 已内置常用品种的诊断模板,打开对应品种页即可看到信号独立性初判,把重复劳动交给小布,你专注决策。

常见问题

通常通过 iCustom 或内置 iMACD、iRSI 等句柄取缓冲值,按柱线闭合判定信号方向,再映射为离散事件写入统计容器,具体封装见本篇“以 MQL 实现”小节。
目前小布提供品种页的信号相关性预览与历史胜率概览,完整 MQL5 框架需自行部署 EA,但可用小布输出的统计做冷启动参数参考。
多数技术指标准由收盘价派生,倾向相关;文中会讨论用历史残差检验独立性,不独立时后验概率会偏乐观,需做去相关或降权处理。
盈亏比率忽略盈亏绝对值与持仓时间,贵金属外汇高杠杆下回撤可能失控,仅适合作为信号层初筛,实盘须叠加风控与波动过滤。