MQL5 细则手册:在 MQL5 中开发多交易品种波动指标·综合运用
(3/3)· 六个品种 ATR 同屏、句柄同步与进度画布,一篇把多品种指标的最后坑填平
「指标子窗口的画布与尺寸自适配」
在 MT5 自定义指标里,想把图形直接画到独立子窗口而不是主图,第一步是拿到子窗口的几何参数。GetSubwindowGeometry() 用 ChartWindowFind(0, subwindow_shortname) 定位窗口序号,再用 CHART_WIDTH_IN_PIXELS 和 CHART_HEIGHT_IN_PIXELS 取宽高,最后算出中心坐标 subwindow_center_x = chart_width/2、subwindow_center_y = subwindow_height/2,后续所有位图标签都按这个中心铺开。 画布本身用 Canvas 类的 CreateBitmapLabel 建:若 ObjectFind 找不到 canvas_name 就新建一个覆盖整个子窗口的位图标签,Erase(ColorToARGB(...,0)) 把背景擦成全透明,Update() 提交。这样你的绘制层不会挡住 K 线,也不会留下默认灰底。 窗口被用户拖拽缩放时,OnSubwindowChange() 会先调 GetSubwindowGeometry 重新取尺寸,再用 SubwindowSizeChanged() 判断是否有变化;若 subwindow_height<1 或中心算错就直接 return,避免崩在负数像素上。确认变化后才 ResizeCanvas() 并重绘最后一条 ShowCanvasMessage(msg_last)。外汇与贵金属行情跳空频繁,子窗口历史补全后可能触发 OC_prev_calculated==0 的重算分支,这时主动再调一次 OnCalculate 比等新 tick 更稳。
if(OC_prev_calculated==class="num">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); } } class="type">void GetSubwindowGeometry() { subwindow_number=ChartWindowFind(class="num">0,subwindow_shortname); chart_width=(class="type">int)ChartGetInteger(class="num">0,CHART_WIDTH_IN_PIXELS); subwindow_height=(class="type">int)ChartGetInteger(class="num">0,CHART_HEIGHT_IN_PIXELS,subwindow_number); subwindow_center_x=chart_width/class="num">2; subwindow_center_y=subwindow_height/class="num">2; } class="type">void SetCanvas() { if(ObjectFind(class="num">0,canvas_name)<class="num">0) { canvas.CreateBitmapLabel(class="num">0,subwindow_number,canvas_name,class="num">0,class="num">0,chart_width,subwindow_height,clr_format); canvas.Erase(ColorToARGB(canvas_background,class="num">0)); canvas.Update(); } } class="type">void OnSubwindowChange() { GetSubwindowGeometry(); if(!SubwindowSizeChanged()) class="kw">return; if(subwindow_height<class="num">1 || subwindow_center_y<class="num">1) class="kw">return; ResizeCanvas(); ShowCanvasMessage(msg_last); } class="type">bool SubwindowSizeChanged() {
◍ 画布生命周期与数据加载的底层控制
在 MT5 指标里用 CCanvas 类做自定义绘制,最容易被忽略的是窗口尺寸变化检测。上面这段逻辑先用 last_chart_width / last_subwindow_height 两个静态变量记录上一次的尺寸,若本次 OnCalculate 拿到的 chart_width 和 subwindow_height 都没变,直接 return(false) 跳过重绘,能省掉大量无谓的 GDI 资源消耗。 ResizeCanvas() 只做一件事:当 ObjectFind(0,canvas_name) 返回的窗口序号等于 subwindow_number,就调 canvas.Resize(chart_width,subwindow_height) 把画布拉伸到新尺寸。注意这里依赖 GetSubwindowGeometry() 提前算好坐标,否则 resize 会画到错误区域。 ShowCanvasMessage() 的消失动画思路值得抄:DeleteCanvas() 里用 for(int i=canvas_opacity; i>0; i-=5) 每次把背景透明度降 5 再 Erase+Update,做出淡出效果,最后 canvas.Destroy() 释放资源。步长 5 是硬编码,若你的 canvas_opacity 是 255,完整淡出约 51 帧,在 30fps 图表上接近 1.7 秒。 LoadAndFormData() 开头把 bars_count 写死成 100,意味着每次只重算最近 100 根 K 线。做外汇或贵金属指标时这属于高风险简化——小周期跳空或非农行情里 100 根可能覆盖不到关键波段,建议改成按图表可见柱数动态取。
class=class="str">"cmt">//--- If the subwindow size has not changed, exit if(last_chart_width==chart_width && last_subwindow_height==subwindow_height) class="kw">return(class="kw">false); class=class="str">"cmt">//--- If the size has changed, save it else { last_chart_width=chart_width; last_subwindow_height=subwindow_height; } class=class="str">"cmt">//--- class="kw">return(true); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Resizing canvas | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void ResizeCanvas() { class=class="str">"cmt">//--- If the canvas has already been added to the indicator subwindow, set the new size if(ObjectFind(class="num">0,canvas_name)==subwindow_number) canvas.Resize(chart_width,subwindow_height); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Displaying message on the canvas | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void ShowCanvasMessage(class="type">class="kw">string message_text) { GetSubwindowGeometry(); class=class="str">"cmt">//--- If the canvas has already been added to the indicator subwindow if(ObjectFind(class="num">0,canvas_name)==subwindow_number) { class=class="str">"cmt">//--- If the class="type">class="kw">string passed is not empty and correct coordinates have been obtained, display the message if(message_text!="" && subwindow_center_x>class="num">0 && subwindow_center_y>class="num">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(); } } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Deleting canvas | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void DeleteCanvas() { class=class="str">"cmt">//--- Delete the canvas if it exists if(ObjectFind(class="num">0,canvas_name)>class="num">0) { class=class="str">"cmt">//--- Before deleting, implement the disappearing effect for(class="type">int i=canvas_opacity; i>class="num">0; i-=class="num">5) { canvas.Erase(ColorToARGB(canvas_background,(class="type">uchar)i)); canvas.Update(); } class=class="str">"cmt">//--- Delete the graphical resource canvas.Destroy(); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Loading and generating the necessary/available amount of data | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void LoadAndFormData() { class="type">int bars_count=class="num">100; class=class="str">"cmt">// Number of loaded bars class=class="str">"cmt">//--- for(class="type">int s=class="num">0; s<SYMBOLS_COUNT; s++)
分批拉历史数据的退出条件
多品种回测前要把终端本地缺的历史 bar 补齐,但不能无脑一直向服务器要数据。这段代码用 while 循环控制:只要已拷数组长度小于目标总量 OC_rates_total,且终端首 bar 与服务器首 bar 的时间差仍大于 PeriodSeconds()*bars_count,就继续 CopyTime 往回搬。 循环里有个防卡死逻辑值得注意:如果连续 100 次拷贝后数组大小都没变大(attempts 计数到 100),就强制 break。外汇与贵金属行情在休市时段或桥接延迟下可能拷不回更早数据,这种退出机制能避免 EA 卡在死循环里空耗。 另外当拷回的首根时间减去一个 bars_count 周期已早于图表首根 OC_time[0],也会直接退出——说明本地已有足够覆盖图表范围的样本,不必再向服务器深挖。 逐行看变量定义与判断:attempts 记无效拷贝次数,array_size 记当前数组长,firstdate_server / firstdate_terminal 分别拿服务器与终端首 bar 时间;SeriesInfoInteger 配合 SERIES_FIRSTDATE 与 SERIES_SERVER_FIRSTDATE 取这两个时间点。while 条件把两者时间差和 PeriodSeconds()*bars_count 比,决定要不要继续搬。
{
class="type">int attempts =class="num">0; class=class="str">"cmt">// Counter of data copying attempts
class="type">int array_size =class="num">0; class=class="str">"cmt">// Array size
class="type">class="kw">datetime firstdate_server =NULL; class=class="str">"cmt">// Time of the first bar on the server
class="type">class="kw">datetime firstdate_terminal=NULL; class=class="str">"cmt">// Time of the first bar in the terminal base
class=class="str">"cmt">//--- Get the first date by the symbol/time frame in the terminal base
SeriesInfoInteger(symbol_names[s],Period(),SERIES_FIRSTDATE,firstdate_terminal);
class=class="str">"cmt">//--- Get the first date of the symbol/time frame on the server
SeriesInfoInteger(symbol_names[s],Period(),SERIES_SERVER_FIRSTDATE,firstdate_server);
class=class="str">"cmt">//--- Print the message
msg_last=msg_load_data="Loading and generating data: "+
symbol_names[s]+"("+(class="type">class="kw">string)(s+class="num">1)+"/"+(class="type">class="kw">string)SYMBOLS_COUNT+") ... ";
ShowCanvasMessage(msg_load_data);
class=class="str">"cmt">//--- Load/generate data.
class=class="str">"cmt">// If the array size is smaller than the maximum number of bars in the terminal, and if
class=class="str">"cmt">// the number of bars between the first date of the series in the terminal and the first date of the series on the server is more than specified
class="kw">while(array_size<OC_rates_total &&
firstdate_terminal-firstdate_server>PeriodSeconds()*bars_count)
{
class="type">class="kw">datetime copied_time[];
class=class="str">"cmt">//--- Get the first date by the symbol/time frame in the terminal base
SeriesInfoInteger(symbol_names[s],Period(),SERIES_FIRSTDATE,firstdate_terminal);
class=class="str">"cmt">//--- Load/copy the specified number of bars
if(CopyTime(symbol_names[s],Period(),class="num">0,array_size+bars_count,copied_time)!=-class="num">1)
{
class=class="str">"cmt">//--- If the time of the first bar in the array, excluding the number of the bars being loaded, is earlier
class=class="str">"cmt">// than the time of the first bar in the chart, terminate the loop
if(copied_time[class="num">0]-PeriodSeconds()*bars_count<OC_time[class="num">0])
class="kw">break;
class=class="str">"cmt">//--- If the array size hasn&class="macro">#x27;t increased, increase the counter
if(ArraySize(copied_time)==array_size)
attempts++;
class=class="str">"cmt">//--- Otherwise get the current size of the array
else
array_size=ArraySize(copied_time);
class=class="str">"cmt">//--- If the array size hasn&class="macro">#x27;t increased over class="num">100 attempts, terminate the loop
if(attempts==class="num">100)「多品种数据就绪前的重试逻辑」
在 MT5 里跑跨品种指标时,最容易被忽略的是历史数据不一定即时到位。上面这段逻辑在遍历 SYMBOLS_COUNT 个品种时,先通过 BarsCalculated 拿指标已计算值,再用 SeriesInfoInteger 取终端当前周期首根数据日期,最后用 Bars 算出从首日期到当下的可用 bar 数。 针对 bar 时间和指标数据,代码各给了 5 次拉取重试:CopyTime 拷贝时间数组,若 ArraySize(time) 大于等于 available_bars 就 break 退出;指标数据同理循环 5 次尝试。这个 5 次上限是硬约束,意味着若服务端历史缺失严重,超过 5 次仍拿不全就会带着残缺数据继续跑。 子窗口尺寸检查放在每 2000 根 bar 触发一次(array_size%2000 为 0 时调 OnSubwindowChange),避免逐 bar 重算画布拖慢回测。外汇与贵金属多品种叠加本就高波动高杠杆,数据缺口会让信号偏移,实盘前应在 MT5 用「打开数据窗口」核对各品种首日期是否一致。
{
attempts=class="num">0;
class="kw">break;
}
}
class=class="str">"cmt">//--- Check the subwindow size once every class="num">2000 bars
class=class="str">"cmt">// and if the size has changed, adjust the canvas size to it
if(!(array_size%class="num">2000))
OnSubwindowChange();
}
}
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Checking the amount of available data for all symbols |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CheckAvailableData()
{
for(class="type">int s=class="num">0; s<SYMBOLS_COUNT; s++)
{
class=class="str">"cmt">//--- If this symbol is available
if(symbol_names[s]!=empty_symbol)
{
class="type">class="kw">double data[]; class=class="str">"cmt">// Array for checking the amount of indicator data
class="type">class="kw">datetime time[]; class=class="str">"cmt">// Array for checking the number of bars
class="type">int calculated_values =class="num">0; class=class="str">"cmt">// Amount of indicator data
class="type">int available_bars =class="num">0; class=class="str">"cmt">// Number of bars of the current period
class="type">class="kw">datetime firstdate_terminal=NULL; class=class="str">"cmt">// First date of the current time frame data available in the terminal
class=class="str">"cmt">//--- Get the number of calculated values of the indicator
calculated_values=BarsCalculated(symbol_handles[s]);
class=class="str">"cmt">//--- Get the first date of the current time frame data in the terminal
firstdate_terminal=(class="type">class="kw">datetime)SeriesInfoInteger(symbol_names[s],Period(),SERIES_TERMINAL_FIRSTDATE);
class=class="str">"cmt">//--- Get the number of available bars from the date specified
available_bars=Bars(symbol_names[s],Period(),firstdate_terminal,TimeCurrent());
class=class="str">"cmt">//--- Check the readiness of bar data: class="num">5 attempts to get values
for(class="type">int i=class="num">0; i<class="num">5; i++)
{
class=class="str">"cmt">//--- Copy the specified amount of data
if(CopyTime(symbol_names[s],Period(),class="num">0,available_bars,time)!=-class="num">1)
{
class=class="str">"cmt">//--- If the required amount has been copied, terminate the loop
if(ArraySize(time)>=available_bars)
class="kw">break;
}
}
class=class="str">"cmt">//--- Check the readiness of indicator data: class="num">5 attempts to get values
for(class="type">int i=class="num">0; i<class="num">5; i++)◍ 历史深度不够时如何优雅退出重采
多品种指标在 OnInit 或 OnCalculate 里最怕一件事:终端还没把足够 K 线塞进内存,你就去算跨品种相关性,结果数组长度对不上,画出来的线直接断头。上面这段逻辑就是专门兜底——先 CopyBuffer 拉数据,拉到了比对 ArraySize 是否达到 calculated_values,够数才 break 跳出循环。 如果 time 或 data 数组尺寸仍小于 available_bars / calculated_values,说明这一轮历史没备齐。代码不硬算,而是把 OC_prev_calculated 置 0、弹一条画布提示并返回 false,等下一 tick 再试。外汇与贵金属行情在跳空或换周时可能临时缺柱,这种重试机制能降低误报概率。 CheckLoadedHistory 则盯着 SERIES_FIRSTDATE:首次跑把首根日期存进 series_first_date_last,之后每次重新取,若新日期比记忆里的更早,就判定「 deeper history 已加载」。实战里你改 SYMBOLS_COUNT 或多开小周期,首次加载常常要等几秒,盯这个函数返回值比盲等更靠谱。
{
class=class="str">"cmt">//--- Copy the specified amount of data
if(CopyBuffer(symbol_handles[s],class="num">0,class="num">0,calculated_values,data)!=-class="num">1)
{
class=class="str">"cmt">//--- If the required amount has been copied, terminate the loop
if(ArraySize(data)>=calculated_values)
class="kw">break;
}
}
class=class="str">"cmt">//--- If the amount of data copied is not sufficient, one more attempt is required
if(ArraySize(time)<available_bars || ArraySize(data)<calculated_values)
{
msg_last=msg_prepare_data;
ShowCanvasMessage(msg_prepare_data);
OC_prev_calculated=class="num">0;
class="kw">return(class="kw">false);
}
}
}
class=class="str">"cmt">//---
class="kw">return(true);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Checking the event of loading a deeper history |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CheckLoadedHistory()
{
class="type">bool loaded=class="kw">false;
class=class="str">"cmt">//---
for(class="type">int s=class="num">0; s<SYMBOLS_COUNT; s++)
{
class=class="str">"cmt">//--- If this symbol is available
if(symbol_names[s]!=empty_symbol)
{
class=class="str">"cmt">//--- If the series need to be updated
if(OC_prev_calculated==class="num">0)
{
class=class="str">"cmt">//--- Get the first date by the symbol/time frame
series_first_date[s]=(class="type">class="kw">datetime)SeriesInfoInteger(symbol_names[s],Period(),SERIES_FIRSTDATE);
class=class="str">"cmt">//--- If this is the first time(no value is available), then
if(series_first_date_last[s]==NULL)
class=class="str">"cmt">//--- Store the first date by the symbol/time frame for further comparison
class=class="str">"cmt">// in order to determine if a deeper history has been loaded
series_first_date_last[s]=series_first_date[s];
}
else
{
class=class="str">"cmt">//--- Get the first date by the symbol/time frame
series_first_date[s]=(class="type">class="kw">datetime)SeriesInfoInteger(symbol_names[s],Period(),SERIES_FIRSTDATE);
class=class="str">"cmt">//--- If the dates are different, i.e. the date in the memory is later than the one we have just obtained,
class=class="str">"cmt">// this means that a deeper history has been loaded深历史加载与同步校验的返回逻辑
指标在刷新前需要确认各交易品种的历史数据是否比上一次更深。代码里用 series_first_date_last[s] 与 series_first_date[s] 做比较,若前者大于后者,说明服务端又补了更早的棒线或本地生成了更深的缓存,此时向日志打印具体品种、周期和日期,并把新日期存回数组,同时将 loaded 置为 true。 一旦发现任意品种加载了更深历史,函数直接 return(false),告诉调用层先别画图,等下一轮再刷新;否则 return(true) 继续后续绘制。这一机制能避免用残缺历史算出偏移的缓冲曲线。 紧接着的 CheckSymbolIsSynchronized() 只做一件事:在 TERMINAL_CONNECTED 为真时,逐个扫 symbol_names 里非空的品种,用 SeriesInfoInteger 查 SERIES_SYNCHRONIZED。只要有一个不同步,就通过画布提示并 return(false),全部通过才 return(true)。外汇与贵金属行情在跳空或重连后常出现不同步,这套检查能降低误绘概率。 TimeframeToString() 则是把 ENUM_TIMEFRAMES 转成 "M1" 这类短串,遇到 WRONG_VALUE 或 NULL 会回退到当前图表 Period(),方便上面日志输出时一眼看清周期。
if(series_first_date_last[s]>series_first_date[s]) { class=class="str">"cmt">//--- Print the relevant message to the log Print("(",symbol_names[s],",",TimeframeToString(Period()), ") > A deeper history has been loaded/generated: ", series_first_date_last[s],"> ",series_first_date[s]); class=class="str">"cmt">//--- Store the date series_first_date_last[s]=series_first_date[s]; loaded=true; } } } } class=class="str">"cmt">//--- If a deeper history has been loaded/generated, then class=class="str">"cmt">// send the command to refresh the plotting series of the indicator if(loaded) class="kw">return(class="kw">false); class=class="str">"cmt">//--- class="kw">return(true); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Checking synchronization by symbol/time frame | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CheckSymbolIsSynchronized() { class=class="str">"cmt">//--- If the connection to the server is established, check the data synchronization if(TerminalInfoInteger(TERMINAL_CONNECTED)) { for(class="type">int s=class="num">0; s<SYMBOLS_COUNT; s++) { class=class="str">"cmt">//--- If the symbol is available if(symbol_names[s]!=empty_symbol) { class=class="str">"cmt">//--- If the data are not synchronized, print the relevant message and try again if(!SeriesInfoInteger(symbol_names[s],Period(),SERIES_SYNCHRONIZED)) { msg_last=msg_not_synchronized; ShowCanvasMessage(msg_not_synchronized); class="kw">return(class="kw">false); } } } } class=class="str">"cmt">//--- class="kw">return(true); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Converting time frame to a class="type">class="kw">string | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string TimeframeToString(ENUM_TIMEFRAMES timeframe) { class="type">class="kw">string str=""; class=class="str">"cmt">//--- If the value passed is incorrect, take the current chart time frame if(timeframe==WRONG_VALUE || timeframe== NULL) timeframe= Period(); class="kw">switch(timeframe) { case PERIOD_M1 : str="M1"; class="kw">break;
「周期映射与首根有效棒判定」
把 ENUM_TIMEFRAMMES 枚举翻译成短字符串,是面板里显示周期标签的基础活。下面这段 switch 把 M2 到 MN1 共 21 个周期常量逐一对应成 "M2""H4" 这类可读文本,缺一个都会在界面上漏标。 周期字符串映射只是表象,真正影响绘制起点的是 DetermineFirstTrueBar()。它按 SYMBOLS_COUNT 遍历品种,遇到 empty_symbol 直接 continue 跳过,再用 Bars() 取当前图表周期的总棒数,随后用 CopyTime() 把时间数组搬进内存。 CopyTime 的返回值若小于 available_bars,说明本次拷贝不完整,函数会进入重试逻辑。外汇与贵金属品种在跨周期拉取时常常因流动性断层导致拷贝失败,属于高风险环境下的常见异常,需留足重试余量。 只要任一品种成功取到首根时间,后续绘制就以该时间为基准对齐;多品种不同步时,首根有效棒决定了整体图形的左边界。
case PERIOD_M2 : str="M2"; class="kw">break; case PERIOD_M3 : str="M3"; class="kw">break; case PERIOD_M4 : str="M4"; class="kw">break; case PERIOD_M5 : str="M5"; class="kw">break; case PERIOD_M6 : str="M6"; class="kw">break; case PERIOD_M10 : str="M10"; class="kw">break; case PERIOD_M12 : str="M12"; class="kw">break; case PERIOD_M15 : str="M15"; class="kw">break; case PERIOD_M20 : str="M20"; class="kw">break; case PERIOD_M30 : str="M30"; class="kw">break; case PERIOD_H1 : str="H1"; class="kw">break; case PERIOD_H2 : str="H2"; class="kw">break; case PERIOD_H3 : str="H3"; class="kw">break; case PERIOD_H4 : str="H4"; class="kw">break; case PERIOD_H6 : str="H6"; class="kw">break; case PERIOD_H8 : str="H8"; class="kw">break; case PERIOD_H12 : str="H12"; class="kw">break; case PERIOD_D1 : str="D1"; class="kw">break; case PERIOD_W1 : str="W1"; class="kw">break; case PERIOD_MN1 : str="MN1"; class="kw">break; } class=class="str">"cmt">//--- class="kw">return(str); } class=class="str">"cmt">//+-----------------------------------------------------------------------+ class=class="str">"cmt">//| Determining the time of the first true bar for the purpose of drawing | class=class="str">"cmt">//+-----------------------------------------------------------------------+ class="type">bool DetermineFirstTrueBar() { for(class="type">int s=class="num">0; s<SYMBOLS_COUNT; s++) { class="type">class="kw">datetime time[]; class=class="str">"cmt">// Bar time array class="type">int available_bars=class="num">0; class=class="str">"cmt">// Number of bars class=class="str">"cmt">//--- If this symbol is not available, move to the next one if(symbol_names[s]==empty_symbol) class="kw">continue; class=class="str">"cmt">//--- Get the total number of bars for the symbol available_bars=Bars(symbol_names[s],Period()); class=class="str">"cmt">//--- Copy the bar time array. If this action failed, try again. if(CopyTime(symbol_names[s],Period(),class="num">0,available_bars,time)<available_bars)
◍ 用相邻棒差定位首根真实K线
多周期脚本里常遇到「合成棒」和「真实棒」混排。要画时间轴起点,得先知道当前时间框架下第一根货真价实的柱子从哪根开始。 思路很直接:遍历时间数组,比较相邻两根的秒差是否等于 PeriodSeconds() 返回的当前周期秒数。一旦相等,那根就是首个真实棒,记下时间并 break。 下面这段 GetFirstTrueBarTime 就是干这个的。注意 ArraySetAsSeries(time,false) 把序列方向转正,i 从 1 起跳,用 time[i]-time[i-1] 判断间隔。 拿到时间后,CreateVerticalLine 在 0 号图表、0 号窗口按 2 像素宽、实线、指定颜色画竖线,文本标出 begin time series 和具体时间。外汇与贵金属波动剧烈,这类标记仅辅助复盘,实际下单仍属高风险行为。
class="type">class="kw">datetime GetFirstTrueBarTime(class="type">class="kw">datetime &time[]) { class="type">class="kw">datetime true_period =NULL; class=class="str">"cmt">// Time of the first true bar class="type">int array_size =class="num">0; class=class="str">"cmt">// Array size class=class="str">"cmt">//--- Get the array size array_size=ArraySize(time); ArraySetAsSeries(time,class="kw">false); class=class="str">"cmt">//--- Check each bar one by one for(class="type">int i=class="num">1; i<array_size; i++) { class=class="str">"cmt">//--- If the bar corresponds to the current time frame if(time[i]-time[i-class="num">1]==PeriodSeconds()) { class=class="str">"cmt">//--- Save it and terminate the loop true_period=time[i]; class="kw">break; } } class=class="str">"cmt">//--- Return the time of the first true bar class="kw">return(true_period); } class="type">void CreateVerticalLine(class="type">long chart_id, class=class="str">"cmt">// chart id class="type">int window_number, class=class="str">"cmt">// window number class="type">class="kw">datetime time, class=class="str">"cmt">// time class="type">class="kw">string object_name, class=class="str">"cmt">// object name class="type">int line_width, class=class="str">"cmt">// line width ENUM_LINE_STYLE line_style, class=class="str">"cmt">// line style class="type">class="kw">color line_color class=class="str">"cmt">// line class="type">class="kw">color
垂直线对象的创建与首帧初始化
在 MT5 自定义指标里画一条可交互的竖向参考线,核心是先调用 ObjectCreate 用 OBJ_VLINE 类型把对象挂到指定图表和子窗口,时间坐标由 time 参数锁定、纵向忽略(传 0)。下面这段函数尾部参数表给出了几个容易被忽略的控制项:selectable 为 false 时鼠标无法选中该线,tooltip 传 "\n" 则强制不显示悬浮提示,description_text 才是对象描述正文。
class="type">bool selectable, class=class="str">"cmt">// cannot select the object if FALSE class="type">class="kw">string description_text, class=class="str">"cmt">// text of the description class="type">class="kw">string tooltip) class=class="str">"cmt">// no tooltip if "\n" { class=class="str">"cmt">//--- If the object has been created successfully if(ObjectCreate(chart_id,object_name,OBJ_VLINE,window_number,time,class="num">0)) { class=class="str">"cmt">//--- set its properties ObjectSetInteger(chart_id,object_name,OBJPROP_TIME,time); ObjectSetInteger(chart_id,object_name,OBJPROP_SELECTABLE,selectable); ObjectSetInteger(chart_id,object_name,OBJPROP_STYLE,line_style); ObjectSetInteger(chart_id,object_name,OBJPROP_WIDTH,line_width); ObjectSetInteger(chart_id,object_name,OBJPROP_COLOR,line_color); ObjectSetString(chart_id,object_name,OBJPROP_TEXT,description_text); ObjectSetString(chart_id,object_name,OBJPROP_TOOLTIP,tooltip); } }
class="type">bool selectable, class=class="str">"cmt">// cannot select the object if FALSE class="type">class="kw">string description_text, class=class="str">"cmt">// text of the description class="type">class="kw">string tooltip) class=class="str">"cmt">// no tooltip if "\n" { class=class="str">"cmt">//--- If the object has been created successfully if(ObjectCreate(chart_id,object_name,OBJ_VLINE,window_number,time,class="num">0)) { class=class="str">"cmt">//--- set its properties ObjectSetInteger(chart_id,object_name,OBJPROP_TIME,time); ObjectSetInteger(chart_id,object_name,OBJPROP_SELECTABLE,selectable); ObjectSetInteger(chart_id,object_name,OBJPROP_STYLE,line_style); ObjectSetInteger(chart_id,object_name,OBJPROP_WIDTH,line_width); ObjectSetInteger(chart_id,object_name,OBJPROP_COLOR,line_color); ObjectSetString(chart_id,object_name,OBJPROP_TEXT,description_text); ObjectSetString(chart_id,object_name,OBJPROP_TOOLTIP,tooltip); } } class=class="str">"cmt">//--- If this is the first calculation or if a deeper history has been loaded or gaps in the history have been filled if(prev_calculated==class="num">0) { class=class="str">"cmt">//--- Zero out arrays for data preparation ZeroCalculatedArrays(); class=class="str">"cmt">//--- Zero out indicator buffers ZeroIndicatorBuffers(); class=class="str">"cmt">//--- Get subwindow properties GetSubwindowGeometry(); class=class="str">"cmt">//--- Add the canvas SetCanvas(); class=class="str">"cmt">//--- Load and generate the necessary/available amount of data LoadAndFormData(); class=class="str">"cmt">//--- If there is an invalid handle, try to get it again if(!GetIndicatorHandles()) class="kw">return(RESET); class=class="str">"cmt">//--- Check the amount of data available for all symbols if(!CheckAvailableData()) class="kw">return(RESET); class=class="str">"cmt">//--- Check if a deeper history has been loaded if(!CheckLoadedHistory()) class="kw">return(RESET); class=class="str">"cmt">//--- Check synchronization by symbol/time frame at the current moment if(!CheckSymbolIsSynchronized()) class="kw">return(RESET); class=class="str">"cmt">//--- For each symbol, determine the bar from which we should start drawing if(!DetermineFirstTrueBar()) class="kw">return(RESET); class=class="str">"cmt">//--- If you reached this point, it means that OnCalculate() will class="kw">return non-zero value and this needs to be saved OC_prev_calculated=rates_total; }
「定时器驱动的多品种数据回填」
MT5 指标若依赖 OnTimer 做异步装载,得防历史数据中途补全导致计算断片。上面这段逻辑里,一旦 CheckLoadedHistory() 返回假,就把 OC_prev_calculated 清零,随即不等新 tick 直接重跑 OnCalculate 做补救。
真正耗时的活是后面那层双重循环:外层扫 SYMBOLS_COUNT 个品种,内层从 limit 到 rates_total 逐根 bar 调 PrepareData。每 1000 根刷新一次画布进度提示,每 2000 根顺手校一次子窗口尺寸,避免后台图表缩放后画布错位。
注意 PrepareData 里写了 attempts=100 的拷贝重试上限,说明跨品种抓价可能因 broker 端短暂无响应而失败。外汇与贵金属行情在高波动时段易丢包,这种重试机制能降低指标空白的概率,但依旧不保证百分百同步。
class="type">void OnTimer() { class=class="str">"cmt">//--- If a deeper history has been loaded if(!CheckLoadedHistory()) OC_prev_calculated=class="num">0; class=class="str">"cmt">//--- If for some reason calculations have not been completed or class=class="str">"cmt">// a deeper history has been loaded or class=class="str">"cmt">// gaps in the history have been filled, class=class="str">"cmt">// then make another attempt without waiting for the new tick if(OC_prev_calculated==class="num">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); } class=class="str">"cmt">//--- Prepare data for drawing for(class="type">int s=class="num">0; s<SYMBOLS_COUNT; s++) { class=class="str">"cmt">//--- If the symbol is available if(symbol_names[s]!=empty_symbol) { class="type">class="kw">double percent=class="num">0.0; class=class="str">"cmt">// For the purpose of calculating the progress percentage msg_last=msg_sync_update="Preparing data("+IntegerToString(rates_total)+" bars) : "+ symbol_names[s]+"("+(class="type">class="kw">string)(s+class="num">1)+"/"+(class="type">class="kw">string)(SYMBOLS_COUNT)+") - class="num">00% ... "; class=class="str">"cmt">//--- Print the message ShowCanvasMessage(msg_sync_update); class=class="str">"cmt">//--- Control every value of the array for(class="type">int i=limit; i<rates_total; i++) { PrepareData(i,s,time); class=class="str">"cmt">//--- Refresh the message once every class="num">1000 bars if(i%class="num">1000==class="num">0) { class=class="str">"cmt">//--- Progress percentage ProgressPercentage(i,s,percent); class=class="str">"cmt">//--- Print the message ShowCanvasMessage(msg_sync_update); } class=class="str">"cmt">//--- Check the subwindow size once every class="num">2000 bars class=class="str">"cmt">// and if the size has changed, adjust the canvas size to it if(i%class="num">2000==class="num">0) OnSubwindowChange(); } } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Preparing data before drawing | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void PrepareData(class="type">int bar_index,class="type">int symbol_number,class="type">class="kw">datetime class="kw">const &time[]) { class="type">int attempts=class="num">100; class=class="str">"cmt">// Number of copying attempts
void OnTimer() 是定时器回调入口;if(!CheckLoadedHistory()) OC_prev_calculated=0; 检测深层历史是否载入,未载全则重置计算标记。if(OC_prev_calculated==0) 内直接调 OnCalculate 补算,跳过等 tick。for(int s=0; s<SYMBOLS_COUNT; s++) 遍历品种,if(symbol_names[s]!=empty_symbol) 跳过空品种。double percent=0.0 仅用于进度百分比;msg_sync_update 拼出“Preparing data (N bars) : 品种名(序号/总数) - 00%”的提示串。ShowCanvasMessage 推到画布。内层 for(int i=limit; i<rates_total; i++) 逐 bar 跑 PrepareData(i,s,time);if(i%1000==0) 每千根算进度并刷新;if(i%2000==0) 每两千根调 OnSubwindowChange() 适配子窗。尾部 PrepareData 函数头定义里 int attempts=100 是拷贝重试次数上限。
class="type">void OnTimer() { class=class="str">"cmt">//--- If a deeper history has been loaded if(!CheckLoadedHistory()) OC_prev_calculated=class="num">0; class=class="str">"cmt">//--- If for some reason calculations have not been completed or class=class="str">"cmt">// a deeper history has been loaded or class=class="str">"cmt">// gaps in the history have been filled, class=class="str">"cmt">// then make another attempt without waiting for the new tick if(OC_prev_calculated==class="num">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); } class=class="str">"cmt">//--- Prepare data for drawing for(class="type">int s=class="num">0; s<SYMBOLS_COUNT; s++) { class=class="str">"cmt">//--- If the symbol is available if(symbol_names[s]!=empty_symbol) { class="type">class="kw">double percent=class="num">0.0; class=class="str">"cmt">// For the purpose of calculating the progress percentage msg_last=msg_sync_update="Preparing data("+IntegerToString(rates_total)+" bars) : "+ symbol_names[s]+"("+(class="type">class="kw">string)(s+class="num">1)+"/"+(class="type">class="kw">string)(SYMBOLS_COUNT)+") - class="num">00% ... "; class=class="str">"cmt">//--- Print the message ShowCanvasMessage(msg_sync_update); class=class="str">"cmt">//--- Control every value of the array for(class="type">int i=limit; i<rates_total; i++) { PrepareData(i,s,time); class=class="str">"cmt">//--- Refresh the message once every class="num">1000 bars if(i%class="num">1000==class="num">0) { class=class="str">"cmt">//--- Progress percentage ProgressPercentage(i,s,percent); class=class="str">"cmt">//--- Print the message ShowCanvasMessage(msg_sync_update); } class=class="str">"cmt">//--- Check the subwindow size once every class="num">2000 bars class=class="str">"cmt">// and if the size has changed, adjust the canvas size to it if(i%class="num">2000==class="num">0) OnSubwindowChange(); } } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Preparing data before drawing | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void PrepareData(class="type">int bar_index,class="type">int symbol_number,class="type">class="kw">datetime class="kw">const &time[]) { class="type">int attempts=class="num">100; class=class="str">"cmt">// Number of copying attempts
◍ 跨周期取数时的边界与重试处理
在多品种监控里,当前图表时间帧之外的 K 线不应强行取数。代码用 time[bar_index]>=limit_time[symbol_number] 做判断:落在当前时间帧范围内的才去 CopyTime 和 CopyBuffer,否则直接把 ATR 缓冲写成 EMPTY_VALUE,避免把错位数据画进指标。 CopyTime 与 CopyBuffer 都不是百分百一次成功,尤其跨品种喊单时服务器偶尔丢包。原文用 for(i=0;i<attempts;i++) 套一层重试,返回值为 1 才写进 tmp 数组并 break,attempts 设小了可能漏数据,设大了拖慢刷新,实盘建议从 3 起步在 MT5 里试。 进度函数 ProgressPercentage 把 bar_index 除以 OC_rates_total 再乘 100,percent<=9.99 时补前导零,让日志显示 '01%' 而非 '1%'。OC_rates_total 是总预处理 bar 数,若它因品种节假日缺失而偏小,进度百分比会虚高,这是外汇贵金属多符号回算的高风险点,需自己打印校验。 填充阶段对 empty_symbol 直接 ArrayInitialize 清零,存在的品种才走更新消息,这样终端不会因某个平台下架的品种卡死整个缓冲写入。
class=class="str">"cmt">//--- Time of the bar of the specified symbol and time frame class="type">class="kw">datetime symbol_time[]; class=class="str">"cmt">//--- Array for copying indicator values class="type">class="kw">double atr_values[]; class=class="str">"cmt">//--- If within the area of the current time frame bars if(time[bar_index]>=limit_time[symbol_number]) { class=class="str">"cmt">//--- Copy the time for(class="type">int i=class="num">0; i<attempts; i++) { if(CopyTime(symbol_names[symbol_number],class="num">0,time[bar_index],class="num">1,symbol_time)==class="num">1) { tmp_symbol_time[symbol_number].time[bar_index]=symbol_time[class="num">0]; class="kw">break; } } class=class="str">"cmt">//--- Copy the indicator value for(class="type">int i=class="num">0; i<attempts; i++) { if(CopyBuffer(symbol_handles[symbol_number],class="num">0,time[bar_index],class="num">1,atr_values)==class="num">1) { tmp_atr_values[symbol_number].value[bar_index]=atr_values[class="num">0]; class="kw">break; } } } class=class="str">"cmt">//--- If outside the area of the current time frame bars, set an empty value else tmp_atr_values[symbol_number].value[bar_index]=EMPTY_VALUE; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Calculating progress percentage | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void ProgressPercentage(class="type">int bar_index,class="type">int symbol_number,class="type">class="kw">double &percent) { class="type">class="kw">string message_text=""; percent=(class="type">class="kw">double(bar_index)/OC_rates_total)*class="num">100; class=class="str">"cmt">//--- if(percent<=class="num">9.99) message_text="class="num">0"+DoubleToString(percent,class="num">0); else if(percent<class="num">99) message_text=DoubleToString(percent,class="num">0); else message_text="class="num">100"; class=class="str">"cmt">//--- msg_last=msg_sync_update="Preparing data("+(class="type">class="kw">string)OC_rates_total+" bars) : "+ symbol_names[symbol_number]+ "("+(class="type">class="kw">string)(symbol_number+class="num">1)+"/"+(class="type">class="kw">string)SYMBOLS_COUNT+") - "+message_text+"% ... "; } class=class="str">"cmt">//--- Fill indicator buffers for(class="type">int s=class="num">0; s<SYMBOLS_COUNT; s++) { class=class="str">"cmt">//--- If the specified symbol does not exist, zero out the buffer if(symbol_names[s]==empty_symbol) ArrayInitialize(atr_buffers[s].data,EMPTY_VALUE); else { class=class="str">"cmt">//--- Generate a message msg_last=msg_sync_update="Updating indicator data: "+
正文
symbol_names[s]+<span class="string">"("</span>+(<span class="keyword">string</span>)(s+<span class="number">1</span>)+<span class="string">"/"</span>+(<span class="keyword">string</span>)SYMBOLS_COUNT+<span class="string">") ... "</span>; <span class="comment">//--- Print the message</span> ShowCanvasMessage(msg_sync_update); <span class="comment">//--- Fill indicator buffers with values</span> <span class="keyword">for</span>(<span class="keyword">int</span> i=limit; i<rates_total; i++) { <span style="background-color:rgb(255, 242, 153);">FillIndicatorBuffers(i,s,time);</span> <span class="comment">//--- Check the subwindow size once every 2000 bars</span> <span class="comment">// and if the size has changed, adjust the canvas size to it</span> <span class="keyword">if</span>(i%<span
「子窗口里批量画水平线的坑」
在指标副图里批量落水平线,第一步得先拿到子窗口编号:用 ChartWindowFind(0, subwindow_shortname) 按短名定位,返回值直接喂给后续绘图函数。若返回 -1,说明该名称的子窗口还没创建,循环里硬画会静默失败。 拿到窗口号后,用 for 循环按 LEVELS_COUNT 的次数调 CreateHorizontalLine,名字拼成 prefix+"level_0"+(i+1),价格则先过一遍 CorrectValueBySymbolDigits 做小数位修正。外汇和贵金属点差跳变快,副图层级线画歪了容易误读支撑阻力,属高风险操作,建议先在模拟盘验证。 CorrectValueBySymbolDigits 只做一件事:当 _Digits 是 3 或 5(即报价带小数尾位 3/5 位的品种,如 XAUUSD 通常 5 位)时,把传入值乘 10 再返回;其余位数原样返回。这一步是为了把用 _Point 算出的相对偏移对齐到实际报价精度,否则 5 位平台上线会偏出一个数量级。 CreateHorizontalLine 的参数表里,selectable/selected/back 三个 bool 全传 false,意味着线不可选中、不置顶、不进背景层;style 用 STYLE_DOT、颜色 clrLightSteelBlue,纯做参考不干扰交互。开 MT5 把这段抄进 EA,改 LEVELS_COUNT 就能看副图层级密度变化。
subwindow_number=ChartWindowFind(class="num">0,subwindow_shortname); class=class="str">"cmt">//--- Set levels for(class="type">int i=class="num">0; i<LEVELS_COUNT; i++) CreateHorizontalLine(class="num">0,subwindow_number, prefix+"level_0"+(class="type">class="kw">string)(i+class="num">1)+"", CorrectValueBySymbolDigits(indicator_levels[i]*_Point), class="num">1,STYLE_DOT,clrLightSteelBlue,class="kw">false,class="kw">false,class="kw">false,"\n"); } class=class="str">"cmt">//+------------------------------------------------------------------------+ class=class="str">"cmt">//| Adjusting the value based on the number of digits in the price(class="type">class="kw">double)| class=class="str">"cmt">//+------------------------------------------------------------------------+ class="type">class="kw">double CorrectValueBySymbolDigits(class="type">class="kw">double value) { class="kw">return(_Digits==class="num">3 || _Digits==class="num">5) ? value*=class="num">10 : value; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Creating a horizontal line at the price level specified | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CreateHorizontalLine(class="type">long chart_id, class=class="str">"cmt">// chart id class="type">int window_number, class=class="str">"cmt">// window number class="type">class="kw">string object_name, class=class="str">"cmt">// object name class="type">class="kw">double price, class=class="str">"cmt">// price level class="type">int line_width, class=class="str">"cmt">// line width ENUM_LINE_STYLE line_style, class=class="str">"cmt">// line style class="type">class="kw">color line_color, class=class="str">"cmt">// line class="type">class="kw">color class="type">bool selectable, class=class="str">"cmt">// cannot select the object if FALSE class="type">bool selected, class=class="str">"cmt">// line is selected class="type">bool back) class=class="str">"cmt">// background position
◍ 水平线与清理函数的落地写法
在 MT5 图表上画水平参考线,核心是用 OBJ_HLINE 配合 ObjectCreate。下面这段把可选取、选中态、背景层、线型线宽颜色一次性设完,tooltip 传空串或换行符时可不显示悬浮提示。
class="type">class="kw">string tooltip) class=class="str">"cmt">// no tooltip if "\n" { class=class="str">"cmt">//--- If the object has been created successfully if(ObjectCreate(chart_id,object_name,OBJ_HLINE,window_number,class="num">0,price)) { class=class="str">"cmt">//--- set its properties ObjectSetInteger(chart_id,object_name,OBJPROP_SELECTABLE,selectable); ObjectSetInteger(chart_id,object_name,OBJPROP_SELECTED,selected); ObjectSetInteger(chart_id,object_name,OBJPROP_BACK,back); ObjectSetInteger(chart_id,object_name,OBJPROP_STYLE,line_style); ObjectSetInteger(chart_id,object_name,OBJPROP_WIDTH,line_width); ObjectSetInteger(chart_id,object_name,OBJPROP_COLOR,line_color); ObjectSetString(chart_id,object_name,OBJPROP_TOOLTIP,tooltip); } }
class="type">class="kw">string tooltip) class=class="str">"cmt">// no tooltip if "\n" { class=class="str">"cmt">//--- If the object has been created successfully if(ObjectCreate(chart_id,object_name,OBJ_HLINE,window_number,class="num">0,price)) { class=class="str">"cmt">//--- set its properties ObjectSetInteger(chart_id,object_name,OBJPROP_SELECTABLE,selectable); ObjectSetInteger(chart_id,object_name,OBJPROP_SELECTED,selected); ObjectSetInteger(chart_id,object_name,OBJPROP_BACK,back); ObjectSetInteger(chart_id,object_name,OBJPROP_STYLE,line_style); ObjectSetInteger(chart_id,object_name,OBJPROP_WIDTH,line_width); ObjectSetInteger(chart_id,object_name,OBJPROP_COLOR,line_color); ObjectSetString(chart_id,object_name,OBJPROP_TOOLTIP,tooltip); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Deleting levels | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void DeleteLevels() { for(class="type">int i=class="num">0; i<LEVELS_COUNT; i++) DeleteObjectByName(prefix+"level_0"+(class="type">class="kw">string)(i+class="num">1)+""); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Deleting vertical lines of the beginning of the series | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void DeleteVerticalLines() { for(class="type">int s=class="num">0; s<SYMBOLS_COUNT; s++) DeleteObjectByName(prefix+symbol_names[s]+": begin time series"); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Deleting the object by name | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void DeleteObjectByName(class="type">class="kw">string object_name) { class=class="str">"cmt">//--- If such object exists if(ObjectFind(class="num">0,object_name)>=class="num">0) { class=class="str">"cmt">//--- If an error occurred when deleting, print the relevant message if(!ObjectDelete(class="num">0,object_name)) Print("Error("+IntegerToString(GetLastError())+") when deleting the object!"); } }
把工具请下神坛
这套多品种 ATR 分析的源码(multisymbolatr.mq5,47.42 KB)已经随文放出,ZIP 里能直接拖进 MT5 编译。它本质上只是把不同品种的真实波幅拉到同一张图上对比,不是点石成金的黑箱。 回头看评论区,2014 年就有用户在测试器里跑出和作者截图不一致的结果,作者自己也说没在测试器里验过这个指标形态——多系统下标准指标偶尔抽风,是 MT5 老毛病,别把锅全甩给策略。 下一篇打算拿它当底子接一个交易系统,专门看波幅扩张时的进出场。你现在就能开 MT5 把 ZIP 里的文件丢进 Indicators,切几个交叉盘看 ATR 线是否同步发散,外汇和贵金属杠杆高,波幅误读容易放大亏损,先验证再谈实盘。