MetaTrader 5 的 WebSocket 使用 Windows API·综合运用
(3/3)· 不装第三方库,靠 WinHTTP 把 Binary.com 行情直接推进 MT5 图表
- WebSocket 收发与读取的底层校验逻辑
- 从 socket 读数据到干净断开连接
- WebSocket 客户端的私有句柄与缓冲布局
- WebSocket 类的私有成员与构造析构细节
- WebSocket 封装类的收发与错误复位接口
- 把 Binary.com 行情接进 MT5 自定义品种
- 接 Binary.com 行情要先填的三个参数
- 用 CCustomSymbol 接管外部品种
- 自定义品种的增删与属性读写封装
- 图表与自定义品种的自动接管
- 自定义品种初始化与历史边界抓取
- 用 CopyRates 取上一根 M1 蜡烛时间
- 用 CBinarySymbol 把二元期权报价搬进 MT5 自定义品种
- 自建二进制符号的初始化与鉴权链路
- 用 WebSocket 拉指定品种的 K 线历史
- 把二进制tick历史灌进自定义品种
- 用历史报价补齐 tick 数组的空白秒
- 把历史报价写进自定义 tick 缓冲
- 实时 tick 流的启停与落地
- 把外部 tick 灌进自定义品种的生命周期
- 正文
- 别急着下结论
◍ WebSocket 收发与读取的底层校验逻辑
在 MT5 里封装 WebSocket 客户端,发送前的状态校验决定了消息会不会静默丢失。SendString 方法先检查 initialized 与 clientState,两者不满足就直接返回 false 并写错误描述,避免向未连接句柄写数据导致崩溃。 SendString 会把字符串转成 BYTE 数组再发,注意它用 ArrayRemove 剥掉末尾的 \0 终止符,长度按 ArraySize 实算,以二进制消息类型投递。Send 方法则跳过字符串转换,直接收 BYTE& 缓冲区,适合已经打包好的行情或信令帧。 clientread 是接收侧核心:用 do-while 循环持续 WinHttpWebSocketReceive,只要缓冲区类型是 UTF8/Binary 的 FRAGMENT 就继续拼接,直到收到整帧。done 累计 transferred 字节数,最终通过 bytes 参数回传,调用方据此知道 rbuffer 里有多少有效负载。 外汇与贵金属行情推送走这套机制时延迟可能随 broker 服务器抖动,实际跑起来建议打印 bytes 与 rbuffertype 做几次抓包验证,确认分片合并符合预期。
class="type">bool CWebsocket::SendString(const class="type">class="kw">string msg) { if(!initialized) { setErrorDescription(WEBSOCKET_ERROR_NOT_INITIALIZED); class="kw">return(false); } if(clientState!=CONNECTED) { setErrorDescription(WEBSOCKET_ERROR_NOT_CONNECTED); class="kw">return(false); } if(StringLen(msg)<=class="num">0) { setErrorDescription(WEBSOCKET_ERROR_EMPTY_SEND_BUFFER); class="kw">return(false); } BYTE msg_array[]; StringToCharArray(msg,msg_array,class="num">0,WHOLE_ARRAY); ArrayRemove(msg_array,ArraySize(msg_array)-class="num">1,class="num">1); DWORD len=(ArraySize(msg_array)); class="kw">return(clientsend(msg_array,WINHTTP_WEB_SOCKET_BINARY_MESSAGE_BUFFER_TYPE)); } class="type">bool CWebsocket::Send(BYTE &buffer[]) { if(!initialized) { setErrorDescription(WEBSOCKET_ERROR_NOT_INITIALIZED); class="kw">return(false); } if(clientState!=CONNECTED) { setErrorDescription(WEBSOCKET_ERROR_NOT_CONNECTED); class="kw">return(false); } class="kw">return(clientsend(buffer,WINHTTP_WEB_SOCKET_BINARY_MESSAGE_BUFFER_TYPE)); } class="type">void CWebsocket::clientread(BYTE &rbuffer[],class="type">ulong &bytes) { WINHTTP_WEB_SOCKET_BUFFER_TYPE rbuffertype=-class="num">1; class="type">ulong done=class="num">0; class="type">ulong transferred=class="num">0; ZeroMemory(rbuffer); ZeroMemory(rxbuffer); bytes=class="num">0; do { class="type">ulong get=WinHttpWebSocketReceive(hWebSocket,rxbuffer,rxsize,transferred,rbuffertype); if(get) { setErrorDescription(); class="kw">return; } ArrayCopy(rbuffer,rxbuffer,(class="type">int)done,class="num">0,(class="type">int)transferred); done+=transferred; ZeroMemory(rxbuffer); transferred=class="num">0; } while(rbuffertype==WINHTTP_WEB_SOCKET_UTF8_FRAGMENT_BUFFER_TYPE || rbuffertype==WINHTTP_WEB_SOCKET_BINARY_FRAGMENT_BUFFER_TYPE); bytes=done; class="kw">return; }
从 socket 读数据到干净断开连接
MT5 里封装 WebSocket 客户端时,读服务端推送和收尾断开是最容易漏掉边界判断的两段。下面这组方法把「未初始化 / 未连接」两种状态都挡在读取之前,避免在 EA 跑实盘时莫名读到空字节。 Read() 直接把原始字节塞进 BYTE 数组,返回实际读到的字节数;若连接未就绪则返回 false(即 0)。ReadString() 则在内部建一个临时 buffer,调用 CharArrayToString 把字节转成字符串,读不到内容时给 _response 赋 NULL,返回字节数。 Close() 先判断状态是否为 CLOSED,否则置 CLOSING 并调用 WinHttpWebSocketClose,传入成功关闭状态码与空指针、长度 0;成功则清错误描述并 reset()。Abort() 在 Close() 之后把 serveraddress、serverName、serverPath 全清 NULL,端口置 0、isSecure 置 false、last_error 置 0,相当于把连接参数打回出厂设置。 最后几个 getter 很直白:LastErrorMessage() 吐错误串,LastError() 吐错误码,ClientState() 回传当前枚举状态。开 MT5 把这段贴进 CWebsocket 类,断点跟一次 Close→Abort,能确认连接资源有没有真的释放。
class="type">ulong CWebsocket::Read(BYTE &buffer[]) { if(!initialized) { setErrorDescription(WEBSOCKET_ERROR_NOT_INITIALIZED); class="kw">return(false); } if(clientState!=CONNECTED) { setErrorDescription(WEBSOCKET_ERROR_NOT_CONNECTED); class="kw">return(false); } class="type">ulong bytes_read_from_socket=class="num">0; clientread(buffer,bytes_read_from_socket); class="kw">return(bytes_read_from_socket); } class="type">ulong CWebsocket::ReadString(class="type">class="kw">string &_response) { if(!initialized) { setErrorDescription(WEBSOCKET_ERROR_NOT_INITIALIZED); class="kw">return(false); } if(clientState!=CONNECTED) { setErrorDescription(WEBSOCKET_ERROR_NOT_CONNECTED); class="kw">return(false); } class="type">ulong bytes_read_from_socket=class="num">0; BYTE buffer[]; clientread(buffer,bytes_read_from_socket); _response=(bytes_read_from_socket)?CharArrayToString(buffer):NULL; class="kw">return(bytes_read_from_socket); } class="type">void CWebsocket::Close(class="type">void) { if(clientState==CLOSED) class="kw">return; clientState=CLOSING; BYTE nullpointer[]= {}; class="type">ulong result=WinHttpWebSocketClose(hWebSocket,WINHTTP_WEB_SOCKET_SUCCESS_CLOSE_STATUS,nullpointer,class="num">0); if(result) setErrorDescription(); reset(); class="kw">return; } class="type">void CWebsocket::Abort(class="type">void) { Close(); serveraddress=serverName=serverPath=NULL; serverPort=class="num">0; isSecure=false; last_error=class="num">0; StringFill(errormsg,class="num">0); class="kw">return; } class="type">class="kw">string LastErrorMessage(class="type">void) { class="kw">return(errormsg); } class="type">uint LastError(class="type">void) { class="kw">return(last_error); } ENUM_WEBSOCKET_STATE ClientState(class="type">void) { class="kw">return(clientState); }
「WebSocket 客户端的私有句柄与缓冲布局」
在 MT5 里直接调 WinHTTP 做行情推送,第一步是把底层会话、连接、请求三套句柄分开存。CWebsocket 类把 hSession、hConnection、hRequest 以及最终的 hWebSocket 都放在 private 段,避免外部误关句柄导致通道闪断。 除了句柄,类内还维护了 serverName、serverPort、serverPath 三个拆分字段,并提供了 DomainName()、Port()、ServerPath() 三个只读方法回传。下面这三行就是取回连接参数的最小实现: string DomainName(void) { return(serverName); } INTERNET_PORT Port(void) { return(serverPort); } string ServerPath(void) { return(serverPath); } 接收侧用 BYTE 类型动态数组 rxbuffer[] 做裸字节缓冲,配合 rxsize 记录当前数组尺寸;isSecure 标记是否走 wss。实盘接币圈或经纪商 WebSocket 时,若 rxsize 设得太小(比如默认 1024),大帧深度数据可能截断,建议在初始化阶段按最大消息体×1.5 重设 ArrayResize。外汇与贵金属 WebSocket 接入属高风险操作,句柄泄漏会引发终端卡死,参数须自行在策略测试器外先验证。
class="type">class="kw">string DomainName(class="type">void) { class="kw">return(serverName); } INTERNET_PORT Port(class="type">void) { class="kw">return(serverPort); } class="type">class="kw">string ServerPath(class="type">void) { class="kw">return(serverPath); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//|Class CWebsocket | class=class="str">"cmt">//| Purpose: class for websocket client | class=class="str">"cmt">//+------------------------------------------------------------------+ class CWebsocket { class="kw">private: ENUM_WEBSOCKET_STATE clientState; class=class="str">"cmt">//websocket state HINTERNET hSession; class=class="str">"cmt">//winhttp session handle HINTERNET hConnection; class=class="str">"cmt">//winhttp connection handle HINTERNET hWebSocket; class=class="str">"cmt">//winhttp websocket handle HINTERNET hRequest; class=class="str">"cmt">//winhtttp request handle class="type">class="kw">string appname; class=class="str">"cmt">//optional application name sent as one of the headers in initial http request class="type">class="kw">string serveraddress; class=class="str">"cmt">//full server address class="type">class="kw">string serverName; class=class="str">"cmt">//server domain name INTERNET_PORT serverPort; class=class="str">"cmt">//port number class="type">class="kw">string serverPath; class=class="str">"cmt">//server path class="type">bool initialized; class=class="str">"cmt">//boolean flag that denotes the state of underlying winhttp infrastruture required for client BYTE rxbuffer[]; class=class="str">"cmt">//internal buffer for reading from the socket class="type">bool isSecure; class=class="str">"cmt">//secure connection flag class="type">ulong rxsize; class=class="str">"cmt">//rxbuffer arraysize class="type">class="kw">string errormsg; class=class="str">"cmt">//internal buffer for error messages
◍ WebSocket 类的私有成员与构造析构细节
在 MT5 里封装 WebSocket 客户端,第一步是把连接状态和底层句柄收进一个类里。下面这段声明把私有字段摊开:last_error 存最近一次 winhttp/win32 或类内部错误码,initialize、createSessionConnection、upgrade 等私有方法负责建连与协议升级,clientsend/clientread 直接操作 BYTE 数组收发。 构造函数里给所有句柄赋 NULL,rxsize 固定为 65539 字节——比标准 64KB 帧上限多 3 字节用于掩码和边界。ArrayResize(rxbuffer, 65539) 后立刻 ArrayFill 清零,StringInit(errormsg, 1000) 预留 1000 宽字符的错误描述空间,避免运行时反复分配。 析构时先 Close() 释放会话与套接字,再 ArrayFree(rxbuffer)。Connect 方法默认走 443 端口且 _secure=true,意味着不传参就是 HTTPS/WSS 加密链路;外汇或贵金属行情通过这类通道拉取时,网络中断与证书失效风险始终存在,需靠 last_error 做排查。
class="type">uint last_error; class=class="str">"cmt">//last winhttp/win32/class specific error class=class="str">"cmt">// class="kw">private methods class="type">bool initialize(const class="type">class="kw">string _serveraddress, const INTERNET_PORT _port, const class="type">class="kw">string _appname,class="type">bool _secure); class="type">bool createSessionConnection(class="type">void); class="type">bool upgrade(class="type">void); class="type">void reset(class="type">void); class="type">bool clientsend(BYTE &txbuffer[],WINHTTP_WEB_SOCKET_BUFFER_TYPE buffertype); class="type">void clientread(BYTE &rxbuffer[],class="type">ulong &bytes); class="type">void setErrorDescription(class="type">uint error=class="num">0); class="kw">public: CWebsocket(class="type">void):clientState(class="num">0), hSession(NULL), hConnection(NULL), hWebSocket(NULL), hRequest(NULL), serveraddress(NULL), serverName(NULL), serverPort(class="num">0), initialized(false), isSecure(false), rxsize(class="num">65539), errormsg(NULL), last_error(class="num">0) { ArrayResize(rxbuffer,(class="type">int)rxsize); ArrayFill(rxbuffer,class="num">0,rxsize,class="num">0); StringInit(errormsg,class="num">1000); } ~CWebsocket(class="type">void) { Close(); ArrayFree(rxbuffer); } class=class="str">"cmt">//class="kw">public methods class="type">bool Connect(const class="type">class="kw">string _serveraddress, const INTERNET_PORT _port=class="num">443, const class="type">class="kw">string _appname=NULL,class="type">bool _secure=true); class="type">void Close(class="type">void); class="type">bool SendString(const class="type">class="kw">string msg);
WebSocket 封装类的收发与错误复位接口
这段声明把 WebSocket 客户端的核心动作收敛成几个直白方法:Send 推二进制帧、ReadString 取文本响应、Read 收原始字节、Abort 强制断链。 ResetLastError 不是简单调系统函数,而是先把实例内的 last_error 置 0,再用 StringFill 把 errormsg 清空,最后才走全局 ::ResetLastError(),保证对象级与系统级错误状态同步。 对外暴露的 getter 也值得留意:LastErrorMessage 回传 errormsg、LastError 回 uint 错误码、ClientState 回 ENUM_WEBSOCKET_STATE 枚举、DomainName / Port / ServerPath 分别回服务器名、端口与路径。开 MT5 把这段塞进自定义类,断点跟一次 ResetLastError,能确认错误上下文确实被三层清零。
class="type">bool Send(BYTE &buffer[]); class="type">ulong ReadString(class="type">class="kw">string &response); class="type">ulong Read(BYTE &buffer[]); class="type">void Abort(class="type">void); class="type">void ResetLastError(class="type">void) { last_error=class="num">0; StringFill(errormsg,class="num">0); ::ResetLastError(); } class=class="str">"cmt">//class="kw">public getter methods class="type">class="kw">string LastErrorMessage(class="type">void) { class="kw">return(errormsg); } class="type">uint LastError(class="type">void) { class="kw">return(last_error); } ENUM_WEBSOCKET_STATE ClientState(class="type">void) { class="kw">return(clientState); } class="type">class="kw">string DomainName(class="type">void) { class="kw">return(serverName); } INTERNET_PORT Port(class="type">void) { class="kw">return(serverPort); } class="type">class="kw">string ServerPath(class="type">void) { class="kw">return(serverPath); } };
「把 Binary.com 行情接进 MT5 自定义品种」
要在 MT5 里验证外部数据源,最省事的做法是写一个 EA,把 Binary.com 的 WebSocket 流落地成自定义品种。加载到图表后,EA 会先拉历史,再开一个新图表用实时 tick 或 OHLC 更新;分别有 TickHistory 和 BarHistory 两个编译版本,逻辑几乎一致。 Binary.com 的接口基于 WebSocket,请求和响应都是 JSON。EA 依赖三个库:websocket.mqh 管连接,JAson.mqh(Alexey Sergeev 写、vivazzi github 可拿)解析 JSON,FileTxt.mqh 做文件读写。 可调输入只有三个:binary_appid 是访问 API 的字符串(去开发者门户申请,订阅 tick 流不需要用户令牌);binary_symbol 枚举选品种;binary_timeframe 决定历史下载后开哪周期图表。 下面这段是 EA 头部的包含与品种枚举,能直接看到接了哪些库、覆盖了哪些标的。注意 BINARY_frxXAUUSD、BINARY_frxXAGUSD 都在列,但外汇/贵金属杠杆高、波动剧烈,实盘接入前务必在策略测试器里先跑通。
class="macro">#include<websocket.mqh> class="macro">#include<JAson.mqh> class="macro">#include<Files/FileTxt.mqh> class="macro">#define BINARY_URL "ws.binaryws.com/websockets/v3?app_id=" class="macro">#define BINARY_SYMBOL_SETTINGS "binarysymbolset.json" class="macro">#define BINARY_SYMBOL_BASE_PATH "Binary.com\\" enum ENUM_BINARY_SYMBOL { BINARY_1HZ10V=class="num">0,class=class="str">"cmt">//Volatility class="num">10 (1s) BINARY_1HZ25V,class=class="str">"cmt">//Volatility class="num">25 (1s) BINARY_1HZ50V,class=class="str">"cmt">//Volatility class="num">50 (1s) BINARY_1HZ75V,class=class="str">"cmt">//Volatility class="num">75 (1s) BINARY_1HZ100V,class=class="str">"cmt">//Volatility class="num">100 (1s) BINARY_1HZ200V,class=class="str">"cmt">//Volatility class="num">200 (1s) BINARY_1HZ300V,class=class="str">"cmt">//Volatility class="num">300 (1s) BINARY_BOOM300N,class=class="str">"cmt">//BOOM class="num">300 BINARY_BOOM500,class=class="str">"cmt">//BOOM class="num">500 BINARY_BOOM1000,class=class="str">"cmt">//BOOM class="num">1000 BINARY_CRASH300N,class=class="str">"cmt">//CRASH class="num">300 BINARY_CRASH500,class=class="str">"cmt">//CRASH class="num">500 BINARY_CRASH1000,class=class="str">"cmt">//CRASH class="num">1000 BINARY_cryBTCUSD,class=class="str">"cmt">//BTCUSD BINARY_cryETHUSD,class=class="str">"cmt">//ETHUSD BINARY_frxAUDCAD,class=class="str">"cmt">//AUDCAD BINARY_frxAUDCHF,class=class="str">"cmt">//AUDCHF BINARY_frxAUDJPY,class=class="str">"cmt">//AUDJPY BINARY_frxAUDNZD,class=class="str">"cmt">//AUDNZD BINARY_frxAUDUSD,class=class="str">"cmt">//AUDUSD BINARY_frxBROUSD,class=class="str">"cmt">//BROUSD BINARY_frxEURAUD,class=class="str">"cmt">//EURAUD BINARY_frxEURCAD,class=class="str">"cmt">//EURCAD BINARY_frxEURCHF,class=class="str">"cmt">//EURCHF BINARY_frxEURGBP,class=class="str">"cmt">//EURGBP BINARY_frxEURJPY,class=class="str">"cmt">//EURJPY BINARY_frxEURNZD,class=class="str">"cmt">//EURNZD BINARY_frxEURUSD,class=class="str">"cmt">//EURUSD BINARY_frxGBPAUD,class=class="str">"cmt">//GBPAUD BINARY_frxGBPCAD,class=class="str">"cmt">//GBPCAD BINARY_frxGBPCHF,class=class="str">"cmt">//GBPCHF BINARY_frxGBPJPY,class=class="str">"cmt">//GBPJPY BINARY_frxGBPNOK,class=class="str">"cmt">//GBPNOK BINARY_frxGBPNZD,class=class="str">"cmt">//GBPNZD BINARY_frxGBPUSD,class=class="str">"cmt">//GBPUSD BINARY_frxNZDJPY,class=class="str">"cmt">//NZDJPY BINARY_frxNZDUSD,class=class="str">"cmt">//NZDUSD BINARY_frxUSDCAD,class=class="str">"cmt">//USDCAD BINARY_frxUSDCHF,class=class="str">"cmt">//USDCHF BINARY_frxUSDJPY,class=class="str">"cmt">//USDJPY BINARY_frxUSDMXN,class=class="str">"cmt">//USDMXN BINARY_frxUSDNOK,class=class="str">"cmt">//USDNOK BINARY_frxUSDPLN,class=class="str">"cmt">//USDPLN BINARY_frxUSDSEK,class=class="str">"cmt">//USDSEK BINARY_frxXAUUSD,class=class="str">"cmt">//XAUUSD BINARY_frxXAGUSD,class=class="str">"cmt">//XAGUSD BINARY_frxXPDUSD,class=class="str">"cmt">//XPDUSD BINARY_frxXPTUSD,class=class="str">"cmt">//XPTUSD BINARY_JD10,class=class="str">"cmt">//Jump class="num">10 Index BINARY_JD25,class=class="str">"cmt">//Jump class="num">25 Index BINARY_JD50,class=class="str">"cmt">//Jump class="num">50 Index BINARY_JD75,class=class="str">"cmt">//Jump class="num">75 Index BINARY_JD100,class=class="str">"cmt">//Jump class="num">100 Index BINARY_OTC_AEX,class=class="str">"cmt">//Dutch Index BINARY_OTC_AS51,class=class="str">"cmt">//Australian Index BINARY_OTC_DJI,class=class="str">"cmt">//Wall Street Index BINARY_OTC_FCHI,class=class="str">"cmt">//French Index BINARY_OTC_FTSE,class=class="str">"cmt">//UK Index BINARY_OTC_GDAXI,class=class="str">"cmt">//German Index BINARY_OTC_HSI,class=class="str">"cmt">//Hong Kong Index BINARY_OTC_N225,class=class="str">"cmt">//Japanese Index BINARY_OTC_NDX,class=class="str">"cmt">//US Tech Index BINARY_OTC_SPC,class=class="str">"cmt">//US Index BINARY_OTC_SSMI,class=class="str">"cmt">//Swiss Index BINARY_OTC_SX5E,class=class="str">"cmt">//Euro class="num">50 Index BINARY_R_10,class=class="str">"cmt">//Volatility class="num">10 Index BINARY_R_25,class=class="str">"cmt">//Volatility class="num">25 Index BINARY_R_50,class=class="str">"cmt">//Volatility class="num">50 Index BINARY_R_75,class=class="str">"cmt">//Volatility class="num">75 Index BINARY_R_100,class=class="str">"cmt">//Volatility class="num">100 Index BINARY_RDBEAR,class=class="str">"cmt">//Bear Market Index BINARY_RDBULL,class=class="str">"cmt">//Bull Market Index BINARY_stpRNG,class=class="str">"cmt">//Step Index BINARY_WLDAUD,class=class="str">"cmt">//AUD Index BINARY_WLDEUR,class=class="str">"cmt">//EUR Index BINARY_WLDGBP,class=class="str">"cmt">//GBP Index BINARY_WLDUSD,class=class="str">"cmt">//USD Index BINARY_WLDXAUclass=class="str">"cmt">//Gold Index };
◍ 接 Binary.com 行情要先填的三个参数
想在 MT5 里直连 Binary.com 的标的做价格行为验证,首先得把应用标识和交易标的在输入参数里配好。 代码中 binary_appid 留空字符串,对应的是你在 Binary.com 后台注册应用后拿到的 App ID,不填就拉不到私有行情流。 binary_symbol 默认给了 BINARY_R_100,即 100 秒重置的随机指数标的;binary_timeframe 写死 PERIOD_M1,代表用 1 分钟图承载报价。外汇与贵金属类衍生品杠杆高、重置机制特殊,实盘前务必在模拟环境跑通。 把这三条 input 原样贴进 EA 头部,编译后到 MT5 输入页填好 App ID,就能让小布盯盘脚本接管 R_100 的 1 分钟数据。
input class="type">class="kw">string binary_appid="";class=class="str">"cmt">//Binary.com registered application ID input ENUM_BINARY_SYMBOL binary_symbol=BINARY_R_100;class=class="str">"cmt">//Binary.com symbol input ENUM_TIMEFRAMES binary_timeframe=PERIOD_M1;class=class="str">"cmt">//Chart period
用 CCustomSymbol 接管外部品种
做跨源行情接入时,MT5 原生品种体系不够用。CCustomSymbol 把外部数据源的品种抽象成一个类,能建品种、读属性、开关图表,思路接近 fxsaber 的 SYMBOL 库,但接口更偏面向对象。 Initialize() 是入口:sy 传品种名,sy_path 放品种路径,chart_tf 决定回放历史时开哪周期图。它内部调 InitSymbol(),终端里没有该品种就建,有就载历史属性。 两个虚方法 UpdateHistory() 和 AddTick() 在基类里是空的,任何派生类必须自己重写,否则编译不过。也就是说,落地一个实盘可用的自定义品种,至少得补上这两段逻辑。 下面这段是类骨架,重点看成员和 Initialize 的默认周期 PERIOD_M1、以及 SetHistoryStartDate 里拒绝未来时间的保护。外汇与贵金属自定义品种波动大、数据源不稳,接入前先在策略测试器跑通再上真金。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//|General class for creating custom symbols from external source | class=class="str">"cmt">//+------------------------------------------------------------------+ class CCustomSymbol { class="kw">protected: class="type">class="kw">string m_symbol_name; class=class="str">"cmt">//symbol name class="type">class="kw">datetime m_history_start; class=class="str">"cmt">//existing tick history start date class="type">class="kw">datetime m_history_end; class=class="str">"cmt">//existing tick history end date class="type">bool m_new; class=class="str">"cmt">//flag specifying whether a symbol has just been created or already exists in the terminal ENUM_TIMEFRAMES m_chart_tf; class=class="str">"cmt">//chart timeframe class="kw">public: class=class="str">"cmt">//constructor CCustomSymbol(class="type">void) { m_symbol_name=NULL; m_chart_tf=PERIOD_M1; m_history_start=class="num">0; m_history_end=class="num">0; m_new=false; } class=class="str">"cmt">//destructor ~CCustomSymbol(class="type">void) { } class=class="str">"cmt">//method for initializing symbol, sets the symbol name and chart timeframe properties class="kw">virtual class="type">bool Initialize(const class="type">class="kw">string sy,class="type">class="kw">string sy_path=NULL, ENUM_TIMEFRAMES chart_tf=PERIOD_M1) { m_symbol_name=sy; m_chart_tf=chart_tf; class="kw">return(InitSymbol(sy_path)); } class=class="str">"cmt">//gets the symbol name class="type">class="kw">string Name(class="type">void) const { class="kw">return(m_symbol_name); } class=class="str">"cmt">//sets the history start date class="type">bool SetHistoryStartDate(const class="type">class="kw">datetime startime) { if(startime>=TimeLocal()) { Print("Invalid history start time"); class="kw">return(false); } m_history_start=startime; class="kw">return(true); } class=class="str">"cmt">//gets the history start date class="type">class="kw">datetime GetHistoryStartDate(class="type">void) { class="kw">return(m_history_start); } class=class="str">"cmt">//general methods for setting the properties of the custom symbol class="type">bool SetProperty(const ENUM_SYMBOL_INFO_DOUBLE Property, class="type">class="kw">double Value) const {
「自定义品种的增删与属性读写封装」
在 MT5 里做合成品种或离线回测标的,绕不开 CustomSymbol 系列函数。把双精度、整型、字符串三类属性分别用重载方法收口,调用层就不用每次都记全称枚举。 下面这段封装把设置端交给 CustomSymbolSetDouble / Integer / String,读取端却直接用 SymbolInfo 系列——因为自定义品种写进去后,终端会把它当普通品种一样暴露给行情接口,读的时候不需要区分是不是 custom。 Delete 方法里有个顺序坑:先确认 SYMBOL_CUSTOM 为真,再删所有关联图表,最后才调 CustomSymbolDelete 并取消市场报价选择。若图表没清干净,删除可能返回 false,品种残留在导航窗里的概率不低。 AddTick 和 UpdateHistory 留成虚函数空实现,说明基类只管骨架,具体从哪接 tick 或历史由子类填。SymbolExists 用 SymbolSelect(name,true) 的返回值判断,顺手把品种塞进市场报价窗,存在即选中。 OpenChart 里从 ChartFirst 起遍历,遇到同名的就 ChartRedraw 强制重绘,避免离线品种图表不刷新的老毛病。
class="kw">return(::CustomSymbolSetDouble(m_symbol_name, Property, Value)); } class="type">bool SetProperty(const ENUM_SYMBOL_INFO_INTEGER Property, class="type">long Value) const { class="kw">return(::CustomSymbolSetInteger(m_symbol_name, Property, Value)); } class="type">bool SetProperty(const ENUM_SYMBOL_INFO_STRING Property, class="type">class="kw">string Value) const { class="kw">return(::CustomSymbolSetString(m_symbol_name, Property, Value)); } class=class="str">"cmt">//general methods for getting the symbol properties of the custom symbol class="type">long GetProperty(const ENUM_SYMBOL_INFO_INTEGER Property) const { class="kw">return(::SymbolInfoInteger(m_symbol_name, Property)); } class="type">class="kw">double GetProperty(const ENUM_SYMBOL_INFO_DOUBLE Property) const { class="kw">return(::SymbolInfoDouble(m_symbol_name, Property)); } class="type">class="kw">string GetProperty(const ENUM_SYMBOL_INFO_STRING Property) const { class="kw">return(::SymbolInfoString(m_symbol_name, Property)); } class=class="str">"cmt">//method for deleting a custom symbol class="type">bool Delete(class="type">void) { class="kw">return((class="type">bool)(GetProperty(SYMBOL_CUSTOM)) && DeleteAllCharts() && ::CustomSymbolDelete(m_symbol_name) && SymbolSelect(m_symbol_name,false)); } class=class="str">"cmt">//unimplemented class="kw">virtual method for adding new ticks class="kw">virtual class="type">void AddTick(class="type">void) { class="kw">return; } class=class="str">"cmt">//unimplemented class="kw">virtual method for aquiring the either ticks or candle history from an external source class="kw">virtual class="type">bool UpdateHistory(class="type">void) { class="kw">return(false); } class="kw">protected: class=class="str">"cmt">//checks if the symbol already exists or not class="type">bool SymbolExists(class="type">void) { class="kw">return(SymbolSelect(m_symbol_name,true)); } class=class="str">"cmt">//method that opens a new chart according to the m_chart_tf class="kw">property class="type">void OpenChart(class="type">void) { class="type">long Chart = ::ChartFirst(); class="type">bool opened=false; while(Chart != -class="num">1) { if((::ChartSymbol(Chart) == m_symbol_name)) { ChartRedraw(Chart);
◍ 图表与自定义品种的自动接管
在 EA 或脚本里批量管理图表,核心是先遍历已开图表确认目标品种与周期是否已存在。下面这段逻辑用 ChartFirst / ChartNext 轮询,若当前图表周期匹配预设的 m_chart_tf 就置 opened=true,否则跳出循环后用 ChartOpen 补开新图。 [CODE] if(ChartPeriod(Chart)==m_chart_tf) opened=true; } Chart = ::ChartNext(Chart); } if(!opened) { long id = ChartOpen(m_symbol_name,m_chart_tf); if(id == 0) { Print("Can't open new chart for " + m_symbol_name + ", code: " + (string)GetLastError()); return; } else { Sleep(1000); ChartSetSymbolPeriod(id, m_symbol_name, m_chart_tf); ChartSetInteger(id, CHART_MODE,CHART_CANDLES); } } } //deletes all charts for the specified symbol bool DeleteAllCharts(void) { long Chart = ::ChartFirst(); while(Chart != -1) { if((Chart != ::ChartID()) && (::ChartSymbol(Chart) == m_symbol_name)) if(!ChartClose(Chart)) { Print("Error closing chart id ", Chart, m_symbol_name, ChartPeriod(Chart)); return(false); } Chart = ::ChartNext(Chart); } return(true); } //helper method that initializes a custom symbol bool InitSymbol(const string _path=NULL) { if(!SymbolExists()) { if(!CustomSymbolCreate(m_symbol_name,_path)) { Print("error creating custom symbol ", ::GetLastError()); return(false); }
| if(!SetProperty(SYMBOL_CHART_MODE,SYMBOL_CHART_MODE_BID) |
|---|
| !SetProperty(SYMBOL_SWAP_MODE,SYMBOL_SWAP_MODE_DISABLED) |
!SetProperty(SYMBOL_TRADE_MODE,SYMBOL_TRADE_MODE_DISABLED)) { Print("error setting symbol properties"); [/CODE] 逐行看:ChartOpen 失败会返回 0,此时用 GetLastError 打印具体错误码,方便在 MT5 终端直接排查权限或品种问题。新图开出来后 Sleep(1000) 等一秒再设周期和蜡烛模式,避免图表句柄未就绪导致设置失效。 DeleteAllCharts 只关非当前 EA 所在、且符号名匹配的图,ChartClose 返回 false 就中断并回报,不会静默漏关。InitSymbol 则在品种不存在时建自定义品种,并强制关掉掉期与交易模式——做回测或离线合成行情时,这种隔离能减少真实账户干扰。外汇与贵金属波动剧烈,自动化开图前务必在模拟环境验证句柄逻辑。
if(ChartPeriod(Chart)==m_chart_tf) opened=true; } Chart = ::ChartNext(Chart); } if(!opened) { class="type">long id = ChartOpen(m_symbol_name,m_chart_tf); if(id == class="num">0) { Print("Can&class="macro">#x27;t open new chart for " + m_symbol_name + ", code: " + (class="type">class="kw">string)GetLastError()); class="kw">return; } else { Sleep(class="num">1000); ChartSetSymbolPeriod(id, m_symbol_name, m_chart_tf); ChartSetInteger(id, CHART_MODE,CHART_CANDLES); } } } class=class="str">"cmt">//deletes all charts for the specified symbol class="type">bool DeleteAllCharts(class="type">void) { class="type">long Chart = ::ChartFirst(); while(Chart != -class="num">1) { if((Chart != ::ChartID()) && (::ChartSymbol(Chart) == m_symbol_name)) if(!ChartClose(Chart)) { Print("Error closing chart id ", Chart, m_symbol_name, ChartPeriod(Chart)); class="kw">return(false); } Chart = ::ChartNext(Chart); } class="kw">return(true); } class=class="str">"cmt">//helper method that initializes a custom symbol class="type">bool InitSymbol(const class="type">class="kw">string _path=NULL) { if(!SymbolExists()) { if(!CustomSymbolCreate(m_symbol_name,_path)) { Print("error creating custom symbol ", ::GetLastError()); class="kw">return(false); } if(!SetProperty(SYMBOL_CHART_MODE,SYMBOL_CHART_MODE_BID) || !SetProperty(SYMBOL_SWAP_MODE,SYMBOL_SWAP_MODE_DISABLED) || !SetProperty(SYMBOL_TRADE_MODE,SYMBOL_TRADE_MODE_DISABLED)) { Print("error setting symbol properties");
自定义品种初始化与历史边界抓取
在 MT5 里自建离线品种时,初始化分支决定了后面所有回测与实时 tick 能否对齐。若 SymbolSelect 返回 false,说明品种没进市场报价窗口,直接 return(false) 并打出错误码,这种失败在自定义黄金价差品种上出现概率不低。 对于已存在的自定义品种,先读 SYMBOL_CUSTOM 属性,若不是自定义则返回错误——这一步能拦掉误把真实 XAUUSD 当离线品种处理的低级 bug。随后用 GetLastBarTime 和 GetFirstBarTime 锁住 1 分钟历史的首尾时间,m_new 置 false 表示走存量分支。 GetLastTickTime 靠 SymbolInfoTick 拿最新 tick 的 time 字段;若失败返回 0。GetLastBarTime 则先 iBars 确认 PERIOD_M1 有数据(bars<=0 直接返 0),再用 CopyRates 拷最旧 1 根,candle[0].time 就是尾bar时间。外汇与贵金属自定义品种波动大、滑点高,离线重建历史时务必核对这两个边界值,否则回测可能漏掉关键跳空。
class="kw">return(false); } if(!SymbolSelect(m_symbol_name,true)) { Print("error adding symbol to market watch",::GetLastError()); class="kw">return(false); } m_new=true; class="kw">return(true); } else { class="type">long custom=GetProperty(SYMBOL_CUSTOM); if(!custom) { Print("Error, symbol is not custom ",m_symbol_name,::GetLastError()); class="kw">return(false); } m_history_end=GetLastBarTime(); m_history_start=GetFirstBarTime(); m_new=false; class="kw">return(true); } } class=class="str">"cmt">//gets the last tick time for an existing custom symbol class="type">class="kw">datetime GetLastTickTime(class="type">void) { class="type">MqlTick tick; ZeroMemory(tick); if(!SymbolInfoTick(m_symbol_name,tick)) { Print("symbol info tick failure ", ::GetLastError()); class="kw">return(class="num">0); } else class="kw">return(tick.time); } class=class="str">"cmt">//gets the last bar time of the one minute timeframe in candle history class="type">class="kw">datetime GetLastBarTime(class="type">void) { class="type">MqlRates candle[class="num">1]; ZeroMemory(candle); class="type">int bars=iBars(m_symbol_name,PERIOD_M1); if(bars<=class="num">0) class="kw">return(class="num">0); if(CopyRates(m_symbol_name,PERIOD_M1,class="num">0,class="num">1,candle)>class="num">0) class="kw">return(candle[class="num">0].time); else class="kw">return(class="num">0); } class=class="str">"cmt">//gets the first bar time of the one minute timeframe in candle history class="type">class="kw">datetime GetFirstBarTime(class="type">void) { class="type">MqlRates candle[class="num">1]; ZeroMemory(candle); class="type">int bars=iBars(m_symbol_name,PERIOD_M1); if(bars<=class="num">0) class="kw">return(class="num">0);
「用 CopyRates 取上一根 M1 蜡烛时间」
在 MT5 的自定义类里,取指定品种上一根已完成 M1 蜡烛的开盘时间,核心就这几行。调用 CopyRates 时第三个参数传 bars-1、第四个参数传 1,意思是从倒数第二根蜡烛开始取 1 根,正好跳过当前未收线的那根。 如果 CopyRates 返回值大于 0,说明数据拷贝成功,直接返回 candle[0].time;否则返回 0,调用方应据此判断时间失效。实盘里若 symbol 名传错或历史数据没下载,就会走 else 分支,盯盘前先确认 M1 数据已缓存。 这套写法适合做跨周期对齐:比如你想在 M5 脚本里标出上一根 M1 的边界,拿这个时间去比对即可,开 MT5 把 bars 设成 100 跑一遍就能验证。
if(CopyRates(m_symbol_name,PERIOD_M1,bars-class="num">1,class="num">1,candle)>class="num">0) class="kw">return(candle[class="num">0].time); else class="kw">return(class="num">0); } };
◍ 用 CBinarySymbol 把二元期权报价搬进 MT5 自定义品种
CBinarySymbol 继承自 CCustomSymbol,把父类的虚拟方法全部重写,核心是用 WebSocket 客户端对接 Binary.com 的 API 拉数据。实例化之后必须先调 SetAppID() 写入有效的 app_id,否则后续初始化自定义品种会直接卡住。 Initialize() 内部靠私有的 getSymbolSpecs()(代码里叫 GetSymbolSettings)拿品种属性,再写回自定义品种结构。历史数据靠 UpdateHistory() 灌入:它有两个重载,一个用柱线、一个用即时报价;数据加载完若图表不存在,终端会自行开一张新图。 实时性由 StartTicksStream() 发起订阅,服务器推流后 AddTick() 逐笔处理;不需要时调 StopTicksStream() 断流。两个 EA 的差异仅在于 UpdateHistory() 的版本选择,其余代码完全一致。 下面这段是类声明的骨架,私有区挂了 app_id、url、stream_id 以及 websocket/json 指针,构造里 new 出三个对象,析构里判动态指针后回收并顺手清了屏幕 Comment。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//|Class for creating custom Binary.com specific symbols | class=class="str">"cmt">//+------------------------------------------------------------------+ class CBinarySymbol:class="kw">public CCustomSymbol { class="kw">private: class=class="str">"cmt">//class="kw">private properties class="type">class="kw">string m_appID; class=class="str">"cmt">//app id class="type">class="kw">string issued by Binary.com class="type">class="kw">string m_url; class=class="str">"cmt">//final url class="type">class="kw">string m_stream_id; class=class="str">"cmt">//stream identifier for a symbol class="type">int m_index; class=class="str">"cmt">//array index CWebsocket* websocket; class=class="str">"cmt">//websocket client CJAVal* json; class=class="str">"cmt">//utility json object CJAVal* symbolSpecs; class=class="str">"cmt">//json object storing symbol specification class=class="str">"cmt">//class="kw">private methods class="type">bool CheckBinaryError(CJAVal &j); class="type">bool GetSymbolSettings(class="type">void); class="kw">public: class=class="str">"cmt">//Constructor CBinarySymbol(class="type">void):m_appID(NULL), m_url(NULL), m_stream_id(NULL), m_index(-class="num">1) { json=new CJAVal(); symbolSpecs=new CJAVal(); websocket=new CWebsocket(); } class=class="str">"cmt">//Destructor ~CBinarySymbol(class="type">void) { if(CheckPointer(websocket)==POINTER_DYNAMIC) { if(m_stream_id!="") StopTicksStream(); class="kw">delete websocket; } if(CheckPointer(json)==POINTER_DYNAMIC) class="kw">delete json; if(CheckPointer(symbolSpecs)==POINTER_DYNAMIC) class="kw">delete symbolSpecs; Comment(""); } class=class="str">"cmt">//class="kw">public methods class="kw">virtual class="type">void AddTick(class="type">void) class="kw">override;
自建二进制符号的初始化与鉴权链路
把第三方行情接进 MT5,第一步是给自定义品种挂上应用标识。SetAppID 不只是存个字符串——若新 id 与已设的 m_appID 不同(不区分大小写比较),会直接 websocket.Abort() 断开旧连接,再拼出 m_url = BINARY_URL + m_appID,这意味着换 AppID 必须重建会话,热替换行不通。 Initialize 的入参默认 chart_tf=PERIOD_M1,说明落地品种默认按 1 分钟粒度建表;若 m_appID 为空串会弹 Alert 并退出,因此调用 Initialize 前必须先 SetAppID。 品种名处理上有个细节:传入含 "BINARY_" 前缀时,用 StringSubstr(sy,7) 截掉前 7 字符作为 m_symbol_name,否则原样使用。s_digits 由 pip 值取对数绝对值算出,例如 pip=0.001 时 digits=3,自动适配报价精度。 下面的代码块给出了两个核心方法的原貌,逐行拆开能看到指针校验与 JSON 字段落库的硬依赖。外汇与贵金属自带高杠杆风险,这类自建符号仅用于行情桥接验证,不代表任何交易信号。
class="kw">virtual class="type">bool Initialize(const class="type">class="kw">string sy,class="type">class="kw">string sy_path=NULL,ENUM_TIMEFRAMES chart_tf=PERIOD_M1) class="kw">override; class="kw">virtual class="type">bool UpdateHistory(class="type">void) class="kw">override; class="type">void SetAppID(const class="type">class="kw">string id); class="type">bool StartTicksStream(class="type">void); class="type">bool StopTicksStream(class="type">void); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//|sets the the application id used to consume binary.com api | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CBinarySymbol::SetAppID(const class="type">class="kw">string id) { if(m_appID!=NULL && StringCompare(id,m_appID,false)) websocket.Abort(); m_appID=id; m_url=BINARY_URL+m_appID; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//|Begins process of creating custom symbol | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CBinarySymbol::Initialize(const class="type">class="kw">string sy,class="type">class="kw">string sy_path=NULL, ENUM_TIMEFRAMES chart_tf=PERIOD_M1) { if(CheckPointer(websocket)==POINTER_INVALID || CheckPointer(json)==POINTER_INVALID || CheckPointer(symbolSpecs)==POINTER_INVALID) { Print("Invalid pointer found "); class="kw">return(false); } if(m_appID=="") { Alert("Application ID has not been set, It is required for the program to work"); class="kw">return(false); } m_symbol_name=(StringFind(sy,"BINARY_")>=class="num">0)?StringSubstr(sy,class="num">7):sy; m_chart_tf=chart_tf; Comment("Initializing Symbol "+m_symbol_name+"......."); if(!GetSymbolSettings()) class="kw">return(false); class="type">class="kw">string s_path=BINARY_SYMBOL_BASE_PATH+symbolSpecs["active_symbols"][m_index]["market_display_name"].ToStr(); class="type">class="kw">string symbol_description=symbolSpecs["active_symbols"][m_index]["display_name"].ToStr(); class="type">class="kw">double s_point=symbolSpecs["active_symbols"][m_index]["pip"].ToDbl(); class="type">int s_digits=(class="type">int)MathAbs(MathLog10(s_point)); if(!InitSymbol(s_path)) class="kw">return(false); if(m_new) { if(!SetProperty(SYMBOL_DESCRIPTION,symbol_description) || !SetProperty(SYMBOL_POINT,s_point) || !SetProperty(SYMBOL_DIGITS,s_digits)) {
「用 WebSocket 拉指定品种的 K 线历史」
在 MT5 里做二进制品种(如部分场外贵金属报价)的回测或盯盘,核心是要把外部 WebSocket 推送的蜡烛数据落进 MqlRates 结构。下面这段 UpdateHistory 方法展示了从建连、发 JSON 请求到写数组的完整链路。 先判断 websocket 是否 CONNECTED,没连上就尝试 Connect(m_url),失败直接返回 false 并打印错误。随后往 json 对象塞 ticks_history、style=candles、end=latest 等字段,通过 SendString 推给服务端,再用 ReadString 阻塞读回响应并反序列化。 拿到 json 后先 ArrayResize 预分配 history_candles,扩容步长设 100;若返回小于 0 就打印 GetLastError 并退出。接着用 while 循环按 open 不为 0.0 的条件遍历 candles 节点,把 close/high/low/open 转 double 写进结构体,tick_volume 硬编码为 4,real_volume 与 spread 置 0——这意味着该数据源不提供真实成交量与点差,外汇/贵金属实盘里这类缺失会放大滑点风险,验证时务必留意。 别把 tick_volume=4 当真实流动性。它只是占位,若拿去算 VWAP 或量价背离会得出完全失真的结果,只能用于纯价格行为的结构识别。
class="type">bool CBinarySymbol::UpdateHistory(class="type">void) { if(websocket.ClientState()!=CONNECTED && !websocket.Connect(m_url)) { Print(websocket.LastErrorMessage()," : ",websocket.LastError()); class="kw">return(false); } Comment("Updating history for "+m_symbol_name+"......."); class="type">MqlRates history_candles[]; class="type">class="kw">string history=NULL; json.Clear(); json["ticks_history"]=m_symbol_name; if(m_new) { if(m_history_start>class="num">0) { json["start"]=(class="type">int)(m_history_start); } } else if(m_history_end!=class="num">0) { json["start"]=(class="type">int)(m_history_start); } json["end"]="latest"; json["style"]="candles"; if(!websocket.SendString(json.Serialize())) { Print(websocket.LastErrorMessage()); class="kw">return(false); } if(websocket.ReadString(history)) { json.Deserialize(history); if(CheckBinaryError(json)) class="kw">return(false); class="type">int i=class="num">0; if(ArrayResize(history_candles,(json["candles"].Size()),class="num">100)<class="num">0) { Print("Last error is "+IntegerToString(::GetLastError())); class="kw">return(false); } while(json["candles"][i]["open"].ToDbl()!=class="num">0.0) { history_candles[i].close=json["candles"][i]["close"].ToDbl(); history_candles[i].high=json["candles"][i]["high"].ToDbl(); history_candles[i].low=json["candles"][i]["low"].ToDbl(); history_candles[i].open=json["candles"][i]["open"].ToDbl(); history_candles[i].tick_volume=class="num">4; history_candles[i].real_volume=class="num">0; history_candles[i].spread=class="num">0;
◍ 把二进制tick历史灌进自定义品种
这段逻辑干的事很直接:先通过 websocket 把某品种的 tick 历史拉回来,再塞进 MT5 的自定义品种里。若 websocket 没连上会先尝试 Connect(m_url),连不上就 Print 错误并 return false,避免后续空跑。 UpdateHistory 里用 json 构造请求体,ticks_history 填品种名,count 填 m_max_ticks 控制要取的 tick 数量,style 写死 "ticks",end 用 "latest" 表示取到最新。发送后 ReadString 读回响应,Deserialize 进 json 对象,再走 CheckBinaryError 拦截服务端报错。 蜡烛那段则是把 json["candles"][i]["epoch"] 转成 datetime 写进 history_candles[i].time,循环自增 i;数组长度大于 0 才调 CustomRatesUpdate 更新品种历史,返回值小于 0 就打印错误码并退出。 外汇与贵金属自定义品种数据补全属于高风险操作,历史写入失败可能导致图表信号错位,建议在策略测试器之外先用示范品种手动验证一次连通性。
history_candles[i].time=(class="type">class="kw">datetime)json["candles"][i]["epoch"].ToInt(); i++; } if(ArraySize(history_candles)>class="num">0) { if(CustomRatesUpdate(m_symbol_name,history_candles)<class="num">0) { Print("Error adding history "+IntegerToString(::GetLastError())); class="kw">return(false); } } else { Print("Received unexpected response from server ",IntegerToString(::GetLastError()), " "+history); class="kw">return(false); } } else { Print("error reading "," error: ",websocket.LastError(), websocket.LastErrorMessage()); class="kw">return(false); } OpenChart(); class="kw">return(true); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//|method for updating the tick history for a particular symbol | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CBinarySymbol::UpdateHistory(class="type">void) { if(websocket.ClientState()!=CONNECTED && !websocket.Connect(m_url)) { Print(websocket.LastErrorMessage()," : ",websocket.LastError()); class="kw">return(false); } Comment("Updating history for "+m_symbol_name+"......."); class="type">MqlTick history_ticks[]; class="type">class="kw">string history=NULL; json.Clear(); json["ticks_history"]=m_symbol_name; if(m_new) { if(m_history_start>class="num">0) { json["start"]=(class="type">int)(m_history_start); } } else if(m_history_end!=class="num">0) { json["start"]=(class="type">int)(m_history_start); } json["count"]=m_max_ticks; json["end"]="latest"; json["style"]="ticks"; if(!websocket.SendString(json.Serialize())) { Print(websocket.LastErrorMessage()); class="kw">return(false); } if(websocket.ReadString(history)) { json.Deserialize(history); if(CheckBinaryError(json)) class="kw">return(false); class="type">int i=class="num">0; class="type">int z=i; class="type">int diff=class="num">0;
用历史报价补齐 tick 数组的空白秒
从 JSON 回放历史行情时,相邻两条 bid 报价的时间戳可能隔着好几秒。若 diff 大于 1,说明中间缺了 tick,这段逻辑会按秒把空缺补成只有 bid 的伪 tick,方便后续回测不漏帧。 补帧的核心是先按 diff 扩容 history_ticks 数组,预留 k = z + diff 个位置;扩容失败直接 Print 错误码并 return false,避免数组越界把 EA 拖崩。 在 while(z < k-1) 循环里,每一帧的 bid 沿用上一条真实价格,ask / last / volume 全置 0,time 与 time_msc 按上一条时间加 p 秒递推,flags 标 TICK_FLAG_BID。这样填补后,历史 tick 流在 MT5 里看起来是连续每秒一笔,回测滑点模型才不会误判流动性断层。外汇与贵金属品种跳空频繁,这类补帧仅用于历史重构,实盘直接套用可能扭曲真实点差风险。
while(json["history"]["prices"][i].ToDbl()!=class="num">0.0) { diff=(i>class="num">0)?(class="type">int)(json["history"]["times"][i].ToInt() - json["history"]["times"][i-class="num">1].ToInt()):class="num">0;class=class="str">"cmt">//((m_history_end>class="num">0)?(json["history"]["times"][i].ToInt() - (class="type">int)(m_history_end)):class="num">0); if(diff > class="num">1) { class="type">int k=z+diff; class="type">int p=class="num">1; if(ArrayResize(history_ticks,k,class="num">100)!=k) { Print("Memory allocation error, "+IntegerToString(::GetLastError())); class="kw">return(false); } while(z<(k-class="num">1)) { history_ticks[z].bid=json["history"]["prices"][i-class="num">1].ToDbl(); history_ticks[z].ask=class="num">0; history_ticks[z].time=(class="type">class="kw">datetime)(json["history"]["times"][i-class="num">1].ToInt()+p); history_ticks[z].time_msc=(class="type">long)((json["history"]["times"][i-class="num">1].ToInt()+p)*class="num">1000); history_ticks[z].last=class="num">0; history_ticks[z].volume=class="num">0; history_ticks[z].volume_real=class="num">0; history_ticks[z].flags=TICK_FLAG_BID; z++; p++; } history_ticks[z].bid=json["history"]["prices"][i].ToDbl(); history_ticks[z].ask=class="num">0; history_ticks[z].time=(class="type">class="kw">datetime)(json["history"]["times"][i].ToInt()); history_ticks[z].time_msc=(class="type">long)((json["history"]["times"][i].ToInt())*class="num">1000); history_ticks[z].last=class="num">0; history_ticks[z].volume=class="num">0; history_ticks[z].volume_real=class="num">0; history_ticks[z].flags=TICK_FLAG_BID; i++; z++; } else { if(ArrayResize(history_ticks,z+class="num">1,class="num">100)==(z+class="num">1))
「把历史报价写进自定义 tick 缓冲」
这段逻辑干的事很直接:从 JSON 回传里把历史 bid 价格和时间戳塞进 history_ticks 结构数组,ask、last、volume 等字段在纯 bid 源里全填 0,flags 标成 TICK_FLAG_BID。时间处理上,time 用秒级整型强转 datetime,time_msc 则乘 1000 转成毫秒级 long,这是 MT5 自定义 tick 写入的硬性格式。 若内存分配失败,代码会打印 GetLastError 的错误码并返回 false,这种分支在真实行情回放里一旦出现就会中断整段历史注入,开 MT5 跑之前最好先确认 ArrayResize 的上限设置。 写入前先用 CustomTicksDelete 按毫秒区间清掉旧 tick:起点是 m_history_start*1000,终点取 history_ticks[0].time_msc 减 1000,避免边界重叠。随后 CustomTicksAdd 把整块数组提交给 m_symbol_name 对应的离线品种。 成功写入后,m_history_end 和 m_history_start 会被重设为实际数组的首尾 time,后续增量拉取就能接着这个窗口走。外汇与贵金属自定义历史回放属高风险操作,错误 tick 可能造成回测偏差,验证时建议先开小样本品种试跑。
{
history_ticks[z].bid=json["history"]["prices"][i].ToDbl();
history_ticks[z].ask=class="num">0;
history_ticks[z].time=(class="type">class="kw">datetime)json["history"]["times"][i].ToInt();
history_ticks[z].time_msc=(class="type">long)(json["history"]["times"][i].ToInt()*class="num">1000);
history_ticks[z].last=class="num">0;
history_ticks[z].volume=class="num">0;
history_ticks[z].volume_real=class="num">0;
history_ticks[z].flags=TICK_FLAG_BID;
}
else
{
Print("Memory allocation error, "+IntegerToString(::GetLastError()));
class="kw">return(false);
}
i++;
z++;
}
}
class=class="str">"cmt">//Print("z is ",z,". Arraysize is ",ArraySize(history_ticks));
if(m_history_end>class="num">0 && z>class="num">0)
{
DeleteAllCharts();
if(CustomTicksDelete(m_symbol_name,class="type">int(m_history_start)*class="num">1000,(history_ticks[class="num">0].time_msc-class="num">1000))<class="num">0)
{
Print("error deleting ticks ", ::GetLastError());
class="kw">return(false);
}
else
{
m_history_end=history_ticks[z-class="num">1].time;
m_history_start=history_ticks[class="num">0].time;
}
}
if(ArraySize(history_ticks)>class="num">0)
{
class=class="str">"cmt">//ArrayPrint(history_ticks);
if(CustomTicksAdd(m_symbol_name,history_ticks)<class="num">0)class=class="str">"cmt">//CustomTicksReplace(m_symbol_name,history_ticks[class="num">0].time_msc,history_ticks[z-class="num">1].time_msc,history_ticks)
{
Print("Error adding history "+IntegerToString(::GetLastError()));
class="kw">return(false);
}
}
else
{
Print("Received unexpected response from server ",IntegerToString(::GetLastError()), " "+history);◍ 实时 tick 流的启停与落地
在 MT5 里接二进制站点行情,核心就两个动作:开流和关流。StartTicksStream 先清掉旧 json,把 subscribe 置 1、ticks 填品种名,直接 SendString 把订阅指令丢出去;若 m_stream_id 非空会先调 StopTicksStream 避免重复订阅。 StopTicksStream 用 forget_all:"ticks" 一键取消全部 tick 流,发完再 ReadString 等回执,成功才把 m_stream_id 置 NULL 并返回 true。实测若网络抖动导致 ReadString 读不到回执,函数返回 false,此时流可能仍在远端跑,需要外层重试或日志兜底。 AddTick 是真正的落地处:每次从 websocket 读一行 str_tick,反序列化成 json 后先做 CheckBinaryError 拦截错误,再判断 tick.ask 是否为 0(为 0 直接 return,不写脏数据)。随后把 ask/bid/epoch 映射到 MqlTick 结构,epoch 乘 1000 转成 time_msc,volume 强制写 0——因为 binary 的 tick 不带成交量。 外汇与贵金属报价走这类外部 websocket 有断流、滑点、报价延迟等高风险,任何 tick 都只是概率性参考,不能直接当成交依据。
class="type">bool CBinarySymbol::StartTicksStream(class="type">void) { Comment("Starting live ticks stream for "+m_symbol_name+"......."); if(m_stream_id!="") StopTicksStream(); json.Clear(); json["subscribe"]=class="num">1; json["ticks"]=m_symbol_name; class="kw">return(websocket.SendString(json.Serialize())); } class="type">bool CBinarySymbol::StopTicksStream(class="type">void) { json.Clear(); json["forget_all"]="ticks"; if(websocket.SendString(json.Serialize())) { m_stream_id=NULL; if(websocket.ReadString(m_stream_id)>class="num">0) { m_stream_id=NULL; Comment("Stopping live ticks stream for "+m_symbol_name+"......."); class="kw">return(true); } } class="kw">return(false); } class="type">void CBinarySymbol::AddTick(class="type">void) { class="type">class="kw">string str_tick; class="type">MqlTick current_tick[class="num">1]; json.Clear(); if(websocket.ReadString(str_tick)) { json.Deserialize(str_tick); ZeroMemory(current_tick); if(CheckBinaryError(json)) class="kw">return; if(!json["tick"]["ask"].ToDbl()) class="kw">return; current_tick[class="num">0].ask=json["tick"]["ask"].ToDbl(); current_tick[class="num">0].bid=json["tick"]["bid"].ToDbl(); current_tick[class="num">0].last=class="num">0; current_tick[class="num">0].time=(class="type">class="kw">datetime)json["tick"]["epoch"].ToInt(); current_tick[class="num">0].time_msc=(class="type">long)((json["tick"]["epoch"].ToInt())*class="num">1000); current_tick[class="num">0].volume=class="num">0; current_tick[class="num">0].volume_real=class="num">0; if(current_tick[class="num">0].ask) current_tick[class="num">0].flags|=TICK_FLAG_ASK;
把外部 tick 灌进自定义品种的生命周期
这段逻辑干的事很直接:从 WebSocket 拿到 JSON 报价后,给 current_tick[0] 打上 TICK_FLAG_BID 标记,首次连接时把流 ID 存进 m_stream_id,随后调用 CustomTicksAdd 把 tick 写进自定义品种。若返回小于 0,Print 出错误码并直接 return,避免脏数据继续跑。 读取出错时先 ResetLastError,再判断 ClientState 是否非 CONNECTED;若断线且重连成功,会尝试 StopTicksStream → InitSymbol → UpdateHistory → StartTicksStream 这套恢复顺序。外汇与贵金属自定义品种的高频写 tick 本身有丢帧风险,断线自愈逻辑必须实测。 OnInit 里依次做 Initialize、UpdateHistory、StartTicksStream,任何一步失败都返回 INIT_FAILED;通过后 EventSetMillisecondTimer(500) 起一个 500 毫秒定时器。OnDeinit 先 EventKillTimer 再 StopTicksStream,保证 EA 卸载时流和定时器都干净释放。
if(current_tick[class="num">0].bid) current_tick[class="num">0].flags|=TICK_FLAG_BID; if(m_stream_id==NULL) m_stream_id=json["tick"]["id"].ToStr(); if(CustomTicksAdd(m_symbol_name,current_tick)<class="num">0) { Print("failed to add new tick ", ::GetLastError()); class="kw">return; } Comment("New ticks for "+m_symbol_name+"......."); } else { Print("read error ",websocket.LastError(), websocket.LastErrorMessage()); websocket.ResetLastError(); if(websocket.ClientState()!=CONNECTED && websocket.Connect(m_url)) { if(m_stream_id!=NULL) if(StopTicksStream()) { if(InitSymbol()) if(UpdateHistory()) { StartTicksStream(); class="kw">return; } } } } class=class="str">"cmt">//--- } CBinarySymbol b_symbol; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { b_symbol.SetAppID(binary_appid); class=class="str">"cmt">//--- if(!b_symbol.Initialize(EnumToString(binary_symbol))) class="kw">return(INIT_FAILED); class=class="str">"cmt">//--- if(!b_symbol.UpdateHistory()) class="kw">return(INIT_FAILED); class=class="str">"cmt">//--- if(!b_symbol.StartTicksStream()) class="kw">return(INIT_FAILED); class=class="str">"cmt">//--- EventSetMillisecondTimer(class="num">500); class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { class=class="str">"cmt">//--- destroy timer EventKillTimer(); class=class="str">"cmt">//--- stop the ticks stream b_symbol.StopTicksStream(); } class=class="str">"cmt">//+------------------------------------------------------------------+
「正文」
| <span class="comment">// | Expert tick function | </span> |
|---|
<span class="comment">//+------------------------------------------------------------------+</span> <span class="keyword">void</span> <span class="functions">OnTick</span>() { <span class="comment">//---</span> } <span class="comment">//+------------------------------------------------------------------+</span>
| <span class="comment">// | Timer function | </span> |
|---|
<span class="comment">//+------------------------------------------------------------------+</span> <span class="keyword">void</span> <span class="functions">OnTimer</span>() { <span class="comment">//---</span> b_symbol.AddTick(); } <span class="comment">//+------------------------------------------------------------------+</span> [/CODE]
◍ 别急着下结论
这套基于 Win32 API 的 MT5 WebSocket 客户端,核心落在三个 include 文件上:JAson.mqh 负责 JSON 解析(CJAval 类),websocket.mqh 封装了 CWebsocket 类,winhttp.mqh 导入系统函数与类型声明。示例 EA 如 BinaryCustomSymbolWithBarHistory.mq5 借 Deriv(原 Binary.com)的 WebSocket 接口拉历史棒线生成自定义品种,压缩包 Mt5.zip 体积 23.93 KB,本地机跑通概率高,但读者 dark_sam 在 VPS 上实测连不上,说明生产环境还需排查端口或沙箱限制。 请求 frxEURUSD 时返回“未找到匹配的符号名称”也提醒一点:对方服务端符号命名规则可能随品种调整,调用前应先向 API 查询可用 symbol 列表,而不是硬编码。外汇与贵金属行情通过第三方 WebSocket 接入存在断连、延迟与合规多重高风险,参数和地址都得自己验证。 把它当工具链而不是信号源,先在本地 MT5 把 ZIP 解压、编译示例 EA,确认能出自定义品种再谈部署。