English Русский Deutsch 日本語
preview
如何构建并优化基于成交量的交易系统——蔡金资金流指标(Chaikin Money Flow - CMF)

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

MetaTrader 5交易 |
20 3
Mohamed Abdelmaaboud
Mohamed Abdelmaaboud

概述

欢迎阅读本文,在此,我们将从创建新指标、基于其概念构建交易系统以及优化这些系统以获取有关利润和风险的更好见解与结果这几个方面,对新指标展开探索。在本文中,我们将介绍一种新的基于成交量的技术指标——蔡金资金流指标(CMF)。

在此有必要提及一个重要事项,这类文章的主要目的是分享新的技术工具,这些工具既可单独使用,也可根据工具特性与其他工具结合使用,此外还会对这些工具进行测试和优化,以获得更好的结果,从而判断这些工具是否实用。

我们将按照以下主题来介绍这个新指标(CMF):

  1. 蔡金资金流指标:通过定义该技术工具、阐述其计算方法以及使用方式,来了解其基础知识。
  2. 自定义蔡金资金流指标:学习如何通过根据自身偏好进行修改或应用来编写我们的专属自定义指标代码。
  3. 蔡金资金流指标策略:我们将了解一些作为我们交易系统组成部分的简单交易策略。
  4. 蔡金资金流指标交易系统:构建、测试并优化这些简易交易系统。
  5. 结论

免责声明:所有信息“仅按原样”提供,且仅用于教学目的,并非为交易目的或建议而准备。该信息不保证任何类型的结果。如果您选择在任何交易账户上使用这些材料,必须自行承担风险,并且您将是唯一的责任人。



蔡金资金流指标(CMF)

蔡金资金流指标(CMF)是一种基于成交量并考虑价格走势的技术指标。正如我们即将所见,CMF可以单独使用,也可以与其他工具结合使用,以提供更深入的见解。CMF指标由马克·蔡金(Marc Chaikin)开发,用于监测某一交易品种在一段时间内的资金积累与分布情况。CMF背后的主要理念是:当收盘价越接近最高价时,表明出现了资金积累。另一方面,当收盘价越接近最低价时,则是资金分布的信号。当价格走势在成交量增加的情况下持续收于K线中点上方时,CMF结果为正。当价格走势在成交量增加的情况下持续收于K线中点下方时,CMF结果为负。

以下是CMF指标的计算步骤:

  • 计算资金流乘数
资金流乘数 = [(收盘价 - 最低价)- (最高价 - 收盘价)] / (最高价 - 最低价)
  • 计算资金流成交量

资金流成交量 = 资金流乘数 × 周期成交量

  • 计算CMF

n周期CMF = n周期资金流成交量之和 / n周期成交量之和

计算出该指标后,取值范围将在+1到-1之间,其呈现形式可参照下图:

cmfInd

我们可以看到,该指标是一条围绕0值上下波动的曲线,CMF穿过0值线(无论是向上还是向下穿过)即可识别出买卖形势的变化。0值以上的正值表明存在买盘方向;相反,若卖盘方向开始占据主导,指标将跌至0值以下。当CMF在0值线附近波动时,表明买卖力量相对均衡,或者没有明显的趋势。CMF被用作识别和评估所交易品种趋势的工具。

在下一部分,我们可以根据自己的意愿对指标进行调整,比如将其绘制成柱状图而非折线图,或者进行其他任何我们认为有助于提升交易效果的改动。这正是通过编写或编辑指标代码,根据自己的偏好定制任何指标的意义所在。


自定义蔡金资金流指标

在本部分,我们将逐步展示如何编写一个自定义的CMF指标代码,以便后续在我们简单的交易系统中使用。正如我们即将所见,定制过程会比较简单,因为主要目的是让读者对这一指标以及基于该成交量指标可采用的策略,产生新的见解和想法。

以下是编写该自定义指标代码将遵循的步骤:

在 #property 之后指定额外的参数,以确定指标的行为和外观的以下值

  • 使用description常量指定指标描述,内容为“Chaikin Money Flow”。
#property description "Chaikin Money Flow"
  • 使用indicator_separate_window常量指定指标在图表中以独立窗口的形式显示。
#property indicator_separate_window
  • 使用indicator_buffers常量指定指标计算所需的缓冲区数量,我们将其设置为4。
#property indicator_buffers 4
  • 使用indicator_plots常量指定指标中的图形序列数量,我们将其设置为1。
#property indicator_plots   1
  • 使用indicator_width1常量指定图形序列中线条的粗细,其中1是图形序列的编号,我们将其设置为3。
#property indicator_width1  3
  • 使用indicator_level1、indicator_level2和 indicator_level3常量,在独立指标窗口中设置水平线1、2和3的位置,分别对应(0)水平线、(0.20)和(-0.20)
#property indicator_level1  0
#property indicator_level2  0.20
#property indicator_level3  -0.20
  • 使用indicator_levelstyle设置指标水平线的样式,此处设为STYLE_DOT(点线样式)
#property indicator_levelstyle STYLE_DOT
  • 使用indicator_levelwidth设置指标水平线的粗细,此处设为 0
#property indicator_levelwidth 0
  • 使用indicator_levelcolor设置指标水平线的颜色,此处设为clrBlack(黑色)
#property indicator_levelcolor clrBlack
  • 使用indicator_type1设置图形绘制类型,此处设为DRAW_HISTOGRAM(绘制柱状图)
#property indicator_type1   DRAW_HISTOGRAM
  • 使用indicator_color1设置显示第N条线的颜色,此处使用 clrBlue(蓝色)
#property indicator_color1  clrBlue
  • 使用input函数指定输入参数,以便在指标计算中设置用户偏好的周期和成交量类型
input int                 periods=20; // Periods
input ENUM_APPLIED_VOLUME volumeTypeInp=VOLUME_TICK;  // Volume Type
  • 声明cmfBuffer数组
double                    cmfBuffer[];

在OnInit()函数中设置指标

使用SetIndexBuffer将一个双精度类型的一维动态数组与CMF指标缓冲区关联起来,其参数如下:

  • index:用于指定缓冲区索引,此处为 0
  • buffer[]:用于指定数组,此处为cmfBuffer 
  • data_type:用于指定存储在指标数组中的数据类型
SetIndexBuffer(0,cmfBuffer,INDICATOR_DATA);

使用IndicatorSetInteger设置指标对应属性的值,其参数如下:

  • prop_id:用于定义标识符,此处为INDICATOR_DIGITS
  • prop_value:用于定义要设置的小数位数,此处为5
IndicatorSetInteger(INDICATOR_DIGITS,5);

使用PlotIndexSetInteger函数设置何时开始绘制图形,其参数如下:

  • plot_index:用于指定绘图样式索引,此处为0
  • prop_id:用于指定属性标识符,此处为LOT_DRAW_BEGIN(属于ENUM_PLOT_PROPERTY_INTEGER枚举)
  • prop_value:用于指定要设置为属性的值,此处为0
PlotIndexSetInteger(0,PLOT_DRAW_BEGIN,0);

使用IndicatorSetString函数指定指标的名称和周期,其参数如下:

  • prop_id:用于指定标识符,可以是ENUM_CUSTOMIND_PROPERTY_STRING枚举中的一个,此处指定为INDICATOR_SHORTNAME
  • prop_value:用于指定指标的文本值,此处为 ("Chaikin Money Flow("+string(periods)+")")
IndicatorSetString(INDICATOR_SHORTNAME,"Chaikin Money Flow("+string(periods)+")");

OnCalculate部分用于计算CMF值

int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])

使用if函数检查是否存在可用于计算CMF值的数据:

   if(rates_total<periods)
      return(0);

当存在数据且之前已计算过CMF时,我不会从当前值开始计算

   int initPos = prev_calculated -1;
   if(initPos<0) initPos = 0;

按以下步骤计算CMF:

  • 选择成交量类型。
  • 循环计算每根K线的CMF值。
  • 声明sumAccDis(累积派发指标之和)、sumVol(成交量之和)。
  • 在声明并定义thisTickVolume变量后,通过循环计算已声明的变量sumAccDis、sumVol。
  • 将结果存储在cmfBuffer缓冲区中。
  • 返回计算出的值。
   if(volumeTypeInp==VOLUME_TICK)
   {
      for(int pos = initPos;pos<=rates_total-periods;pos++)
      {
         double sumAccDis = 0;
         long sumVol = 0;
         
         for(int i = 0; i < periods && !IsStopped(); ++i)
         {
            long thisTickVolume = tick_volume[pos+i];
            sumVol += thisTickVolume;
            sumAccDis += AccDis(high[pos+i], low[pos+i], close[pos+i], thisTickVolume);
         }
         
         cmfBuffer[pos+periods-1] = sumAccDis/sumVol;
      }
   }
   else
   {
      for(int pos = initPos;pos<=rates_total-periods;pos++)
      {
         double sumAccDis = 0;
         long sumVol = 0;
         
         for(int i = 0; i < periods && !IsStopped(); ++i)
         {
            long thisTickVolume = volume[pos+i];
            sumVol += thisTickVolume;
            sumAccDis += AccDis(high[pos+i], low[pos+i], close[pos+i], thisTickVolume);
         }
         
         cmfBuffer[pos+periods-1] = sumAccDis/sumVol;
      }
   }
   
   return (rates_total-periods-10);

声明一个名为AccDis的函数,该函数在代码中使用,包含4个变量参数(最高价 high、最低价 low、收盘价 close 和成交量 volume),用于帮助我们计算该K线的资金流乘数(Money Flow Multiplier)和资金流成交量(Money Flow Volume),而这两者将用于计算CMF。

double AccDis(double high,double low,double close,long volume)
{
   double res=0;
   
   if(high!=low)
      res=(2*close-high-low)/(high-low)*volume;
   
   return(res);
}

现在,我们已经完成了自定义CMF指标的代码编写,完整代码整合如下:

#property description "Chaikin Money Flow"
#property indicator_separate_window
#property indicator_buffers 4
#property indicator_plots   1
#property indicator_width1  3
#property indicator_level1  0
#property indicator_level2  0.20
#property indicator_level3  -0.20
#property indicator_levelstyle STYLE_DOT
#property indicator_levelwidth 0
#property indicator_levelcolor clrBlack
#property indicator_type1   DRAW_HISTOGRAM
#property indicator_color1  clrBlue
input int                 periods=20; // Periods
input ENUM_APPLIED_VOLUME volumeTypeInp=VOLUME_TICK;  // Volume Type
double                    cmfBuffer[];
void OnInit()
{
   SetIndexBuffer(0,cmfBuffer,INDICATOR_DATA);
   IndicatorSetInteger(INDICATOR_DIGITS,5);
   PlotIndexSetInteger(0,PLOT_DRAW_BEGIN,0);
   IndicatorSetString(INDICATOR_SHORTNAME,"Chaikin Money Flow("+string(periods)+")");
}
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
{
   if(rates_total<periods)
      return(0);
   int initPos = prev_calculated -1;
   if(initPos<0) initPos = 0;
   if(volumeTypeInp==VOLUME_TICK)
   {
      for(int pos = initPos;pos<=rates_total-periods;pos++)
      {
         double sumAccDis = 0;
         long sumVol = 0;
         
         for(int i = 0; i < periods && !IsStopped(); ++i)
         {
            long thisTickVolume = tick_volume[pos+i];
            sumVol += thisTickVolume;
            sumAccDis += AccDis(high[pos+i], low[pos+i], close[pos+i], thisTickVolume);
         }
         cmfBuffer[pos+periods-1] = sumAccDis/sumVol;
      }
   }
   else
   {
      for(int pos = initPos;pos<=rates_total-periods;pos++)
      {
         double sumAccDis = 0;
         long sumVol = 0;
         
         for(int i = 0; i < periods && !IsStopped(); ++i)
         {
            long thisTickVolume = volume[pos+i];
            sumVol += thisTickVolume;
            sumAccDis += AccDis(high[pos+i], low[pos+i], close[pos+i], thisTickVolume);
         }
         
         cmfBuffer[pos+periods-1] = sumAccDis/sumVol;
      }
   }
   return (rates_total-periods-10);
}
double AccDis(double high,double low,double close,long volume)
{
   double res=0;
   
   if(high!=low)
      res=(2*close-high-low)/(high-low)*volume;
   
   return(res);
}

将这段代码整合并编译后,您会在指标文件夹中找到该文件。将其添加到图表中时,您会看到其显示在图表上,效果如下图所示:

CMFInd

正如您所见,这个指标的外观和运行方式与我们自定义代码中的指标完全一致。您可以根据自身交易系统的需求对其进行调整修改。至此,我们已拥有了一个可根据简单策略应用于交易系统的自定义CMF指标,相关策略将在下一部分详细介绍。


CMF策略

在本节中,我将分享一些可与CMF指标配合使用的简单策略。这些策略遵循不同的理念和规则,有助于我们以科学系统的方式开展性能测试与优化。这些策略概念简单,但您可以根据个人偏好进行扩展开发,通过测试验证其有效性。

我们将采用以下三种简单策略:

  • CMF零轴穿越策略
  • CMF超买超卖策略
  • CMF趋势验证策略

CMF零轴穿越策略:

该策略的操作非常直观。它通过CMF指标值的变化来判断买卖时机。若前一期CMF值为负,当前或最新值为正,则触发买入信号。反之,若前一期CMF值为正,当前或最新值为负,则触发卖出信号。

简单地说,

前一期CMF值 < 0 且当前CMF值 > 0 → 买入

前一期CMF值 > 0 且当前CMF值 < 0 → 卖出 

CMF超买超卖策略:

该策略与前一种策略有所不同。它通过观察其他关键水平位来判断市场是否处于超买或超卖区域。这些区域可能随时间变化或因交易品种而异,您可通过观察指标走势的视觉判断来确定具体阈值。当CMF值低于或等于-0.20时,触发买入信号,反之亦然。当CMF值高于或等于0.20时,触发卖出信号。

简单地说,

CMF ≤ -0.20 → 买入

CMF ≥ 0.20 → 卖出

CMF趋势验证策略:

该策略在生成信号时采用另一种方法:结合另一个指标来确认趋势,或至少通过零轴穿越信号来验证价格的涨跌方向。我们将把移动平均线与CMF指标结合使用。当上一根K线收盘价低于前一根K线移动平均值,同时当前买价高于移动平均线且CMF当前值大于零时,触发买入信号,反之亦然。当上一根K线收盘价高于前一根K线移动平均值,同时当前卖价低于移动平均线且CMF当前值小于零时,触发卖出信号。

简单地说,

前收盘价(prevClose)< 前移动平均值(prevMA)、当前卖出价(ask)> 移动平均线(MA)且CMF > 0 → 买入

前收盘价(prevClose)> 前移动平均值(prevMA)、当前买入价(bid)< 移动平均线(MA)且CMF < 0 → 卖出

我们将通过尝试不同概念来测试和优化每个策略,以尽可能获得更好的结果。下一节将展示这些策略的实际效果。


CMF交易系统

在本节中,我将逐步演示如何为每个策略编写代码。随后进行测试,并展示如何优化策略以获得更优表现。请继续阅读这篇极具实用价值的文章内容。在编写策略代码前,我将先开发一个简易EA,用于在图表上打印CMF数值。这将成为所有EA的基准模板。同时确保我们在每个策略中使用的指标及其数值都准确无误。

该简易EA的后续开发计划如下:

设置EA输入参数,用于配置指标计算所需的周期和成交量类型

input int                 periods=20; // Periods
input ENUM_APPLIED_VOLUME volumeTypeInp=VOLUME_TICK;  // Volume Type

声明一个用于存储CMF值的整型变量:

int cmf;

在OnInit()事件中,我们将通过调用iCustom函数来初始化自定义CMF指标,其参数说明如下:

  • Symbol(交易品种):在此输入品种名称的字符串格式。使用_Symbol表示应用于当前图表品种。
  • Period(时间周期):在此输入时间周期的枚举类型(ENUM_TIMEFRAMES)。使用PERIOD_CURRENT表示应用当前图表的时间框架。
  • Name(指标名称):在此处,您需要输入本地计算机上自定义指标的完整路径与文件名,格式为“文件夹/自定义指标名称”。本例为 "Chaikin_Money_Flow"。
  • ...:若指标有输入参数,在此按顺序输入。本例需输入两个参数:periods(计算周期)和volumeTypeInp(成交量类型)。

初始化成功后,程序将返回INIT_SUCCEEDED常量,随后继续执行代码的后续部分。

int OnInit()
  {
   cmf = iCustom(_Symbol,PERIOD_CURRENT,"Chaikin_Money_Flow",periods,volumeTypeInp);
   return(INIT_SUCCEEDED);
  }

在OnDeinit() 事件中,我们将输出EA被移除的信息。

void OnDeinit(const int reason)
  {
   Print("EA is removed");
  }

在OnTick()事件中,我们把cmfInd[]声明为双精度类型的数组。

double cmfInd[];

使用CopyBuffer函数获取CMF指标指定缓冲区的数据。其参数如下:

  • indicator_handle:传入之前声明的双精度类型的CMF句柄。 
  • buffer_num:指定CMF缓冲区编号(0)。 
  • start_pos:起始位置(0)。
  • count:要复制的数据条数(3)。
  • buffer[]:目标数组cmfInd[]。
CopyBuffer(cmf,0,0,3,cmfInd);

为cmfInd[]设置AS_SERIES标识,使其按时间序列索引。您可以使用数组本身及成功返回true的标识作为参数。

ArraySetAsSeries(cmfInd,true);

定义一个双精度性变量保存CMF的当前值,并保留5位小数:

double cmfVal = NormalizeDouble(cmfInd[0], 5);

使用Comment函数在图表上输出刚才得到的CMF当前值。

Comment("CMF value = ",cmfVal);

我们将完整代码整合如下:

//+------------------------------------------------------------------+
//|                                                      CMF_Val.mq5 |
//+------------------------------------------------------------------+
input int                 periods=20; // Periods
input ENUM_APPLIED_VOLUME volumeTypeInp=VOLUME_TICK;  // Volume Type
int cmf;
int OnInit()
  {
   cmf = iCustom(_Symbol,PERIOD_CURRENT,"Chaikin_Money_Flow",periods,volumeTypeInp);
   return(INIT_SUCCEEDED);
  }
void OnDeinit(const int reason)
  {
   Print("EA is removed");
  }
void OnTick()
  {
    double cmfInd[];
    CopyBuffer(cmf,0,0,3,cmfInd);
    ArraySetAsSeries(cmfInd,true);
    double cmfVal = NormalizeDouble(cmfInd[0], 5);
    Comment("CMF value = ",cmfVal);
  }

将上述代码编译并作为EA加载到图表后,运行结果显示如下:

CMF_Val

如图所示,图表中显示的CMF数值(0.15904)与加载的指标数值完全一致。我们可以此作为开发其他EA和策略的基准模板。

CMF零轴穿越策略:

如前所述,我们需要编写零轴穿越策略的代码,使交易信号能根据CMF与零轴的交叉自动生成。程序需持续监测每根新K线的形成:当CMF从下向上穿越零轴时,触发买入订单。当CMF从上向下穿越零轴时,触发卖出订单。

以下是实现该策略的完整代码:
//+------------------------------------------------------------------+
//|                                           CMF_Zero_Crossover.mq5 |
//+------------------------------------------------------------------+
#include <trade/trade.mqh>
input int                 periods=20; // Periods
input ENUM_APPLIED_VOLUME volumeTypeInp=VOLUME_TICK;  // Volume Type
input double cmfPosLvls = 0.20; // CMF OB Level
input double cmfNegLvls = -0.20; // CMF OS Level
input int maPeriodInp=20; //MA Period
input double      lotSize=1;
input double      slLvl=300;
input double      tpLvl=900;
int cmf;
CTrade trade;
int barsTotal;
int OnInit()
  {
   barsTotal=iBars(_Symbol,PERIOD_CURRENT);
   cmf = iCustom(_Symbol,PERIOD_CURRENT,"Chaikin_Money_Flow",maPeriodInp,volumeTypeInp);
   return(INIT_SUCCEEDED);
  }
void OnDeinit(const int reason)
  {
   Print("EA is removed");
  }
void OnTick()
  {
   int bars=iBars(_Symbol,PERIOD_CURRENT);
   if(barsTotal != bars)
     {
      barsTotal=bars;
      double cmfInd[];
      CopyBuffer(cmf,0,0,3,cmfInd);
      ArraySetAsSeries(cmfInd,true);
      double cmfVal = NormalizeDouble(cmfInd[0], 5);
      double cmfPreVal = NormalizeDouble(cmfInd[1], 5);
      double ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK);
      double bid = SymbolInfoDouble(_Symbol,SYMBOL_BID);
      if(cmfPreVal<0 && cmfVal>0)
        {
         double slVal=ask - slLvl*_Point;
         double tpVal=ask + tpLvl*_Point;
         trade.Buy(lotSize,_Symbol,ask,slVal,tpVal);
        }
      if(cmfPreVal>0 && cmfVal<0)
        {
         double slVal=bid + slLvl*_Point;
         double tpVal=bid - tpLvl*_Point;
         trade.Sell(lotSize,_Symbol,bid,slVal,tpVal);
	}
     }
  }
//+------------------------------------------------------------------+

请注意,以下代码包含几处关键调整:

调用交易函数库文件

#include <trade/trade.mqh>

新增用户可配置参数:cmfPosLvls(超卖阈值)、cmfNegLvls(超买阈值)、lot size(交易手数)、stop-loss(止损点数/价格)和take-profit(止盈点数/价格)

input double cmfPosLvls = 0.20; // CMF OB Level
input double cmfNegLvls = -0.20; // CMF OS Level
input double      lotSize=1;
input double      slLvl=300;
input double      tpLvl=900;

声明交易对象,以及整数变量barsTotal:

CTrade trade;
int barsTotal;

在OnTick()事件里,我们只需先判断是否出现了新K线,以避免重复执行。可以这样做:先定义一个整型变量bars,然后用if条件进行判断。

int bars=iBars(_Symbol,PERIOD_CURRENT);
if(barsTotal != bars)

若出现新K线,则继续执行剩余代码,并做如下更新:

将barsTotal更新为bars

barsTotal=bars;

声明上一根K线的CMF值

double cmfPreVal = NormalizeDouble(cmfInd[1], 5);

声明ask与bid两个双精度类型变量

double ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK);
double bid = SymbolInfoDouble(_Symbol,SYMBOL_BID);

如果满足 (cmfPreVal < 0 且 cmfVal > 0),则声明止损(SL)、止盈(TP),并触发买入订单:

      if(cmfPreVal<0 && cmfVal>0)
        {
         double slVal=ask - slLvl*_Point;
         double tpVal=ask + tpLvl*_Point;
         trade.Buy(lotSize,_Symbol,ask,slVal,tpVal);
        }

若满足 (cmfPreVal > 0 且 cmfVal < 0),则声明止损(SL)、止盈(TP)并触发卖出订单。

      if(cmfPreVal>0 && cmfVal<0)
        {
         double slVal=bid + slLvl*_Point;
         double tpVal=bid - tpLvl*_Point;
         trade.Sell(lotSize,_Symbol,bid,slVal,tpVal);
        }

当我们将代码整合并加载到图表运行后,可观察到实际交易信号与测试阶段示例一致,如下图所示:

买入持仓:

买入

卖出持仓:

卖出

现在,我们需要对全部策略在欧元兑美元(EURUSD)上进行一整年(2023/1/1–2023/12/31)的测试。止损固定为300点,止盈固定为900点。我们的主要优化思路是测试其他概念或者引入额外的工具(例如移动平均线)。也可以尝试不同的时间周期,找出表现最优的时段。此类优化值得一试。

关于策略测试结果的对比分析,我们将重点关注以下核心评估指标:

  • 净利润:这是从毛利润中减去毛亏损计算得出。该值越高越好。
  • 相对余额最大回撤:它指账户在交易过程中的最大损失。该值越低越好。
  • 盈利因子:这是毛利润与毛亏损的比率。该值越高越好。
  • 预期收益:它指交易的平均利润或损失。该值越高越好。
  • 恢复系数:它用于衡量在经历投资损失后测试策略的恢复能力。该值越高越好。
  • 夏普比率:它通过比较回报与无风险回报来确定测试交易系统的风险和稳定性。夏普比率越高越好。

15分钟周期的测试结果将如下所示:

15min-Backtest1

 15min-Backtest2

15min-Backtest3

根据15分钟周期的测试结果,可得到以下关键数值:

  • 净利润:29019.10 USD。
  • 相对余额最大回撤:23%。
  • 盈利系数:1.09。
  • 预期收益:19.21。
  • 恢复系数:0.88。
  • 夏普比率:0.80。

从15分钟周期的结果看,该策略可以盈利,但回撤较大、风险偏高。因此,我们换成另一个概念——CMF超买超卖策略。

CMF超买超卖策略:

按上述思路,我们需要将该策略的EA编码,使其能够根据CMF指标自动识别超买/超卖区域并执行买卖订单。我们关注的超卖区域阈值为0.20,超买区域阈值为-0.20。EA需实时监控每根K线的CMF值,并与上述阈值进行动态比对:当CMF值≤ -0.20时,EA自动触发买入订单。当CMF值≥ 0.20时,EA自动触发卖出订单。

以下为实现该策略的完整EA代码框架:

//+------------------------------------------------------------------+
//|                                                 CMF_MA_OB&OS.mq5 |
//+------------------------------------------------------------------+
#include <trade/trade.mqh>
input int                 periods=20; // Periods
input ENUM_APPLIED_VOLUME volumeTypeInp=VOLUME_TICK;  // Volume Type
input double cmfPosLvls = 0.20; // CMF OB Level
input double cmfNegLvls = -0.20; // CMF OS Level
input double      lotSize=1;
input double      slLvl=300;
input double      tpLvl=900;
int cmf;
CTrade trade;
int barsTotal;
int OnInit()
  {
   barsTotal=iBars(_Symbol,PERIOD_CURRENT);
   cmf = iCustom(_Symbol,PERIOD_CURRENT,"Chaikin_Money_Flow",periods,volumeTypeInp);
   return(INIT_SUCCEEDED);
  }
void OnDeinit(const int reason)
  {
   Print("EA is removed");
  }
void OnTick()
  {
   int bars=iBars(_Symbol,PERIOD_CURRENT);
   if(barsTotal != bars)
     {
      barsTotal=bars;
      double cmfInd[];
      CopyBuffer(cmf,0,0,3,cmfInd);
      ArraySetAsSeries(cmfInd,true);
      double cmfVal = NormalizeDouble(cmfInd[0], 5);
      double cmfPreVal = NormalizeDouble(cmfInd[1], 5);
      double ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK);
      double bid = SymbolInfoDouble(_Symbol,SYMBOL_BID);
      if(cmfVal<=cmfNegLvls)
        {
         double slVal=ask - slLvl*_Point;
         double tpVal=ask + tpLvl*_Point;
         trade.Buy(lotSize,_Symbol,ask,slVal,tpVal);
        }
      if(cmfVal>=cmfPosLvls)
        {
         double slVal=bid + slLvl*_Point;
         double tpVal=bid - tpLvl*_Point;
         trade.Sell(lotSize,_Symbol,bid,slVal,tpVal);
        }
     }
  }
//+------------------------------------------------------------------+

代码差异说明如下:

基于用户需求,为超买和超卖区域设置两个阈值输入参数。暂时使用默认值(0.20和-0.20)。

input double cmfPosLvls = 0.20; // CMF OB Level
input double cmfNegLvls = -0.20; // CMF OS Level

策略条件

当CMF值小于或等于用户设定的超卖阈值(cmfNegLvls)时,执行买入开仓操作。

      if(cmfVal<=cmfNegLvls)
        {
         double slVal=ask - slLvl*_Point;
         double tpVal=ask + tpLvl*_Point;
         trade.Buy(lotSize,_Symbol,ask,slVal,tpVal);
        }

当CMF值大于或等于用户设定的超买阈值(cmfPosLvls)时,执行卖出开仓操作。

      if(cmfVal>=cmfPosLvls)
        {
         double slVal=bid + slLvl*_Point;
         double tpVal=bid - tpLvl*_Point;
         trade.Sell(lotSize,_Symbol,bid,slVal,tpVal);
        }

将代码整合并运行,以图表呈现后,我们即可直观看到实际持仓是否与策略条件完全匹配(正如测试阶段示例中所展示的那样)。

买入持仓:

买入

卖出持仓:

卖出

我们将采用与之前测试CMF零轴交叉策略相同的方法,对当前策略进行验证。测试标的为欧元兑美元(EURUSD),时间范围设定为2023年1月1日至12月31日,采用15分钟(M15)时间周期,止损(SL)设为300点,止盈(TP)设为900点。以下图表呈现了本次测试的完整结果:

15min-Backtest1

 15min-Backtest2

15min-Backtest3

根据15分钟周期的测试结果,可得到以下关键数值:

  • 净利润:58029.90 USD。
  • 相对余额最大回撤:55.60%。
  • 盈利系数:1.06。
  • 预期收益:14.15。
  • 恢复系数:0.62。
  • 夏普比率:0.69。

既然当前策略在收益增加的同时也带来了更高的回撤,我们计划通过叠加另一个技术指标(移动平均线)与CMF零轴交叉信号进行组合优化,以验证趋势方向。让我们看看新策略的表现如何。

CMF趋势验证策略:

如前所述,我们需要在CMF零轴交叉信号的基础上,叠加移动平均线指标来双向验证趋势(上涨和下跌)。EA需实时监测以下数据:每根K线的收盘价、当前卖出价和CMF值。该数值用于决定各要素之间的相对位置。当最新收盘价低于最新移动平均值、当前卖出价高于当前移动平均值,且CMF值大于0时,执行买入订单。反之,当最新收盘价高于最新移动平均值、当前买入价低于当前移动平均值,且CMF值小于0时,执行卖出订单。

以下是该EA的完整代码:

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

本段代码的核心改进点如下:

新增用户可配置的移动平均周期参数

input int maPeriodInp=20; //MA Period

声明移动平均线的专用整型变量

int ma;

使用iMA函数定义ma-created变量,其参数如下:

  • symbol:要设置的交易品种名称,将_symbol应用于当前交易品种。 
  • period:设置移动平均周期,将period_CURRENT应用于当前使用的时间周期。
  • ma_period:要设置的平均周期,将是maPeriodInp的用户输入。
  • ma_shift:根据需要设置水平偏移。
  • ma_method:要指定的平滑类型或移动平均类型,将MODE_SMA使用简单的MA。
  • applied_price:要指定的价格类型,将是price_CLOSE。
ma = iMA(_Symbol,PERIOD_CURRENT,maPeriodInp,0,MODE_SMA,PRICE_CLOSE);

声明maInd[]数组。

double maInd[];

使用CopyBuffer函数获取MA指标指定的缓冲区数据。

CopyBuffer(ma,0,0,3,maInd);

将AS_SERIES标识设置为maInd[],使其元素按时间序列索引。

ArraySetAsSeries(maInd,true);

定义当前和前一周期的MA值。

double maVal= NormalizeDouble(maInd[0],5);
double prevClose = iClose(_Symbol,PERIOD_CURRENT,1);

买入订单的触发条件: prevClose<maPreVal、ask>maVal且cmfVal>0。

      if(prevClose<maPreVal && ask>maVal)
        {
         if(cmfVal>0)
           {
            double slVal=ask - slLvl*_Point;
            double tpVal=ask + tpLvl*_Point;
            trade.Buy(lotSize,_Symbol,ask,slVal,tpVal);
           }
        }

卖出订单的触发条件:prevClose>maPreVal、bid<maVal且cmfVal<0。

      if(prevClose>maPreVal && bid<maVal)
        {
         if(cmfVal<0)
           {
            double slVal=bid + slLvl*_Point;
            double tpVal=bid - tpLvl*_Point;
            trade.Sell(lotSize,_Symbol,bid,slVal,tpVal);
           }
        }

运行该EA后,我们可在图表中看到已触发的订单,如下图所示:

买入持仓:

买入

卖出持仓:

卖出

我们将采用与之前测试CMF零轴交叉策略及超买(OB)/超卖(OS)策略相同的方法,对当前策略进行测试。测试交易品种为欧元兑美元(EURUSD),时间范围为2023年1月1日至2023年12月31日,采用15分钟(M15)时间周期,止损(SL)设为300点,止盈(TP)设为900点。本次测试的结果如下图所示:

15min-Backtest1

15min-Backtest2

15min-Backtest3

根据15分钟周期的测试结果,可得到以下关键数值:

  • 净利润:40723.80 USD。
  • 相对余额最大回撤:6.30%。
  • 盈利系数:1.91。
  • 预期收益:167.59。
  • 恢复系数:3.59。
  • 夏普比率:3.90。

通过对比各策略的测试结果,可以清晰地看出CMF趋势验证策略在关键指标上表现最优。


结论

正如本文所述,成交量在交易中是至关重要的概念,尤其是当它与其他关键工具结合使用时。此外,我们认识到,在测试任何策略时,优化是一项极其重要的任务,因为策略表现可能因微小参数的变动而产生显著差异。我建议您通过调整这些变量(时间周期、止损、止盈或叠加其他工具)进行自主测试,直到获得稳健的测试结果。

我们默认您已经掌握了以下内容:如何使用CMF成交量指标、如何对其进行个性化设置与编程实现以及如何基于不同的简单策略来构建、优化并测试EA。

  • CMF零轴穿越策略。
  • CMF超买和超卖策略。
  • CMF趋势验证策略。

此外,您已根据优化与测试结果,判断出哪种策略更优。希望本文对您有所帮助,如果您想阅读更多由我撰写的文章(例如基于最热门技术指标构建交易系统等),可前往我的出版物页面订购。

您可以在下方找到我的附加的源代码文件:

文件名 描述
Chaikin_Money_Flow 这是创建的自定义CMF指示器
CMF_Zero_Crossover 适用于CMF的EA,采用零交叉交易策略
CMF_OBOS 适用于CMF的EA,采用超买和超卖交易策略
CMF_trendValidation 适用于CMF的EA,采用运动/趋势验证交易策略


本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/16469

附加的文件 |
CMF_OBeOS.mq5 (1.72 KB)
最近评论 | 前往讨论 (3)
Javier Santiago Gaston De Iriarte Cabrera
Javier Santiago Gaston De Iriarte Cabrera | 17 12月 2024 在 18:51
好样的
Francis Laquerre
Francis Laquerre | 18 12月 2024 在 04:49
它在元编辑器中不起作用。请提供帮助!
Kyle Young Sangster
Kyle Young Sangster | 27 4月 2025 在 06:33
文章写得很好。我喜欢你的写作风格:简洁明了。我也很欣赏你坚持让程序员 "动手",在实践中学习。这确实是唯一的方法
交易策略 交易策略
各种交易策略的分类都是任意的,下面这种分类强调从交易的基本概念上分类。
从基础到中级:数组(二) 从基础到中级:数组(二)
在本文中,我们将了解动态数组和静态数组是什么。使用一个或另一个有区别吗?还是它们总是一样的?何时应该使用一种类型,何时应该使用另一种类型?那么常数数组呢?我们将尝试了解它们的设计目的,并考虑不初始化数组中所有值的风险。
新手在交易中的10个基本错误 新手在交易中的10个基本错误
新手在交易中会犯的10个基本错误: 在市场刚开始时交易, 获利时不适当地仓促, 在损失的时候追加投资, 从最好的仓位开始平仓, 翻本心理, 最优越的仓位, 用永远买进的规则进行交易, 在第一天就平掉获利的仓位,当发出建一个相反的仓位警示时平仓, 犹豫。
市场轮廓指标 (第二部分):基于画布的优化与渲染 市场轮廓指标 (第二部分):基于画布的优化与渲染
本文探讨了一种优化后的市场轮廓指标,该版本用基于 CCanvas 类对象(即画布)的渲染,取代了原先使用多个图形对象进行渲染的方式。