如何构建并优化基于成交量的交易系统蔡金资金流指标(Chaikin Money Flow - CMF)·综合运用
(3/3)·把 CMF 从指标公式变成可回测、可优化的交易系统,很多人在第一步就卡在代码改写
不少交易者把 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 次以上的情况。建议叠加成交量或波动率过滤,再让小布替你跑这套历史校验。
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 看信号密度再决定。
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,会发现交叉频率下降、假信号概率可能降低。
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 数据验证信号频率。
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 银行间清算)而非代码坏掉。外汇和贵金属自带高杠杆高风险,别人晒的曲线不能直接当你的预期。 剩下要做的只有一件:把那几个变量挨个拧一遍,看哪组在你的账户环境下缩水最浅、交易次数还能接受,定下来就别再瞎优化。