多货币和多时间范围指标
到目前为止,我们讨论的都是处理当前图表交易品种的报价或分时报价的指标。但有时需要分析多个金融工具,或分析与当前金融工具不同的某个金融工具。在这种情况下,正如我们在分时报价分析中所看到的,通过OnCalculate参数传递给指标的标准时间序列是不够的。需要以某种方式请求“外部”报价,等待它们构建完成,然后才能基于它们计算指标。
请求和构建与当前图表时间范围不同的报价数据,其机制与处理其他交易品种没有区别。因此,在本节中,我们将讨论多货币指标的创建,而多时间范围指标也可以按照类似原则进行创建。
我们需要解决的问题之一是柱线在时间上的同步性。具体来说,不同交易品种可能有不同的交易时段、周末休市安排,总体而言,父图表上的柱线编号可能与“外部”交易品种的报价柱线编号有所不同。
首先,我们先简化任务,将范围限定为任意一个交易品种,该交易品种可能与当前交易品种不同。交易者常常需要同时查看不同交易品种的多个图表(例如相关对中的主导交易品种和跟随交易品种)。我们来创建一个名为 IndSubChartSimple.mq5的指标,用于在子窗口中显示用户选择的交易品种的报价。
IndSubChartSimple
为了复制主图表的外观,在输入参数中,我们将不仅提供交易品种选择,还提供绘图模式:DRAW_CANDLES、DRAW_BARS、DRAW_LINE。前两种模式需要四个缓冲区,它们输出全部四种价格:Open、High、Low 和 Close(日式蜡烛图或柱线),而后者使用一个缓冲区显示Close 价格的折线。为了支持所有模式,我们将使用所需的最大缓冲区数量。
#property indicator_separate_window
#property indicator_buffers 4
#property indicator_plots 1
#property indicator_type1 DRAW_CANDLES
#property indicator_color1 clrBlue,clrGreen,clrRed // border,bullish,bearish
|
缓冲区数组将根据价格类型命名。
double open[];
double high[];
double low[];
double close[];
|
默认情况下启用日式蜡烛图显示。在这种模式下,MQL5 允许指定多种颜色,而非单一颜色。在#property indicator_colorN指令中,这些颜色用逗号隔开。如果有两种颜色,则第一种颜色决定蜡烛图轮廓的颜色,第二种颜色决定填充颜色。如果像示例中一样有三种颜色,则第一种颜色决定轮廓的颜色,第二种和第三种颜色分别决定蜡烛图阳线和阴线的实体颜色。
在专门讨论 图表的章节中,我们将了解 ENUM_CHART_MODE 枚举,它定义了三种可用的图表绘制模式。
ENUM_CHART_MODE 元素
|
ENUM_DRAW_TYPE 元素
|
CHART_CANDLES
|
DRAW_CANDLES
|
CHART_BARS
|
DRAW_BARS
|
CHART_LINE
|
DRAW_LINE
|
它们对应我们选择的绘图模式,因为我们特意选择了与标准模式一致的绘图方法。在这里,ENUM_CHART_MODE 使用便捷,因为它只包含我们需要的 3 种元素,与 ENUM_DRAW_TYPE 不同,它包含许多其他绘图方法。
因此,输入变量有以下定义。
input string SubSymbol = ""; // Symbol
input ENUM_CHART_MODE Mode = CHART_CANDLES;
|
通过实现一个简单的函数,将 ENUM_CHART_MODE 转换为 ENUM_DRAW_TYPE。
ENUM_DRAW_TYPE Mode2Style(const ENUM_CHART_MODE m)
{
switch(m)
{
case CHART_CANDLES: return DRAW_CANDLES;
case CHART_BARS: return DRAW_BARS;
case CHART_LINE: return DRAW_LINE;
}
return DRAW_NONE;
}
|
SubSymbol输入参数中的空字符串表示当前图表交易品种。但由于 MQL5 不允许编辑输入变量,我们需要添加一个全局变量来存储实际使用的交易品种,并在 OnInit处理程序中对其赋值。
string symbol;
...
int OnInit()
{
symbol = SubSymbol;
if(symbol == "") symbol = _Symbol;
else
{
// making sure the symbol exists and is selected in the Market Watch
if(!SymbolSelect(symbol, true))
{
return INIT_PARAMETERS_INCORRECT;
}
}
...
}
|
我们还需要检查用户输入的交易品种是否存在并将其添加到Market Watch:这可以通过 SymbolSelect 函数实现,我们将在关于 交易品种的章节中学习该函数。
为了统一管理缓冲区和图表设置,源代码有多个辅助函数:
- InitBuffer 设置一个缓冲区
- InitBuffers 设置完整的缓冲区集
- InitPlot 设置一个图表
单独的函数将注册相同实体时重复执行的多个操作组合在一起。它们也为该指标在 图表一章中进一步开发铺平了道路:我们将支持绘图设置的交互式更改,以响应用户对图表的操作(请参阅 图表显示模式一章中指标 IndSubChart.mq5的完整版)。
void InitBuffer(const int index, double &buffer[],
const ENUM_INDEXBUFFER_TYPE style = INDICATOR_DATA,
const bool asSeries = false)
{
SetIndexBuffer(index, buffer, style);
ArraySetAsSeries(buffer, asSeries);
}
string InitBuffers(const ENUM_CHART_MODE m)
{
string title;
if(m == CHART_LINE)
{
InitBuffer(0, close, INDICATOR_DATA, true);
// hiding all buffers not used for the line chart
InitBuffer(1, high, INDICATOR_CALCULATIONS, true);
InitBuffer(2, low, INDICATOR_CALCULATIONS, true);
InitBuffer(3, open, INDICATOR_CALCULATIONS, true);
title = symbol + " Close";
}
else
{
InitBuffer(0, open, INDICATOR_DATA, true);
InitBuffer(1, high, INDICATOR_DATA, true);
InitBuffer(2, low, INDICATOR_DATA, true);
InitBuffer(3, close, INDICATOR_DATA, true);
title = "# Open;# High;# Low;# Close";
StringReplace(title, "#", symbol);
}
return title;
}
|
请注意,当开启折线图模式时,仅使用close数组。该数组被分配索引 0。由于 INDICATOR_CALCULATIONS 特性,剩余的三个数组对用户完全隐藏。所有四个数组均用于蜡烛图和柱线模式,且其编号遵循 OHLC 标准(这是 DRAW_CANDLES 和 DRAW_BARS 绘图类型的要求)。所有数组均分配“串行”特性,即索引按从右到左的顺序排列。
InitBuffers 函数返回 Data Window 中缓冲区的标题。
所有必需的绘图特性均在 InitPlot函数中设置。
void InitPlot(const int index, const string name, const int style,
const int width = -1, const int colorx = -1,
const double empty = EMPTY_VALUE)
{
PlotIndexSetInteger(index, PLOT_DRAW_TYPE, style);
PlotIndexSetString(index, PLOT_LABEL, name);
PlotIndexSetDouble(index, PLOT_EMPTY_VALUE, empty);
if(width != -1) PlotIndexSetInteger(index, PLOT_LINE_WIDTH, width);
if(colorx != -1) PlotIndexSetInteger(index, PLOT_LINE_COLOR, colorx);
}
|
单个图表(索引为 0)的初始设置通过 OnInit处理程序中的新函数完成。
int OnInit()
{
...
InitPlot(0, InitBuffers(Mode), Mode2Style(Mode));
IndicatorSetString(INDICATOR_SHORTNAME, "SubChart (" + symbol + ")");
IndicatorSetInteger(INDICATOR_DIGITS, (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS));
return INIT_SUCCEEDED;
}
|
尽管在该指标版本中设置仅执行一次,但它是动态完成的,会考虑 mode输入参数,这与#property 指令提供的静态设置不同。未来,在指标的完整版中,我们将能够多次调用 InitPlot函数,动态更改指标的外部显示形式。
缓冲区通过OnCalculate填充。在最简单的情况下,当给定交易品种与图表一致时,我们可以直接使用以下实现方式。
int OnCalculate(const int rates_total, const int prev_calculated,
const datetime &time[],
const double &op[], const double &hi[], const double &lo[], const double &cl[],
const long &[], const long &[], const int &[]) // unused
{
if(prev_calculated ==0) // needs clarification (see further)
{
ArrayInitialize(open, EMPTY_VALUE);
ArrayInitialize(high, EMPTY_VALUE);
ArrayInitialize(low, EMPTY_VALUE);
ArrayInitialize(close, EMPTY_VALUE);
}
if(_Symbol != symbol)
{
// being developed
...
}
else
{
ArraySetAsSeries(op, true);
ArraySetAsSeries(hi, true);
ArraySetAsSeries(lo, true);
ArraySetAsSeries(cl, true);
for(int i = 0; i < MathMax(rates_total - prev_calculated, 1); ++i)
{
open[i] = op[i];
high[i] = hi[i];
low[i] = lo[i];
close[i] = cl[i];
}
}
return rates_total;
}
|
但在处理任意交易品种时,数组参数并不包含所需报价,且可用柱线总数可能有所不同。此外,当第一次将指标添加到图表时,如果没有提前在附近打开该“外部”交易品种的图表窗口,其报价可能根本尚未准备就绪。此外,第三方交易品种的报价将异步加载,因此新的一批柱线数据可能随时“到达”,这需要进行完整的重新计算。
因此,让我们创建用于控制另一个交易品种的柱线数量的变量 (lastAvailable)、常量参数 prev_calculated 的可编辑“克隆”,以及报价就绪标志。
static bool initialized; // symbol quotes readiness flag
static int lastAvailable; // number of bars for a symbol (and the current timeframe)
int _prev_calculated = prev_calculated; // editable copy of prev_calculated
|
在 OnCalculate的起始位置,我们要添加一项检查,用于判断是否同时出现多个柱线:我们将使用 lastAvailable 变量,该变量的值是基于函数上一次正常退出(即成功计算)时iBars(symbol, _Period) 的值进行填充的。一旦加载额外的历史数据,我们应将 _prev_calculated和柱线数量重置为 0,同时移除就绪标志,以重新计算指标。
int OnCalculate(const int rates_total, const int prev_calculated,
const datetime &time[],
const double &op[], const double &hi[], const double &lo[], const double &cl[],
const long &[], const long &[], const int &[]) // unused
{
...
if(iBars(symbol, _Period) - lastAvailable > 1)
{
// loading additional history or first start
_prev_calculated = 0;
initialized = false;
lastAvailable = 0;
}
// then everywhere we use a copy of _prev_calculated
if(_prev_calculated == 0)
{
ArrayInitialize(open, EMPTY_VALUE);
ArrayInitialize(high, EMPTY_VALUE);
ArrayInitialize(low, EMPTY_VALUE);
ArrayInitialize(close, EMPTY_VALUE);
}
if(_Symbol != symbol)
{
// request quotes and "wait" till they are ready
...
// main calculation (filling buffers)
...
}
else
{
... // as is
}
lastAvailable = iBars(symbol, _Period);
return rates_total;
}
|
注释中“等待”一词加上引号并非偶然。正如我们所知道的,在指标中我们无法真正地“等待”(以免拖慢终端的界面线程)。相反,如果数据不足,我们应直接退出函数。因此,“等待”意味着等待下一次计算事件:即新的分时报价到来时或响应图表更新请求时。
以下代码将检查报价是否就绪。
int OnCalculate(const int rates_total, const int prev_calculated,
const datetime &time[],
const double &op[], const double &hi[], const double &lo[], const double &cl[],
const long &[], const long &[], const int &[]) // unused
{
...
if(_Symbol != symbol)
{
if(!initialized)
{
Print("Host ", _Symbol, " ", rates_total, " bars up to ", (string)time[0]);
Print("Updating ", symbol, " ", lastAvailable, " -> ", iBars(symbol, _Period), " / ",
(iBars(symbol, _Period) > 0 ?
(string)iTime(symbol, _Period, iBars(symbol, _Period) - 1) : "n/a"),
"... Please wait");
if(QuoteRefresh(symbol, _Period, time[0]))
{
Print("Done");
initialized = true;
}
else
{
// asynchronous request to update the chart
ChartSetSymbolPeriod(0, _Symbol, _Period);
return 0; // nothing to show yet
}
}
...
|
主要工作由特殊的QuoteRefresh函数完成。该函数接收三个参数:所需交易品种、时间范围,以及当前图表上最早(最旧)柱线的时间,我们对更早日期不感兴趣,但请求的交易品种可能没有这么深的历史数据。正因如此,通过一个单独的函数隐藏检查的所有复杂性会很便捷。
该函数会在数据完成下载且同步到可用范围时立即返回true。我们稍后将详细分析其内部结构。
同步完成后,我们使用 iBarShift函数查找同步柱线,并复制它们的 OHLC 值(iOpen、iHigh、iLow、iClose 函数)。
ArraySetAsSeries(time, true); // go from present to past
for(int i = 0; i < MathMax(rates_total - _prev_calculated, 1); ++i)
{
int x = iBarShift(symbol, _Period, time[i], true);
if(x != -1)
{
open[i] = iOpen(symbol, _Period, x);
high[i] = iHigh(symbol, _Period, x);
low[i] = iLow(symbol, _Period, x);
close[i] = iClose(symbol, _Period, x);
}
else
{
open[i] = high[i] = low[i] = close[i] = EMPTY_VALUE;
}
}
|
表面上看,使用 Copy 函数复制整个价格数组的替代方案似乎更加高效,但这种方法在此处并不适用,因为不同交易品种中相同索引的柱线可能对应完全不同的时间戳。因此,复制后,还需分析日期并在缓冲区内部移动元素,使其与当前图表上的时间对齐。
由于在 iBarShift 函数中,true作为最后一个参数传递,因此该函数将查找与柱线的时间精确匹配的值。如果另一个交易品种中没有柱线,我们将得到 -1,并在图表上显示空白值 (EMPTY_VALUE)。
成功完成完整计算后,新的柱线将以经济模式计算,即考虑 _prev_calculated和 rates_total。
现在让我们来看看QuoteRefresh函数。这是一个通用且实用的函数,因此它被放在头文件 QuoteRefresh.mqh中。
在函数的起始位置,我们将检查是否已从指标类型的 MQL 程序中请求当前交易品种和当前时间范围的时间序列。此类请求被禁止,因为指标运行所依赖的“原生”时间序列已由终端构建或已准备就绪:再次请求可能会导致循环或阻塞。因此,我们只需返回同步标志 (SERIES_SYNCHRONIZED),如果尚未就绪,指标应在后续(下一个分时报价、通过计时器等)检查数据。
bool QuoteRefresh(const string asset, const ENUM_TIMEFRAMES period,
const datetime start)
{
if(MQL5InfoInteger(MQL5_PROGRAM_TYPE) == PROGRAM_INDICATOR
&& _Symbol == asset && _Period == period)
{
return (bool)SeriesInfoInteger(asset, period, SERIES_SYNCHRONIZED);
}
...
|
第二项检查涉及柱线数量:如果它已经等于图表上允许的最大值,则继续下载任何内容没有意义。
if(Bars(asset, period) >= TerminalInfoInteger(TERMINAL_MAXBARS))
{
return (bool)SeriesInfoInteger(asset, period, SERIES_SYNCHRONIZED);
}
...
|
接下来的代码部分会按顺序从终端请求可用报价的开始日期:
- 通过指定的时间范围 (SERIES_FIRSTDATE)
- 不与终端本地数据库中的时间范围建立关联 (SERIES_TERMINAL_FIRSTDATE)
- 不与服务器上的时间范围建立关联 (SERIES_SERVER_FIRSTDATE)
如果在任何阶段,请求的日期已存在于可用数据区域中,我们将获得true作为就绪标志。否则,会从终端本地数据库或服务器请求数据,随后构建时间序列(所有这些操作都是异步自动完成的,以响应我们的 CopyTime调用;也可使用其他 Copy 函数)。
datetime times[1];
datetime first = 0, server = 0;
if(PRTF(SeriesInfoInteger(asset, period, SERIES_FIRSTDATE, first)))
{
if(first > 0 && first <= start)
{
// application data exists, it is already ready or is being prepared
return (bool)SeriesInfoInteger(asset, period, SERIES_SYNCHRONIZED);
}
else
if(PRTF(SeriesInfoInteger(asset, period, SERIES_TERMINAL_FIRSTDATE, first)))
{
if(first > 0 && first <= start)
{
// technical data exists in the terminal database,
// initiate the construction of a timeseries or immediately get the desired
return PRTF(CopyTime(asset, period, first, 1, times)) == 1;
}
else
{
if(PRTF(SeriesInfoInteger(asset, period, SERIES_SERVER_FIRSTDATE, server)))
{
// technical data exists on the server, let's request it
if(first > 0 && first < server)
PrintFormat(
"Warning: %s first date %s on server is less than on terminal ",
asset, TimeToString(server), TimeToString(first));
// you can't ask for more than the server has - so fmax
return PRTF(CopyTime(asset, period, fmax(start, server), 1, times)) == 1;
}
}
}
}
return false;
}
|
指标已就绪。我们可以对它进行编译和运行,例如在 EURUSD, H1 图表上,指定 USDRUB 为附加交易品种。日志将显示类似以下内容:
Host EURUSD 20001 bars up to 2018.08.09 13:00:00
Updating USDRUB 0 -> 14123 / 2014.12.22 11:00:00... Please wait
SeriesInfoInteger(symbol,period,SERIES_FIRSTDATE,first)=false / HISTORY_NOT_FOUND(4401)
Host EURUSD 20001 bars up to 2018.08.09 13:00:00
Updating USDRUB 0 -> 14123 / 2014.12.22 11:00:00... Please wait
SeriesInfoInteger(symbol,period,SERIES_FIRSTDATE,first)=true / ok
Done
|
流程完成后(显示“Done”消息),子窗口将显示另一个图表的蜡烛图。

IndSubChartSimple 指标 - DRAW_CANDLES,包含第三方交易品种报价
需要注意的是,由于交易时段缩短,USDRUB 的有效柱线仅占据每个日时间间隔的日间部分。
IndUnityPercent
我们在本节要创建的第二个指标是一个真正意义的多货币(多资产)指标,即IndUnityPercent.mq5。其设计用于显示给定金融工具中包含的所有独立货币(资产)的相对强弱。例如,当我们交易包含 EURUSD 和 XAUUSD 两个交易品种的投资组合时,则美元、欧元和黄金都会计入该组合的价值,这些资产中的每种资产都与其他资产存在比值关系。
在每个时间点上,都会有当前价格,这些价格可用以下公式表示:
EUR / USD = EURUSD
XAU / USD = XAUUSD
|
其中,变量 EUR、USD、XAU 分别代表各资产的独立“基准值”,而 EURUSD 和 XAUUSD 则是常量(已知报价)。
为求解这些变量,我们在方程组中再添加一个方程,将变量的平方和限制为 1(因此该指标名称的第一个词为“Unity”):
EUR * EUR + USD * USD + XAU * XAU = 1
|
变量数量可能更多,按逻辑可将其表示为 xi。请注意, x0 是所有金融工具共有的必需基础货币。
一般来说,变量的计算公式如下(我们将省略其推导过程):
x0 = sqrt(1 / (1 + sum(C(xi, x0)2))), i = 1..n
xi = C(xi, x0) * x0, i = 1..n
|
其中,n 为变量总数,C(xi,x0) 表示第 i 个交易对的报价。需注意,变量数量比金融工具数量多 1 个。
由于计算中涉及的报价通常存在显著差异(例如 EURUSD 和 XAUUSD 的情况),且仅通过彼此相互表示(即不参考任何稳定基准),因此将绝对值转换为百分比变化值是合理的。因此,当根据上述公式编写算法时,我们未采用 C(xi,x0),而是采用比值 C(xi,x0)[0] / C(xi,x0)[1],其中方括号中的索引表示当前柱线 [0] 和前一根柱线 [1]。此外,为加快计算速度,可省略平方与开方运算。
为了将线条可视化,我们会规定货币和指标缓冲区的具体最大允许数量。当然,如果用户输入的交易品种较少,计算中可以仅使用其中一部分。但无法动态增大该限制:需要修改指令并重新编译指标。
#define BUF_NUM 15
#property indicator_separate_window
#property indicator_buffers BUF_NUM
#property indicator_plots BUF_NUM
|
在实现该指标的过程中,我们将一并解决一个棘手的问题。由于将存在许多同类缓冲区,标准方法是通过“乘法”对其进行大量编码(即应当避免的“复制粘贴”式编程方法)。
double buffer1[];
...
double buffer15[];
void OnInit()
{
SetIndexBuffer(0, buffer1);
...
SetIndexBuffer(14, buffer15);
}
|
该方法操作繁琐、效率低下,且容易出错。相反,我们将应用 OOP。我们将创建一个类,用于存储指标缓冲区的数组,并负责对其进行统一设置,因为我们的缓冲区应保持一致(颜色除外,可能还需要为构成当前图表交易品种的货币进行加粗处理,但这将在用户输入参数后进行调整)。
采用该类后,我们只需分配其对象数组,即可自动连接和配置所需数量的指标缓冲区。如图所示,该方法可通过以下伪代码说明。
// "engine" code supporting an array of unified indicator buffers
class Buffer
{
static int count; // global buffer counter
double array[]; // array for this buffer
int cursor; // pointer of assigned element
public:
// constructor sets up and connects the array
Buffer()
{
SetIndexBuffer(count++, array);
ArraySetAsSeries(array, ...);
}
// overload to set the number of the element of interest
Buffer *operator[](int index)
{
cursor = index;
return &this;
}
// overload to write value to selected element
double operator=(double x)
{
buffer[cursor] = x;
return x;
}
...
};
static int Buffer::count;
|
通过运算符重载,我们仍可沿用为缓冲区对象元素赋值的熟悉语法:buffer[i] = value。
在指标代码中,无需编写许多行描述单独数组的代码,只需定义一个“多维数组”即可,
// indicator code
// construct 15 buffer objects with auto-registration and configuration
Buffer buffers[15];
...
|
实现该机制的类完整版可在文件IndBufArray.mqh中找到。请注意,它仅支持缓冲区,不支持图表。理想情况下,应通过新增类来扩展类集合,以便创建可直接使用的对象,这些对象可根据特定图表类型,在缓冲区数组中占用所需数量的缓冲区。我们建议你自行研究并补充该文件。特别说明,代码中包含一个管理指标缓冲区数组的类BufferArray,用于创建具有相同特性值(如 ENUM_INDEXBUFFER_TYPE 类型、索引方向、空值)的“多维数组”。我们在新指标中的使用方式如下:
BufferArray buffers(BUF_NUM, true);
|
此处,构造函数的第一个参数传入所需的缓冲区数量,第二个参数传入时间序列中的索引指示符(下文将详细说明)。
完成此定义后,我们可以在代码任意位置使用便捷表示法,设置第 i 个缓冲区中第 j 根柱线的值(它使用了缓冲区对象及缓冲区数组中运算符 [] 的双重重载):
在指标的输入变量中,我们将允许用户指定以逗号隔开的交易品种列表,并限制用于历史计算的柱线数量,以便控制潜在大量金融工具的加载和同步。如果你决定显示全部可用历史,则应识别并采用不同金融工具的最小可用柱线数量,并控制从服务器加载额外历史。
input string Instruments = "EURUSD,GBPUSD,USDCHF,USDJPY,AUDUSD,USDCAD,NZDUSD";
input int BarLimit = 500;
|
程序启动时,解析交易品种列表并形成一个大小为 SymbolCount的独立 Symbols 数组。
string Symbols[];
int direction[]; // direct(+1)/reverse(-1) rate to the common currency
int SymbolCount;
|
所有交易品种必须具有相同的基准货币(通常为 USD),以显示相互关联性。根据特定交易品种中的该基准货币是基础货币(即外汇货币对中的第一位货币)还是报价货币(即外汇货币对中的第二位货币),计算时将使用其直接报价或反向报价(1.0/汇率)。该方向将存储在 Direction数组中。
现在,我们来看看执行上述操作的InitSymbols函数。如果成功解析列表,该函数将返回基准货币的名称。内置 SymbolInfoString 可以获取任何金融工具的基础货币和报价货币,我们将在 金融工具章节详细学习该函数。
string InitSymbols()
{
SymbolCount = fmin(StringSplit(Instruments, ',', Symbols), BUF_NUM - 1);
ArrayResize(Symbols, SymbolCount);
ArrayResize(Direction, SymbolCount);
ArrayInitialize(Direction, 0);
string common = NULL; // common currency
for(int i = 0; i < SymbolCount; i++)
{
// guarantee the presence of the symbol in the Market Review
if(!SymbolSelect(Symbols[i], true))
{
Print("Can't select ", Symbols[i]);
return NULL;
}
// get the currencies that make up the symbol
string first, second;
first = SymbolInfoString(Symbols[i], SYMBOL_CURRENCY_BASE);
second = SymbolInfoString(Symbols[i], SYMBOL_CURRENCY_PROFIT);
// count the number of inclusions of each currency
if(first != second)
{
workCurrencies.inc(first);
workCurrencies.inc(second);
}
else
{
workCurrencies.inc(Symbols[i]);
}
}
...
|
循环使用辅助模板类MapArray跟踪每种货币在所有金融工具中出现的次数。该对象在指标的全局层面描述,需要连接头文件 MapArray.mqh。
#include <MQL5Book/MapArray.mqh>
...
// array of pairs [name; number]
// to calculate currency usage statistics
MapArray<string,int> workCurrencies;
...
string InitSymbols()
{
...
}
|
由于该类起到支持作用,这里不做详细描述。你可以查看源代码获取更多细节。关键在于,当你为一个新的货币名称调用其 inc方法时,该货币将加入内部数组,且计数器初始值为 1,如果该名称已存在,则计数器加 1。
随后,我们将计数器大于 1 的货币确定为基准货币。在设置正确的情况下,其余货币应正好出现一次。以下是 InitSymbols函数的后续部分。
...
// find the common currency based on currency usage statistics
for(int i = 0; i < workCurrencies.getSize(); i++)
{
if(workCurrencies[i] > 1) // counter greater than 1
{
if(common == NULL)
{
common = workCurrencies.getKey(i); // get the name of the i-th currency
}
else
{
Print("Collision: multiple common symbols");
return NULL;
}
}
}
if(common == NULL) common = workCurrencies.getKey(0);
// knowing the common currency, determine the "direction" of each symbol
for(int i = 0; i < SymbolCount; i++)
{
if(SymbolInfoString(Symbols[i], SYMBOL_CURRENCY_PROFIT) == common)
Direction[i] = +1;
else if(SymbolInfoString(Symbols[i], SYMBOL_CURRENCY_BASE) == common)
Direction[i] = -1;
else
{
Print("Ambiguous symbol direction ", Symbols[i], ", defaults used");
Direction[i] = +1;
}
}
return common;
}
|
准备好 InitSymbols函数后,我们可以编写 OnInit 函数(简化版)。
int OnInit()
{
const string common = InitSymbols();
if(common == NULL) return INIT_PARAMETERS_INCORRECT;
string base = SymbolInfoString(_Symbol, SYMBOL_CURRENCY_BASE);
string profit = SymbolInfoString(_Symbol, SYMBOL_CURRENCY_PROFIT);
// setting up lines by the number of currencies (number of symbols + 1)
for(int i = 0; i <= SymbolCount; i++)
{
string name = workCurrencies.getKey(i);
PlotIndexSetString(i, PLOT_LABEL, name);
PlotIndexSetInteger(i, PLOT_DRAW_TYPE, DRAW_LINE);
PlotIndexSetInteger(i, PLOT_SHOW_DATA, true);
PlotIndexSetInteger(i, PLOT_LINE_WIDTH, 1 + (name == base || name == profit));
}
// hide extra buffers in the Data Window
for(int i = SymbolCount + 1; i < BUF_NUM; i++)
{
PlotIndexSetInteger(i, PLOT_SHOW_DATA, false);
}
// single level at 1.0
IndicatorSetInteger(INDICATOR_LEVELS, 1);
IndicatorSetDouble(INDICATOR_LEVELVALUE, 0, 1.0);
// Name with parameters
IndicatorSetString(INDICATOR_SHORTNAME,
"Unity [" + (string)workCurrencies.getSize() + "]");
// accuracy
IndicatorSetInteger(INDICATOR_DIGITS, 5);
return INIT_SUCCEEDED;
}
|
现在我们来了解主事件处理程序 OnCalculate。
需特别注意,主循环中柱线的遍历顺序是逆向的,即按照时间序列从当前到过去的顺序处理。这种方式对于多货币指标尤为方便,因为不同交易品种的历史数据深度可能不同,并且从当前柱线开始回溯计算,直至首次出现任一交易品种缺失数据的位置,这样处理更为合理。在这种情况下,循环提前终止不应被视为错误,我们应返回 rates_total,以便在图表上显示已计算的最有效的柱线的值。
但在这个简化版 IndUnityPercent指标中,我们不采用上述方式,而是使用一种更简单、更严格的方式:用户必须使用BarLimit 参数定义无条件的历史查询深度。换言之,对于所有交易品种,必须存在直至图表交易品种编号为BarLimit的柱线时间戳的数据。否则,指标将尝试下载缺失数据。
int OnCalculate(const int rates_total,
const int prev_calculated,
const int begin,
const double& price[])
{
if(prev_calculated == 0)
{
buffers.empty(); // delegate total cleanup to the BufferArray class
}
// main loop in the direction "as in a timeseries" from the present to the past
const int limit = MathMin(rates_total - prev_calculated + 1, BarLimit);
for(int i = 0; i < limit; i++)
{
if(!calculate(i))
{
EventSetTimer(1); // give 1 more second to upload and prepare data
return 0; // let's try to recalculate on the next call
}
}
return rates_total;
}
|
Calculate函数(见下文)会计算第 i 根柱线上所有缓冲区的值。如果缺失数据,该函数将返回false,我们将启动一个计时器,为构建所有所需金融工具的时间序列留出时间。在计时器处理程序中,我们将以常规方式向终端发送更新图表的请求。
void OnTimer()
{
EventKillTimer();
ChartSetSymbolPeriod(0, _Symbol, _Period);
}
|
在 Calculate函数中,我们先确定当前柱线和前一根柱线的日期范围,基于此计算价格变化。
bool Calculate(const int bar)
{
const datetime time0 = iTime(_Symbol, _Period, bar);
const datetime time1 = iTime(_Symbol, _Period, bar + 1);
...
|
调用下一个CopyClose函数的特定版本时需要两个日期,用于指定日期区间。在该指标中,我们不能使用基于柱线数量的选项,因为任何交易品种都可能存在任意柱线缺口,与其他交易品种的缺口情况不同。例如,如果某个交易品种存在当前柱线 t和前一根柱线 t-1,则可以正确计算价格变化Close[t]/Close[t-1]。然而,在另一个交易品种中,第 t根柱线可能不存在,此时请求两根柱线会返回左侧“最近的”柱线(历史方向),而该历史时间点可能与“当前”时间点相距较远(例如,如果该交易品种不是 24 小时交易,可能对应前一交易日的交易时段)。
为避免这种情况,指标会严格按时间区间请求报价,如果某个交易品种在该区间内无数据,则意味着无价格变动。
同时,可能出现查询返回超过两根柱线的情况,此时始终取最后两根(右侧)柱线作为最有效的柱线。例如,当该指标加载到 USDRUB,H1 图表时,会“识别”到在每个工作日 17:00 的柱线之后,是下一个工作日 10:00 的柱线。但对于 EURUSD 等主要外汇货币对,这两个时间点之间会存在 16 根晚间、夜间和早间的 H1 柱线。
bool Calculate(const int bar)
{
...
double w[]; // receiving array of quotes (by bar)
double v[]; // character changes
ArrayResize(v, SymbolCount);
// find quote changes for each symbol
for(int j = 0; j < SymbolCount; j++)
{
// try to get at least 2 bars for the j-th symbol,
// corresponding to two bars of the symbol of the current chart
int x = CopyClose(Symbols[j], _Period, time0, time1, w);
if(x < 2)
{
// if there are no bars, try to get the previous bar from the past
if(CopyClose(Symbols[j], _Period, time0, 1, w) != 1)
{
return false;
}
// then duplicate it as no change indication
// (in principle, it was possible to write any constant 2 times)
x = 2;
ArrayResize(w, 2);
w[1] = w[0];
}
// find the reverse course when needed
if(Direction[j] == -1)
{
w[x - 1] = 1.0 / w[x - 1];
w[x - 2] = 1.0 / w[x - 2];
}
// calculating changes as a ratio of two values
v[j] = w[x - 1] / w[x - 2]; // last / previous
}
...
|
收到变动后,算法将根据前文所述公式进行计算,并将值写入指标缓冲区。
double sum = 1.0;
for(int j = 0; j < SymbolCount; j++)
{
sum += v[j];
}
const double base_0 = (1.0 / sum);
buffers[0][bar] = base_0 * (SymbolCount + 1);
for(int j = 1; j <= SymbolCount; j++)
{
buffers[j][bar] = base_0 * v[j - 1] * (SymbolCount + 1);
}
return true;
}
|
让我们看看该指标在默认设置下对一组基础外汇金融工具的表现(第一次加载时,如果尚未打开这些金融工具的图表,获取时间序列数据可能需要较长时间)。

多货币指标 IndUnityPercent(主要外汇币种)
在指标窗口中,两条货币线之间的距离等于对应报价的百分比变动率(两个相邻Close之间)。这正是指标名称中第二个词“Percent”的由来。
在下一章有关指标编程应用的内容中,我们将介绍进阶版的 IndUnityPercentPro.mq5,在该版本中,Copy函数将被内置指标 iMA调用替代,从而无需额外操作即可实现任意价格类型的平滑计算。