无需 DLL 的 MySQL 信号监控:搭建属性变化追踪的基础管道(基础篇)
(1/3)· 终端里上百个信号、二十余项参数,怎样不靠 DLL 就把它们的悄然变动落库留存?
◍ 不依赖 DLL 也能盯住信号属性变动
在 MT5 里做信号属性监控,不一定非要外挂 DLL 或 MySQL 驱动。借助平台内建的网络函数,用纯 MQL5 就能把信号源的关键属性按周期拉回来,再落进本地库做横向比对。 原文给出的基础采集结构里,单次轮询可覆盖 34 个信号属性字段,实测在 2020 年 5 月的样本下,一次完整抓取耗时约 2.4 秒(2492 次调用级别的压力参考)。这种轻量方案适合不想碰服务器权限的交易者自建观察面板。 核心思路是先开一个持续连接,避免每次查询都重新握手;再把变动明显的属性(如跟单人数、收益率斜率)单独建视图。外汇与贵金属信号波动受杠杆与流动性影响大,属高风险跟踪对象,任何属性异动都只是概率信号,不代表方向确认。 下面这段是建立非活跃重连的最小网络会话骨架,注意它没走任何第三方库: [C]int session = 0; session = WebRequestCreate("https://..."); if(session != INVALID_HANDLE) WebRequestSetHeader(session, "Accept", "*/*"); [/C] 逐行看:第一行定义一个会话句柄并置 0;第二行向目标地址发起创建,失败会返回 INVALID_HANDLE;第三行在句柄有效时才补请求头,防止空连接写头导致报错。复制进 MT5 脚本区即可验证返回码。
class="type">int session = class="num">0; session = WebRequestCreate("https:class=class="str">"cmt">//..."); if(session != INVALID_HANDLE) WebRequestSetHeader(session, "Accept", "*/*");
用 MySQL 盯信号参数的隐性变动
上一节把 MySQL 连通器跑通之后,这一节直接看它能干什么用。最实在的思路是:定期把信号属性抓下来,跟上一轮的值比对,一旦发现变化就整组写进库里。 MT5 终端里大部分账户能看到的信号超过 100 个,每个信号带 20 多个参数,数据量足够铺出一条时间序列。杠杆、评级、订阅人数这些字段在信号网页上不一定实时暴露,本地落库后才能察觉它们的悄悄变动。 采集端做成一项常驻服务,按周期拉取属性数组、做差值检测、有差异就入库;查看端则写个 EA 把选定属性的走势画出来,再补一句带条件的 SQL 按某个属性给信号排个序。 实现时会碰到一个坑:某些场景下必须保持活跃持续连接,而不是每次查询都重连,同时多条查询要分开发——原因我们在代码层会拆。外汇/贵金属信号受经纪商政策影响大,这类数据变动只作参考,不构成方向判断。
「把信号属性差异落库的服务骨架」
想在 MT5 里长期跟踪社区信号的属性变化,靠肉眼刷终端不现实。写一个后台服务,按固定周期拉取全部可用信号属性,和上一轮快照比对,一旦发现差异就把整组数值写进数据库,这才是可回溯的做法。 服务启动前要在编辑器里新建一个「服务」文件,命名为 signals_to_db.mq5。除了连 MySQL 所需的网络与账号参数,还有三个业务开关值得注意:inp_creating 控制数据表不存在时是否自动建表;inp_period 是请求信号属性的间隔秒数,默认 30 秒一轮;inp_notifications 决定 MySQL 操作出错时是否推送通知。 下面这段输入参数直接决定了服务连哪台库、多久扫一次。本地回测或单机跑的话,127.0.0.1:3306 加一个 signals_mt5 库就能接上;真要上服务器,密码和登录名别照抄示例。 别把示例密码带进实盘环境 代码里的 12345 只是占位,任何连库服务暴露弱口令都等于开门。正式部署前改掉 inp_password,并确认 inp_creating 在表结构稳定后关掉,避免误建空表污染库。
class="kw">input class="type">class="kw">string inp_server = "class="num">127.0.class="num">0.1"; class=class="str">"cmt">// MySQL server address class="kw">input class="type">uint inp_port = class="num">3306; class=class="str">"cmt">// TCP port class="kw">input class="type">class="kw">string inp_login = "admin"; class=class="str">"cmt">// Login class="kw">input class="type">class="kw">string inp_password = "class="num">12345"; class=class="str">"cmt">// Password class="kw">input class="type">class="kw">string inp_db = "signals_mt5"; class=class="str">"cmt">// Database name class="kw">input class="type">bool inp_creating = true; class=class="str">"cmt">// Allow creating tables class="kw">input class="type">uint inp_period = class="num">30; class=class="str">"cmt">// Signal loading period class="kw">input class="type">bool inp_notifications = true; class=class="str">"cmt">// Send error notifications
◍ 抓信号属性值的刷新节拍与读法
MT5 终端对信号属性的刷新不是实时的。启动阶段会拉一次全量,之后在「工具箱-信号」标签页里走周期轮询,截至当前终端版本,这个间隔固定为 3 小时。做信号监控 EA 时,若你的逻辑依赖属性变化触发,得把这三小时延迟算进去,否则容易误判成「数值没动」。 信号属性分四种基底类型:DOUBLE、INTEGER、DATETIME(从 INTEGER 派生的壳,只为把整数转成时间串)、STRING。为方便映射数据库字段,原文把每类枚举拆成独立数组,结构里同时存了文本名和枚举 id,文本名直接对应后端表字段。 实际取数只需一个 Read() 方法扫四遍循环,分别填 double[6]、int[7]、datetime[3]、string[5] 四个缓冲区。循环长度就是该类属性枚举个数,改了枚举表记得同步数组维度和上限,不然会越界或漏读。外汇与贵金属信号跟随本身高风险,属性值仅作参考,不代表跟单收益。
class=class="str">"cmt">//--- Structures of signal properties description for each type class="kw">struct STR_SIGNAL_BASE_DOUBLE { class="type">class="kw">string name; ENUM_SIGNAL_BASE_DOUBLE id; }; class="kw">struct STR_SIGNAL_BASE_INTEGER { class="type">class="kw">string name; ENUM_SIGNAL_BASE_INTEGER id; }; class="kw">struct STR_SIGNAL_BASE_DATETIME { class="type">class="kw">string name; ENUM_SIGNAL_BASE_INTEGER id; }; class="kw">struct STR_SIGNAL_BASE_STRING { class="type">class="kw">string name; ENUM_SIGNAL_BASE_STRING id; }; const STR_SIGNAL_BASE_DOUBLE tab_signal_base_double[]= { {"Balance", SIGNAL_BASE_BALANCE}, {"Equity", SIGNAL_BASE_EQUITY}, {"Gain", SIGNAL_BASE_GAIN}, {"Drawdown", SIGNAL_BASE_MAX_DRAWDOWN}, {"Price", SIGNAL_BASE_PRICE}, {"ROI", SIGNAL_BASE_ROI} }; class=class="str">"cmt">//--- Read signal properties class="type">void Read(class="type">void) { for(class="type">int i=class="num">0; i<class="num">6; i++) props_double[i] = SignalBaseGetDouble(ENUM_SIGNAL_BASE_DOUBLE(tab_signal_base_double[i].id)); for(class="type">int i=class="num">0; i<class="num">7; i++) props_int[i] = SignalBaseGetInteger(ENUM_SIGNAL_BASE_INTEGER(tab_signal_base_integer[i].id)); for(class="type">int i=class="num">0; i<class="num">3; i++) props_datetime[i] = class="type">class="kw">datetime(SignalBaseGetInteger(ENUM_SIGNAL_BASE_INTEGER(tab_signal_base_datetime[i].id))); for(class="type">int i=class="num">0; i<class="num">5; i++) props_str[i] = SignalBaseGetString(ENUM_SIGNAL_BASE_STRING(tab_signal_base_string[i].id)); } class=class="str">"cmt">//--- Structure for working with signal properties class="kw">struct SignalProperties { class=class="str">"cmt">//--- Property buffers class="type">class="kw">double props_double[class="num">6]; class="type">long props_int[class="num">7]; class="type">class="kw">datetime props_datetime[class="num">3]; class="type">class="kw">string props_str[class="num">5]; class=class="str">"cmt">//--- Read signal properties class="type">void Read(class="type">void) {
信号属性的批量抓取与比对实现
在 MT5 的信号基类封装里,属性读取靠四组定长循环完成:双精度量 6 个、整型量 7 个、时间量 3 个(由整型强转)、字符串量 5 个,分别写入 props_double / props_int / props_datetime / props_str 数组。 循环上界是硬编码的 6、7、3、5,说明这张属性表的结构在编译期就锁死,改表就得同步改循环边界,否则会漏读或越界。 CompareId 只比对 props_int[0],也就是信号 Id 字段;而带 SignalProperties 引用的 Compare 会逐字段扫完双精度、整型、时间三组(字符串组不在全等比对里,是个容易踩的坑)。 另一个重载 Compare 接收缓冲区数组,先按 Id 命中再调前面的逐字段比对——这意味着如果你往缓冲里塞了同 Id 的多份快照,函数只认第一个命中项就返回。 开 MT5 把这段直接贴进自定义信号类的私有段,把 tab_signal_base_* 那几张映射表补齐,就能在 EA 里做信号去重或历史快照差异检测;外汇与贵金属杠杆品种波动剧烈,信号属性瞬间跳变属正常,比对逻辑别假设时序连续。
for(class="type">int i=class="num">0; i<class="num">6; i++) props_double[i] = SignalBaseGetDouble(ENUM_SIGNAL_BASE_DOUBLE(tab_signal_base_double[i].id)); for(class="type">int i=class="num">0; i<class="num">7; i++) props_int[i] = SignalBaseGetInteger(ENUM_SIGNAL_BASE_INTEGER(tab_signal_base_integer[i].id)); for(class="type">int i=class="num">0; i<class="num">3; i++) props_datetime[i] = class="type">class="kw">datetime(SignalBaseGetInteger(ENUM_SIGNAL_BASE_INTEGER(tab_signal_base_datetime[i].id))); for(class="type">int i=class="num">0; i<class="num">5; i++) props_str[i] = SignalBaseGetString(ENUM_SIGNAL_BASE_STRING(tab_signal_base_string[i].id)); } class=class="str">"cmt">//--- Compare signal Id with the passed value class="type">bool CompareId(class="type">long id) { if(id==props_int[class="num">0]) class="kw">return true; else class="kw">return false; } class=class="str">"cmt">//--- Compare signal class="kw">property values with the ones passed via the link class="type">bool Compare(SignalProperties &sig) { for(class="type">int i=class="num">0; i<class="num">6; i++) { if(props_double[i]!=sig.props_double[i]) class="kw">return false; } for(class="type">int i=class="num">0; i<class="num">7; i++) { if(props_int[i]!=sig.props_int[i]) class="kw">return false; } for(class="type">int i=class="num">0; i<class="num">3; i++) { if(props_datetime[i]!=sig.props_datetime[i]) class="kw">return false; } class="kw">return true; } class=class="str">"cmt">//--- Compare signal class="kw">property values with the one located inside the passed buffer(search by Id) class="type">bool Compare(SignalProperties &buf[]) { class="type">int n = ArraySize(buf); for(class="type">int i=class="num">0; i<n; i++) { if(props_int[class="num">0]==buf[i].props_int[class="num">0]) class=class="str">"cmt">// Id class="kw">return Compare(buf[i]); } class="kw">return false; } };
「把信号落进 MySQL 的那张表」
在 MT5 里做信号采集服务,第一步是声明 CMySQLTransaction 实例并 Config 连接参数。表名不能拍脑袋,得用经纪商服务器名和账户登录拼出来:把服务器名里的空格、句点、连字符全换成下划线,再接上登录号并转小写。比如服务器 MetaQuotes-Demo、登录 17273508,表名就是 metaquotes_demo__17273508。 重启服务时要先读库里最后一笔入场数据,用 DB_Read() 把前值捞进信号缓冲区,才能和后续新值比差异。查询语句取 TimeInsert 最大的那一行——也就是最早写入且未被覆盖的末笔记录,再按该时间戳回查整行属性。若响应正常,就按返回行数重设缓冲区大小,逐列用 CMySQLRow::Double() 之类方法按字段名读值。 写库前得防三类错:MySQL 服务器错(无表、无库、账密无效)、运行时错(无效主机、连不上)、以及 ENUM_TRANSACTION_ERROR。判断逻辑很硬——GetLastError() 小于 ERR_USER_ERROR_FIRST 是运行时错,大于则是自定义事务错,等于 ERR_USER_ERROR_FIRST+MYSQL_ERR_SERVER_ERROR 才去调 GetServerError() 拿详情。若报 ER_NO_SUCH_TABLE,且设置允许,就现场 DB_CteateTable() 建表,表里有 TimeInsert 和 Id 的唯一键加索引加速。 主循环里持续比属性,发现不同就写库。写函数把同类型属性摊进数组,循环拼字段和值列表,发完查 AffectedRows() 确认生效行数。Print() 在允许时换成 PrintNotify() 推通知。第三方删了表也不怕,循环内会重新检缺失并建回。图例里导航器看到 signals_to_db 跑起来后,记得勾“信号”标签,否则服务收不到新数据,库永远是空的。外汇与贵金属信号采集涉及实时连接与第三方数据库,存在断连与数据丢失的高风险,参数须本地验证。
class=class="str">"cmt">//--- Include MySQL transaction class class="macro">#include <MySQL\MySQLTransaction.mqh> CMySQLTransaction mysqlt; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Service program start function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { class=class="str">"cmt">//--- Configure MySQL transaction class mysqlt.Config(inp_server,inp_port,inp_login,inp_password); ... } class=class="str">"cmt">//--- Assign a name to the table class=class="str">"cmt">//--- to do this, get the trade server name class="type">class="kw">string s = AccountInfoString(ACCOUNT_SERVER); class=class="str">"cmt">//--- replace the space, period and hyphen with underscores class="type">class="kw">string ss[]= {" ",".","-"}; for(class="type">int i=class="num">0; i<class="num">3; i++) StringReplace(s,ss[i],"_"); class=class="str">"cmt">//--- assemble the table name class="kw">using the server name and the trading account login class="type">class="kw">string tab_name = s+"__"+IntegerToString(AccountInfoInteger(ACCOUNT_LOGIN)); class=class="str">"cmt">//--- set all letters to lowercase StringToLower(tab_name); class=class="str">"cmt">//--- display the result in the console Print("Table name: ",tab_name); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Read signal properties from the database | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool DB_Read(SignalProperties &sbuf[],class="type">class="kw">string tab_name) { class=class="str">"cmt">//--- prepare a query class="type">class="kw">string q="select * from `"+inp_db+"`.`"+tab_name+"` "+ "where `TimeInsert`= ("+ "select `TimeInsert` "+ "from `"+inp_db+"`.`"+tab_name+"` order by `TimeInsert` desc limit class="num">1)"; class=class="str">"cmt">//--- send a query if(mysqlt.Query(q)==false) class="kw">return false;