应用网络函数,或无需 DLL 的 MySQL:第 II 部分 - 监视信号属性变化的程序·进阶篇
把信号表读进 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) 验证行数,比盲跑稳妥。
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 做了唯一索引,保证同一次抓取不会重复写。其余字段如 Gain、Drawdown、ROI、Pips、Subscribers、Trades 都是后续做信号筛选的硬指标——例如 Drawdown 用 DOUBLE 存,意味着你能直接按浮点阈值过滤掉回撤超 30% 的信号。
外汇与贵金属信号的高风险在于:历史 Gain 和 ROI 漂亮,不等于未来概率高;建表时把 BrokerServer、Leverage、TradeMode 一并落库,才可能后期按经纪商和点差环境做分组回测。
别把建表当纯存储
字段宽度 CHAR(50) 对 Broker、 Currency 够用,但 Name 若超长会被截断,跑批量抓取前先确认信号源命名规则,否则连表成功却丢字段内容。
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 在生产库上可能让脚本自说自话建表,建议只在隔离测试库验证后再关掉。
"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 小时内同文本只推一次。外汇与贵金属行情受杠杆影响波动剧烈,这类告警仅作技术状态提示,不代表任何方向判断。
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 分析。
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 就能看到自己的动态图。外汇与贵金属信号受杠杆与滑点影响,属性延迟可能放大,属高风险验证项。