MQL5 中创建订单号指标·进阶篇
◍ 买卖价落盘与缓冲区滚动对齐
这段逻辑处理的是历史 tick 已读和实时追加两种路径。若文件里已有数据,就按空格切出 Bid 与 Ask 位置,用 StringSubstr 截段后 StringToDouble 写进对应 Buffer;若是首次运行,则 FileSeek 跳到文件尾,把 TimeCurrent、Bid、Ask 拼成一行 FileWrite 落盘,同时把最新价推入数组。 当 ticks_stored 大于等于 rates_total 时,代码做了一半窗口滑动:只保留后半段 tick_stored/2 个报价,前半段直接丢弃,计数器同步减半。这意味着指标常驻内存的 tick 量约为图表柱数的一倍,EURUSD 在 M1 下若 rates_total=500,则缓冲上限约 1000 个报价。 最后用 PlotIndexSetInteger 的 PLOT_SHIFT 把两条线往左推 rates_total-ticks_stored 根,保证买卖价线贴合最新 K 线;并把 last_price_bid / last_price_ask 写进 rates_total-1 位置,让指标窗口始终显示实时报价。外汇与贵金属点差跳动频繁,这种自存文件方式在跳空时可能丢失部分 tick,验证前先开 MT5 用脚本跑一分钟观察文件行数。
AskPosition=StringFind(file_buffer," ",BidPosition)+class="num">1; class=class="str">"cmt">// 如果要绘制卖价线,将其加到BidBuffer[]数组中 if(BidLineEnable) BidBuffer[ticks_stored]=StringToDouble(StringSubstr(file_buffer,BidPosition,AskPosition-BidPosition-class="num">1)); class=class="str">"cmt">// 如果要绘制买价线,将其加到AskBuffer[]数组中 if(AskLineEnable) AskBuffer[ticks_stored]=StringToDouble(StringSubstr(file_buffer,AskPosition)); class=class="str">"cmt">// 报价计数器值加一 ticks_stored++; } } } class=class="str">"cmt">// 如果数据已经被读取过 else { class=class="str">"cmt">// 将文件指针移动到文件末端 FileSeek(file_handle,class="num">0,SEEK_END); class=class="str">"cmt">// 形成一个要写入文件的字符串 StringConcatenate(file_buffer,TimeCurrent()," ",DoubleToString(last_price_bid,_Digits)," ",DoubleToString(last_price_ask,_Digits)); class=class="str">"cmt">// 将一个字符串写入文件 FileWrite(file_handle,file_buffer); class=class="str">"cmt">// 如果要绘制卖价线,将最新的卖价加到BidBuffer[]数组中 if(BidLineEnable) BidBuffer[ticks_stored]=last_price_bid; class=class="str">"cmt">// 如果要绘制买价线,将最新的买价加到AskBuffer[]数组中 if(AskLineEnable) AskBuffer[ticks_stored]=last_price_ask; class=class="str">"cmt">// 报价计数器值加一 ticks_stored++; } class=class="str">"cmt">// 关闭文件 FileClose(file_handle); class=class="str">"cmt">// 如果报价数量大于等于图表中的柱形图 if(ticks_stored>=rates_total) { class=class="str">"cmt">// 移除前半部分tick_stored/2个报价,转移剩余报价 for(i=ticks_stored/class="num">2;i<ticks_stored;i++) { class=class="str">"cmt">// 如果要绘制卖价线,将BidBuffer[]数组后tick_stored/2个元素移到前面来 if(BidLineEnable) BidBuffer[i-ticks_stored/class="num">2]=BidBuffer[i]; class=class="str">"cmt">// 如果要绘制买价线,将AskBuffer[]数组后tick_stored/2个元素移到前面来 if(AskLineEnable) AskBuffer[i-ticks_stored/class="num">2]=AskBuffer[i]; } class=class="str">"cmt">// 改变计数器的值 ticks_stored-=ticks_stored/class="num">2; } class=class="str">"cmt">// 移动卖价线使其和价格图表对齐 PlotIndexSetInteger(class="num">0,PLOT_SHIFT,rates_total-ticks_stored); class=class="str">"cmt">// 移动买价线使其和价格图表对齐 PlotIndexSetInteger(class="num">1,PLOT_SHIFT,rates_total-ticks_stored); class=class="str">"cmt">// 如果要绘制卖价线,将值存入BidBuffer []数组的最后一个元素中 class=class="str">"cmt">// 指标窗口中显示最新的卖价 if(BidLineEnable) BidBuffer[rates_total-class="num">1]=last_price_bid; class=class="str">"cmt">// 如果要绘制买价线,将值存入AskBuffer []数组的最后一个元素中 class=class="str">"cmt">// 在指标窗口中显示最新的买价 if(AskLineEnable) AskBuffer[rates_total-class="num">1]=last_price_ask; class=class="str">"cmt">// 从OnCalculate()返回一个非零值 class="kw">return(rates_total); }
「把成交笔数塞进一根蜡烛里」
常规 MT5 蜡烛按固定时间切片,但「订单号烛形」换了个切法:每根蜡烛由预定义的 N 笔报价(tick)堆出来,而不是由分钟或小时框出来。这样在流动性突变的贵金属或外汇盘面,等量成交结构可能比等时结构更早就暴露出价格推进的节奏断层。 指标在独立子窗口绘制,只挂一种图形——彩色蜡烛。它需要 5 个缓冲区:开高低收各占一个,外加一个颜色索引缓存;中间计算的报价流另用 TicksBuffer[] 承接,不绘出。默认配色给了 Gray、Red、Green 三档,用来区分不同状态蜡烛。 用户能改的输入就三个:ticks_in_candle(每根蜡烛的 tick 数,样例写死 16)、applied_price(取 Bid 还是 Ask,枚举 0/1)、path_prefix(历史 tick 数据文件的前缀与路径)。所有传入报价都会被写进文件,数据格式和前文订单号指标一致,所以两套工具可以共用同一批落盘数据。 OnInit 里先把 0~4 号缓冲区绑成指标数据/颜色索引,5 号绑成计算用,再把开高低收和颜色数组全部 ArraySetAsSeries(true)——最新一根索引为 0,符合 MT5 时间序列习惯。 实盘验证建议:把 ticks_in_candle 从 16 调到 50 看 XAUUSD 的 M1 窗口,等量蜡烛对亚盘窄幅、美盘放量两种时段的形态差异会非常直观;外汇和贵金属杠杆高,等量切片只是视角切换,不预示方向。
class=class="str">"cmt">// 指标将在独立的窗口中绘制 class="macro">#class="kw">property indicator_separate_window class=class="str">"cmt">// 只用到一个绘图形状,彩色蜡烛线 class="macro">#class="kw">property indicator_plots class="num">1 class=class="str">"cmt">// 我们需要4个缓存存储开盘、最高、最低和收盘价格,以及1个颜色索引缓存 class="macro">#class="kw">property indicator_buffers class="num">5 class=class="str">"cmt">// 确定绘图类型:彩色蜡烛线 class="macro">#class="kw">property indicator_type1 DRAW_COLOR_CANDLES class=class="str">"cmt">// 确定蜡烛线的颜色 class="macro">#class="kw">property indicator_color1 Gray,Red,Green / / 声明枚举值 enum price_types( Bid, Ask ) class=class="str">"cmt">// 输入参数ticks_in_candle确定 class=class="str">"cmt">// 一根蜡烛线的tick数量 class="kw">input class="type">int ticks_in_candle=class="num">16; class=class="str">"cmt">//蜡烛线的tick数量 class=class="str">"cmt">// price_types类型的输入参数applied_price,表示 class=class="str">"cmt">// 用于计算指标的数据:买价或卖价。 class="kw">input price_types applied_price=class="num">0; class=class="str">"cmt">// 价格 class=class="str">"cmt">// path_prefix输入参数确定了路径和文件名的前缀 class="kw">input class="type">class="kw">string path_prefix=""; class=class="str">"cmt">// 文件名前缀 class=class="str">"cmt">// ticks_stored变量保存报价的数量 class="type">int ticks_stored; class=class="str">"cmt">// TTicksBuffer []数组用来存储价格 class=class="str">"cmt">// OpenBuffer [], HighBuffer [], LowBuffer [] 以及 CloseBuffer [] 数组 class=class="str">"cmt">// 用来存储蜡烛线的开盘、最高、最低和收盘价 class=class="str">"cmt">// ColorIndexBuffer []数组用来存储彩色蜡烛线的索引 class="type">class="kw">double TicksBuffer[],OpenBuffer[],HighBuffer[],LowBuffer[],CloseBuffer[],ColorIndexBuffer[]; class="type">void OnInit() { class=class="str">"cmt">// OpenBuffer[]数组是一个指标缓存 SetIndexBuffer(class="num">0,OpenBuffer,INDICATOR_DATA); class=class="str">"cmt">// HighBuffer[]数组是一个指标缓存 SetIndexBuffer(class="num">1,HighBuffer,INDICATOR_DATA); class=class="str">"cmt">// LowBuffer[]数组是一个指标缓存 SetIndexBuffer(class="num">2,LowBuffer,INDICATOR_DATA); class=class="str">"cmt">// CloseBuffer[]数组是一个指标缓存 SetIndexBuffer(class="num">3,CloseBuffer,INDICATOR_DATA); class=class="str">"cmt">// ColorIndexBuffer[]数组是颜色索引的缓存 SetIndexBuffer(class="num">4,ColorIndexBuffer,INDICATOR_COLOR_INDEX); class=class="str">"cmt">// TicksBuffer[]数组用来存储中间计算值 SetIndexBuffer(class="num">5,TicksBuffer,INDICATOR_CALCULATIONS); class=class="str">"cmt">// 设置OpenBuffer[]数组为时间序列 ArraySetAsSeries(OpenBuffer,true); class=class="str">"cmt">// 设置HighBuffer[]数组为时间序列 ArraySetAsSeries(HighBuffer,true); class=class="str">"cmt">// 设置LowBuffer[]数组为时间序列 ArraySetAsSeries(LowBuffer,true);
把外部报价塞进自定义指标的缓冲区
在 MT5 写自定义指标时,若想用外部 txt 里的逐笔报价还原 K 线,第一步是把缓冲数组按时间序列排好。CloseBuffer 和 ColorIndexBuffer 都调用 ArraySetAsSeries(...,true),这样索引 0 永远对应最新一根棒,避免画图错位。 四个价格图形(开高低收分别对应 PlotIndex 0~3)都要设 PLOT_EMPTY_VALUE 为 0,否则空值会被画成 0 轴附近的杂线,干扰价格行为判读。外汇与贵金属波动剧烈,这类外部数据回放仅用于复盘,实盘高风险。
| OnCalculate 里先抓 SymbolInfoDouble 的 BID/ASK 作为 last_price_bid / last_price_ask,再用 StringConcatenate 拼出『路径前缀+品种名.txt』。FileOpen 用 FILE_READ | FILE_WRITE | FILE_ANSI | FILE_SHARE_READ,意味着指标运行期间文件可被其他程序并行读,适合小布盯盘类 AIGC 工具边写边读。 |
|---|
首根计算(prev_calculated==0)时,先量首行长度 line_string_len,若 FileSize 超过 line_string_len*rates_total/2,就 FileSeek 到末尾倒数 rates_total/2 行,只加载最近半屏报价;否则从头读。这个阈值直接决定历史回放的内存占用,调小 rates_total/2 能更快冷启动。
class=class="str">"cmt">// 设置CloseBuffer[]数组为时间序列 ArraySetAsSeries(CloseBuffer,true); class=class="str">"cmt">// 设置ColorIndexBuffer []数组为时间序列 ArraySetAsSeries(ColorIndexBuffer,true); class=class="str">"cmt">// 开盘价(第0个图形)的空值不应被绘制 PlotIndexSetDouble(class="num">0,PLOT_EMPTY_VALUE,class="num">0); class=class="str">"cmt">// 最高价(第1个图形)的空值不应被绘制 PlotIndexSetDouble(class="num">1,PLOT_EMPTY_VALUE,class="num">0); class=class="str">"cmt">// 最低价(第2个图形)的空值不应被绘制 PlotIndexSetDouble(class="num">2,PLOT_EMPTY_VALUE,class="num">0); class=class="str">"cmt">// 收盘价(第3个图形)的空值不应被绘制 PlotIndexSetDouble(class="num">3,PLOT_EMPTY_VALUE,class="num">0); class="type">int OnCalculate(const class="type">int rates_total, const class="type">int prev_calculated, const class="type">class="kw">datetime &time[], const class="type">class="kw">double &open[], const class="type">class="kw">double &high[], const class="type">class="kw">double &low[], const class="type">class="kw">double &close[], const class="type">long &tick_volume[], const class="type">long &volume[], const class="type">int &spread[]) { class=class="str">"cmt">//file_handle变量定义文件的句柄 class=class="str">"cmt">// BidPosition和AskPosition是字符串中头寸的卖出和买价; class=class="str">"cmt">// line_string_len是从文件中读取的字符串的长度, class=class="str">"cmt">// CandleNumber确定开盘、最高、最低和收盘价的蜡烛线的编号 class=class="str">"cmt">// i - 循环计数器; class="type">int file_handle,BidPosition,AskPosition,line_string_len,CandleNumber,i; class=class="str">"cmt">// last_price_bid变量是最近接收到的卖价 class="type">class="kw">double last_price_bid=SymbolInfoDouble(Symbol(),SYMBOL_BID); class=class="str">"cmt">// last_price_ask变量是最近接收到的买价 class="type">class="kw">double last_price_ask=SymbolInfoDouble(Symbol(),SYMBOL_ASK); class=class="str">"cmt">// filename是文件名,the file_buffer是一个字符串, class=class="str">"cmt">// 用来读取和写入字符串数据的缓存 class="type">class="kw">string filename,file_buffer; class=class="str">"cmt">// 设置TicksBuffer[]数组的大小 ArrayResize(TicksBuffer,ArraySize(CloseBuffer)); class=class="str">"cmt">// 由path_prefix变量和交易品种 class=class="str">"cmt">// 以及“.Txt”构成的文件名 StringConcatenate(filename,path_prefix,Symbol(),".txt"); class=class="str">"cmt">// 打开一个文件供读写,编码类型ANSI,读共享模式 file_handle=FileOpen(filename,FILE_READ|FILE_WRITE|FILE_ANSI|FILE_SHARE_READ); if(prev_calculated==class="num">0) { class=class="str">"cmt">// 读取文件的首行并确定字符串的长度 line_string_len=StringLen(FileReadString(file_handle))+class="num">2; class=class="str">"cmt">// 如果文件很大(包含了大于rates_total/2个报价) if(FileSize(file_handle)>(class="type">class="kw">ulong)line_string_len*rates_total/class="num">2) { class=class="str">"cmt">// 设置文件指针,读取最近的rates_total/2个报价 FileSeek(file_handle,-line_string_len*rates_total/class="num">2,SEEK_END); class=class="str">"cmt">// 将文件指针移动到下一行的开头 FileReadString(file_handle); } class=class="str">"cmt">// 如果文件大小很小 else { class=class="str">"cmt">// 将文件指针移动到文件的开头
◍ 从tick文件回放重建蜡烛线缓冲
这段逻辑处理的是历史 tick 文件的二次读取与实时追加两条路径。若文件已存在且之前读过,先把指针拨回头部 FileSeek(file_handle,0,SEEK_SET),重置 ticks_stored=0,然后逐行 FileReadString 直到 FileIsEnding 返回 false;只处理长度大于 6 的行,避免表头或空行污染缓冲。 行内解析靠空格定位:第一次 StringFind 找空格,第二次从该位置+1 再找空格得到卖价起点 BidPosition,第三次从 BidPosition 找空格得到买价起点 AskPosition。applied_price==0 取卖价子串转 double 存入 TicksBuffer,applied_price==1 则直接取 AskPosition 之后的买价。每读入一个有效报价 ticks_stored 自增。 若数据未被读取过,则 FileSeek 到 SEEK_END,用 TimeCurrent、last_price_bid、last_price_ask 拼成字符串 FileWrite 追加,并把最新价按 applied_price 写入 TicksBuffer 末尾。注意这里 DoubleToString 用了 _Digits 精度,黄金 XAUUSD 通常 _Digits=3 或 2,写文件前确认品种小数位,否则回读时 StringToDouble 精度不对。 文件关闭后有个关键裁剪:当 ticks_stored>=rates_total,把前 ticks_stored/2 个旧报价丢弃,剩余前半移回数组头部,计数器减半。这意味着你的可视 tick 窗口始终约等于当前图表柱数,回测大周期时内存不会无限涨。 最后用 MathFloor((ticks_stored-1)/ticks_in_candle) 推算每根自定义蜡烛包含多少 tick,CandleNumber 从 -1 起,循环里若报价仍属当前蜡烛就更新 CloseBuffer 与 HighBuffer。HighBuffer 仅在 TicksBuffer[i]>HighBuffer[CandleNumber] 时刷新,符合价格行为里「影子才代表流动性试探」的直觉。外汇与贵金属杠杆高,自建 tick 蜡烛仅用于复盘验证,实盘信号须结合点差与滑点评估。
FileSeek(file_handle,class="num">0,SEEK_SET); } class=class="str">"cmt">// 重置报价计数器 ticks_stored=class="num">0; class=class="str">"cmt">//一直读到文件结尾 class="kw">while(FileIsEnding(file_handle)==false) { class=class="str">"cmt">// 从文件中读取一个字符串 file_buffer=FileReadString(file_handle); class=class="str">"cmt">// 如果字符串长度大于6,处理该字符串 if(StringLen(file_buffer)>class="num">6) { class=class="str">"cmt">// 在行中找到卖价的开始位置 BidPosition=StringFind(file_buffer," ",StringFind(file_buffer," ")+class="num">1)+class="num">1; class=class="str">"cmt">//在行中找到买价的开始位置 AskPosition=StringFind(file_buffer," ",BidPosition)+class="num">1; class=class="str">"cmt">// 如果使用了卖价,将其加到TicksBuffer[]数组中 if(applied_price==class="num">0) TicksBuffer[ticks_stored]=StringToDouble(StringSubstr(file_buffer,BidPosition,AskPosition-BidPosition-class="num">1)); class=class="str">"cmt">// 如果使用了买价,将其加到TicksBuffer[]数组中 if(applied_price==class="num">1) TicksBuffer[ticks_stored]=StringToDouble(StringSubstr(file_buffer,AskPosition)); class=class="str">"cmt">// 报价计数器值加一 ticks_stored++; } } } class=class="str">"cmt">// 如果数据已经被读取过 else { class=class="str">"cmt">// 将文件指针移动到文件末端 FileSeek(file_handle,class="num">0,SEEK_END); class=class="str">"cmt">// 形成一个要写入文件的字符串 StringConcatenate(file_buffer,TimeCurrent()," ",DoubleToString(last_price_bid,_Digits)," ",DoubleToString(last_price_ask,_Digits)); class=class="str">"cmt">// 将一个字符串写入文件 FileWrite(file_handle,file_buffer); class=class="str">"cmt">// 如果使用了卖价,将最新的卖价加到TicksBuffer[]数组中 if(applied_price==class="num">0) TicksBuffer[ticks_stored]=last_price_bid; class=class="str">"cmt">// 如果使用了买价,将最新的买价加到TicksBuffer[]数组中 if(applied_price==class="num">1) TicksBuffer[ticks_stored]=last_price_ask; class=class="str">"cmt">// 报价计数器值加一 ticks_stored++; } class=class="str">"cmt">// 关闭文件 FileClose(file_handle); class=class="str">"cmt">// 如果报价数量大于等于图表中的柱形图的数量 if(ticks_stored>=rates_total) { class=class="str">"cmt">// 移除前半部分tick_stored/2个报价,转移剩余报价 for(i=ticks_stored/class="num">2;i<ticks_stored;i++) { class=class="str">"cmt">// 将数据移到TicksBuffer[]数组开头的tick_stored/2个元素中 TicksBuffer[i-ticks_stored/class="num">2]=TicksBuffer[i]; } class=class="str">"cmt">// 改变报价计数器 ticks_stored-=ticks_stored/class="num">2; } class=class="str">"cmt">// 为CandleNumber分配一个无效的蜡烛线数字 CandleNumber=-class="num">1; class=class="str">"cmt">// 查找所有能构成蜡烛线的价格数据 for(i=class="num">0;i<ticks_stored;i++) { class=class="str">"cmt">//如果这个蜡烛线已经形成 if(CandleNumber==(class="type">int)(MathFloor((ticks_stored-class="num">1)/ticks_in_candle)-MathFloor(i/ticks_in_candle))) { class=class="str">"cmt">// 当前报价仍就是当前蜡烛线的收盘价 CloseBuffer[CandleNumber]=TicksBuffer[i]; class=class="str">"cmt">// 如果当前价格大于当前蜡烛线的最高价, class=class="str">"cmt">// 那么它将是蜡烛线的新高价 if(TicksBuffer[i]>HighBuffer[CandleNumber]) HighBuffer[CandleNumber]=TicksBuffer[i];
「用逐笔报价重建蜡烛线的着色逻辑」
把逐笔报价(tick)聚合成自定义蜡烛线时,每一根的形态和颜色都依赖缓冲区实时刷新。下面这段逻辑直接决定你在副图里看到的是绿、红还是灰。 若蜡烛线已在之前被计算过,新报价只需更新边界:当TicksBuffer[i]低于LowBuffer[CandleNumber]时,最低价被刷新;收盘高于开盘则ColorIndexBuffer记为2(绿),低于则记1(红),相等记0(灰)。 若蜡烛线尚未建立,先用MathFloor算出CandleNumber,然后把当前报价同时写入开高低收四个缓冲区,颜色先置0(灰)等待后续tick确认。这样每根蜡烛在形成过程中从灰起步,方向明朗后才染色。 外汇与贵金属点差跳变频繁,这种按tick重绘的蜡烛在重要数据发布时可能出现瞬间灰→红→绿,属正常高波动现象,复盘时需注意滑点风险。
class=class="str">"cmt">// 如果当前价格小于当前蜡烛线的最低价,那么它将是蜡烛线的新低价 if(TicksBuffer[i]<LowBuffer[CandleNumber]) LowBuffer[CandleNumber]=TicksBuffer[i]; class=class="str">"cmt">// 如果蜡烛线看涨,它的颜色索引将为2(绿色) if(CloseBuffer[CandleNumber]>OpenBuffer[CandleNumber]) ColorIndexBuffer[CandleNumber]=class="num">2; class=class="str">"cmt">// 如果蜡烛线看跌,它的颜色索引将为1(红色) if(CloseBuffer[CandleNumber]<OpenBuffer[CandleNumber]) ColorIndexBuffer[CandleNumber]=class="num">1; class=class="str">"cmt">// 如果开盘和收盘价一样,它的颜色索引将为0(灰色) if(CloseBuffer[CandleNumber]==OpenBuffer[CandleNumber]) ColorIndexBuffer[CandleNumber]=class="num">0; } class=class="str">"cmt">// 如果这个蜡烛线还没被计算过 else { class=class="str">"cmt">// 让我们来确定蜡烛线的索引 CandleNumber=(class="type">int)(MathFloor((ticks_stored-class="num">1)/ticks_in_candle)-MathFloor(i/ticks_in_candle)); class=class="str">"cmt">// 当前报价是蜡烛线的开盘价 OpenBuffer[CandleNumber]=TicksBuffer[i]; class=class="str">"cmt">// 当前报价是蜡烛线的最高价 HighBuffer[CandleNumber]=TicksBuffer[i]; class=class="str">"cmt">// 当前报价是蜡烛线的最低价 LowBuffer[CandleNumber]=TicksBuffer[i]; class=class="str">"cmt">// 当前报价是蜡烛线的收盘价 CloseBuffer[CandleNumber]=TicksBuffer[i]; class=class="str">"cmt">// 此蜡烛线的颜色索引将为0(灰色) ColorIndexBuffer[CandleNumber]=class="num">0; } } class=class="str">"cmt">// 从OnCalculate()返回一个非零值 class="kw">return(rates_total); }
把工具请下神坛
这两套指标——订单号图表与订单号烛形图——说到底只是把 MT5 里被忽略的 tick 流重新摆回肉眼可见的位置。tickindicator.mq5 占 12.68 KB,tickcolorcandles.mq5 占 16.96 KB,下载解压后直接丢进 MQL5/Indicators 就能编译挂盘。 有读者在 EURUSD M1 上原样编译后遇到底部红蓝柱不显示、只有左上角白字买卖价,左移图表才出图;后来换用作者更新的修正版指标,问题消失。这说明老版本对当前蜡烛可视区的缓冲刷新有 bug,新版本才修了。 外汇与贵金属 tick 数据量大、跳空频繁,这类工具只帮你看见微观结构,不替你判断方向。真要验证,自己开 MT5 挂上修正版,把图表左右拖几下,看底部窗口什么时候补出柱体,比看任何说明都直观。