无需 DLL 的 MySQL 信号监控:搭建属性变化追踪的基础管道(基础篇)
🛰️

无需 DLL 的 MySQL 信号监控:搭建属性变化追踪的基础管道(基础篇)

(1/3)· 终端里上百个信号、二十余项参数,怎样不靠 DLL 就把它们的悄然变动落库留存?

偏理论进阶 第 1/3 篇
很多人以为盯信号变化只能手动刷网页或接第三方 DLL,其实 MT5 自带网络函数就能直连 MySQL。把属性差异自动落库,后续回看杠杆、评级、订阅数的拐点都省事。本篇先把管道骨架搭清楚,后面两篇再填实战与查询。

◍ 不依赖 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 脚本区即可验证返回码。

MQL5 / C++
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 在表结构稳定后关掉,避免误建空表污染库。

MQL5 / C++
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] 四个缓冲区。循环长度就是该类属性枚举个数,改了枚举表记得同步数组维度和上限,不然会越界或漏读。外汇与贵金属信号跟随本身高风险,属性值仅作参考,不代表跟单收益。

MQL5 / C++
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 里做信号去重或历史快照差异检测;外汇与贵金属杠杆品种波动剧烈,信号属性瞬间跳变属正常,比对逻辑别假设时序连续。

MQL5 / C++
    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 跑起来后,记得勾“信号”标签,否则服务收不到新数据,库永远是空的。外汇与贵金属信号采集涉及实时连接与第三方数据库,存在断连与数据丢失的高风险,参数须本地验证。

MQL5 / C++
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;
把信号轮询交给小布盯盘
这些信号属性的周期抓取与差异比对,小布盯盘的 AIGC 已内置,打开对应品种页即可看到异动提示,你只需决定要不要跟。

常见问题

在撰写原文时的终端版本中,启动期间和工具箱信号选项卡里约每 3 小时定期刷新一次,服务需据此安排轮询节奏。
它衍生自 ENUM_SIGNAL_BASE_INTEGER,作用是将整数型属性转换为可读的时间字符串,方便入库后直接按时间维度观察。
可以,小布盯盘已把信号属性轮询和差异标记做成内置能力,不用你维护 MySQL 连通与定时任务,打开页就能看。
它允许服务在库表不存在时自动建表,便利性高但可能因字段设计随意导致后续查询混乱,建议先在测试库确认结构。
没有稳定落库的差异数据,图形化查看和筛选都无从谈起,先打通采集端才能支撑后两篇的动态展示与条件查询。