手撕多品种ATR指标开发实战
从同步难题到真实柱判定的MQL5完整方案
为什么多交易品种指标开发更难
对于刚接触MQL5的新手来说,写单一品种指标只需关心当前图表的时间序列;但多交易品种指标要同时拉取多个符号在不同时间框架下的数据,并绘制在同一子窗口中。核心难点有三:第一,其他交易品种的数据与当前图表品种之间存在时间错位,必须做同步;第二,某些品种可能由于服务器限制或本地缺失而没有足够历史,导致指标句柄无效或数据空洞;第三,MT5的所有时间表均由分钟数据构建,若服务器只提供1993年日线却从2000年才有分钟数据,那么小时图上的2000年之前柱其实是日线压缩的伪柱,直接画指标会误导分析。本文以六个品种的ATR为例,逐一解决这些问题。
指标总体设计与外部参数
我们使用MQL5向导生成自定义指标模板,将其改为独立子窗口指标,并预留6个缓冲区对应6条ATR线(当前图表品种+5个外部指定品种)。外部参数中,IndicatorPeriod控制ATR均值周期;Symbol02至Symbol06以字符串输入品种名;Level01至Level06设置水平参考线。系统在初始化时会检查名称是否在总表内,不在则标记为EMPTY并不计算。所有可用品种会被自动加入市场报价窗口。
- #property indicator_separate_window 确保指标在独立子窗口
- #property indicator_buffers 6 与 indicator_plots 6 对应六品种
- sinput string dlm 仅作界面分隔,不参与逻辑
- 外部品种从2编号,因图表自身品种视为第一个
input int IndicatorPeriod=14; input string Symbol02 ="GBPUSD"; input string Symbol03 ="AUDUSD"; input string Symbol04 ="NZDUSD"; input string Symbol05 ="USDCAD"; input string Symbol06 ="USDCHF"; input int Level01 =10; input int Level02 =50; input int Level03 =100; input int Level04 =200; input int Level05 =400; input int Level06 =600;
初始化流程与全局结构
OnInit()中依次执行:检查输入参数、设置1秒计时器、初始化数组、填充品种名、获取ATR句柄、设置指标属性。我们将数组与全局变量集中声明,用结构体封装每个品种的临时时间与ATR值,便于在主循环同步。其中CCanvas类用于绘制进度提示,避免用画布绘图而是显示文本,让用户感知加载状态。
int OnInit()
{
if(!CheckInputParameters())
return(INIT_PARAMETERS_INCORRECT);
EventSetTimer(1);
canvas.FontSet(font_name,font_size,FW_NORMAL);
InitArrays();
InitSymbolNames();
InitLevels();
GetIndicatorHandles();
SetIndicatorProperties();
Comment("");
ChartRedraw();
return(INIT_SUCCEEDED);
}
品种校验与句柄获取机制
AddSymbolToMarketWatch()遍历SymbolsTotal(false)确认名称合法,合法则SymbolSelect加入报价窗,否则返回EMPTY。GetIndicatorHandles()对每个合法品种调用iATR(symbol,Period(),IndicatorPeriod),若失败记invalid_handle并靠计时器重试。这样设计避免了初始化时因网络或历史未就绪直接崩溃。
string AddSymbolToMarketWatch(string symbol)
{
if(symbol=="") return(empty_symbol);
int total=SymbolsTotal(false);
for(int i=0;i<total;i++)
{
string name=SymbolName(i,false);
if(name==symbol)
{
SymbolSelect(name,true);
return(name);
}
}
return(empty_symbol);
}
OnCalculate与计时器重试逻辑
首次调用prev_calculated=0,需完整计算;若历史缺口填补终端也会归零。我们复制OC_前缀全局变量保存参数,ResizeCalculatedArrays匹配大小。若市场关闭无新tick导致计算卡住,OnTimer每秒触发OnCalculate重试。这是多品种指标稳健性的关键:不依赖单一tick事件。
void OnTimer()
{
if(OC_prev_calculated==0)
{
OnCalculate(OC_rates_total,OC_prev_calculated,
OC_time,OC_open,OC_high,OC_low,OC_close,
OC_tick_volume,OC_volume,OC_spread);
}
}
数据完备性检查链
prev_calculated==0分支中,依次执行:LoadAndFormData加载生成必要柱、GetIndicatorHandles补句柄、CheckAvailableData确认每个品种Bars与BarsCalculated足够、CheckLoadedHistory探测更深历史、CheckSymbolIsSynchronized确认与服务器同步、DetermineFirstTrueBar找真实柱。任何一步失败return(RESET)交给定时器重试。
if(prev_calculated==0)
{
ZeroCalculatedArrays();
ZeroIndicatorBuffers();
GetSubwindowGeometry();
SetCanvas();
LoadAndFormData();
if(!GetIndicatorHandles()) return(RESET);
if(!CheckAvailableData()) return(RESET);
if(!CheckLoadedHistory()) return(RESET);
if(!CheckSymbolIsSynchronized()) return(RESET);
if(!DetermineFirstTrueBar()) return(RESET);
OC_prev_calculated=rates_total;
}
真实柱判定与垂直标记
真实柱指当前时间框架由分钟数据真实构建的起始柱。GetFirstTrueBarTime遍历时间数组,找到相邻差等于PeriodSeconds()的首个索引,此前皆为压缩伪柱。DetermineFirstTrueBar用CreateVerticalLine在子窗口画同色竖线标记,既美观又防误导。该概念在EA多周期优化时同样重要,跨周期数据不可混用。
datetime GetFirstTrueBarTime(datetime &time[])
{
int array_size=ArraySize(time);
ArraySetAsSeries(time,false);
for(int i=1;i<array_size;i++)
{
if(time[i]-time[i-1]==PeriodSeconds())
{ return(time[i]); }
}
return(NULL);
}
画布进度提示与子窗口自适应
CCanvas创建位图标签,背景透明度0隐藏,加载时设190显消息。OnSubwindowChange检测宽高变化调用ResizeCanvas,ShowCanvasMessage居中打印如“Preparing data 5000 bars: EURUSD(2/6) - 42%”。DeleteCanvas用渐隐循环消除。这让用户在等待多品种历史时明确进度,而非以为卡死。
void ShowCanvasMessage(string message_text)
{
GetSubwindowGeometry();
if(ObjectFind(0,canvas_name)==subwindow_number)
{
if(message_text!="" && subwindow_center_x>0 && subwindow_center_y>0)
{
canvas.Erase(ColorToARGB(canvas_background,canvas_opacity));
canvas.TextOut(subwindow_center_x,subwindow_center_y,message_text,ColorToARGB(clrRed),TA_CENTER|TA_VCENTER);
canvas.Update();
}
}
}
双循环同步与缓冲区填充
第一主循环PrepareData按“倾尽所能获取数值”原则,对每个bar尝试CopyTime与CopyBuffer最多100次,存入临时数组;第二主循环FillIndicatorBuffers比对当前品种时间与各品种时间,一致且过IndicatorPeriod才写atr_buffers。这样消除序列间间隙,保证线连续。进度百分比由ProgressPercentage计算并刷新画布。
void FillIndicatorBuffers(int bar_index,int symbol_number,datetime const &time[])
{
static int bars_count=0;
if(bar_index==0) bars_count=0;
if(bars_count<IndicatorPeriod && time[bar_index]>=limit_time[symbol_number]) bars_count++;
if(bars_count>=IndicatorPeriod && time[bar_index]==tmp_symbol_time[symbol_number].time[bar_index])
{
if(tmp_atr_values[symbol_number].value[bar_index]!=EMPTY_VALUE)
atr_buffers[symbol_number].data[bar_index]=tmp_atr_values[symbol_number].value[bar_index];
}
}
水平线与资源清理
收尾时SetIndicatorLevels用OBJ_HLINE画水平参考,CorrectValueBySymbolDigits处理3/5位小数点。OnDeinit删除画布、水平线、竖线。完整测试表明,在20分钟图上NZDUSD真实柱始于2000年而其它货币对始于1999年,竖线清晰分隔,避免用日线伪柱计算ATR的谬误。