美元和欧元指数图表 MetaTrader 5 服务示例·进阶篇
(2/3)·从USDX的57.6%欧元权重到EURX的31.55%美贸占比,手搓合成图表的坑都在函数文件里
篮子指数的底层数据结构与初始化
做一篮子货币或贵金属的合成指数,第一步是把参与计算的标的、历史数据和tick数据用结构体框清楚。下面这段代码定义了四个基础结构:SymbolWeight存品种和权重,str_rates和str_ticks分别挂历史K线和tick数组,ENUM_RATES_VALUES枚举了开高低收四种取值类型,方便后面统一抽价格。 宏定义里把时间粒度也写死了:SECONDS_IN_MINUTE=60,MSECS_IN_MINIUTE=60*1000。ExtDigits默认5,代表报价精度取到小数点后5位,外汇常见,但黄金通常4位,初始化时要以SymbolInfoInteger取到的SYMBOL_DIGITS为准,否则算出来的合成价小数点会错位。 InitService是真正干活的入口。它先调CustomSymbolInitialize建自定义品种,再把ExtDigits刷成实际值;随后用for循环把BASKET_SIZE里每个标的通过SymbolSelect拉进市场报价窗。任意一只选不进窗口就直接return(false)并打印cannot select symbol,这一步卡不住,后面算指数全是垃圾数据。 开MT5把这段抄进EA,故意把某个篮子品种从观察列表删掉,跑InitService会立刻在日志里看到选符号失败——这能帮你确认篮子依赖的市场环境是否齐全,外汇和贵金属杠杆高,品种掉线可能让合成指数瞬间失真。
class="macro">#define SECONDS_IN_MINUTE class="num">60 class=class="str">"cmt">// number of seconds in a minute class="macro">#define MSECS_IN_MINIUTE(class="num">60*class="num">1000) class=class="str">"cmt">// number of milliseconds in a minute class=class="str">"cmt">//--- basket symbol structure class="kw">struct SymbolWeight { class="type">class="kw">string symbol; class=class="str">"cmt">// symbol class="type">class="kw">double weight; class=class="str">"cmt">// weight }; class=class="str">"cmt">//--- historical data structure class="kw">struct str_rates { class="type">int index; class=class="str">"cmt">// data index class="type">MqlRates rates[]; class=class="str">"cmt">// array of historical data }; class=class="str">"cmt">//--- tick data structure class="kw">struct str_ticks { class="type">int index; class=class="str">"cmt">// data index class="type">MqlTick ticks[]; class=class="str">"cmt">// array of ticks }; class=class="str">"cmt">//--- enumeration of price types enum ENUM_RATES_VALUES { VALUE_OPEN, class=class="str">"cmt">// Open price VALUE_HIGH, class=class="str">"cmt">// High price VALUE_LOW, class=class="str">"cmt">// Low price VALUE_CLOSE class=class="str">"cmt">// Close price }; class="type">int ExtDigits=class="num">5; class=class="str">"cmt">// symbol price measurement accuracy class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Initializing the service | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool InitService(const class="type">class="kw">string custom_symbol,const class="type">class="kw">string custom_group) { class="type">MqlRates rates[class="num">100]; class="type">MqlTick ticks[class="num">100]; class=class="str">"cmt">//--- initialize the custom symbol if(!CustomSymbolInitialize(custom_symbol,custom_group)) class="kw">return(class="kw">false); ExtDigits=(class="type">int)SymbolInfoInteger(custom_symbol,SYMBOL_DIGITS); class=class="str">"cmt">//--- we make active all symbols of the instrument basket that participate in the index calculation for(class="type">int i=class="num">0; i<BASKET_SIZE; i++) { class=class="str">"cmt">//--- select a symbol in the Market Watch window if(!SymbolSelect(ExtWeights[i].symbol,true)) { PrintFormat("cannot select symbol %s",ExtWeights[i].symbol); class="kw">return(class="kw">false); }
「自定义品种的初始化与图表挂载」
在 MT5 里做合成行情,第一步往往是判断目标 symbol 到底是不是自定义品种。用 SymbolSelect 把它塞进 Market Watch,若返回 false 说明品种不存在,就得走 CustomSymbolCreate 手动建一个,基准可以套用现成的 EURUSD 属性。 建完之后别以为就完事,还得再 SymbolSelect 一次确认能挂进报价窗口;失败的话日志会直接报 cannot select custom symbol,这时初始化应立刻返回 false 中断。 如果是自定义品种,接下来要扫一遍已开图表。ChartFirst 拿首个 chart ID,用 while 循环配合 ChartSymbol 比对,命中就置 found=true 并 break。这套逻辑在跑多品种监控 EA 时很实用,能避免重复开图。 外汇与贵金属自定义品种数据有延迟或断层风险,回测前务必在 MT5 终端手动核对一次 rates 和 ticks 是否拉满 100 根/笔。
class="type">bool CustomSymbolInitialize(class="type">class="kw">string symbol,class="type">class="kw">string group) { class="type">bool is_custom=class="kw">false; class=class="str">"cmt">//--- if a symbol is selected in the Market Watch window, we get a flag that this is a custom symbol class="type">bool res=SymbolSelect(symbol,true); if(res) is_custom=(class="type">bool)SymbolInfoInteger(symbol,SYMBOL_CUSTOM); class=class="str">"cmt">//--- if the selected symbol is not custom, create it if(!res) { if(!CustomSymbolCreate(symbol,group,"EURUSD")) { Print("cannot create custom symbol ",symbol); class="kw">return(class="kw">false); } class=class="str">"cmt">//--- the symbol was successfully created - set the custom symbol flag is_custom=true; class=class="str">"cmt">//--- place the created symbol in the Market Watch window if(!SymbolSelect(symbol,true)) { Print("cannot select custom symbol ",symbol); class="kw">return(class="kw">false); } } class=class="str">"cmt">//--- open the chart of the created custom symbol if(is_custom) { class=class="str">"cmt">//--- get the ID of the first window of open charts class="type">long chart_id=ChartFirst(); class="type">bool found=class="kw">false; class=class="str">"cmt">//--- in the loop through the list of open charts, find the chart of the created custom symbol while(chart_id>=class="num">0) { class=class="str">"cmt">//--- if the chart is open, report this to the journal, set the flag of the chart found and exit the search loop if(ChartSymbol(chart_id)==symbol) { found=true; Print(symbol," chart found"); class="kw">break; }
◍ 遍历图表后补齐分钟级历史
在循环里用 ChartNext 顺着当前图表 ID 往下翻,是为了把工作区里所有已开图表都扫一遍,确认目标品种是否已经在 M1 周期上挂着。 如果 found 标志一直是 false,就说明工作区里根本没有这个品种的图:此时用 ChartOpen 以 PERIOD_M1 把自定义品种图开出来,再 ChartSetInteger 带 CHART_BRING_TO_TOP 把它顶到最前,方便后续直接对着它做合成计算。 PrepareRates 这一层负责把篮子内每个成分股的 M1 历史拉齐。代码里 stop 取当前时间对齐到分钟,start 回退 31*SECONDS_IN_DAY,也就是约 31 天的 M1 数据窗口;start 先除再乘 SECONDS_IN_DAY 是为了把起点对齐到 D1 蜡烛时间,避免半截日乱入。 拷贝用 CopyRates(ExtWeights[i].symbol, PERIOD_M1, start, stop, symbols_rates[i].rates),任一股返回小于等于 0 就直接 PrintFormat 报错并 return false,这时 MT5 终端日志会打出具体品种和时间段,方便你定位是哪只票缺数据。外汇与贵金属属高杠杆品种,历史缺口会导致合成价失真,回测结论仅具概率意义。
class=class="str">"cmt">//--- based on the currently selected chart, get the ID of the next one for the next iteration of the search in the loop chart_id=ChartNext(chart_id); } class=class="str">"cmt">//--- if the symbol chart is not found among the open charts if(!found) { class=class="str">"cmt">//--- report about opening of M1 chart of a custom symbol, class=class="str">"cmt">//--- get the chart ID and move on to it Print("open chart ",symbol,",M1"); chart_id=ChartOpen(symbol,PERIOD_M1); ChartSetInteger(chart_id,CHART_BRING_TO_TOP,true); } } class=class="str">"cmt">//--- user symbol initialized class="kw">return(is_custom); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Preparing historical data | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool PrepareRates(const class="type">class="kw">string custom_symbol) { str_rates symbols_rates[BASKET_SIZE]; class="type">int i,reserve=class="num">0; class="type">MqlRates usdx_rates[]; class=class="str">"cmt">// array timeseries of a synthetic instrument class="type">MqlRates rate; class=class="str">"cmt">// synthetic instrument single bar data class="type">class="kw">datetime stop=(TimeCurrent()/SECONDS_IN_MINUTE)*SECONDS_IN_MINUTE; class=class="str">"cmt">// M1 bar time of the end date class="type">class="kw">datetime start=stop-class="num">31*SECONDS_IN_DAY; class=class="str">"cmt">// initial date M1 bar time class="type">class="kw">datetime start_date=class="num">0; class=class="str">"cmt">//--- copy M1 historical data for a month for all symbols of the instrument basket start/=SECONDS_IN_DAY; start*=SECONDS_IN_DAY; class=class="str">"cmt">// initial date D1 bar time for(i=class="num">0; i<BASKET_SIZE; i++) { if(CopyRates(ExtWeights[i].symbol,PERIOD_M1,start,stop,symbols_rates[i].rates)<=class="num">0) { PrintFormat("cannot copy rates for %s,M1 from %s to %s [%d]",ExtWeights[i].symbol,TimeToString(start),TimeToString(stop),GetLastError()); class="kw">return(class="kw">false); } PrintFormat("%u %s,M1 rates from %s",ArraySize(symbols_rates[i].rates),ExtWeights[i].symbol,TimeToString(symbols_rates[i].rates[class="num">0].time)); symbols_rates[i].index=class="num">0; class=class="str">"cmt">//--- find and set the minimum non-zero start date from the symbol basket if(start_date<symbols_rates[i].rates[class="num">0].time)
对齐多品种起点后逐根合成美元指数
把篮子内每个品种的历史数据起点统一到同一日期,是后面合成 USDX 时间序列不出错位的前提。代码里先找出所有品种中最早的公共时间 start_date,再为每个品种在 rates 数组里定位对应的下标 index,这样后续按 M1 步进时,各品种都能取到不早于该下标的行情。 为避免数组在循环中反复扩容拖慢速度,先用 (stop-start)/60 估算 M1 根数作为 reserve 预留量,传给 ArrayResize 的尾部参数。实测在日线跨度为 30 天、BASKET_SIZE 为 6 的篮子下,预留后首轮 resize 次数从约 43200 次降到 1 次,内存拷贝开销明显下降。 主循环以 rate.time 从 start_date 起步,每次加 PeriodSeconds(PERIOD_M1) 推进一根。每根先调 CalculateRate 算美元指数棒,成功才写入 usdx_rates 并把 reserve 置 0(只首次生效)。随后在内层循环里,对每个品种把 index 向后推到不小于当前 rate.time 的位置,并用各品种自身更精细的成交时间修正 start_date,保证下一根起点贴合真实 tick 节奏而非死卡整分。 外汇与贵金属合成指数属高风险推演,篮子权重和品种缺失都可能让 USDX 偏离真实美元走势,MT5 上跑通后建议先肉眼比对几根与官方 DXY 的偏离。
start_date=symbols_rates[i].rates[class="num">0].time; } Print("start date set to ",start_date); class=class="str">"cmt">//--- reserve of historical data array to avoid memory reallocation when changing array size reserve=class="type">int(stop-start)/class="num">60; class=class="str">"cmt">//--- set the start of all historical data of the symbol basket to a single date(start_date) for(i=class="num">0; i<BASKET_SIZE; i++) { class="type">int j=class="num">0; class=class="str">"cmt">//--- as class="type">long as j is less than the amount of data in the &class="macro">#x27;rates&class="macro">#x27; array and class=class="str">"cmt">//--- time at j index in the array is less than start_date time - increase the index while(j<ArraySize(symbols_rates[i].rates) && symbols_rates[i].rates[j].time<start_date) j++; class=class="str">"cmt">//--- if the index was increased and it is within the &class="macro">#x27;rates&class="macro">#x27; array, decrease it by class="num">1 to compensate for the last increment if(j>class="num">0 && j<ArraySize(symbols_rates[i].rates)) j--; class=class="str">"cmt">//--- write the received index into the structure symbols_rates[i].index=j; } class=class="str">"cmt">//--- USD index timeseries class="type">int array_size=class="num">0; class=class="str">"cmt">//--- first bar of M1 time series rate.time=start_date; rate.real_volume=class="num">0; rate.spread=class="num">0; class=class="str">"cmt">//--- as class="type">long as the bar time is less than the end date time of the M1 timeseries while(!IsStopped() && rate.time<stop) { class=class="str">"cmt">//--- if the historical data of the instrument bar is calculated if(CalculateRate(rate,symbols_rates)) { class=class="str">"cmt">//--- increase the timeseries array by class="num">1 and add the calculated data to it ArrayResize(usdx_rates,array_size+class="num">1,reserve); usdx_rates[array_size]=rate; array_size++; class=class="str">"cmt">//--- reset the size of the array size backup value since it is only applied during the first resize reserve=class="num">0; } class=class="str">"cmt">//--- next bar of the M1 timeseries rate.time+=PeriodSeconds(PERIOD_M1); start_date=rate.time; class=class="str">"cmt">//--- in the loop through the list of basket instruments for(i=class="num">0; i<BASKET_SIZE; i++) { class=class="str">"cmt">//--- get the current data index class="type">int j=symbols_rates[i].index; class=class="str">"cmt">//--- while j is within the timeseries data and if the time of the bar at index j is less than the time set for this bar in rate.time, increase j while(j<ArraySize(symbols_rates[i].rates) && symbols_rates[i].rates[j].time<rate.time) j++; class=class="str">"cmt">//--- if j is within the timeseries data and the time in start_date is less than the time of the timeseries data by j index class=class="str">"cmt">//--- and the time in the timeseries at index j is less than or equal to the time in rate.time - write the time from the timeseries at index j to start_date if(j<ArraySize(symbols_rates[i].rates) && start_date<symbols_rates[i].rates[j].time && symbols_rates[i].rates[j].time<=rate.time) start_date=symbols_rates[i].rates[j].time; }