简介
在这篇文章中,我们会继续探讨根据 L. Raschke 和 L. Connors 的 华尔街智慧: 高胜算短线交易策略一书中描述的交易策略来编程, 致力于测试价格的范围界限。最后完整的部分是动量弹球(Momentum Pinball), 它根据包含在两个每日柱形中的形态来进行运作。通过第一个柱来确定第二天的交易方向,以及在第二个柱开始的价格变化设定了进入和退出市场的交易价格水平。
这篇文章的目的是向已经掌握 MQL5 的开发人员做演示,实现一种动量弹珠交易策略(Momentum Pinball TS), 在其中将会应用面向对象编程的简化方法。与完整功能的面向对象编程相比,代码中的不同是没有类 - 它们将会被结构代替。与类相反,代码中的设计和应用程序对象中的这种类型对于编程新手,特别是习惯过程化编程的人来说差别最小。另一方面,结构中提供的功能对于解决这样的任务来说已经足够了。
和前面一篇文章类似,首先,创建一个信号模块,然后 - 一个使用这个模块,用于人工交易和做历史标记的指标,第三个程序将是用于自动交易的EA,它也会使用信号模块。最终,我们将会在最新报价中测试这个EA交易,因为书的作者使用的是20年前的报价数据。
动量弹球交易策略的规则
L. Raschke 和 L. Connors 在使用由 George Taylor 描述的交易技巧时遇到了不确定的问题,这也是他们编写这个交易策略规则的原因。在泰勒的策略中,在一天之前会确定它交易的方向 - 这一天将是卖出的一天或是买入的一天。但是,作者实际的交易经常会违反这一决定,根据书作者的观点,这会使交易规则变得混乱,
为了更加确定地决定第二天交易的方向,作者使用的是 ROC (Rate Of Change,变化频率) 指标。RSI (Relative Strength Index,相对强弱指数)震荡指标会应用在它的数值中,并且 ROC 指标值的周期性可以清楚地看到。最后,交易策略的作者加入了信号水平 - 在 RSI 图表上超买和超卖水平的边界。这个指标(它被命名为 LBR/RSI, 来自 Linda Bradford Raschke)的指标线在相关范围中会侦测最适合的卖出日和买入日。LBR/RSI 的详细信息在下方,
动量弹球交易策略买入的完整规则归纳如下:
- 在 D1 时段中, LBR/RSI 在最新关闭的日柱上应该在超卖区域之内 - 低于 30.
- 在新的一天中的第一个小时柱关闭之后,在高于那个柱的最高点处设置买入挂单。
- 在挂单被触发之后,把止损设置到第一个小时柱的最低价。
- 如果仓位因为止损而关闭,再次在同一水平设置卖出挂单。
- 如果在当天结束之前,仓位还是获利的,就把它留到下一天。在第二个交易日,仓位必须关闭。
在下面所述的两个指标的帮助下,入场规则看起来如下:
— LBR/RSI 在每日时段处于超卖区域 (参见2017年10月30日)
— TS_Momentum_Pinball 位于不确定的时段 (从 M1 到 D1) 显示了交易水平和这一天第一个小时的价格范围, 这些水平就根据它们来计算的:
出场的规则在书中没有清晰描述: 作者说的是使用跟踪止损,以及在下一个早晨平仓,以及在高于第一个交易日的最高价时平仓。
卖出入场的规则是类似的 - LBR/RSI 应当在超买区域之内 (高于 70), 挂单应当设置在第一个小时柱的最低价处。
LBR/RSI 指标
当然,所有需要用于接收信号的计算应当在信号模块内进行,但是本文中除了支持自动化交易之外,也提供了人工交易。拥有一个独立的 LBR/RSI 指标,突出显示超买/超卖区域将对人工交易时从视觉上识别这种形态更加方便。并且,为了优化我们的工作,我们将不需要对多个版本的 LBR/RSI 估算进行编程 (‘有缓冲区’的用于指标而 ‘无缓冲区’的用于EA). 让我们通过标准的 iCustom 函数来连接外部信号模块。这个指标将不会进行很消耗资源的计算,并且也不需要在每个分时来查询 - 在交易策略中,指标值只是在关闭的日柱上使用。我们并不关心对当前值的持续提醒。所以,对于这样的方案就没有本质上的障碍了。
在此,把 ROC 和 RSI 的算法联合到一起, 将会画出结果中的振荡指标曲线。为了简单侦测到所需的数值,加入了在超买区域和超卖区域内使用各种颜色进行填充的功能。为此,我们需要5个缓冲区来显示,以及另外4个缓冲区 - 用于辅助计算。
加入了在最初的交易系统规则中没有提供的标准设置 (RSI 周期数和两个区域的边界数值)。您将不仅能够使用每日柱的收盘价格进行计算,还可以使用中间价,典型价格或者加权平均价格来计算。事实上,在实验中用户可以从 ENUM_APPLIED_PRICE 中提供的七种类型中做出选择。
缓冲区的声明,用户文字框和初始化模块将看起来如下:
#property indicator_separate_window #property indicator_buffers 9 #property indicator_plots 3 #property indicator_label1 “超买区域" #property indicator_type1 DRAW_FILLING #property indicator_color1 C'255,208,234' #property indicator_width1 1 #property indicator_label2 “超卖区域" #property indicator_type2 DRAW_FILLING #property indicator_color2 C'179,217,255' #property indicator_width2 1 #property indicator_label3 "RSI от ROC" #property indicator_type3 DRAW_LINE #property indicator_style3 STYLE_SOLID #property indicator_color3 clrTeal #property indicator_width3 2 #property indicator_minimum 0 #property indicator_maximum 100 input ENUM_APPLIED_PRICE TS_MomPin_Applied_Price = PRICE_CLOSE; // 用于 ROC 计算的价格 input uint TS_MomPin_RSI_Period = 3; // RSI 周期数 input double TS_MomPin_RSI_Overbought = 70; // RSI 超卖水平 input double TS_MomPin_RSI_Oversold = 30; // RSI 超买水平 double buff_Overbought_High[], buff_Overbought_Low[], // 超买区域背景 buff_Oversold_High[], buff_Oversold_Low[], // 超卖区域背景 buff_Price[], // 计算价格的数组 buff_ROC[], // 来自计算价格的 ROC 数组 buff_RSI[], // ROC 的 RSI buff_Positive[], buff_Negative[] // RSI 计算的辅助数组 ; int OnInit() { // 指标缓冲区设计: // 超买区域 SetIndexBuffer(0, buff_Overbought_High, INDICATOR_DATA); PlotIndexSetDouble(0, PLOT_EMPTY_VALUE, EMPTY_VALUE); PlotIndexSetInteger(0, PLOT_SHOW_DATA, false); SetIndexBuffer(1, buff_Overbought_Low, INDICATOR_DATA); // 超卖区域 SetIndexBuffer(2, buff_Oversold_High, INDICATOR_DATA); PlotIndexSetDouble(1, PLOT_EMPTY_VALUE, EMPTY_VALUE); PlotIndexSetInteger(1, PLOT_SHOW_DATA, false); SetIndexBuffer(3, buff_Oversold_Low, INDICATOR_DATA); // RSI 曲线 SetIndexBuffer(4, buff_RSI, INDICATOR_DATA); PlotIndexSetDouble(2, PLOT_EMPTY_VALUE, EMPTY_VALUE); // RSI 计算使用的缓冲区 SetIndexBuffer(5, buff_Price, INDICATOR_CALCULATIONS); SetIndexBuffer(6, buff_ROC, INDICATOR_CALCULATIONS); SetIndexBuffer(7, buff_Negative, INDICATOR_CALCULATIONS); SetIndexBuffer(8, buff_Positive, INDICATOR_CALCULATIONS); IndicatorSetInteger(INDICATOR_DIGITS, 2); IndicatorSetString(INDICATOR_SHORTNAME, "LBR/RSI"); return(INIT_SUCCEEDED); }
在标准事件处理函数 OnCalculate 中, 进行两个独立的循环: 第一个准备 ROC 数据数组, 第二个根据这个数组的数据来计算振荡指标值.
在由 Linda Raschke 提出的变化频率指标版本中,我们不应该比较每个柱与它们之后柱的价格,而是应该在它们之间间隔一个再做比较。换句话说,在交易策略中,他们使用的是第一天与之前的第三个交易日之间做比较。这是比较容易做到的; 继续在循环中使用背景填充超买和超卖区域。另外,确保实现价格类型选择功能。
int i_RSI_Period = int(TS_MomPin_RSI_Period), // 把 RSI 周期数转换为整数类型 i_Bar, i_Period_Bar // 用于同时访问的两个柱的索引 ; double d_Sum_Negative, d_Sum_Positive, // 用于 RSI 计算的辅助变量 d_Change // 用于 ROC 计算的辅助变量 ; // Fill in ROC buffer and fill areas: i_Period_Bar = 1; while(++i_Period_Bar < rates_total && !IsStopped()) { // 计算的柱的价格: switch(TS_MomPin_Applied_Price) { case PRICE_CLOSE: buff_Price[i_Period_Bar] = Close[i_Period_Bar]; break; case PRICE_OPEN: buff_Price[i_Period_Bar] = Open[i_Period_Bar]; break; case PRICE_HIGH: buff_Price[i_Period_Bar] = High[i_Period_Bar]; break; case PRICE_LOW: buff_Price[i_Period_Bar] = Low[i_Period_Bar]; break; case PRICE_MEDIAN: buff_Price[i_Period_Bar] = 0.50000 * (High[i_Period_Bar] + Low[i_Period_Bar]); break; case PRICE_TYPICAL: buff_Price[i_Period_Bar] = 0.33333 * (High[i_Period_Bar] + Low[i_Period_Bar] + Open[i_Period_Bar]); break; case PRICE_WEIGHTED: buff_Price[i_Period_Bar] = 0.25000 * (High[i_Period_Bar] + Low[i_Period_Bar] + Open[i_Period_Bar] + Open[i_Period_Bar]); break; } // 计算价格的差 (ROC 数值): if(i_Period_Bar > 1) buff_ROC[i_Period_Bar] = buff_Price[i_Period_Bar] - buff_Price[i_Period_Bar - 2]; // 填充背景: buff_Overbought_High[i_Period_Bar] = 100; buff_Overbought_Low[i_Period_Bar] = TS_MomPin_RSI_Overbought; buff_Oversold_High[i_Period_Bar] = TS_MomPin_RSI_Oversold; buff_Oversold_Low[i_Period_Bar] = 0; }
第二个循环 (RSI 计算) 没有什么特殊之处,它几乎是完全重复了这种类型标准指标的算法:
i_Period_Bar = prev_calculated - 1; if(i_Period_Bar <= i_RSI_Period) { buff_RSI[0] = buff_Positive[0] = buff_Negative[0] = d_Sum_Positive = d_Sum_Negative = 0; i_Bar = 0; while(i_Bar++ < i_RSI_Period) { buff_RSI[0] = buff_Positive[0] = buff_Negative[0] = 0; d_Change = buff_ROC[i_Bar] - buff_ROC[i_Bar - 1]; d_Sum_Positive += (d_Change > 0 ? d_Change : 0); d_Sum_Negative += (d_Change < 0 ? -d_Change : 0); } buff_Positive[i_RSI_Period] = d_Sum_Positive / i_RSI_Period; buff_Negative[i_RSI_Period] = d_Sum_Negative / i_RSI_Period; if(buff_Negative[i_RSI_Period] != 0) buff_RSI[i_RSI_Period] = 100 - (100 / (1. + buff_Positive[i_RSI_Period] / buff_Negative[i_RSI_Period])); else buff_RSI[i_RSI_Period] = buff_Positive[i_RSI_Period] != 0 ? 100 : 50; i_Period_Bar = i_RSI_Period + 1; } i_Bar = i_Period_Bar - 1; while(++i_Bar < rates_total && !IsStopped()) { d_Change = buff_ROC[i_Bar] - buff_ROC[i_Bar - 1]; buff_Positive[i_Bar] = (buff_Positive[i_Bar - 1] * (i_RSI_Period - 1) + (d_Change> 0 ? d_Change : 0)) / i_RSI_Period; buff_Negative[i_Bar] = (buff_Negative[i_Bar - 1] * (i_RSI_Period - 1) + (d_Change <0 ? -d_Change : 0)) / i_RSI_Period; if(buff_Negative[i_Bar] != 0) buff_RSI[i_Bar] = 100 - 100. / (1. + buff_Positive[i_Bar] / buff_Negative[i_Bar]); else buff_RSI[i_Bar] = buff_Positive[i_Bar] != 0 ? 100 : 50; }
让我们把指标命名为 LBR_RSI.mq5 并把它放到终端数据目录的标准指标文件夹中,就是这个名称将在信号模块的 iCustom 函数中使用,所以您不应该改变它。
信号模块
在联系指标和EA交易的信号模块中,加入动量弹球策略的用户设置,作者提供的用于计算 LBR/RSI 指标的是固定的数值( RSI 周期数 = 3, 超买水平 = 30, 超卖水平 = 70). 但是我们将使它们可以修改,以便于实验,就像仓位关闭的方法一样 - 书中提到了三种不同方法。我们将对它们中的全部进行编程,而用户将有功能可以选择所需的选项:
- 使用跟踪止损来平仓;
- 在第二天早晨平仓;
- 等到第二天突破建仓日价格极值的时候平仓。
“早晨” 是一个不确定的概念,为了使规则更加规范,需要更确切的定义。Raschke 和 Connors 没有谈到它,但是假如把24小时时间尺度中的'早晨'绑定为第一个日柱(在另外的交易策略规则中都是如此使用的),这也是比较合理的。
还要记住另外两个交易策略设置 - 距离每日第一个小时柱的偏移;这个偏移在设置挂单和止损水平的时候会用到:
enum ENUM_EXIT_MODE { // 退场方法列表 CLOSE_ON_SL_TRAIL, // 只通过跟踪止损 CLOSE_ON_NEW_1ST_CLOSE, // 通过在后面一天的第一个柱平仓 CLOSE_ON_DAY_BREAK // 通过突破建仓日价格极值平仓 }; // user settings input ENUM_APPLIED_PRICE TS_MomPin_Applied_Price = PRICE_CLOSE; // 动量弹球: 用于计算 ROC 的价格 input uint TS_MomPin_RSI_Period = 3; // 动量弹球: RSI 周期数 input double TS_MomPin_RSI_Overbought = 70; // 动量弹球: RSI 超卖水平 input double TS_MomPin_RSI_Oversold = 30; // 动量弹球: RSI 超买水平 input uint TS_MomPin_Entry_Offset = 10; // 动量弹球: 入场水平距离 H1 边界的偏移(点数) input uint TS_MomPin_Exit_Offset = 10; // 动量弹球: 出场水平距离 H1 边界的偏移(点数) input ENUM_EXIT_MODE TS_MomPin_Exit_Mode = CLOSE_ON_SL_TRAIL; // 动量弹球: 获利仓位的平仓方法
主模块函数 fe_Get_Entry_Signal 将会与 之前 Raschke 和 Connors 书中交易策略的功能统一,并且在本代码中其他随后的交易策略模块统一。这意思是说应该给这个函数传入这样的参数、变量的连接以及相同类型的返回值:
ENUM_ENTRY_SIGNAL fe_Get_Entry_Signal( // 两个柱形模式分析 (D1 + H1) datetime t_Time, // 当前时间 double& d_Entry_Level, // 进场水平 (与变量的连接) double& d_SL, // 止损水平 (与变量的连接) double& d_TP, // 获利水平 (与变量的连接) double& d_Range_High, // 范围内第一个小时柱的最高价 (与变量的连接) double& d_Range_Low // 范围内第一个小时柱的最低价 (与变量的连接) ) { // function body }
和之前的版本类似,我们将不会在每个分时在EA交易中调用函数时再次计算一切内容,我们将在分时之间使用静态变量来保存这些计算的水平。然而,在人工交易的指标中使用这个函数会有本质的区别; 在指标中调用函数时,应当提供把静态变量清零的功能。为了把从EA中调用和从指标中调用区分开,我们使用 t_Time 变量。指标将会反转它,也就是把它的值变成负的:
static ENUM_ENTRY_SIGNAL se_Trade_Direction = ENTRY_UNKNOWN; // 今天的交易方向 static double // 用于在分时之间保存计算的水平的变量 sd_Entry_Level = 0, sd_SL = 0, sd_TP = 0, sd_Range_High = 0, sd_Range_Low = 0 ; if(t_Time < 0) { // 只用于在指标中调用 sd_Entry_Level = sd_SL = sd_TP = sd_Range_High = sd_Range_Low = 0; se_Trade_Direction = ENTRY_UNKNOWN; } // 默认使用之前保存的水平来做进场/出场: d_Entry_Level = sd_Entry_Level; d_SL = sd_SL; d_TP = sd_TP; d_Range_High = sd_Range_High; d_Range_Low = sd_Range_Low;
下面是当第一次调用函数时取得 LBR/RSI 指标句柄的代码:
static int si_Indicator_Handle = INVALID_HANDLE; if(si_Indicator_Handle == INVALID_HANDLE) { // 在第一次调用函数时取得指标句柄: si_Indicator_Handle = iCustom(_Symbol, PERIOD_D1, "LBR_RSI", TS_MomPin_Applied_Price, TS_MomPin_RSI_Period, TS_MomPin_RSI_Overbought, TS_MomPin_RSI_Oversold ); if(si_Indicator_Handle == INVALID_HANDLE) { // 没有得到指标句柄 if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: 收取 LBR_RSI 指标句柄出错代码 #%u", __FUNCTION__, _LastError); return(ENTRY_INTERNAL_ERROR); } }
每隔24个小时,EA应当在最近关闭的日柱上分析指标值,并且建立当天允许进行的交易方向,或者,如果 LBR/RSI 指标值位于中性区域时禁止交易。从指标缓冲区中取得这个数值的代码和它的分析,以及记录可能出错的函数,以及从人工交易指标中调用的代码:
static int si_Indicator_Handle = INVALID_HANDLE; if(si_Indicator_Handle == INVALID_HANDLE) { // receiving indicator handle at first function call: si_Indicator_Handle = iCustom(_Symbol, PERIOD_D1, "LBR_RSI", TS_MomPin_Applied_Price, TS_MomPin_RSI_Period, TS_MomPin_RSI_Overbought, TS_MomPin_RSI_Oversold ); if(si_Indicator_Handle == INVALID_HANDLE) { // 没有取得指标句柄 if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: 指标句柄接收错误 LBR_RSI #%u", __FUNCTION__, _LastError); return(ENTRY_INTERNAL_ERROR); } } // 查找前一天日柱的时间: datetime ta_Bar_Time[]; if(CopyTime(_Symbol, PERIOD_D1, fabs(t_Time), 2, ta_Bar_Time) < 2) { if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyTime: 错误 #%u", __FUNCTION__, _LastError); return(ENTRY_INTERNAL_ERROR); } // 如果这是今天第一次调用,对前一天的分析: static datetime st_Prev_Day = 0; if(t_Time < 0) st_Prev_Day = 0; // only for call from indicator if(st_Prev_Day < ta_Bar_Time[0]) { // 之前一天参数清零: se_Trade_Direction = ENTRY_UNKNOWN; d_Entry_Level = sd_Entry_Level = d_SL = sd_SL = d_TP = sd_TP = d_Range_High = sd_Range_High = d_Range_Low = sd_Range_Low = 0; // 取得前一天的 LBR/RSI 指标值: double da_Indicator_Value[]; if(1 > CopyBuffer(si_Indicator_Handle, 4, ta_Bar_Time[0], 1, da_Indicator_Value)) { if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyBuffer: 错误 #%u", __FUNCTION__, _LastError); return(ENTRY_INTERNAL_ERROR); } // 如果 LBR/RSI 指标值有错误: if(da_Indicator_Value[0] > 100. || da_Indicator_Value[0] < 0.) { if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: 指标缓冲区值出错 (%f)", __FUNCTION__, da_Indicator_Value[0]); return(ENTRY_UNKNOWN); } st_Prev_Day = ta_Bar_Time[0]; // 重试次数计数 // 记录今天的交易方向: if(da_Indicator_Value[0] > TS_MomPin_RSI_Overbought) se_Trade_Direction = ENTRY_SELL; else se_Trade_Direction = da_Indicator_Value[0] > TS_MomPin_RSI_Oversold ?ENTRY_NONE : ENTRY_BUY; // 记到记录中: if(Log_Level == LOG_LEVEL_DEBUG) PrintFormat("%s: 交易方向 %s: %s. LBR/RSI: (%.2f)", __FUNCTION__, TimeToString(ta_Bar_Time[1], TIME_DATE), StringSubstr(EnumToString(se_Trade_Direction), 6), da_Indicator_Value[0] ); }
我们已经清楚了允许的交易方向,下面的任务将是确定入场的水平和亏损限制 (止损)。这同样是每24小时做一次就够了 - 就在每小时时段中每天关闭前一天小时柱的时候就可以了。但是,人工交易指标有自己的特点,我们的算法会稍微复杂一点。这是因为那个指标不仅应该侦测实时的信号水平,还要在历史中做出标记。
// 今天没有搜索信号 if(se_Trade_Direction == ENTRY_NONE) return(ENTRY_NONE); // 分析今天第一个H1上的小时柱,除非已经做过了: if(sd_Entry_Level == 0.) { // to receive data of last 24 bars H1: MqlRates oa_H1_Rates[]; int i_Price_Bars = CopyRates(_Symbol, PERIOD_H1, fabs(t_Time), 24, oa_H1_Rates); if(i_Price_Bars == WRONG_VALUE) { // 处理 CopyRates 函数错误 if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyRates: 错误 #%u", __FUNCTION__, _LastError); return(ENTRY_INTERNAL_ERROR); } // 在24个柱中找到今天的第一个柱,并且记录最高价和最低价: int i_Bar = i_Price_Bars; while(i_Bar-- > 0) { if(oa_H1_Rates[i_Bar].time < ta_Bar_Time[1]) break; // 前一天在 H1 上的最后一个柱 // H1第一个柱的范围边界: sd_Range_High = d_Range_High = oa_H1_Rates[i_Bar].high; sd_Range_Low = d_Range_Low = oa_H1_Rates[i_Bar].low; } // H1 第一个柱还没有关闭: if(i_Price_Bars - i_Bar < 3) return(ENTRY_UNKNOWN); // 计算交易水平: // 进入市场的水平: d_Entry_Level = _Point * TS_MomPin_Entry_Offset; // 辅助计算 sd_Entry_Level = d_Entry_Level = se_Trade_Direction == ENTRY_SELL ?d_Range_Low - d_Entry_Level : d_Range_High + d_Entry_Level; // 初始止损水平: d_SL = _Point * TS_MomPin_Exit_Offset; // 辅助计算 sd_SL = d_SL = se_Trade_Direction == ENTRY_BUY ?d_Range_Low - d_SL : d_Range_High + d_SL; }
在那之后,我们只能通过返回侦测的交易方向来推出这个函数:
现在,让我们对仓位关闭信号的条件进行编程。我们有三个方法,其中的一个(跟踪止损)已经在前一个版本的EA代码中实现过了。其他两种方法,它们都需要进场的价格和时间,以及仓位的方向来用于计算,我们将把它们和当前时间以及选择的平仓方法一起传给函数 fe_Get_Exit_Signal:
ENUM_EXIT_SIGNAL fe_Get_Exit_Signal( // 侦测平仓信号 double d_Entry_Level, // 进场水平 datetime t_Entry_Time, // 进场时间 ENUM_ENTRY_SIGNAL e_Trade_Direction, // 交易方向 datetime t_Current_Time, // 当前时间 ENUM_EXIT_MODE e_Exit_Mode // 退出模式 ) { static MqlRates soa_Prev_D1_Rate[]; // 前一天D1柱上的数据 static int si_Price_Bars = 0; // 辅助计数器 if(t_Current_Time < 0) { // 用于区分从指标中或是从EA中调用 t_Current_Time = -t_Current_Time; si_Price_Bars = 0; } double d_Curr_Entry_Level, d_SL, d_TP, d_Range_High, d_Range_Low ; if(e_Trade_Direction < 1) { // 没有仓位,一切清零 si_Price_Bars = 0; } switch(e_Exit_Mode) { case CLOSE_ON_SL_TRAIL: // 只有跟踪止损 return(EXIT_NONE); case CLOSE_ON_NEW_1ST_CLOSE: // 在后面一天第一个柱时平仓 if((t_Current_Time - t_Current_Time % 86400) == (t_Entry_Time - t_Current_Time % 86400) ) return(EXIT_NONE); // 建仓日尚未结束 if(fe_Get_Entry_Signal(t_Current_Time, d_Curr_Entry_Level, d_SL, d_TP, d_Range_High, d_Range_Low) < ENTRY_UNKNOWN ) { if(Log_Level > LOG_LEVEL_ERR) PrintFormat("%s: 后一天的第一个柱关闭了", __FUNCTION__); return(EXIT_ALL); } return(EXIT_NONE); // 不做平仓 case CLOSE_ON_DAY_BREAK: // 根据突破建仓日价格极值平仓 if((t_Current_Time - t_Current_Time % 86400) == (t_Entry_Time - t_Current_Time % 86400) ) return(EXIT_NONE); // 建仓日尚未结束 if(t_Current_Time % 86400 > 36000) return(EXIT_ALL); // 时间超出 if(si_Price_Bars < 1) { si_Price_Bars = CopyRates(_Symbol, PERIOD_D1, t_Current_Time, 2, soa_Prev_D1_Rate); if(si_Price_Bars == WRONG_VALUE) { // 处理 CopyRates 函数错误 if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyRates: 错误 #%u", __FUNCTION__, _LastError); return(EXIT_UNKNOWN); } if(e_Trade_Direction == ENTRY_BUY) { if(soa_Prev_D1_Rate[1].high < soa_Prev_D1_Rate[0].high) return(EXIT_NONE); // 没有突破 if(Log_Level > LOG_LEVEL_ERR) PrintFormat("%s: 价格突破了昨天的最高价: %s > %s", __FUNCTION__, DoubleToString(soa_Prev_D1_Rate[1].high, _Digits), DoubleToString(soa_Prev_D1_Rate[0].high, _Digits)); return(EXIT_BUY); } else { if(soa_Prev_D1_Rate[1].low > soa_Prev_D1_Rate[0].low) return(EXIT_NONE); // 没有突破 if(Log_Level > LOG_LEVEL_ERR) PrintFormat("%s: 价格突破了昨天的最低价: %s < %s", __FUNCTION__, DoubleToString(soa_Prev_D1_Rate[1].low, _Digits), DoubleToString(soa_Prev_D1_Rate[0].low, _Digits)); return(EXIT_SELL); } } return(EXIT_NONE); // for each } return(EXIT_UNKNOWN); }
在此,如果选择了‘跟踪止损退出’的选项,我们有一个‘帽子’ - 函数会不加分析二返回没有信号. 对于另外两个选项,会识别 ‘早晨已经到了’ 和 ‘昨天的极值被突破’ 的事件。由函数返回的各个 ENUM_EXIT_SIGNAL 类型的值和入场信号值 (ENUM_ENTRY_SIGNAL) 列表很类似:
enum ENUM_EXIT_SIGNAL { // 出场信号的列表 EXIT_UNKNOWN, // 没有标记 EXIT_BUY, // 关闭买入仓位 EXIT_SELL, // 关闭卖出仓位 EXIT_ALL, // 全部平仓 EXIT_NONE // 不做平仓 };
用于人工交易的指标
上面描述的信号模块应当可以用在用于自动交易的EA中,让我们在下面再详细介绍使用方法。首先,让我们创建一个工具来在终端图表中更加清晰地探讨交易策略的特点,这将是一个应用了信号模块的指标,它不需要修改以及显示所计算的交易水平 - 设置挂单水平和止损水平。在这个指标中关闭一个获利的交易只要加一点简单变化 - 当预先设置的水平 (获利) 达到。您应该记得,在 EA 中,我们在模块中已经使用了更加复杂的算法来编程,以便侦测交易退出信号,但是没有实现它们。
除了交易水平,这个指标还会填充一天中的第一个小时柱,这样可以更清楚地显示为什么使用这些水平。这样的标记会有助于可视化地评估大多数动量弹球策略规则的优点和缺点 - 发现那些无法从策略测试器报告中取得的信息。可视化分析加上测试器的统计将会使交易策略的规则更加高效。
为了使用用于通常人工交易的指标,让我们在其中加上一个实时交易通知系统,这样的通知将包含由信号模块建议的进场方向,设置挂单和紧急退出(止损)的水平。有三种方法来传送通知 - 标准的弹出窗口,含有文字和声音信号,电子邮件信息以及给手机发推送通知。
上面列出了指标的所有需求,这样,我们就可以继续编程了。为了在图表上画出我们计划中所有对象,指标应当含有: 一个 DRAW_FILLING 类型的缓冲区 (用于填充一天中第一个小时的柱的范围) 以及三个用于显示交易水平的缓冲区 (入场水平,获利水平,止损水平). 它们中的一个(设置挂单水平)应当有功能可以根据交易方向改变颜色 (DRAW_COLOR_LINE 类型), 对于另外两条,它们有一个单色类型 DRAW_LINE 就够了:
#property indicator_chart_window #property indicator_buffers 6 #property indicator_plots 4 #property indicator_label1 “一天中的第一个小时" #property indicator_type1 DRAW_FILLING #property indicator_color1 C'255,208,234', C'179,217,255' #property indicator_width1 1 #property indicator_label2 “进场水平" #property indicator_type2 DRAW_COLOR_LINE #property indicator_style2 STYLE_DASHDOT #property indicator_color2 clrDodgerBlue, clrDeepPink #property indicator_width2 2 #property indicator_label3 "止损" #property indicator_type3 DRAW_LINE #property indicator_style3 STYLE_DASHDOTDOT #property indicator_color3 clrCrimson #property indicator_width3 1 #property indicator_label4 "获利" #property indicator_type4 DRAW_LINE #property indicator_color4 clrGreen #property indicator_width4 1
现在,声明列表中有一部分在指标中不需要(它们是在EA中使用的), 但是它们参与在信号模块函数中,这些枚举类型的变量是用于进行记录和平仓的各种方法的; 我们在指标中也不会删除它们 - 我想在此提醒你,我们只想用预先设置的简单获利水平 (获利). 下面的那些变量的声明可以与外部模块相关联,列出用户设置和声明全局变量:
enum ENUM_LOG_LEVEL { // 记录的级别 LOG_LEVEL_NONE, // 禁止记录 LOG_LEVEL_ERR, // 只记录错误 LOG_LEVEL_INFO, // 记录错误和EA注释 LOG_LEVEL_DEBUG // 全部记录,没有例外 }; enum ENUM_ENTRY_SIGNAL { // 入场信号列表 ENTRY_BUY, // 买入信号 ENTRY_SELL, // 卖出信号 ENTRY_NONE, // 没有信号 ENTRY_UNKNOWN, // 状态不确定 ENTRY_INTERNAL_ERROR // 内部函数错误 }; enum ENUM_EXIT_SIGNAL { // 退出信号列表 EXIT_UNKNOWN, // 没有识别 EXIT_BUY, // 关闭买入仓位 EXIT_SELL, // 关闭卖出仓位 EXIT_ALL, // 关闭全部 EXIT_NONE // 不做关闭 }; #include <Expert\Signal\Signal_Momentum_Pinball.mqh> // 动量弹球交易策略的信号模块 input uint TS_MomPin_Take_Profit = 10; // 动量弹球: 获利 (点数) input bool Show_1st_H1_Bar = true; // 是否显示每天第一个小时柱的范围? input bool Alert_Popup = true; // 提醒: 显示弹出窗口? input bool Alert_Email = false; // 提醒: 发送 e-mail? input string Alert_Email_Subj = ""; // 提醒: e-mail 提醒的主题 input bool Alert_Push = true; // 提醒: 发送推送通知? input uint Days_Limit = 7; // 历史深度 (日历天数) ENUM_LOG_LEVEL Log_Level = LOG_LEVEL_DEBUG; // 记录模式 double buff_1st_H1_Bar[], buff_1st_H1_Bar_Zero[], // 用于填充每天第一个小时柱的缓冲区 buff_Entry[], buff_Entry_Color[], // 挂单线的缓冲区 buff_SL[], // 止损线的缓冲区 buff_TP[], // 获利线的缓冲区 gd_Entry_Offset = 0, // 交易品种价格的 TS_MomPin_Entry_Offset gd_Exit_Offset = 0 // 交易品种价格的 TS_MomPin_Exit_Offset ;
初始化函数没有什么特别的 - 在此,把指标缓冲区的索引赋给前面所声明缓冲区的数组。另外,让我们把用户设置从点数改为交易品种的价格,这看起来至少可以减少部分资源的小号,而不用再在主程序中进行数以千计的这样的转换:
int OnInit() { // 把点数转换为交易品种的价格: gd_Entry_Offset = TS_MomPin_Entry_Offset * _Point; gd_Exit_Offset = TS_MomPin_Exit_Offset * _Point; // 指标缓冲区的设计: // 每日第一个小时柱的长方形 SetIndexBuffer(0, buff_1st_H1_Bar, INDICATOR_DATA); PlotIndexSetDouble(0, PLOT_EMPTY_VALUE, 0); SetIndexBuffer(1, buff_1st_H1_Bar_Zero, INDICATOR_DATA); PlotIndexSetDouble(1, PLOT_EMPTY_VALUE, 0); // 挂单设置线 SetIndexBuffer(2, buff_Entry, INDICATOR_DATA); PlotIndexSetDouble(1, PLOT_EMPTY_VALUE, 0); SetIndexBuffer(3, buff_Entry_Color, INDICATOR_COLOR_INDEX); // 止损线 SetIndexBuffer(4, buff_SL, INDICATOR_DATA); PlotIndexSetDouble(2, PLOT_EMPTY_VALUE, 0); // 获利线 SetIndexBuffer(5, buff_TP, INDICATOR_DATA); PlotIndexSetDouble(3, PLOT_EMPTY_VALUE, 0); IndicatorSetInteger(INDICATOR_DIGITS, _Digits); IndicatorSetString(INDICATOR_SHORTNAME, "Momentum Pinball"); return(INIT_SUCCEEDED); }
在本系列前一篇文章的指标代码中, 创建了一些程序使用的结构, 它们的设计用途是保存在任意分时之间的信息。您读到这里就会知道为什么需要它以及如何构建它了。我们将不做任何修改地使用它。在这个指标版本中,在全部功能里面我们只会使用新柱的标志这个部分,但是如果您想使人工交易指标拥有更多功能,就会需要更多的结构中的特性。go_Brownie 结构中的完整代码在这篇文章附件的指标源代码文件 (TS_Momentum_Pinball.mq5) 中提供。另外,在那里您还可以看到推送通知功能的代码 f_Do_Alert — 它与系列文章中前面指标部分也没有做修改,所以就没必要详细探讨它了。
在标准的分时接收事件处理函数中 (OnCalculate), 在程序的主循环开始之前,必须声明所需的变量。如果这不是在主循环中第一次调用,重新计算的范围应当限制在只对当前真实的柱进行计算 - 对于这个交易策略,就是昨天和今天的柱。如果这是在初始化之后第一次循环中调用,就应当清除指标缓冲区的剩余数据,如果不这样做,在切换时段的时候,不真实存在的区域内会被剩余内容填充。另外,调用主函数应当被限制在每个柱只调用一次,使用 go_Brownie 结构会非常方便:
go_Brownie.f_Update(prev_calculated, prev_calculated); // “提供” 数据给 Brownie datetime t_Time = TimeCurrent(); // 最近知道的服务器时间 int i_Period_Bar = 0, // 辅助计数器 i_Current_TF_Bar = 0 // 循环起始柱的索引 ; if(go_Brownie.b_First_Run) { // 如果这是第一次运行 i_Current_TF_Bar = rates_total — Bars(_Symbol, PERIOD_CURRENT, t_Time — t_Time % 86400 — 86400 * Days_Limit, t_Time); // clearing buffer at re-initialization: ArrayInitialize(buff_1st_H1_Bar, 0); ArrayInitialize(buff_1st_H1_Bar_Zero, 0); ArrayInitialize(buff_Entry, 0); ArrayInitialize(buff_Entry_Color, 0); ArrayInitialize(buff_TP, 0); ArrayInitialize(buff_SL, 0); } else if(!go_Brownie.b_Is_New_Bar) return(rates_total); // 等待柱的关闭 else { // 新柱 // 最小重新计算深度 - 从开始的一天计算: i_Current_TF_Bar = rates_total — Bars(_Symbol, PERIOD_CURRENT, t_Time — t_Time % 86400, t_Time); } ENUM_ENTRY_SIGNAL e_Entry_Signal = ENTRY_UNKNOWN; // 入场信号 double d_SL = WRONG_VALUE, // 止损水平 d_TP = WRONG_VALUE, // 获利水平 d_Entry_Level = WRONG_VALUE, // 入场水平 d_Range_High = WRONG_VALUE, d_Range_Low = WRONG_VALUE // 模式的第一个柱的范围边界 ; datetime t_Curr_D1_Bar = 0, // 当前D1柱的时间 (模式的第二个柱) t_Last_D1_Bar = 0, // 最近的D1柱的时间,信号可以在这个柱上 t_Entry_Bar = 0 // 设置挂单的柱的时间 ; // 确保初始重计算柱的索引在允许的范围之内: i_Current_TF_Bar = int(fmax(0, fmin(i_Current_TF_Bar, rates_total — 1)));
现在,让我们对主工作循环进行编程,在每个迭代的开始,我们应当从信号模块接收数据,控制效率,如果没有信号就转到下一个迭代中:
while(++i_Current_TF_Bar < rates_total && !IsStopped()) { // 迭代当前时段的柱 // 从信号模块接收数据: e_Entry_Signal = fe_Get_Entry_Signal(-Time[i_Current_TF_Bar], d_Entry_Level, d_SL, d_TP, d_Range_High, d_Range_Low); if(e_Entry_Signal == ENTRY_INTERNAL_ERROR) { // 从外部指标缓冲区复制数据出错 // 计算和画图应当在下一个分时重复: go_Brownie.f_Reset(); return(rates_total); } if(e_Entry_Signal > 1) continue; // 这个柱上没有活动信号
如果模块在柱上侦测到信号并返回了估算的入场水平,首先要计算获利水平 (获利):
然后,如果这是新的一天的第一个柱,这个交易的过程就会在历史上绘制出来:
t_Curr_D1_Bar = Time[i_Current_TF_Bar] - Time[i_Current_TF_Bar] % 86400; // 柱所属于的当天的起始时间 if(t_Last_D1_Bar < t_Curr_D1_Bar) { // 这是有信号的当天的第一个柱 t_Entry_Bar = Time[i_Current_TF_Bar]; // 记住交易开始的时间
开始绘制这一天第一个小时柱的背景,用于水平的计算:
// 填充第一个小时柱的背景: if(Show_1st_H1_Bar) { i_Period_Bar = i_Current_TF_Bar; while(Time[--i_Period_Bar] >= t_Curr_D1_Bar && i_Period_Bar > 0) if(e_Entry_Signal == ENTRY_BUY) { // 牛市模式 buff_1st_H1_Bar_Zero[i_Period_Bar] = d_Range_High; buff_1st_H1_Bar[i_Period_Bar] = d_Range_Low; } else { // 熊市模式 buff_1st_H1_Bar[i_Period_Bar] = d_Range_High; buff_1st_H1_Bar_Zero[i_Period_Bar] = d_Range_Low; } }
然后,画出设置挂单的线,直到挂单变成开启的仓位,也就是当价格达到这一水平:
// 入场线,直到与柱相交叉: i_Period_Bar = i_Current_TF_Bar - 1; if(e_Entry_Signal == ENTRY_BUY) { // 牛市模式 while(++i_Period_Bar < rates_total) { if(Time[i_Period_Bar] > t_Curr_D1_Bar + 86399) { // 一天的结束 e_Entry_Signal = ENTRY_NONE; // 挂单没有被触发 break; } // 延长线: buff_Entry[i_Period_Bar] = d_Entry_Level; buff_Entry_Color[i_Period_Bar] = 0; if(d_Entry_Level <= High[i_Period_Bar]) break; // 在这个柱有入场 } } else { // 熊市模式 while(++i_Period_Bar < rates_total) { if(Time[i_Period_Bar] > t_Curr_D1_Bar + 86399) { // 一天结束 e_Entry_Signal = ENTRY_NONE; // 挂单没有触发 break; } // 延长线: buff_Entry[i_Period_Bar] = d_Entry_Level; buff_Entry_Color[i_Period_Bar] = 1; if(d_Entry_Level >= Low[i_Period_Bar]) break; // 在这个柱有入场 } }
如果在一天结束之前价格没有达到计算的水平,就在主循环中继续到下面的步骤:
if(e_Entry_Signal == ENTRY_NONE) { // 挂单在一天结束之前没有触发 i_Current_TF_Bar = i_Period_Bar; // 我们对这一天的柱就不再感兴趣 continue; }
如果这一天还没有结束,而挂单的未来还不明确,就没必要继续主程序循环:
在这两个过滤之后,只剩下一个事件可能存在了 - 挂单被触发。让我们找到挂单执行柱,并且从这个柱开始,画出获利和止损水平,直到它们中的一条与价格交叉,也就是直到仓位关闭。如果仓位的开启和关闭在一个柱上发生,这条线应当被向过去延长一个柱,这样它才能在图表上显示出来:
// 挂单被触发,查找仓位关闭的柱: i_Period_Bar = fmin(i_Period_Bar, rates_total - 1); buff_SL[i_Period_Bar] = d_SL; while(++i_Period_Bar < rates_total) { if(TS_MomPin_Exit_Mode == CLOSE_ON_SL_TRAIL) { if(Time[i_Period_Bar] >= t_Curr_D1_Bar + 86400) break; // 这是后一天的柱 // 画获利和止损线,直到它们与价格柱有交叉: buff_SL[i_Period_Bar] = d_SL; buff_TP[i_Period_Bar] = d_TP; if(( e_Entry_Signal == ENTRY_BUY && d_SL >= Low[i_Period_Bar] ) || ( e_Entry_Signal == ENTRY_SELL && d_SL <= High[i_Period_Bar] )) { // 止损退出 if(buff_SL[int(fmax(0, i_Period_Bar - 1))] == 0.) { // 在一个柱上开始和结束,向过去延伸一个柱 buff_SL[int(fmax(0, i_Period_Bar - 1))] = d_SL; buff_TP[int(fmax(0, i_Period_Bar - 1))] = d_TP; } break; } if(( e_Entry_Signal == ENTRY_BUY && d_TP <= High[i_Period_Bar] ) || ( e_Entry_Signal == ENTRY_SELL && d_SL >= Low[i_Period_Bar] )) { // 获利退出 if(buff_TP[int(fmax(0, i_Period_Bar - 1))] == 0.) { // 在一个柱上开始和结束,向过去延伸一个柱 buff_SL[int(fmax(0, i_Period_Bar - 1))] = d_SL; buff_TP[int(fmax(0, i_Period_Bar - 1))] = d_TP; } break; } } }
在仓位关闭之前,当日剩余的柱可以在程序的主循环中被忽略:
i_Period_Bar = i_Current_TF_Bar; t_Curr_D1_Bar = Time[i_Period_Bar] - Time[i_Period_Bar] % 86400; while( ++i_Period_Bar < rates_total && t_Curr_D1_Bar == Time[i_Period_Bar] - Time[i_Period_Bar] % 86400 ) i_Current_TF_Bar = i_Period_Bar;
在此,主循环代码就结束了,现在,如果在当前柱上侦测到信号,就应当发送通知:
i_Period_Bar = rates_total - 1; // 当前柱 if(Alert_Popup + Alert_Email + Alert_Push == 0) return(rates_total); // 全部禁用 if(t_Entry_Bar != Time[i_Period_Bar]) return(rates_total); // 这个柱没有信号 // message wording: string s_Message = StringFormat("ТС Momentum Pinball: 需要 %s @ %s, SL: %s", e_Entry_Signal == ENTRY_BUY ?"BuyStop" : "SellStop", DoubleToString(d_Entry_Level, _Digits), DoubleToString(d_SL, _Digits) ); // 提醒: f_Do_Alert(s_Message, Alert_Popup, false, Alert_Email, Alert_Push, Alert_Email_Subj);
完整的指标代码在附件里的 TS_Momentum_Pinball.mq5 文件中提供。
用于测试动量弹珠交易策略的 EA 交易
基本EA交易的功能应当在一定程度上扩展,以便为测试来自 Raschke Connors 书中另外的交易策略做准备,您可以在这个版本的源代码中看到前面文章中所做的详细注释,在此,我们将不再重复,只会研究基本的修改和增加,而这里有两点。
第一点增加 - 退出信号的列表, 它在之前版本的交易机器人中并不存在。另外,还在入场信号列表中加入了 ENTRY_INTERNAL_ERROR 状态。这些数字列表和上面学习过的指标中的相同枚举列表没有区别。在EA代码中,我们把它们放置在关联标准库交易操作类的代码之前。在文章附件的 Street_Smarts_Bot_MomPin.mq5 文件中,它们位于24行到32行。
第二点修改是与信号模块相关,它现在也提供了关闭仓位的信号,让我们把相应代码块加上,这样这个信号也可以用了。在之前版本的EA中,还有一个操作符 ‘if’用于检查已有仓位是否为新的(139 行); 检查是用于计算和设置初始止损水平的。在这个版本中,让我们加上另一个 'else' 部分,相关代码用于调用信号模块。如果调用结果需要,EA应当关闭仓位:
} else { // 不是新的仓位 // 用于关闭仓位的条件准备好了吗? ENUM_EXIT_SIGNAL e_Exit_Signal = fe_Get_Exit_Signal(d_Entry_Level, datetime(PositionGetInteger(POSITION_TIME)), e_Entry_Signal, TimeCurrent(), TS_MomPin_Exit_Mode); if(( e_Exit_Signal == EXIT_BUY && e_Entry_Signal == ENTRY_BUY ) || ( e_Exit_Signal == EXIT_SELL && e_Entry_Signal == ENTRY_SELL ) || e_Exit_Signal == EXIT_ALL ) { // 它必须被关闭 CTrade o_Trade; o_Trade.LogLevel(LOG_LEVEL_ERRORS); o_Trade.PositionClose(_Symbol); return; } }
在EA源代码中,它们是从171行到186行。
还有些代码中的修改是在用于控制交易水平之间距离足够的函数 fb_Is_Acceptable_Distance (424行到434行).
策略的回测
我们已经创建了一系列工具 (指标和EA交易) 用于研究 L. Raschke 和 L. Connors 书中著名的交易系统,EA交易回测的主要目的就是检查这样的交易机器人是否可行。所以,我没有优化参数,测试就是使用的默认设置。
所有测试的完整结果在附件的档案中; 在此,只提供了余额变化图表。测试的第二个(根据重要性)目的是为了演示 - 粗略 (没有参数优化) 评估交易策略在当前市场条件下的效能。我想提醒一下,作者演示的策略使用的是来自上世纪晚期的图表。
EA交易使用的是从 MetaQuotes 模拟服务器2014年开始的报价,测试的余额变化图表。交易品种 — EURJPY, 时段 — H1:
类似的针对 EURUSD 交易品种的图表, 相同时段,相同的测试时间段:
当不修改设置,在一种金属 (XAUUSD) 报价中测试相同的时段和时间段,余额变化图表看起来如下:
结论
对于华尔街智慧: 高胜算短线交易策略一书中的动量弹球交易系统的规则,我们使用指标和EA交易代码实现了,不幸的是,描述不是很详细,因为它对仓位的跟踪和关闭提供了多种方法。所以,对于想详细研究交易系统特点的人来说,还有广阔的空间来选择优化的参数和EA活动的算法,我们已经创建的代码可以用来做到这一点;另外,我们希望源代码对学习面向对象编程也会有用。
源代码,编译好的文件和开发库都在 MQL5.zip中,它们被放到了相应的目录中。它们之中每个文件的描述:
# | File name | 类型 | 描述 |
---|---|---|---|
1 | LBR_RSI.mq5 | 指标 | 合并了ROC和RSI的指标. 用于确定一天开始交易的方向(或者它的禁止状态) |
2 | TS_Momentum_Pinball.mq5 | 指标 | 使用本交易策略人工交易的指标。显示了计算的入场和出场水平,突出显示了开始计算时第一个小时的范围 |
3 | Signal_Momentum_Pinball.mqh | 开发库 | 函数,结构和用户设置的开发库,用于指标和EA交易中 |
4 | Street_Smarts_Bot_MomPin.mq5 | EA 交易 | 使用本交易策落进行自动交易的EA |