'80-20' 是出自 Linda Raschke 与 Laurence Connors 合著的书籍中所述的, 作者将其阶段归类为价格测试范围边界。它也侧重于从边界的虚假突破和回滚中获利。但这一次, 我们仅利用前一天的极短线历史间隔来分析价格走势。所获信号的寿命也相对较短, 因为系统用于日内交易。
本文的第一个目标是讲述使用 MQL5 语言开发 '80-20' 交易策略信号模块。然后, 我们将把这个模块连接到本系列前一篇文章中开发的基本交易机器人的略作改动版本。此外, 我们将使用同样的模块来开发手工交易的指标。
如前所述, 文章系列中提供的代码主要针对稍微高级的新进程序员。因此, 除其主要目标, 代码还设计用来帮助从过程编程转移到面向对象编程。代码将不会体现类特征。代之, 它将完全采用更容易掌握的结构来实现。
本文的另一个目标是开发工具, 以便我们能够检查策略是否至今仍然可行, 因为 Raschke 和 Connors 创建它时, 使用的是上个世纪末的市场行为。几次基于最新历史数据的 EA 测试结果在文章结尾处给出。
'80-20' 交易系统
作者名为 George Taylor 的 , 以及 Steve Moore 利用计算机进行期货市场分析的工作, 和 Derek Gipson' 的交易经验作为他们自己工作的理论基础。交易策略的本质可以简要描述如下: 如果前一天的开盘价和收盘价位于日线范围相反区域, 则当日开盘与前一天开盘逆向的概率非常高。前一天的开盘价和收盘价应位于范围边界附近。逆转应该从当日开始 (并非在前一天的蜡烛收盘之前)。买入策略规则如下:
1. 确认昨日开盘价处于全日区间的高位 20% 范围内, 而收盘价在全日区间的低位 20% 范围内。
2. 等待...直到当日最低价的至少 5 次 实时报价突破前日最低价
3. 将买入挂单放在昨日区间的下边界
4. 一旦挂单触发, 将其初始止损设置为当日的最低价
5. 使用尾随停止来保护已获盈利
卖单入场规则是类似的, 但前日的柱线应为看涨, 卖单应挂于柱线的上边界, 而止损应该放在当日的最高价。
另一个重要的细节是已收盘的日线柱线的大小。根据 Linda Raschke 的经验, 它应该足够长 - 超过日线柱线的平均长度。不过, 她没有指定计算平均日线范围时应考虑多少根历史天数。
我们还应记住, 此交易策略是专为日内交易而设计的 — 本书中展示的示例使用 M15 图表。
进行布局的信号块和指标所依据的策略如下。您还可以看到几个利用指标操作的结果截图。它们清晰地描绘了与系统规则相应的形态, 以及交易价位与形态的连接关系。
M5 时间帧:
形态分析的结果应放置一笔买入挂单。出示在 M1 时间帧上的相应较佳交易价位:
在 M5 时间帧上的相反交易方向的类似形态:
其交易价位 (M1 时间帧):
信号模型
我们以加入止盈价位计算为例来描述如何在自定义交易策略中添加新选项。在原始版本中没有这样的价位, 只用尾随停止来平仓。我们令止盈取决于自定义的最小突破级别 (TS_8020_Extremum_Break) — 我们将之乘以 TS_8020_Take_Profit_Ratio 自定义比率。
我们需要以下 fe_Get_Entry_Signal 信号模型的主函数元素: 当前信号状态, 经过计算的入场/离场价位 (止损和止盈), 以及昨日的范围边界。所有价位都通过与传递给函数的变量相链接来接收, 而信号的返回状态使用前一篇文章中的选项列表:
enum ENUM_ENTRY_SIGNAL { // 入场信号列表
ENTRY_BUY, // 买入信号
ENTRY_SELL, // 卖出信号
ENTRY_NONE, // 无信号
ENTRY_UNKNOWN // 未定义状态
};
ENUM_ENTRY_SIGNAL fe_Get_Entry_Signal( // D1 双蜡烛形态分析
datetime t_Time, // 当前时间
double& d_Entry_Level, // 入场价位 (链接到变量)
double& d_SL, // 止损位 (链接到变量)
double& d_TP, // 止盈位 (链接到变量)
double& d_Range_High, // 形态第一根柱线的最高价 (链接到变量)
double& d_Range_Low // 形态第一根柱线的最低价 (链接到变量)
) {}
为了检测信号, 我们需要分析 D1 时间帧的最后两根柱线。让我们从第一根开始 — 如果它不符合交易策略标准, 没有必要检查第二根柱线。有两条标准:
1. 柱线大小 (最高价和最低价之间的差值) 应超过最后 XX 日的均值 (由 TS_8020_D1_Average_Period 设置)
2. 柱线的开盘价和收盘价应位于柱线区间相反的 20% 范围内
如果满足这些条件, 则应保存最高价和最低价以供进一步使用。由于第一根柱线参数在全日内不可改变, 所以在每次函数调用时检查它们没有意义。我们来把它们存储在静态变量中:
// 自定义设置
input uint TS_8020_D1_Average_Period = 20; // 80-20: 计算日线均值的天数
input uint TS_8020_Extremum_Break = 50; // 80-20: 昨日极值突破的最小点数
static ENUM_ENTRY_SIGNAL se_Possible_Signal = ENTRY_UNKNOWN; // 形态的首根柱线信号方向
static double
// 在即时报价之间存储计算价位的变量
sd_Entry_Level = 0,
sd_SL = 0, sd_TP = 0,
sd_Range_High = 0, sd_Range_Low = 0
;
// 检查形态 D1 图表上的首根柱线:
if(se_Possible_Signal == ENTRY_UNKNOWN) { // 未知
st_Last_D1_Bar = t_Curr_D1_Bar; // 首根柱线当日无变化
// 日线范围均值
double d_Average_Bar_Range = fd_Average_Bar_Range(TS_8020_D1_Average_Period, PERIOD_D1, t_Time);
if(ma_Rates[0].high — ma_Rates[0].low <= d_Average_Bar_Range) {
// 首根柱线长度不够
se_Possible_Signal = ENTRY_NONE; // 意味着当日无信号
return(se_Possible_Signal);
}
double d_20_Percents = 0.2 * (ma_Rates[0].high — ma_Rates[0].low); // 昨日范围的 20%
if((
// 阴线:
ma_Rates[0].open > ma_Rates[0].high — d_20_Percents // 柱线开盘价位于上部 20% 之内
&&
ma_Rates[0].close < ma_Rates[0].low + d_20_Percents // 收盘价在柱线下部 20% 之内
) || (
// 阳线:
ma_Rates[0].close > ma_Rates[0].high — d_20_Percents // 柱线收盘价在上部 20% 之内
&&
ma_Rates[0].open < ma_Rates[0].low + d_20_Percents // 开盘价在下部 20% 之内
)) {
// 首根柱线对应条件
// 为形态的首根柱线定义当日交易方向:
se_Possible_Signal = ma_Rates[0].open > ma_Rates[0].close ?ENTRY_BUY : ENTRY_SELL;
// 入场价位:
sd_Entry_Level = d_Entry_Level = se_Possible_Signal == ENTRY_BUY ?ma_Rates[0].low : ma_Rates[0].high;
// 形态首根柱线的范围边界:
sd_Range_High = d_Range_High = ma_Rates[0].high;
sd_Range_Low = d_Range_Low = ma_Rates[0].low;
} else {
// 首根柱线的开盘价/收盘价与条件不匹配
se_Possible_Signal = ENTRY_NONE; // 意即当日无信号
return(se_Possible_Signal);
}
}
用来定义指定时间帧上, 从指定时间开始至指定柱线数量内的平均柱线范围的函数列表:
double fd_Average_Bar_Range( // 计算平局柱线大小
int i_Bars_Limit, // 要考虑的柱线数量
ENUM_TIMEFRAMES e_TF = PERIOD_CURRENT, // 柱线时间帧
datetime t_Time = WRONG_VALUE // 当开始计算
) {
double d_Average_Range = 0; // 数值汇总的变量
if(i_Bars_Limit < 1) return(d_Average_Range);
MqlRates ma_Rates[]; // 柱线信息数组
// 从指定历史间隔获取柱线信息:
if(t_Time == WRONG_VALUE) t_Time = TimeCurrent();
int i_Price_Bars = CopyRates(_Symbol, e_TF, t_Time, i_Bars_Limit, ma_Rates);
if(i_Price_Bars == WRONG_VALUE) { // 处理 CopyRates 函数错误
if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyRates: error #%u", __FUNCTION__, _LastError);
return(d_Average_Range);
}
if(i_Price_Bars < i_Bars_Limit) { // CopyRates 函数未能获取请求的数据量
if(Log_Level > LOG_LEVEL_NONE) PrintFormat("%s: CopyRates: 拷贝 %u 根柱线 %u", __FUNCTION__, i_Price_Bars, i_Bars_Limit);
}
// 范围之和:
int i_Bar = i_Price_Bars;
while(i_Bar-- > 0)
d_Average_Range += ma_Rates[i_Bar].high — ma_Rates[i_Bar].low;
// 均值:
return(d_Average_Range / double(i_Price_Bars));
}
形态的第二根 (当前) 柱线仅有一个标准 — 昨日范围边界的突破不应小于在设定中 (TS_8020_Extremum_Break) 指定的值。一旦触及此价位, 将出现放置挂单的信号:
// 检查在 D1 时间帧内形态的第二根 (当前) 柱线:
if(se_Possible_Signal == ENTRY_BUY) {
sd_SL = d_SL = ma_Rates[1].low; // 止损 — 当日最高价
if(TS_8020_Take_Profit_Ratio > 0) sd_TP = d_TP = d_Entry_Level + _Point * TS_8020_Extremum_Break * TS_8020_Take_Profit_Ratio; // 止盈
return(
// 是否向下突破清晰可见?
ma_Rates[1].close < ma_Rates[0].low — _Point * TS_8020_Extremum_Break ?
ENTRY_BUY : ENTRY_NONE
);
}
if(se_Possible_Signal == ENTRY_SELL) {
sd_SL = d_SL = ma_Rates[1].high; // 止损 — 当日最低价
if(TS_8020_Take_Profit_Ratio > 0) sd_TP = d_TP = d_Entry_Level — _Point * TS_8020_Extremum_Break * TS_8020_Take_Profit_Ratio; // 止盈
return(
// 是否向上突破清晰可见?
ma_Rates[1].close > ma_Rates[0].high + _Point * TS_8020_Extremum_Break ?
ENTRY_SELL : ENTRY_NONE
);
}
保存上述两个函数 (fe_Get_Entry_Signal 和 fd_Average_Bar_Range) 以及与接收信号相关的自定义设置至 mqh 库文件。完整的列表附加于下。我们将文件命名为 Signal_80-20.mqh, 并将其放在终端数据文件夹 (MQL5\Include\Expert\Signal)。
手工交易指标
就像这款 EA, 指标使用如上描述的信号模块。指标应通知交易者接收挂单信号并提供计算过的价位 — 订单放置, 止盈和止损价位。用户可选择通知方法 — 标准弹出窗口, 邮件警报或是推送通知。可以一次选择所有, 或任何您喜欢的组合。
指标的另一目标是根据 '80-20' 交易策略提供交易历史布局。指标加亮与系统标准对应的日线, 并绘制计算出的交易价位。价位线显示出的状况随时间变化。为了更明确, 我们按如下来做: 当价格触及信号线时, 交易价位被替换为挂单。当挂单被激活时, 信号线被替换为止盈线和止损线。当价格触及止盈或止损之一 (定单被平仓) 时, 这些线被中断。这种布局令交易系统规则的效率评估, 以及哪些定义可改进变得更容易。
让我们从声明缓存区和它们的显示参数开始。首先, 我们需要生命两个具有垂直填充区域的缓存区 (DRAW_FILLING)。第一个是为了加亮昨日的整根柱线, 另一个是为了加亮交易策略用到的上部 20% 和下部 20% 之间的内部区域。之后, 为多色信号线和挂单价位声明两个缓冲区 (DRAW_COLOR_LINE)。它们的颜色取决于交易方向。还有其它两个价位 (止盈和止损), 它们的颜色保持不变 (DRAW_LINE) — 它们将使用终端分配的标准颜色。所有选定的显示类型, 除了一条简单的线, 每条需要两个缓冲区, 因此代码如下所示:
#property indicator_chart_window
#property indicator_buffers 10
#property indicator_plots 6
#property indicator_label1 "形态的第一根柱线"
#property indicator_type1 DRAW_FILLING
#property indicator_color1 clrDeepPink, clrDodgerBlue
#property indicator_width1 1
#property indicator_label2 "形态的第一根柱线"
#property indicator_type2 DRAW_FILLING
#property indicator_color2 clrDeepPink, clrDodgerBlue
#property indicator_width2 1
#property indicator_label3 "信号价位"
#property indicator_type3 DRAW_COLOR_LINE
#property indicator_style3 STYLE_SOLID
#property indicator_color3 clrDeepPink, clrDodgerBlue
#property indicator_width3 2
#property indicator_label4 "入场价位"
#property indicator_type4 DRAW_COLOR_LINE
#property indicator_style4 STYLE_DASHDOT
#property indicator_color4 clrDeepPink, clrDodgerBlue
#property indicator_width4 2
#property indicator_label5 "止损"
#property indicator_type5 DRAW_LINE
#property indicator_style5 STYLE_DASHDOTDOT
#property indicator_color5 clrCrimson
#property indicator_width5 1
#property indicator_label6 "止盈"
#property indicator_type6 DRAW_LINE
#property indicator_style6 STYLE_DASHDOTDOT
#property indicator_color6 clrLime
#property indicator_width6 1
让我们为交易者提供一些选项, 禁止填充日线形态第一根柱线, 选择信号通知, 和限制历史布局深度。来自信号模块的所有交易系统设置也包括在此。为此, 我们需要初步枚举模块中使用的变量, 即使它们中的一些仅在 EA 中使用, 而在指标中不需要:
#include <Expert\Signal\Signal_80-20.mqh> // '80-20' 交易策略信号模块
input bool Show_Outer = true; // 形态的第一根柱线: 显示完整范围?
input bool Show_Inner = true; // 形态的第一根柱线: 显示内部区域?
input bool Alert_Popup = true; // 警报显示为弹出窗口?
input bool Alert_Email = false; // 警报发送邮件?
input string Alert_Email_Subj = ""; // 警报邮件题头
input bool Alert_Push = true; // 警报发送为一条推送通知?
input uint Bars_Limit = 2000; // 历史布局深度 (当前时间帧的柱线数)
ENUM_LOG_LEVEL Log_Level = LOG_LEVEL_NONE; // 日志模式
double
buff_1st_Bar_Outer[], buff_1st_Bar_Outer_Zero[], // 绘制形态第一根柱线的缓存区
buff_1st_Bar_Inner[], buff_1st_Bar_Inner_Zero[], // 绘制形态第一根柱线内部 60% 的缓存区
buff_Signal[], buff_Signal_Color[], // 信号线缓存区
buff_Entry[], buff_Entry_Color[], // 挂单线缓存区
buff_SL[], buff_TP[], // 止损位和止盈位缓存区
gd_Extremum_Break = 0 // TS_8020_Extremum_Break 为品种价格
;
int
gi_D1_Average_Period = 1, // TS_8020_D1_Average_Period 的调整值
gi_Min_Bars = WRONG_VALUE // 重新计算所需的最小柱线数量
;
int OnInit() {
// 检查 TS_8020_D1_Average_Period 参数:
gi_D1_Average_Period = int(fmin(1, TS_8020_D1_Average_Period));
// 转换品种价格的点数:
gd_Extremum_Break = TS_8020_Extremum_Break * _Point;
// 重新计算所需的最小柱线数量 = 当前时间帧的一日内柱线数
gi_Min_Bars = int(86400 / PeriodSeconds());
// 指标缓存区的目标:
// 第一根柱线完整范围的长方形
SetIndexBuffer(0, buff_1st_Bar_Outer, INDICATOR_DATA);
PlotIndexSetDouble(0, PLOT_EMPTY_VALUE, 0);
SetIndexBuffer(1, buff_1st_Bar_Outer_Zero, INDICATOR_DATA);
// 第一根柱线内部区域长方形
SetIndexBuffer(2, buff_1st_Bar_Inner, INDICATOR_DATA);
PlotIndexSetDouble(1, PLOT_EMPTY_VALUE, 0);
SetIndexBuffer(3, buff_1st_Bar_Inner_Zero, INDICATOR_DATA);
// 信号线
SetIndexBuffer(4, buff_Signal, INDICATOR_DATA);
PlotIndexSetDouble(2, PLOT_EMPTY_VALUE, 0);
SetIndexBuffer(5, buff_Signal_Color, INDICATOR_COLOR_INDEX);
// 挂单放置线
SetIndexBuffer(6, buff_Entry, INDICATOR_DATA);
PlotIndexSetDouble(3, PLOT_EMPTY_VALUE, 0);
SetIndexBuffer(7, buff_Entry_Color, INDICATOR_COLOR_INDEX);
// 止损线
SetIndexBuffer(8, buff_SL, INDICATOR_DATA);
PlotIndexSetDouble(4, PLOT_EMPTY_VALUE, 0);
// 止盈线
SetIndexBuffer(9, buff_TP, INDICATOR_DATA);
PlotIndexSetDouble(5, PLOT_EMPTY_VALUE, 0);
IndicatorSetInteger(INDICATOR_DIGITS, _Digits);
IndicatorSetString(INDICATOR_SHORTNAME, "80-20 TS");
return(INIT_SUCCEEDED);
}
将主程序的代码内置到 OnCalculate 函数中 — 分配循环遍历当前时间帧从过去到将来的柱线, 使用来自信号模块的函数搜索信号。声明必要变量并使用初始值初始化。对于首次计算的循环柱线数, 我们考虑由用户定义历史深度限制 (Bars_Limit)。对于后续调用, 重新计算当日的所有柱线 (而非最后一根柱线), 因为两根柱线形态实际上属于 D1 图表, 而与当前时间帧无关。
此外, 我们应防止所谓的幻像: 如果我们在重新初始化期间没有执行强制指标缓冲区清除, 则切换时间帧或品种时屏幕上相关的填充区域不再保留。缓冲区清除应在指标初始化之后绑定到第一次 OnCalculate 函数调用。然而, 标准的 prev_calculated 变量不足以定义调用是否是第一次, 因为不仅在第一次函数调用期间它可能为零, 而且 "当校验和变化时" 也为零。让我们花一点时间来正确解决这个问题, 这要通过创建不受 prev_calculated 变量为零影响的结构。结构用来存储和处理指标中的常用数据:
- 首先启动 OnCalculate 函数的标志;
- 当校验和变化时已计算柱线的计数器不会设置为零;
- 校验和变化标志;
- 柱线开始标志;
- 当前柱线开始时间。
组合所有这些数据的结构将在全局层面声明。它能够从任何内置或自定义函数中收集或呈现数据。我们把这个结构命名为 Brownie。它可以放在指标代码的末尾。在此还会声明一个名为 go_Brownie 的单一全局类型结构对象:
struct BROWNIE { // Brownie: 在全局层面存储和处理数据的结构
datetime t_Last_Bar_Time; // 最后处理过的柱线时间
int i_Prew_Calculated; // 已计算柱线的数量
bool b_First_Run; // 首次启动标志
bool b_History_Updated; // 历史更新标志
bool b_Is_New_Bar; // 新柱线开盘标志
BROWNIE() { // 构造器
// 省缺值:
t_Last_Bar_Time = 0;
i_Prew_Calculated = WRONG_VALUE;
b_First_Run = b_Is_New_Bar = true;
b_History_Updated = false;
}
void f_Reset(bool b_Reset_First_Run = true) { // 变量设置为零
// 省缺值:
t_Last_Bar_Time = 0;
i_Prew_Calculated = WRONG_VALUE;
if(b_Reset_First_Run) b_First_Run = true; // 如果允许则设为零
b_Is_New_Bar = true;
b_History_Updated = false;
}
void f_Update(int i_New_Prew_Calculated = WRONG_VALUE) { // 更新变量
// OnCalculate 内置函数首次调用标志
if(b_First_Run && i_Prew_Calculated > 0) b_First_Run = false;
// 新柱线?
datetime t_This_Bar_Time = TimeCurrent() - TimeCurrent() % PeriodSeconds();
b_Is_New_Bar = t_Last_Bar_Time == t_This_Bar_Time;
// 更新当前柱线时间?
if(b_Is_New_Bar) t_Last_Bar_Time = t_This_Bar_Time;
if(i_New_Prew_Calculated > -1) {
// 历史数据有任何变化?
b_History_Updated = i_New_Prew_Calculated == 0 && i_Prew_Calculated > WRONG_VALUE;
// 若首次调用 OnCalculate 的情况, 使用 prew_calculated
if(i_Prew_Calculated == WRONG_VALUE) i_Prew_Calculated = i_New_Prew_Calculated;
// 或历史数据无更新
else if(i_New_Prew_Calculated > 0) i_Prew_Calculated = i_New_Prew_Calculated;
}
}
};
BROWNIE go_Brownie;
我们通知指标逆初事件的 Brownie:
void OnDeinit(const int reason) {
go_Brownie.f_Reset(); // 通知 Brownie
}
若有必要, 如果自定义函数或类需要价格、交易量或是当前柱线的点差值 (开盘价, 最高价, 最低价, 收盘价, 即时报价交易量, 交易量, 点差), 由 Brownie 存储的数据量可以进行扩展。使用来自 OnCalculate 函数的现成数据, 并通过 Brownie 替代时间序列复制函数 (CopyOpen, CopyHigh 或 CopyRates 等等) 来传递它们则更为便利— 这可节省 CPU 资源, 并消除了为这些语言函数的错误分配处理的必要性。
让我们返回主指标函数。使用如下所示 go_Brownie 结构声明变量并准备数组:
go_Brownie.f_Update(prev_calculated); // 为 Brownie 投送数据
int
i_Period_Bar = 0, // 辅助计数器
i_Current_TF_Bar = rates_total - int(Bars_Limit) // 当前时间帧循环开始的柱线索引
;
static datetime st_Last_D1_Bar = 0; // 一对 D1 时间帧柱线的最后一根已处理时间 (形态的第二根柱线)
static int si_1st_Bar_of_Day = 0; // 当日首根柱线的索引
if(go_Brownie.b_First_Run) { // 如果这是首次启动
// 在重新初始化期间清除缓存区:
ArrayInitialize(buff_1st_Bar_Inner, 0); ArrayInitialize(buff_1st_Bar_Inner_Zero, 0);
ArrayInitialize(buff_1st_Bar_Outer, 0); ArrayInitialize(buff_1st_Bar_Outer_Zero, 0);
ArrayInitialize(buff_Entry, 0); ArrayInitialize(buff_Entry_Color, 0);
ArrayInitialize(buff_Signal, 0); ArrayInitialize(buff_Signal_Color, 0);
ArrayInitialize(buff_TP, 0);
ArrayInitialize(buff_SL, 0);
st_Last_D1_Bar = 0;
si_1st_Bar_of_Day = 0;
} else { // 这不是首次启动
datetime t_Time = TimeCurrent();
// 重计算最小深度 - 从当日开始:
i_Current_TF_Bar = rates_total - Bars(_Symbol, PERIOD_CURRENT, t_Time - t_Time % 86400, t_Time) - 1;
}
ENUM_ENTRY_SIGNAL e_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 // borders 形态第一根柱线范围的边界
;
datetime
t_Curr_D1_Bar = 0, // 当前 D1 柱线的时间 (形态的第二根柱线)
t_D1_Bar_To_Fill = 0 // 要填充的 D1 柱线时间 (形态的第一根柱线)
;
// 确保重新计算的初始柱线索引在可接受的范围内:
i_Current_TF_Bar = int(fmax(0, fmin(i_Current_TF_Bar, rates_total - gi_Min_Bars)));
while(++i_Current_TF_Bar < rates_total && !IsStopped()) { // 遍历当前时间帧柱线
// 主程序循环位于此处
}
遍历当前时间帧柱线时检查信号的存在:
e_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_Signal > 1) continue; // 柱线所属交易日内无信号
如果在新交易日的第一根柱线上存在信号, 则应填充上一个交易日所有柱线的范围。变量 t_D1_Bar_To_Fill 的日期类型值作为标志。如果它等于 WRONG_VALUE, 则不需要在此柱线上填充。信号线应同样开始于第一根柱线, 但让我们将它延伸到前一日的最后一根柱线, 以便获得更好的布局感观。由于信号线的计算, 以及多空柱线和填充颜色均有所不同, 我们要打造两个相似的块:
t_Curr_D1_Bar = Time[i_Current_TF_Bar] — Time[i_Current_TF_Bar] % 86400; // 柱线所属日的开始
if(st_Last_D1_Bar < t_Curr_D1_Bar) { // 这是新一日的柱线
t_D1_Bar_To_Fill = Time[i_Current_TF_Bar — 1] — Time[i_Current_TF_Bar — 1] % 86400;
si_1st_Bar_of_Day = i_Current_TF_Bar;
}
else t_D1_Bar_To_Fill = WRONG_VALUE; // 昨日柱线, 没有新的填充需求
st_Last_D1_Bar = t_Curr_D1_Bar; // 记忆
if(t_D1_Bar_To_Fill != WRONG_VALUE) { // 新的 D1 柱线
// 填充昨日的 D1 柱线:
i_Period_Bar = i_Current_TF_Bar;
if(d_Entry_Level < d_Range_High) { // D1 阴线
if(Show_Outer) while(--i_Period_Bar > 0) { // 全部范围
if(Time[i_Period_Bar] < t_D1_Bar_To_Fill) break;
buff_1st_Bar_Outer_Zero[i_Period_Bar] = d_Range_Low;
buff_1st_Bar_Outer[i_Period_Bar] = d_Range_High;
}
if(Show_Inner) { // 内部区域
i_Period_Bar = i_Current_TF_Bar;
while(--i_Period_Bar > 0) {
if(Time[i_Period_Bar] < t_D1_Bar_To_Fill) break;
buff_1st_Bar_Inner_Zero[i_Period_Bar] = d_Range_Low + 0.2 * (d_Range_High — d_Range_Low);
buff_1st_Bar_Inner[i_Period_Bar] = d_Range_High — 0.2 * (d_Range_High — d_Range_Low);
}
}
// 信号线的开始 — 从昨日的最后一根柱线
buff_Signal[i_Current_TF_Bar] = buff_Signal[i_Current_TF_Bar — 1] = d_Range_Low — gd_Extremum_Break;
buff_Signal_Color[i_Current_TF_Bar] = buff_Signal_Color[i_Current_TF_Bar — 1] = 0;
} else { // D1 阳线
if(Show_Outer) while(--i_Period_Bar > 0) { // 全部范围
if(Time[i_Period_Bar] < t_D1_Bar_To_Fill) break;
buff_1st_Bar_Outer_Zero[i_Period_Bar] = d_Range_High;
buff_1st_Bar_Outer[i_Period_Bar] = d_Range_Low;
}
if(Show_Inner) { // 内部区域
i_Period_Bar = i_Current_TF_Bar;
while(--i_Period_Bar > 0) {
if(Time[i_Period_Bar] < t_D1_Bar_To_Fill) break;
buff_1st_Bar_Inner_Zero[i_Period_Bar] = d_Range_High — 0.2 * (d_Range_High — d_Range_Low);
buff_1st_Bar_Inner[i_Period_Bar] = d_Range_Low + 0.2 * (d_Range_High — d_Range_Low);
}
}
// 信号线的开始 — 从昨日的最后一根柱线
buff_Signal[i_Current_TF_Bar] = buff_Signal[i_Current_TF_Bar — 1] = d_Range_High + gd_Extremum_Break;
buff_Signal_Color[i_Current_TF_Bar] = buff_Signal_Color[i_Current_TF_Bar — 1] = 1;
}
} else continue;
所有剩余的布局线将在当前时间帧的柱线迭代循环内绘制。如上提及, 信号线应在价格触及它时于柱线上结束。挂单线应在同一根柱线上开始, 并于柱线收盘时结束, 它应与价格产生联系。止盈和止损线也应在同一根柱线。形态的布局在柱线上完成, 价格会触及其一:
// 信号线依旧与柱线交叉:
i_Period_Bar = i_Current_TF_Bar;
if(d_Entry_Level < d_Range_High) { // D1 阴线
while(++i_Period_Bar < rates_total) {
if(Time[i_Period_Bar] > t_Curr_D1_Bar + 86399) break;
buff_Signal[i_Period_Bar] = d_Range_Low — gd_Extremum_Break;
buff_Signal_Color[i_Period_Bar] = 0;
if(d_Range_Low — gd_Extremum_Break >= Low[i_Period_Bar]) break;
}
} else { // D1 阳线
while(++i_Period_Bar < rates_total) {
if(Time[i_Period_Bar] > t_Curr_D1_Bar + 86399) break;
buff_Signal[i_Period_Bar] = d_Range_High + gd_Extremum_Break;
buff_Signal_Color[i_Period_Bar] = 1;
if(d_Range_High + gd_Extremum_Break <= High[i_Period_Bar]) break;
}
}
// 入场线依旧与柱线交叉:
if(d_Entry_Level < d_Range_High) { // D1 阴线
while(++i_Period_Bar < rates_total) {
if(Time[i_Period_Bar] > t_Curr_D1_Bar + 86399) break;
buff_Entry[i_Period_Bar] = d_Range_Low;
buff_Entry_Color[i_Period_Bar] = 0;
if(d_Range_Low <= High[i_Period_Bar]) {
if(buff_Entry[i_Period_Bar — 1] == 0.) {
// 在单根柱线上开始并结束, 延伸一根柱线到过去
buff_Entry[i_Period_Bar — 1] = d_Range_Low;
buff_Entry_Color[i_Period_Bar — 1] = 0;
}
break;
}
}
} else { // D1 阳线
while(++i_Period_Bar < rates_total) {
if(Time[i_Period_Bar] > t_Curr_D1_Bar + 86399) break;
buff_Entry[i_Period_Bar] = d_Range_High;
buff_Entry_Color[i_Period_Bar] = 1;
if(d_Range_High >= Low[i_Period_Bar]) {
if(buff_Entry[i_Period_Bar — 1] == 0.) {
// 在单根柱线上开始并结束, 延伸一根柱线到过去
buff_Entry[i_Period_Bar — 1] = d_Range_High;
buff_Entry_Color[i_Period_Bar — 1] = 1;
}
break;
}
}
}
// 止盈和止损线之一依旧与柱线交叉:
if(d_Entry_Level < d_Range_High) { // D1 阴线
// 止损线等于日内最低价:
d_SL = Low[ArrayMinimum(Low, si_1st_Bar_of_Day, i_Period_Bar — si_1st_Bar_of_Day)];
while(++i_Period_Bar < rates_total) {
if(Time[i_Period_Bar] > t_Curr_D1_Bar + 86399) break;
buff_SL[i_Period_Bar] = d_SL;
buff_TP[i_Period_Bar] = d_TP;
if(d_TP <= High[i_Period_Bar] || d_SL >= Low[i_Period_Bar]) {
if(buff_SL[i_Period_Bar — 1] == 0.) {
// 在单根柱线上开始并结束, 延伸一根柱线到过去
buff_SL[i_Period_Bar — 1] = d_SL;
buff_TP[i_Period_Bar — 1] = d_TP;
}
break;
}
}
} else { // D1 阳线
// 止损线等于日内最高价:
d_SL = High[ArrayMaximum(High, si_1st_Bar_of_Day, i_Period_Bar — si_1st_Bar_of_Day)];
while(++i_Period_Bar < rates_total) {
if(Time[i_Period_Bar] > t_Curr_D1_Bar + 86399) break;
buff_SL[i_Period_Bar] = d_SL;
buff_TP[i_Period_Bar] = d_TP;
if(d_SL <= High[i_Period_Bar] || d_TP >= Low[i_Period_Bar]) {
if(buff_SL[i_Period_Bar — 1] == 0.) {
// 在单根柱线上开始并结束, 延伸一根柱线到过去
buff_SL[i_Period_Bar — 1] = d_SL;
buff_TP[i_Period_Bar — 1] = d_TP;
}
break;
}
}
}
我们在循环之外放置调用 f_Do_Alert 信号通知函数的代码。事实上, 与指标内涉及的相比, 它的机会略微更广 — 函数能够处理音频文件, 意味着此选项可以添加到自定义设置。可为买入和卖出信号选择单独文件的能力。函数列表:
void f_Do_Alert( // 搜索信号和通知的函数
string s_Message, // 警报消息
bool b_Alert = true, // 显示弹出窗口?
bool b_Sound = false, // 播放音频文件?
bool b_Email = false, // 发送邮件?
bool b_Notification = false, // 发送推送通知?
string s_Email_Subject = "", // 邮件标题
string s_Sound = "alert.wav" // 音频文件
) {
static string ss_Prev_Message = "三是静音"; // 之前的警报消息
static datetime st_Prev_Time; // 之前的警报柱线时间
datetime t_This_Bar_Time = TimeCurrent() — PeriodSeconds() % PeriodSeconds(); // 当前柱线时间
if(ss_Prev_Message != s_Message || st_Prev_Time != t_This_Bar_Time) {
// 另一个 和/或 第一次
// 记忆:
ss_Prev_Message = s_Message;
st_Prev_Time = t_This_Bar_Time;
// 发自消息串:
s_Message = StringFormat("%s | %s | %s | %s",
TimeToString(TimeLocal(), TIME_SECONDS), // 本地时间
_Symbol, // 品种
StringSubstr(EnumToString(ENUM_TIMEFRAMES(_Period)), 7), // 时间帧
s_Message // 消息
);
// 激活通知信号:
if(b_Alert) Alert(s_Message);
if(b_Email) SendMail(s_Email_Subject + " " + _Symbol, s_Message);
if(b_Notification) SendNotification(s_Message);
if(b_Sound) PlaySound(s_Sound);
}
}