应用网络函数,或无需 DLL 的 MySQL:第 II 部分 - 监视信号属性变化的程序·进阶篇
📘

应用网络函数,或无需 DLL 的 MySQL:第 II 部分 - 监视信号属性变化的程序·进阶篇

第 2/3 篇

把信号表读进 MT5 的结构化缓冲

从 MySQL 拉信号数据,第一步是拿到响应指针并确认有效。下面这段 C++ 风格代码在 EA 里很常见:查询成功后用 Response() 取指针,若 CheckPointer 返回 POINTER_INVALID 直接 return false,避免后续空指针崩在实盘环境。 读行数后必须用 ArrayResize 把 sbuf 一次性扩到 rows 大小;若返回值不等于 rows,说明内存分配失败,函数退出。外汇与贵金属行情高波动,信号表行数可能在毫秒级变化,这个判断能拦掉一半静默丢数据的问题。 嵌套循环按字段类型拆读:6 个 double、7 个 long、3 个 datetime、5 个 string,分别对应 tab_signal_base_* 里的列名。row.Double / row.Long 任一失败就 return false,datetime 走 MySQLToDatetime 转换,string 直接赋值。 配套 SQL 只取 TimeInsert 最新的那一行(子查询 order by desc limit 1),降低 MT5 端解析压力。若 DB_Read 报错且服务端是 ER_NO_SUCH_TABLE,在 inp_creating=true 时尝试建表,否则置 exit=true 终止。开 MT5 把这段接进 OnTick 前先用 Print(rows) 验证行数,比盲跑稳妥。

MQL5 / C++
class=class="str">"cmt">//--- if the query is successful, get the pointer to it
  CMySQLResponse *r = mysqlt.Response();
  if(CheckPointer(r)==POINTER_INVALID)
     class="kw">return false;
class=class="str">"cmt">//--- read the number of rows in the accepted response
  class="type">uint rows = r.Rows();
class=class="str">"cmt">//--- prepare the array
  if(ArrayResize(sbuf,rows)!=rows)
     class="kw">return false;
class=class="str">"cmt">//--- read class="kw">property values to the array
  for(class="type">uint n=class="num">0; n<rows; n++)
     {
     class=class="str">"cmt">//--- read the pointer to the current row
     CMySQLRow *row = r.Row(n);
     if(CheckPointer(row)==POINTER_INVALID)
        class="kw">return false;
     for(class="type">int i=class="num">0; i<class="num">6; i++)
       {
        if(row.Double(tab_signal_base_double[i].name,sbuf[n].props_double[i])==false)
           class="kw">return false;
       }
     for(class="type">int i=class="num">0; i<class="num">7; i++)
       {
        if(row.Long(tab_signal_base_integer[i].name,sbuf[n].props_int[i])==false)
           class="kw">return false;
       }
     for(class="type">int i=class="num">0; i<class="num">3; i++)
        sbuf[n].props_datetime[i] = MySQLToDatetime(row[tab_signal_base_datetime[i].name]);
     for(class="type">int i=class="num">0; i<class="num">5; i++)
        sbuf[n].props_str[i] = row[tab_signal_base_string[i].name];
     }
  class="kw">return true;
  }
select *
from `signals_mt5`.`metaquotes_demo__17273508`
where `TimeInsert`= (
      select `TimeInsert`
      from `signals_mt5`.`metaquotes_demo__17273508`
      order by `TimeInsert` desc limit class="num">1)
class=class="str">"cmt">//--- Declare the buffer of signal properties
  SignalProperties sbuf[];
class=class="str">"cmt">//--- Raise the data from the database to the buffer
  class="type">bool exit = false;
  if(DB_Read(sbuf,tab_name)==false)
     {
     class=class="str">"cmt">//--- if the reading function returns an error,
     class=class="str">"cmt">//--- the table is possibly missing
     if(mysqlt.GetServerError().code==ER_NO_SUCH_TABLE && inp_creating==true)
       {
       class=class="str">"cmt">//--- if we need to create a table and this is allowed in the settings
       if(DB_CteateTable(tab_name)==false)
          exit=true;
       }
     else
       exit=true;
     }

「建表语句里藏着的信号字段」

在 MT5 里接第三方信号数据库,第一步往往是动态建表。下面这段函数在指定库 inp_db 下按传入的 name 建一张宽表,字段覆盖了信号评价所需的几乎全部维度。 表结构里 PKey 是自增 BIGINT 主键,TimeInsert 记插入时间,配合 Id 做了唯一索引,保证同一次抓取不会重复写。其余字段如 GainDrawdownROIPipsSubscribersTrades 都是后续做信号筛选的硬指标——例如 Drawdown 用 DOUBLE 存,意味着你能直接按浮点阈值过滤掉回撤超 30% 的信号。 外汇与贵金属信号的高风险在于:历史 GainROI 漂亮,不等于未来概率高;建表时把 BrokerServerLeverageTradeMode 一并落库,才可能后期按经纪商和点差环境做分组回测。 别把建表当纯存储 字段宽度 CHAR(50)BrokerCurrency 够用,但 Name 若超长会被截断,跑批量抓取前先确认信号源命名规则,否则连表成功却丢字段内容。

MQL5 / C++
class="type">bool  DB_CteateTable(class="type">class="kw">string name)
  {
class=class="str">"cmt">//--- prepare a query
   class="type">class="kw">string q="CREATE TABLE `"+inp_db+"`.`"+name+"` ("+
                  "`PKey`                BIGINT(class="num">20)   NOT NULL AUTO_INCREMENT,"+
                  "`TimeInsert`    DATETIME    NOT NULL,"+
                  "`Id`                INT(class="num">11)     NOT NULL,"+
                  "`Name`              CHAR(class="num">50)     NOT NULL,"+
                  "`AuthorLogin`     CHAR(class="num">50)     NOT NULL,"+
                  "`Broker`            CHAR(class="num">50)     NOT NULL,"+
                  "`BrokerServer`  CHAR(class="num">50)     NOT NULL,"+
                  "`Balance`           DOUBLE       NOT NULL,"+
                  "`Equity`           DOUBLE       NOT NULL,"+
                  "`Gain`              DOUBLE       NOT NULL,"+
                  "`Drawdown`       DOUBLE       NOT NULL,"+
                  "`Price`             DOUBLE       NOT NULL,"+
                  "`ROI`                DOUBLE       NOT NULL,"+
                  "`Leverage`       INT(class="num">11)     NOT NULL,"+
                  "`Pips`              INT(class="num">11)     NOT NULL,"+
                  "`Rating`           INT(class="num">11)     NOT NULL,"+
                  "`Subscribers`    INT(class="num">11)     NOT NULL,"+
                  "`Trades`           INT(class="num">11)     NOT NULL,"+
                  "`TradeMode`      INT(class="num">11)     NOT NULL,"+
                  "`Published`       DATETIME    NOT NULL,"+
                  "`Started`         DATETIME    NOT NULL,"+
                  "`Updated`        DATETIME    NOT NULL,"+
                  "`Currency`       CHAR(class="num">50)     NOT NULL,"+
                  "PRIMARY KEY(`PKey`),"+
                  "UNIQUE INDEX `TimeInsert_Id` (`TimeInsert`, `Id`),"+
                  "INDEX `TimeInsert` (`TimeInsert`),"+

◍ 建表索引与服务轮询的容错写法

把信号落库到 MySQL 时,给 Currency、Broker、AuthorLogin、Id 四个字段都带 TimeInsert 建了联合索引,存储引擎用 InnoDB、行格式 DYNAMIC。这样按币种或账号追历史信号时,查询大概率走索引而不是全表扫,数据量上到几十万行也倾向不卡。 主循环里用 TimeLocal() 减上次读取时间 chk_ts,小于 inp_period 就 Sleep(1000) 后 continue,等于用 1 秒粒度空转代替忙等。外汇和贵金属信号高频刷新,这种轮询能压住 CPU,但 MT5 终端异常退出时可能有漏写,属高风险操作环节。 写库失败不是直接崩。若服务端报 ER_NO_SUCH_TABLE 且 inp_creating 开关为真,先 DB_CteateTable 补表再重试写入;若还失败就走错误分支打印。GetLastError 大于等于 ERR_USER_ERROR_FIRST 时转成 ENUM_TRANSACTION_ERROR 枚举名,比光看数字好排错。 别把建表权限默认开着。inp_creating=true 在生产库上可能让脚本自说自话建表,建议只在隔离测试库验证后再关掉。

MQL5 / C++
"INDEX `Currency` (`Currency`, `TimeInsert`),"+
"INDEX `Broker` (`Broker`, `TimeInsert`),"+
"INDEX `AuthorLogin` (`AuthorLogin`, `TimeInsert`),"+
"INDEX `Id` (`Id`, `TimeInsert`)"+
" ) COLLATE=&class="macro">#x27;utf8_general_ci&class="macro">#x27; "+
"ENGINE=InnoDB "+
"ROW_FORMAT=DYNAMIC";
class=class="str">"cmt">//--- send a query
   if(mysqlt.Query(q)==false)
      class="kw">return false;
   class="kw">return true;
   }
   if(exit==true)
     {
      if(GetLastError()==(ERR_USER_ERROR_FIRST+MYSQL_ERR_SERVER_ERROR))
        {
         class=class="str">"cmt">// in case of a server error
         Print("MySQL Server Error: ",mysqlt.GetServerError().code," (",mysqlt.GetServerError().message,")");
        }
      else
        {
         if(GetLastError()>=ERR_USER_ERROR_FIRST)
            Print("Transaction Error: ",EnumToString(ENUM_TRANSACTION_ERROR(GetLastError()-ERR_USER_ERROR_FIRST)));
         else
            Print("Error: ",GetLastError());
        }
      class="kw">return;
     }
class=class="str">"cmt">//--- set the time label of the previous reading of signal properties
   class="type">class="kw">datetime chk_ts = class="num">0;
   ...
class=class="str">"cmt">//--- Main loop of the service operation
   do
     {
      if((TimeLocal()-chk_ts)<inp_period)
        {
         Sleep(class="num">1000);
         class="kw">continue;
        }
      class=class="str">"cmt">//--- it is time to read signal properties
      chk_ts = TimeLocal();
      ...
     }
   class="kw">while(!IsStopped());
      if(newdata==true)
        {
         class="type">bool bypass = false;
         if(DB_Write(buf,tab_name,chk_ts)==false)
           {
            class=class="str">"cmt">//--- if we need to create a table and this is allowed in the settings
            if(mysqlt.GetServerError().code==ER_NO_SUCH_TABLE && inp_creating==true)
              {
               if(DB_CteateTable(tab_name)==true)
                 {
                  class=class="str">"cmt">//--- if the table is created successfully, send the data
                  if(DB_Write(buf,tab_name,chk_ts)==false)

MySQL 报错兜底与通知节流

在 MT5 的 MySQL 事务封装里,一旦建表失败、无表且禁止新建,或发送请求异常,都会把 bypass 置为 true 并跳过本轮循环。此时先抓 GetLastError():若是 ERR_USER_ERROR_FIRST+MYSQL_ERR_SERVER_ERROR,就打出服务端错误码与信息;否则按是否大于等于 ERR_USER_ERROR_FIRST 区分交易错误枚举或裸错误号。 PrintNotify 同时承担控制台打印和手机推送。通知开关由 inp_notifications 控制,内部用 static 变量 ts 和 prev_text 做节流:相同内容若间隔不足 3600*6 秒(6 小时)不再重发,避免刷屏。 开 MT5 把这段代码塞进 EA,故意连错一次库,就能在专家日志看到对应的 Error/Transaction Error 分支,并验证 6 小时内同文本只推一次。外汇与贵金属行情受杠杆影响波动剧烈,这类告警仅作技术状态提示,不代表任何方向判断。

MQL5 / C++
bypass = true; class=class="str">"cmt">// sending failed
      }
      else
        bypass = true; class=class="str">"cmt">// failed to create the table
      }
    else
      bypass = true; class=class="str">"cmt">// there is no table and it is not allowed to create one
      }
    if(bypass==true)
      {
      if(GetLastError()==(ERR_USER_ERROR_FIRST+MYSQL_ERR_SERVER_ERROR))
        {
        class=class="str">"cmt">// in case of a server error
        PrintNotify("MySQL Server Error: "+IntegerToString(mysqlt.GetServerError().code)+" ("+mysqlt.GetServerError().message+")");
        }
      else
        {
        if(GetLastError()>=ERR_USER_ERROR_FIRST)
          PrintNotify("Transaction Error: "+EnumToString(ENUM_TRANSACTION_ERROR(GetLastError()-ERR_USER_ERROR_FIRST)));
        else
          PrintNotify("Error: "+IntegerToString(GetLastError()));
        }
      class="kw">continue;
      }
    }
  else
    class="kw">continue;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Print to console and send notification                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void PrintNotify(class="type">class="kw">string text)
  {
class=class="str">"cmt">//--- display in the console
  Print(text);
class=class="str">"cmt">//--- send a notification
  if(inp_notifications==true)
    {
    class="kw">static class="type">class="kw">datetime ts = class="num">0;       class=class="str">"cmt">// last notification sending time
    class="kw">static class="type">class="kw">string prev_text = ""; class=class="str">"cmt">// last notification text
    if(text!=prev_text || (text==prev_text && (TimeLocal()-ts)>=(class="num">3600*class="num">6)))
      {
      class=class="str">"cmt">// identical notifications are sent one after another no more than once every class="num">6 hours
      if(SendNotification(text)==true)
        {
        ts = TimeLocal();
        prev_text = text;
        }
      }
    }

「把信号特征批量落库的那段插入逻辑」

做信号回测时,最怕每根 K 线都往数据库写一条,表很快被撑爆。下面这段函数用 insert ignore 配合外部传入的时间戳 tc,把一批 SignalProperties 一次性写进指定表,重复键直接跳过,实际落库行数往往小于数组长度。 函数先拼字段名:6 个 double、7 个 int、3 个 datetime、5 个 string,共 21 个信号属性列,再加一个 TimeInsert 时间列。double 统一保留 4 位小数(DoubleToString(...,4)),这是后续做价格行为聚类时够用又不炸精度的折中。 写入不是逐条提交,而是用循环把多行 value 拼进同一条 SQL,减少 MT5 与 MySQL 的往返开销。在 1 分钟周期跑 EURUSD,单批 sbuf 大小常见为 200~500,网络正常时整批插入耗时通常在几十毫秒级。 执行后若收到 OK 包,会打印 Added N entries,N 就是真实受影响行数——如果 N 比 ArraySize(sbuf) 小,说明那批里有一部分因唯一索引被 ignore 了。外汇和贵金属波动大、跳空多,落库前务必确认 tc 与信号索引逻辑一致,否则忽略机制可能误吞有效样本,这类数据偏差会直接污染后续 AIGC 分析。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Write signal properties to the database                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool  DB_Write(SignalProperties &sbuf[],class="type">class="kw">string tab_name,class="type">class="kw">datetime tc)
  {
class=class="str">"cmt">//--- prepare a query
   class="type">class="kw">string q = "insert ignore into `"+inp_db+"`.`"+tab_name+"` (";
   q+= "`TimeInsert`";
   for(class="type">int i=class="num">0;i<class="num">6;i++)
      q+= ",`"+tab_signal_base_double[i].name+"`";
   for(class="type">int i=class="num">0;i<class="num">7;i++)
      q+= ",`"+tab_signal_base_integer[i].name+"`";
   for(class="type">int i=class="num">0;i<class="num">3;i++)
      q+= ",`"+tab_signal_base_datetime[i].name+"`";
   for(class="type">int i=class="num">0;i<class="num">5;i++)
      q+= ",`"+tab_signal_base_string[i].name+"`";
   q+= ") values ";
   class="type">int sz = ArraySize(sbuf);
   for(class="type">int s=class="num">0;s<sz;s++)
     {
      q+=(s==class="num">0)?"(":",(";
      q+= "&class="macro">#x27;"+DatetimeToMySQL(tc)+"&class="macro">#x27;";
      for(class="type">int i=class="num">0;i<class="num">6;i++)
         q+= ",&class="macro">#x27;"+DoubleToString(sbuf[s].props_double[i],class="num">4)+"&class="macro">#x27;";
      for(class="type">int i=class="num">0;i<class="num">7;i++)
         q+= ",&class="macro">#x27;"+IntegerToString(sbuf[s].props_int[i])+"&class="macro">#x27;";
      for(class="type">int i=class="num">0;i<class="num">3;i++)
         q+= ",&class="macro">#x27;"+DatetimeToMySQL(sbuf[s].props_datetime[i])+"&class="macro">#x27;";
      for(class="type">int i=class="num">0;i<class="num">5;i++)
         q+= ",&class="macro">#x27;"+sbuf[s].props_str[i]+"&class="macro">#x27;";
      q+=")";
     }
class=class="str">"cmt">//--- send a query
   if(mysqlt.Query(q)==false)
      class="kw">return false;
class=class="str">"cmt">//--- if the query is successful, get the pointer to it
   CMySQLResponse *r = mysqlt.Response(class="num">0);
   if(CheckPointer(r)==POINTER_INVALID)
      class="kw">return false;
class=class="str">"cmt">//--- the Ok type packet should be received as a response featuring the number of affected rows; display it
   if(r.Type()==MYSQL_RESPONSE_OK)
      Print("Added ",r.AffectedRows()," entries");
class=class="str">"cmt">//
   class="kw">return true;
  }

◍ 把信号属性做成动态看图程序

上一节的服务只负责把变化的信号属性写进数据库,这一节要补一个前端程序:按设定间隔把选定的属性画成走势图,并支持按属性值做筛选,只留你关心的信号。 界面底层用了 Anatoli Kazharski 的 EasyAndFastGUI 图形库,省去从零搭高级 GUI 的麻烦。左侧放日期区间,右侧直接渲染所选信号的净值曲线。 实测对照信号官网的净值图(图例 2b),两者形状接近但存在细微差异:PC 空闲时数据库会出现“空洞”,而 MT5 终端刷新信号属性本身有较长延迟,这两点都会让本地图比网页图少几段采样。 如果你要复现,先确认服务层已在跑,再把图形库挂进 EA/脚本,指定 property="净值" 和刷新间隔,开 MT5 就能看到自己的动态图。外汇与贵金属信号受杠杆与滑点影响,属性延迟可能放大,属高风险验证项。

常见问题

用网络函数拉取 JSON 后按字段映射进预定义结构体数组,避免每次重连建表,本地只保留最新快照即可。
信号 ID、更新时间、品种名这三个字段必须加索引,否则服务轮询多品种时全表扫描会拖垮响应。
小布可对接你的信号库,属性异动时推送到对应品种页,不用自己写轮询和通知节流逻辑。
捕获错误码后做指数退避重连,并把失败批次写入本地文件,恢复后补插,同时限制每秒通知条数。
把属性变化写进缓冲并绑定图表对象,用定时器重绘,属性一变图形自动跟着改,省去手动刷新。