如何构建并优化基于成交量的交易系统蔡金资金流指标(Chaikin Money Flow - CMF)·进阶篇
(2/3)· 当CMF不再只是看0轴穿越,自定义编写与系统回测才是把成交量信号变成可执行策略的关键一步
◍ CMF 指标骨架与成交量口径切换
在 MT5 里手搓一个 Chaikin Money Flow(CMF)指标,第一步是把画图属性和输入参数钉死。下面这段预处理指令把第 1 号绘图对象设为柱状图(DRAW_HISTOGRAM),颜色蓝,零轴线条设为黑色细线;输入项只留两个:周期 periods 默认 20,成交量类型 volumeTypeInp 默认用 tick 量(VOLUME_TICK)。 OnInit 里把 cmfBuffer 绑到 0 号指标缓冲,精度给到 5 位小数,短名拼成 "Chaikin Money Flow(20)" 这种可以直接在副图左上角看到的标签。注意 PlotIndexSetInteger 的 PLOT_DRAW_BEGIN 设 0,意味着从第一根 K 就开始画——但实际计算受 rates_total<periods 保护,不够 20 根直接 return(0)。 OnCalculate 的核心分支在 volumeTypeInp:选 VOLUME_TICK 就走 tick_volume 数组,选实盘成交量走 volume 数组。两段 for 循环结构几乎一致,都是拿 pos 到 pos+periods-1 这 20 根窗口做累加。默认 20 周期下,若当前加载历史少于 20 根 K 线,指标不输出任何值,这是回测或加载初期常见的“空白副图”原因。 别把正态当圣经:CMF 的 sumAccDis/sumVol 结果落在 -1 到 1 之间只是数学边界,实盘外汇和贵金属跳空时分母 sumVol 可能为 0 触发除零,建议自己在循环里加一句保护。
class="macro">#class="kw">property indicator_levelwidth class="num">0 class="macro">#class="kw">property indicator_levelcolor clrBlack class="macro">#class="kw">property indicator_type1 DRAW_HISTOGRAM class="macro">#class="kw">property indicator_color1 clrBlue 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 class="type">class="kw">double cmfBuffer[]; class="type">void OnInit() { SetIndexBuffer(class="num">0,cmfBuffer,INDICATOR_DATA); IndicatorSetInteger(INDICATOR_DIGITS,class="num">5); PlotIndexSetInteger(class="num">0,PLOT_DRAW_BEGIN,class="num">0); IndicatorSetString(INDICATOR_SHORTNAME,"Chaikin Money Flow("+class="type">class="kw">string(periods)+")"); } class="type">int OnCalculate(const class="type">int rates_total, const class="type">int prev_calculated, const class="type">class="kw">datetime &time[], const class="type">class="kw">double &open[], const class="type">class="kw">double &high[], const class="type">class="kw">double &low[], const class="type">class="kw">double &close[], const class="type">long &tick_volume[], const class="type">long &volume[], const class="type">int &spread[]) { if(rates_total<periods) class="kw">return(class="num">0); class="type">int initPos = prev_calculated -class="num">1; if(initPos<class="num">0) initPos = class="num">0; if(volumeTypeInp==VOLUME_TICK) { for(class="type">int pos = initPos;pos<=rates_total-periods;pos++) { class="type">class="kw">double sumAccDis = class="num">0; class="type">long sumVol = class="num">0; for(class="type">int i = class="num">0; i < periods && !IsStopped(); ++i) { class="type">long thisTickVolume = tick_volume[pos+i]; sumVol += thisTickVolume; sumAccDis += AccDis(high[pos+i], low[pos+i], close[pos+i], thisTickVolume); } cmfBuffer[pos+periods-class="num">1] = sumAccDis/sumVol; } } else { for(class="type">int pos = initPos;pos<=rates_total-periods;pos++) { class="type">class="kw">double sumAccDis = class="num">0;
CMF 累加与返回值里的隐藏约束
上面这段把 Chaikin Money Flow 的核心累加写得很直白:在 for 循环里逐根取 tick volume,用 AccDis 算单根累积派发,再除以 sumVol 得到 cmfBuffer 某一位置的值。注意循环条件带 !IsStopped(),这是 EA 被强制停止时及时退出的保命写法,回测时若忽略可能导致历史计算卡死。 AccDis 函数里有个硬判断:high!=low 才计算,否则 res 保持 0。公式 (2*close-high-low)/(high-low)*volume 本质是把收盘价在高低区间的位置映射到成交量权重,外汇和贵金属品种里十字星 K 线(high==low 极少但存在)会直接被跳过,不污染指标。 末尾 return (rates_total-periods-10) 不是随便写的。它告诉 MT5 下一轮从哪个 bar 索引重算,减掉 10 根相当于留了缓冲;若你把 periods 调很大,这个值可能变负,指标会报错或不绘制。外汇与贵金属杠杆高、滑点大,用这套 CMF 前务必在策略测试器里用真实 tick 验证 periods 与缓冲的匹配。
class="type">long sumVol = class="num">0; for(class="type">int i = class="num">0; i < periods && !IsStopped(); ++i) { class="type">long thisTickVolume = volume[pos+i]; sumVol += thisTickVolume; sumAccDis += AccDis(high[pos+i], low[pos+i], close[pos+i], thisTickVolume); } cmfBuffer[pos+periods-class="num">1] = sumAccDis/sumVol; } } class="kw">return (rates_total-periods-class="num">10); } class="type">class="kw">double AccDis(class="type">class="kw">double high,class="type">class="kw">double low,class="type">class="kw">double close,class="type">long volume) { class="type">class="kw">double res=class="num">0; if(high!=low) res=(class="num">2*close-high-low)/(high-low)*volume; class="kw">return(res); }
「CMF的三种可回测信号逻辑」
蔡金资金流量(CMF)本身只是量价资金指标,但把它变成可机械执行的规则,才能在 MT5 策略测试器里跑出统计意义。下面三套逻辑都只依赖 CMF 与价格本身,不引入外部黑箱,方便你直接复制验证。 零轴穿越是最朴素的用法:前一根 CMF 为负、当前转正即买;前一根为正、当前转负即卖。它的假信号集中在震荡市,但规则透明,适合做基线对照。 超买超卖把阈值钉在 ±0.20:CMF ≤ -0.20 视为资金挤压后的潜在反转买点,CMF ≥ 0.20 视为抛压累积的卖点。这个 0.20 不是金科玉律,黄金 XAUUSD 在 2023 年多次在 -0.25~0.30 区间才出现肉眼可辨的拐点,建议你按品种视觉校准。 趋势验证则叠加一根移动平均线:买入需满足前收低于前 MA、现 ask 上穿 MA 且 CMF>0;卖出对称处理。这套过滤能砍掉零轴策略近四成逆向信号,代价是进场更滞后。外汇与贵金属杠杆高,任何信号都只是概率倾斜,实盘前务必用历史数据跑过分步优化。
◍ CMF三套EA的写法与2023年回测对照
先做基准模板:用一个极简EA把自定义Chaikin_Money_Flow的数值打印到图表,确认和加载的指标完全一致(示例读到0.15904),后面所有策略都复用这套句柄初始化逻辑。 零轴穿越版只判断相邻两根K线的CMF符号翻转:前根小于0、当前大于0开买;前根大于0、当前小于0开卖。EURUSD、M15、2023全年、SL300点/TP900点下,净利润29019.10 USD,最大回撤23%,盈利因子1.09,夏普0.80——能盈利但回撤偏高,外汇贵金属本身杠杆风险大,这种曲线普通账户未必扛得住。 超买超卖版把阈值写死为±0.20:CMF≤-0.20买,≥0.20卖。同参数测试净利润冲到58029.90 USD,但回撤飙到55.60%,盈利因子反而降到1.06,夏普0.69。收益翻倍是用翻倍的风险换的,实盘大概率触发强平。 趋势验证版叠加一条SMA:收盘低于均线、卖价高于均线且CMF>0才买;反之才卖。这套把回撤压到6.30%,盈利因子1.91,预期收益167.59,夏普3.90。三个策略摆一起,加均线过滤的那版在关键指标上明显更稳,值得直接开MT5把代码跑一遍验证。
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 class="type">int cmf; class="type">int OnInit() { 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">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); Comment("CMF value = ",cmfVal); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| CMF_Val.mq5 |
把 CMF 信号接进 EA 的实盘骨架
CMF 指标算完不落地,等于白看。下面两段 MQL5 给了最小可用骨架:第一段只把 Chaikin Money Flow 的数值抓出来挂到图表注释;第二段接了交易类,准备按穿越阈值下单。
第一段里 iCustom 加载名为 Chaikin_Money_Flow 的自定义指标,周期 20、成交量类型用 tick 量。OnTick 中 CopyBuffer 取最近 3 根、数组倒序后读 [0] 即当前值,NormalizeDouble 到 5 位小数,用 Comment 直接显示。
第二段加上 CTrade 与阈值参数:cmfPosLvls=0.20 视为超买区、cmfNegLvls=-0.20 超卖区,lotSize=1、slLvl=300 点、tpLvl=900 点。用 iBars 变化触发新 K 线逻辑,避免每 tick 重复计算。
实盘里先把 sl/tp 点数和你的品种点值对齐,贵金属和外汇杠杆差异大,300/900 点放在 XAUUSD 和 EURUSD 上风险完全两样,务必先在策略测试器跑历史。
class=class="str">"cmt">//+------------------------------------------------------------------+ 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 class="type">int cmf; class="type">int OnInit() { 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">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); Comment("CMF value = ",cmfVal); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| CMF_Zero_Crossover.mq5 | 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 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">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; CTrade trade; class="type">int barsTotal; class="type">int OnInit() { barsTotal=iBars(_Symbol,PERIOD_CURRENT); cmf = iCustom(_Symbol,PERIOD_CURRENT,"Chaikin_Money_Flow",maPeriodInp,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);