小记 在 MQL 中 编写自定义指标时的一些需要注意的地方。
环境: Meta Trader 5
指标(Indicator)有一个生命周期函数OnCalculate
,
其函数原型为:
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[]);
其返回值意义为“完成计算的条(Bar)数”。
其真实计算产生的数据并不通过此函数的返回值返回,而是通过全局变量 Buffer
来返回。
这里强调 invoke 的原因是为了与主动调用 call 区分开来,生命周期函数通常意味着一个事件。你不应该主动去调用 OnCalculate 函数,而应该等待系统接受某种信号后自己去调用这个函数。
那么所谓信号是什么?
当交易系统接受到新的数据时,在将数据推入 time, open, high, low, close, … 数组后立即调用 OnCalculate 函数。
在 OnCalculate 函数 开始执行时,系统保证:
而编程者应该保证:
基于以上保证,prev_calculate即你之前处理完毕的数据数,默认为0(什么都没有处理)。
也就是说, 上一次调用 OnCalculate 所返回的值会被缓存成prev_calculated 在这次调用 OnCalculate的时候作为参数传入。
首先考虑一下 OnCalculate 函数可能处理的数据量。
市场价格更新频率: 分笔(全世界只要发生可观测交易就会变化,可能每秒都有若干笔数据)
数据量:按照周期缓存(简单估算一下如果1小时缓存一个数据,每1年有5000左右个数据)
几年的数据积累下来会有10000 ~ 30000 bars 的数据,这在回测中是比较正常的。
这通常意味着在通常情况下,OnCalculate 需要完整更新整个指标序列,在1秒内计算若干次 10000~ 30000 的数据量,有时候,完成这些计算还不是常数时间代价的……更致命的是,一旦你在计算上慢了一步,商机有可能就会被错过。
很容易发现,通常情况下,过去的指标是不需要发生变化的。比如“2016年3月10日 19:00处的3小时收盘均值”是不会随着时间改变而改变的。因此我们其实并不需要重复计算这些部分。
那么问题简单了,我们需要多记录一个,哪个位置之前的数据是不需要计算的,那就是 prev_calculated。因此,在编程时,编程人员应该时刻遵守这个约定来给出返回值。
通常情况下,在函数结尾 return rates_total;
是最好的方法。
既然它已经把数据给OnCalculate了,OnCalculate就应该处理完它。
接下来给出一个基本的范式。
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[])
{
//--- selectively update
for(int i = prev_calculated; i < rates_total; i++){
//--- TODO: deal with new bars
}
//--- return value of prev_calculated for next call
return rates_total;
}
另外,传入参数使用了引用(&)可以避免拷贝构造,优化参数传入的效率。
const 修饰符只是纯粹起保护数据的作用。
通常情况下,指标并不是基于一个数据2的,而是需要回溯之前的若干数据的。
如移动平均线(Moving Average),波动率(Volatility)通常基于多个时间点的数据的计算。
因此不可避免地会遇到下标越界的问题:
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[])
{
//--- selectively update
for(int i = prev_calculated; i < rates_total; i++){
Buffer[i] = (close[i] - close[i - 1]) / close[i - 1]; // => over range!
}
//--- return value of prev_calculated for next call
return rates_total;
}
上面的代码会遭遇越界。因为在第一次调用 OnCalculate
时 prev_calculated
的值是 0
,而我们在这里访问了 close[-1]
,这将抛出越界异常。
在MQL5中,具有严格如Java一般的运行时越界检查来保证不会因为访问非法的数据导致结果的不正确并产生财产损失。
所以我们需要一个特殊的逻辑来处理这个初始状态下的数据。
{
int from = prev_calculated, to = rates_total;
//--- first state
if(from <= 0){
Buffer[0] = 0; // or any value you like
from = 1; // change the value of from
}
// range update
for(int i = from; i < to; i++){
Buffer[i] = (close[i] - close[i - 1]) / close[i - 1]; // => okay
}
//--- return value of prev_calculated for next call
return rates_total;
}
由于 prev_calculated
与 rates_total
是不可变量,因此复制它们的值是一个比较可行的做法。
上述代码在实际运行时仍然会发生错误,具体请看接下来的尾部动态更新。
在MT5中,接收周期内数据是不增加实际的bars数量的。
比如,在1小时周期内,最后1小时的所有数据都会被整合到bar[rates_total - 1]
上,显而易见地,由于之前的范围优化,OnCalculate会选择不更新最后一个 bar,因此Buffer的尾部会体现出以下两个特征:
这是因为实际上系统可能是这样调用OnCalculate的:
OnCalculate(3, 0, ...) // => return 3 (first state)
OnCalculate(4, 3, ...) // => return 4 (selectively update [3,4) )
OnCalculate(4, 4, ...) // => return 4 (receive data in period, but do nothing)
OnCalculate(4, 4, ...) // => return 4 (receive data in period, but do nothing)
// ...
OnCalculate(5, 4, ...) // => return 5 (selectively update [4,5) )
真的什么都不要做吗?
你会发现系统多次调用 OnCalculate 根本没有卵用,事实上,在重复调用的时候,数据尾部的close,high, low, 等的数据都可能已经发生改变了(由于接收到了新的周期内数据)。
显然,这个时候不能忽略这些数据,应当进行尾部更新
{
int from = prev_calculated, to = rates_total;
//--- first state
if(from <= 0){
Buffer[0] = 0; // or any value you like
from = 1; // change the value of from
}
// range update
for(int i = from; i < to; i++){
Buffer[i] = (close[i] - close[i - 1]) / close[i - 1]; // => okay
}
// dynamic update in tail
if(from == to){
Buffer[from - 1] = (close[from - 1] - close[from - 2]) / close[from - 2];
}
//--- return value of prev_calculated for next call
return rates_total;
}
如此,尾部的动态更新就完成了。
在本文中总结了编写指标的几个坑点。
希望别人(或者是几个月以后的我自己)能够不再入坑而浪费时间。
rates_total -1
,忽略最后一柱的更新,来保证指标值的正确性,但牺牲了更好的实时性,这个既不值得,也不优雅。 ↩