自定义品种(符号):实践基础(基础篇)
自定义品种能玩出哪些衍生图表
MT5 从 2020 年 12 月就已支持创建自定义品种(符号),可用 MQL API 在终端和程序层直接访问。这类品种能挂标准图表、加载指标、画对象,甚至拿去跑 EA,不局限于经纪商给的原始报价。 数据源可以是券商真实品种,也可以是外部喂价。实操里常见的几种转换玩法包括:等量和等范围图表、即时报价图、报价时移附带 K 线形态变换、以及 Renko。这些衍生图给老手多了一层过滤噪音的视角,但外汇和贵金属本身高杠杆高风险,衍生图只是分析辅助,不替你兜底。 文中约定用黑底图表示源品种、白底图表示自定义品种,方便对照形态差异。后续我们还会搭一套自适应 EA 机制,让它交易真实品种,但信号来自图表上的衍生自定义品种。
◍ 用自定义品种在 MT5 拼出等量柱
等量图表不按固定时间帧切柱,而是等相邻柱线的交易量(或价格变动点数)累到预设值才收一根。MT4/5 都不自带这种图,MT5 里得靠自定义品种加一段 EA 把真实 tick 或 M1 数据重排成等量/等范围柱。 原 EqualVolumeBars.mq4 移植到 MT5 时,extern 改成 input,删掉 CustomPeriod,用 StartDate 替掉 StartYear/StartMonth。MT5 不需要再写 hst 文件,直接走自定义品种 API 读真实报价历史。若经纪商对外汇给的是真实成交量(交易所品种常见,外汇通常没有),WorkMode 设 EqualRealVolumes 就能按实际交易量切柱,而不数 tick 次数。 FromM1 参数决定吃 M1 柱(true)还是裸 tick 历史(false)。跑 tick 模式别把起点拉太远,EURUSD 每根柱装 1000 个 tick 的实测图里,除最后一根在长,其余 tick 数严丝合缝相等;等范围模式设 100 点波动收一根也正常出图。自定义品种图表时间帧必须锁 M1,剧烈行情下柱时间可能领先真实钟,但这类图本就弱化绝对时间。 下方片段是品种克隆与写柱的核心:SymbolSelect 失败就 CloneProperties 从当前图拷贝属性;WriteToFile 用 CustomRatesUpdate 推一根 MqlRates;RefreshWindow 用 CustomTicksAdd 造 tick 触发 OnTick。Tick 模式靠 TicksBuffer 类封 CopyTicks/CopyTicksRange,一次拉 10000 条缓着读。
class="macro">#include <Symbol.mqh> if(!SymbolSelect(symbolName, true)) { class="kw">const SYMBOL Symb(symbolName); Symb.CloneProperties(_Symbol); if(!SymbolSelect(symbolName, true)) { Alert("Can&class="macro">#x27;t select symbol:", symbolName, " err:", GetLastError()); class="kw">return INIT_FAILED; } } class="type">void WriteToFile(class="type">class="kw">datetime t, class="type">class="kw">double o, class="type">class="kw">double l, class="type">class="kw">double h, class="type">class="kw">double c, class="type">long v, class="type">long m = class="num">0) { class="type">MqlRates r[class="num">1]; r[class="num">0].time = t; r[class="num">0].open = o; r[class="num">0].low = l; r[class="num">0].high = h; r[class="num">0].close = c; r[class="num">0].tick_volume = v; r[class="num">0].spread = class="num">0; r[class="num">0].real_volume = m; class="type">int code = CustomRatesUpdate(symbolName, r); if(code < class="num">1) { Print("CustomRatesUpdate failed: ", GetLastError()); } } class="type">void RefreshWindow(class="kw">const class="type">class="kw">datetime t) { class="type">MqlTick ta[class="num">1]; SymbolInfoTick(_Symbol, ta[class="num">0]); ta[class="num">0].time = t; ta[class="num">0].time_msc = ta[class="num">0].time * class="num">1000; if(CustomTicksAdd(symbolName, ta) == -class="num">1) { Print("CustomTicksAdd failed:", GetLastError(), " ", (class="type">long) ta[class="num">0].time); ArrayPrint(ta); } } class="macro">#define TICKS_ARRAY class="num">10000 class TicksBuffer { class="kw">private: class="type">MqlTick array[]; class="type">int tick; class="kw">public: class="type">bool fill(class="type">class="kw">ulong &cursor, class="kw">const class="type">bool history = class="kw">false) { class="type">int size = history ? CopyTicks(_Symbol, array, COPY_TICKS_ALL, cursor, TICKS_ARRAY) : CopyTicksRange(_Symbol, array, COPY_TICKS_ALL, cursor); if(size == -class="num">1) { Print("CopyTicks failed: ", GetLastError());
「用游标分页把历史 tick 啃完」
这段逻辑解决的是 MT5 里 CopyTicks 一次性拉不全的问题:用 cursor 做毫秒级游标,每次 fill 之后把 cursor 推进到 array[size-1].time_msc + 1,下一轮从这条 tick 之后接着取,避免重复和漏数据。 当 size==0 且 history 为真,说明历史区间已经到底,Print 打出 End of CopyTicks 的 datetime(cursor/1000 转成秒级时间),直接 return false 退出循环,不会空转烧 CPU。 read 方法靠 tick 下标自增遍历当前缓冲,ArraySize 边界内返回 true 并吐出 MqlTick,越界返回 false;外层 while 里 tb.fill 成功且未点停止,就 inner while 把这一批 tick 逐个读出来处理。 IsNewBar() 或 now_volume<1 时触发 WriteToFile,意味着新 K 线起点或首笔成交才落盘,盘中重复 tick 不写,文件不会爆。外汇与贵金属 tick 流在高波动时段可能瞬时过万笔,这种分页读法是实盘回放脚本的底线写法,断点重跑也只需存 cursor。
class="kw">return class="kw">false; } else if(size == class="num">0) { if(history) Print("End of CopyTicks at ", (class="type">class="kw">datetime)(cursor / class="num">1000)); class="kw">return class="kw">false; } cursor = array[size - class="num">1].time_msc + class="num">1; tick = class="num">0; class="kw">return true; } class="type">bool read(class="type">MqlTick &t) { if(tick < ArraySize(array)) { t = array[tick++]; class="kw">return true; } class="kw">return class="kw">false; } }; TicksBuffer tb; class="kw">while(tb.fill(cursor, true) && !IsStopped()) { class="type">MqlTick t; class="kw">while(tb.read(t)) { ... class=class="str">"cmt">// New or first bar if(IsNewBar() || now_volume < class="num">1) { WriteToFile(...); } } }
把逐笔报价变成能挂EA的M1柱
MT5 的「市场观察」里自带即时报价图表,但最多只留 2000 个 tick,不能全屏、不能加指标和 EA,基本只能肉眼瞅。想拿高频数据跑策略,得绕开这个残血面板。 办法是用自定义品种把 tick 重建成柱线:在 EURUSD 任意周期图上跑一个非交易 EA,它会生出 EURUSD_ticks 这个品种,每根 M1 其实就是一笔报价。核心靠 CopyTicks 和 CustomRatesUpdate 两个 API 搬数据,示例文件 Ticks2Bars.mq5 直接能用。 EA 几个输入要理清:Limit 默认 1000,设 0 就只联线不补历史;Reset 默认 true 会清掉旧 tick 历史;LoopBack 默认 true 时图表永远只留 Limit 根柱,超出的从尾部挤出;EmulateTicks 默认 true 用来模拟新 tick 到达事件,EA 逻辑才跑得动;RenderBars 选 OHLC 时报价带实体(高=ask、低=bid、收盘=last),选 HighLow 就画成针线(开收都在中间价)。 自定义品种的柱时间是编的:开 LoopBack 时最后一根对齐当前分钟,前一根倒推一分钟;关掉就自启动起每分钟累加,超 Limit 的部分全在虚拟未来。最右一根 M1 仍对应最新价,所以联线和测试器都能跑。真要下单得把代码里 EURUSD_ticks 换回 EURUSD,因为自定义品种券商服务器不认。 外汇和贵金属 tick 刷新极快,手动标图基本不可能,只能靠指标或脚本全自动。这套把 tick 当柱线的思路,能让你用普通指标测剥头皮策略,不用自己攒缓冲区算信号。
class="type">bool apply(class="kw">const class="type">class="kw">datetime cursor, class="kw">const class="type">MqlTick &t, class="type">MqlRates &r) { class="kw">static class="type">MqlTick p; class=class="str">"cmt">// eliminate strange things if(t.ask == class="num">0 || t.bid == class="num">0 || t.ask < t.bid) class="kw">return class="kw">false; r.high = t.ask; r.low = t.bid; if(t.last != class="num">0) { if(RenderBars == OHLC) { if(t.last > p.last) { r.open = r.low; r.close = r.high; } else { r.open = r.high; r.close = r.low; } } else { r.open = r.close = (r.high + r.low) / class="num">2; } if(t.last < t.bid) r.low = t.last; if(t.last > t.ask) r.high = t.last; r.close = t.last; } else { if(RenderBars == OHLC) { if((t.ask + t.bid) / class="num">2 > (p.ask + p.bid) / class="num">2) { r.open = r.low; r.close = r.high; } else { r.open = r.high; r.close = r.low; } } else { r.open = r.close = (r.high + r.low) / class="num">2; } } r.time = cursor; r.spread = (class="type">int)((t.ask - t.bid)/_Point); r.tick_volume = class="num">1; r.real_volume = (class="type">long)t.volume; p = t; class="kw">return true; }
◍ 把时间轴挪一挪,锤子线就变尖刺线
烛条形态本质是「价格序列 + 计算起点」的函数。把 H1 图表整体平移 15 分钟,原来成型的形态可能彻底消失,别处却长出新形态,但价格行为本身没变过。这说明形态外观很大程度是被经纪商时区和柱线开盘时间绑架的。 具体变形很直观:时间轴平移半根柱线,「锤子」能转成「尖刺」,「吊死鬼」能转成「乌云盖顶」;平移值和局部波动不同,还可能变成看涨或看跌的「吞噬」。切到更低周期,这些形态又可能变成「晨十字星」或「昏十字星」。 实战上,自定义品种能按指定秒数把报价偏移到过去或未来。例如把报价前移 5 分钟,M15 图表的柱线开盘收盘就从整点 0/15/30/45 分变成 10/25/40/55 分——价格没动,但指标和形态信号会提前冒出来。EURUSD H1 平移 30 分钟后,原图没出现的信号在自定义图上直接显现,常规烛条指标能给出双倍警报。外汇和贵金属波动剧烈,这类信号只是概率性参考,不等于走势反转确认。 TimeShift.mq5 这个 EA 用 Shift 参数(秒)控制偏移,从 Start 日期起重算历史,并默认开启 EmulateTicks 用 OnTick 实时处理。下面这段 add 函数就是偏移核心:先拉取新 tick,给每笔的时间戳加上 Shift,再写进自定义品种。这样你开 MT5 挂上 EA,改一个 Shift 值就能对比同源图表的不同形态分布。
class="type">class="kw">ulong lastTick; class="type">void add() { class="type">MqlTick array[]; class="type">int size = CopyTicksRange(_Symbol, array, COPY_TICKS_ALL, lastTick + class="num">1, LONG_MAX); if(size > class="num">0) { lastTick = array[size - class="num">1].time_msc; for(class="type">int i = class="num">0; i < size; i++) { array[i].time += Shift; array[i].time_msc += Shift * class="num">1000; } if(CustomTicksAdd(symbolName, array) == -class="num">1) { Print("Tick error: ", GetLastError()); } } } class="type">void OnTick(class="type">void) { ... if(EmulateTicks) { add(); } }