如何构建并优化基于成交量的交易系统蔡金资金流指标(Chaikin Money Flow - CMF)·综合运用
📊

如何构建并优化基于成交量的交易系统蔡金资金流指标(Chaikin Money Flow - CMF)·综合运用

(3/3)·把 CMF 从指标公式变成可回测、可优化的交易系统,很多人在第一步就卡在代码改写

实战向 第 3/3 篇

不少交易者把 CMF 当成单纯的零轴穿越信号,却从没试过把它写进自己的指标逻辑里做参数微调。等真要跑系统回测,才发现原版周期和成交量处理方式根本不匹配自己的品种波动。这系列最后一篇,我们把前面铺垫的概念收口成能落地的系统。

◍ CMF 零轴穿越的下单触发逻辑

蔡金资金流量(CMF)在零轴上下穿越,常被用作动量反转的参考信号。下面这段 MT5 代码片段展示了当上一根 Bar 的 CMF 为负、当前转正时,以实时 ask 价做多;反向则做空,属于典型的零轴穿越触发框架。 参数层面,默认 slLvl=300、tpLvl=900,即止损 300 点、止盈 900 点,盈亏比倾向 1:3;lotSize 默认 1 手,cmfPosLvls / cmfNegLvls 设 ±0.20 作为超买超卖阈值。外汇与贵金属杠杆高,实盘前务必在策略测试器用 Tick 数据回测验证。 代码里先取 SymbolInfoDouble 的 ask / bid,再用 cmfPreVal(前一根归一化到 5 位小数)与 cmfVal 比较。注意 slVal、tpVal 均基于 _Point 计算,XAUUSD 的 _Point 通常是 0.01,300 点即 3 美元跳动。 别把正负穿越当圣杯 CMF 零轴穿越在震荡市可能连续假突破,EURUSD 在 2023 年多次出现单日来回穿越 4 次以上的情况。建议叠加成交量或波动率过滤,再让小布替你跑这套历史校验。

MQL5 / C++
class="macro">#include <trade/trade.mqh>
input class="type">class="kw">double cmfPosLvls = class="num">0.20; class=class="str">"cmt">// CMF OB Level
input class="type">class="kw">double cmfNegLvls = -class="num">0.20; class=class="str">"cmt">// CMF OS Level
input class="type">class="kw">double lotSize=class="num">1;
input class="type">class="kw">double slLvl=class="num">300;
input class="type">class="kw">double tpLvl=class="num">900;
CTrade trade;
class="type">int barsTotal;
class="type">int bars=iBars(_Symbol,PERIOD_CURRENT);
if(barsTotal != bars)
   barsTotal=bars;
class="type">class="kw">double cmfPreVal = NormalizeDouble(cmfInd[class="num">1], class="num">5);
class="type">class="kw">double ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK);
class="type">class="kw">double bid = SymbolInfoDouble(_Symbol,SYMBOL_BID);
if(cmfPreVal<class="num">0 && cmfVal>class="num">0)
  {
   class="type">class="kw">double slVal=ask - slLvl*_Point;
   class="type">class="kw">double tpVal=ask + tpLvl*_Point;
   trade.Buy(lotSize,_Symbol,ask,slVal,tpVal);
  }
if(cmfPreVal>class="num">0 && cmfVal<class="num">0)
  {
   class="type">class="kw">double slVal=bid + slLvl*_Point;
   class="type">class="kw">double tpVal=bid - tpLvl*_Point;
   trade.Sell(lotSize,_Symbol,bid,slVal,tpVal);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include <trade/trade.mqh>
input class="type">int periods=class="num">20; class=class="str">"cmt">// Periods
input ENUM_APPLIED_VOLUME volumeTypeInp=VOLUME_TICK; class=class="str">"cmt">// Volume Type

「用 CMF 极值触发双向市价单」

这套逻辑把蔡金资金流(CMF)当成区间触发器:±0.20 是人为划定的超买 / 超卖门槛,CMF 下穿 -0.20 倾向认为抛压耗尽,上穿 +0.20 倾向认为吸筹过剩。外汇与贵金属杠杆高,这种反转假设失效时回撤可能很快吃掉保证金。 核心只在 OnTick 里做「新K线才动手」的判断:用 iBars 总数变化detect一根新柱,CopyBuffer 取最近 3 根 CMF 值,ArraySetAsSeries 设成时间序列后比对 cmfInd[0] 与门槛。slLvl=300、tpLvl=900 意味着用 3:9 的盈亏距挂单,点值乘 _Point 直接算出止损止盈价。 别把 ±0.20 当万能阈值。黄金 M5 在趋势市里 CMF 常贴着 +0.30 走,原样抄参数会连续在超买区反手吃损;建议先开 MT5 用策略测试器跑 EURUSD H1,把 cmfNegLvls 调到 -0.10 看信号密度再决定。

MQL5 / C++
input class="type">class="kw">double cmfPosLvls = class="num">0.20; class=class="str">"cmt">// CMF OB Level
input class="type">class="kw">double cmfNegLvls = -class="num">0.20; class=class="str">"cmt">// CMF OS Level
input class="type">class="kw">double      lotSize=class="num">1;
input class="type">class="kw">double      slLvl=class="num">300;
input class="type">class="kw">double      tpLvl=class="num">900;
class="type">int cmf;
CTrade trade;
class="type">int barsTotal;
class="type">int OnInit()
  {
   barsTotal=iBars(_Symbol,PERIOD_CURRENT);
   cmf = iCustom(_Symbol,PERIOD_CURRENT,"Chaikin_Money_Flow",periods,volumeTypeInp);
   class="kw">return(INIT_SUCCEEDED);
  }
class="type">void OnDeinit(const class="type">int reason)
  {
   Print("EA is removed");
  }
class="type">void OnTick()
  {
   class="type">int bars=iBars(_Symbol,PERIOD_CURRENT);
   if(barsTotal != bars)
     {
      barsTotal=bars;
      class="type">class="kw">double cmfInd[];
      CopyBuffer(cmf,class="num">0,class="num">0,class="num">3,cmfInd);
      ArraySetAsSeries(cmfInd,true);
      class="type">class="kw">double cmfVal = NormalizeDouble(cmfInd[class="num">0], class="num">5);
      class="type">class="kw">double cmfPreVal = NormalizeDouble(cmfInd[class="num">1], class="num">5);
      class="type">class="kw">double ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK);
      class="type">class="kw">double bid = SymbolInfoDouble(_Symbol,SYMBOL_BID);
      if(cmfVal<=cmfNegLvls)
        {
         class="type">class="kw">double slVal=ask - slLvl*_Point;
         class="type">class="kw">double tpVal=ask + tpLvl*_Point;
         trade.Buy(lotSize,_Symbol,ask,slVal,tpVal);
        }
      if(cmfVal>=cmfPosLvls)
        {
         class="type">class="kw">double slVal=bid + slLvl*_Point;
         class="type">class="kw">double tpVal=bid - tpLvl*_Point;
         trade.Sell(lotSize,_Symbol,bid,slVal,tpVal);
        }
     }
  }

用 CMF 与均线交叉过滤开仓

这段 EA 片段把 Chaikin Money Flow 和一条 20 周期 SMA 绑在一起,只在价格上穿均线且 CMF 为正时考虑做多。CMF 默认取 20 周期、VOLUME_TICK 类型,意味着资金流向读数对 tick 成交量敏感,在贵金属跳空时段容易出现尖峰。 入场逻辑写在 OnTick 里:先比对上一根收盘 prevClose 是否低于前一根均线值 maPreVal,且当前 ask 已高于 maVal,属于典型的回踩转多结构;再要求 cmfVal>0 确认买盘资金未撤退。SL 硬编码 300 点、TP 900 点,盈亏比 1:3,但外汇与贵金属杠杆高,滑点可能吞掉这部分空间。 代码里 CopyBuffer 取了 3 根长度却只用了 [0] 和 [1],属于留了余量;若你想看信号稳定性,把 periods 和 maPeriodInp 都改成 50 在 MT5 里回测 EURUSD,会发现交叉频率下降、假信号概率可能降低。

MQL5 / C++
class="macro">#include <trade/trade.mqh>
input class="type">int periods=class="num">20; class=class="str">"cmt">// Periods
input ENUM_APPLIED_VOLUME volumeTypeInp=VOLUME_TICK;  class=class="str">"cmt">// Volume Type
input class="type">int maPeriodInp=class="num">20; class=class="str">"cmt">//MA Period
input class="type">class="kw">double lotSize=class="num">1;
input class="type">class="kw">double slLvl=class="num">300;
input class="type">class="kw">double tpLvl=class="num">900;
class="type">int cmf;
class="type">int ma;
CTrade trade;
class="type">int barsTotal;
class="type">int OnInit()
  {
   barsTotal=iBars(_Symbol,PERIOD_CURRENT);
   cmf = iCustom(_Symbol,PERIOD_CURRENT,"Chaikin_Money_Flow",periods,volumeTypeInp);
   ma = iMA(_Symbol,PERIOD_CURRENT,maPeriodInp,class="num">0,MODE_SMA,PRICE_CLOSE);
   class="kw">return(INIT_SUCCEEDED);
  }
class="type">void OnDeinit(const class="type">int reason)
  {
   Print("EA is removed");
  }
class="type">void OnTick()
  {
   class="type">int bars=iBars(_Symbol,PERIOD_CURRENT);
   if(barsTotal != bars)
     {
      barsTotal=bars;
      class="type">class="kw">double cmfInd[];
      class="type">class="kw">double maInd[];
      CopyBuffer(cmf,class="num">0,class="num">0,class="num">3,cmfInd);
      CopyBuffer(ma,class="num">0,class="num">0,class="num">3,maInd);
      ArraySetAsSeries(cmfInd,true);
      ArraySetAsSeries(maInd,true);
      class="type">class="kw">double cmfVal = NormalizeDouble(cmfInd[class="num">0], class="num">5);
      class="type">class="kw">double maVal= NormalizeDouble(maInd[class="num">0],class="num">5);
      class="type">class="kw">double cmfPreVal = NormalizeDouble(cmfInd[class="num">1], class="num">5);
      class="type">class="kw">double maPreVal = NormalizeDouble(maInd[class="num">1],class="num">5);;
      class="type">class="kw">double ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK);
      class="type">class="kw">double bid = SymbolInfoDouble(_Symbol,SYMBOL_BID);
      class="type">class="kw">double prevClose = iClose(_Symbol,PERIOD_CURRENT,class="num">1);
      if(prevClose<maPreVal && ask>maVal)
        {
         if(cmfVal>class="num">0)
           {

◍ 均线穿越叠加 CMF 的双向下单逻辑

这段片段把「前一根收盘与均线位置关系」和「Chaikin Money Flow 正负」组合成过滤条件,只在动量方向一致时才允许市价进场。多头分支要求前收低于前一根均线、当前 ask 上穿均线且 cmfVal 大于 0,空頭分支则镜像反转,cmfVal 小于 0 才放行。 double slVal=ask - slLvl*_Point 与 double tpVal=ask + tpLvl*_Point 说明止损、止盈都以当前 tick 的 _Point 为最小单位按 slLvl / tpLvl 整数倍偏移,slLvl 和 tpLvl 是外部可调参数,直接在 MT5 里改这两个数就能压缩或放大风险敞口。 下方 input int maPeriodInp=20 把均线周期写死为 20 根 K 线,用 iMA 取当前周期收盘价计算的 SMA,CopyBuffer 拉 3 根值后 ArraySetAsSeries 置为时间倒序,maInd[0] 即最新值并用 NormalizeDouble(...,5) 规整到 5 位小数。 外汇与贵金属杠杆高,穿均线失败或 CMF 假信号都可能触发连续止损,上机前先用策略测试器跑 3 个月以上 tick 数据验证信号频率。

MQL5 / C++
class="type">class="kw">double slVal=ask - slLvl*_Point;
class="type">class="kw">double tpVal=ask + tpLvl*_Point;
trade.Buy(lotSize,_Symbol,ask,slVal,tpVal);
 }
 }
 if(prevClose>maPreVal && bid<maVal)
 {
 if(cmfVal<class="num">0)
 {
 class="type">class="kw">double slVal=bid + slLvl*_Point;
 class="type">class="kw">double tpVal=bid - tpLvl*_Point;
 trade.Sell(lotSize,_Symbol,bid,slVal,tpVal);
 }
 }
 }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
input class="type">int maPeriodInp=class="num">20; class=class="str">"cmt">//MA Period
class="type">int ma;
ma = iMA(_Symbol,PERIOD_CURRENT,maPeriodInp,class="num">0,MODE_SMA,PRICE_CLOSE);
class="type">class="kw">double maInd[];
CopyBuffer(ma,class="num">0,class="num">0,class="num">3,maInd);
ArraySetAsSeries(maInd,true);
class="type">class="kw">double maVal= NormalizeDouble(maInd[class="num">0],class="num">5);
class="type">class="kw">double prevClose = iClose(_Symbol,PERIOD_CURRENT,class="num">1);
 if(prevClose<maPreVal && ask>maVal)
 {
 if(cmfVal>class="num">0)
 {
 class="type">class="kw">double slVal=ask - slLvl*_Point;
 class="type">class="kw">double tpVal=ask + tpLvl*_Point;
 trade.Buy(lotSize,_Symbol,ask,slVal,tpVal);
 }
 }
 if(prevClose>maPreVal && bid<maVal)
 {
 if(cmfVal<class="num">0)
 {
 class="type">class="kw">double slVal=bid + slLvl*_Point;
 class="type">class="kw">double tpVal=bid - tpLvl*_Point;
 trade.Sell(lotSize,_Symbol,bid,slVal,tpVal);
 }
 }

「记住这一条就够了」

CMF 这类成交量派生指标本身不替你赚钱,真正拉开差距的是参数空间:时间周期、止损、止盈每动一点,回测曲线就可能换一副面孔。前面三套 EA(零轴穿越、超买超卖、趋势验证)源码都已挂在附件里,Chaikin_Money_Flow.mq5 是 5.57 KB 的自定义指标,四个 mq5 文件总占用不到 8 KB,直接拖进 MT5 就能编译跑。 有读者拿 2023 全年欧美对做趋势验证回测,最大回撤跑到 50%,而原作者环境只报 6%——差值大概率来自经纪商报价差异(差价合约 vs 银行间清算)而非代码坏掉。外汇和贵金属自带高杠杆高风险,别人晒的曲线不能直接当你的预期。 剩下要做的只有一件:把那几个变量挨个拧一遍,看哪组在你的账户环境下缩水最浅、交易次数还能接受,定下来就别再瞎优化。

把回测监控交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到 CMF 与量价背离的实时标注,你专注决策而不是手动算乘数。

常见问题

可结合成交量阈值或更长周期 CMF 做确认,系统回测中常见做法是要求穿越后连续 N 根 K 线守住方向再触发,概率上能砍掉部分毛刺。
可以,小布的品种页支持自定义周期与成交量加权方式,改完即看实时资金流分布,不必自己搭 MT5 环境。
当最高价等于最低价时分母为 0,实战写法里通常跳过该根 K 线或令乘数为 0,避免运行时报错中断回测。
周期长度和出场阈值对回测曲线影响最大,外汇贵金属本身高杠杆高风险,参数过拟合可能在样本外快速衰减。
倾向与趋势类或波动率类工具组合,用 CMF 确认资金方向、用另一层过滤震荡,具体组合见本系列前两篇的基础定义与策略拆解。