应用网络函数,或无需 DLL 的 MySQL:第 II 部分 - 监视信号属性变化的程序·综合运用
「先把筛选条件钉死」
做信号数据库分析前,得先让程序知道要捞哪些数据。打开 MT5 终端的信号表,第一步不是看曲线,而是框定样本边界:日期范围先切出来,不然回测和统计会被早期低流动性数据带偏。 具体筛选项落到四个维度:按 SIGNAL_BASE_CURRENCY、SIGNAL_BASE_AUTHOR_LOGIN、SIGNAL_BASE_BROKER 做等值条件过滤;对 SIGNAL_BASE_EQUITY、SIGNAL_BASE_GAIN、SIGNAL_BASE_MAX_DRAWDOWN、SIGNAL_BASE_SUBSCRIBERS 设有效数值区间。比如把 GAIN 卡在 20%~200%、MAX_DRAWDOWN 压到 30% 以内,样本量可能直接砍掉七成,但噪音也跟着降了。 选定 SIGNAL_BASE_ID 之后,程序才按指定属性拉出图形。这一步之前的所有条件,都是在帮后面的图型构造缩圈——条件越窄,图越能暴露单一信号源的真实行为,外汇与贵金属信号本身高波动高杠杆,样本不清就谈相关性等于蒙眼走钢丝。
◍ 筛选模块怎么搭才不卡查询
图形界面里最占地方的是双日历模块,用来框定 from 和 to 的日期区间;组合框负责从已有数据里挑货币、经纪商、作者之类的值,输入字段则改极值属性。想完全不限某一条件,就在组合框列表最前面选 All,这比手动清空更不容易漏发查询。 输入字段旁要配一个默认禁用的复选框,勾上才允许改极端值,否则沿用服务端回传的边界。日期范围是硬要求,其余都能按需留空。图例 2a 里货币和经纪商写死,作者名放 All,查询量明显更小。 每个组合框的候选项、输入字段的极值,都来自上一次查询返回的结果,不是写死的。先圈出一批信号 ID,再发绘图查询,能避免一次性拉全表把 MT5 卡死。 状态栏里跑了三个计数器:已收字节、已发字节、最后一笔业务时间(图例 2)。业务失败直接抛错误代码(图例 3),开 MT5 连自己的 MySQL 时可拿这三项和原版比对延迟。外汇与贵金属数据查询涉及实时行情,误配区间可能返回空图,属正常高风险现象。
错误提示落点的取舍
状态栏空间有限,多数交易服务器的错误文本描述过长,硬塞进进度栏只会截断或乱码,所以实际要把报错抛到“智能系统”选项卡里才看得全。 本文主线不碰 UI 实现,图形库的交互细节作者在前序系列里拆过,这里只动三个文件:MainWindow.mqh 管界面搭建,Program.mqh 管界面交互,Main.mqh 是后加的,专门接数据库操控。 如果你在 MT5 里跑 EA 遇到报错看不见全文,先确认日志是不是落在“智能系统”页,而不是盯着底部状态栏找原因。外汇与贵金属杠杆高,报错漏看可能让仓位风险失控。
「批量拉数据的查询打包法」
MT5 信号面板启动时要填好几类下拉框和极值区间,数据库查询可归成三组:组合框列表值、输入字段模块极值、绘图数据。后两类各一条 SELECT 就能搞定,列表值却得给每个框单独发一条。 界面要求所有值尽量同时刷新,且不能只更新列表的一部分。把多条查询用分号拼进一个包发出去,就能让客户端等到所有响应齐了再动界面;某一笔业务哪怕在传输或处理上卡了,也只是整体延后。若其中出错,对应图形元素列表的部分更新会被禁用。 下面这段是程序启动立刻抛出的五条查询序列,前四条在 2019-11-01 至 2019-12-15 的区间里按 Currency、Broker、AuthorLogin、Id 各自 group by 取唯一值;第五条在同一区间捞 Equity、Gain、Drawdown、Subscribers 的最小最大值。抓包看,查询是在单个 TCP 包里送出的,响应则分散在多个包返回。 嵌套 SELECT 里只要有一条报错,后续查询就直接不处理——MySQL 是逐条跑直到撞上第一个错误。写批量查询时得把易错的条件放后面,或者提前单独验一遍。
select `Currency` from `signals_mt5`.`metaquotes_demo__17273508` where `TimeInsert`>=&class="macro">#x27;class="num">2019-class="num">11-class="num">01 class="num">00:class="num">00:class="num">00&class="macro">#x27; AND `TimeInsert`<=&class="macro">#x27;class="num">2019-class="num">12-class="num">15 class="num">23:class="num">59:class="num">59&class="macro">#x27; group by `Currency`; select `Broker` from `signals_mt5`.`metaquotes_demo__17273508` where `TimeInsert`>=&class="macro">#x27;class="num">2019-class="num">11-class="num">01 class="num">00:class="num">00:class="num">00&class="macro">#x27; AND `TimeInsert`<=&class="macro">#x27;class="num">2019-class="num">12-class="num">15 class="num">23:class="num">59:class="num">59&class="macro">#x27; group by `Broker`; select `AuthorLogin` from `signals_mt5`.`metaquotes_demo__17273508` where `TimeInsert`>=&class="macro">#x27;class="num">2019-class="num">11-class="num">01 class="num">00:class="num">00:class="num">00&class="macro">#x27; AND `TimeInsert`<=&class="macro">#x27;class="num">2019-class="num">12-class="num">15 class="num">23:class="num">59:class="num">59&class="macro">#x27; group by `AuthorLogin`; select `Id` from `signals_mt5`.`metaquotes_demo__17273508` where `TimeInsert`>=&class="macro">#x27;class="num">2019-class="num">11-class="num">01 class="num">00:class="num">00:class="num">00&class="macro">#x27; AND `TimeInsert`<=&class="macro">#x27;class="num">2019-class="num">12-class="num">15 class="num">23:class="num">59:class="num">59&class="macro">#x27; group by `Id`; select Min(`Equity`) as EquityMin, Max(`Equity`) as EquityMax, Min(`Gain`) as GainMin, Max(`Gain`) as GainMax, Min(`Drawdown`) as DrawdownMin, Max(`Drawdown`) as DrawdownMax, Min(`Subscribers`) as SubscribersMin, Max(`Subscribers`) as SubscribersMax from `signals_mt5`.`metaquotes_demo__17273508` where `TimeInsert`>=&class="macro">#x27;class="num">2019-class="num">11-class="num">01 class="num">00:class="num">00:class="num">00&class="macro">#x27; AND `TimeInsert`<=&class="macro">#x27;class="num">2019-class="num">12-class="num">15 class="num">23:class="num">59:class="num">59&class="macro">#x27;
◍ 用 WHERE 把信号池砍到能盯的量级
直接在查询里挂 WHERE 条件,能大幅压缩信号列表。比如只想要基准货币为 USD、增长落在 100–1399 区间、且订阅数非零的信号,就别把全表拉回来再肉眼筛。 有个细节容易踩坑:组合框选了 Currency 值时,查询里反而不会从 Currency 列表取数——因为选定具体值等于排除了其他值。但输入字段里的极值条件仍会执行,目的是让用户看见真实可取范围。 实测若最小增长填 100,服务端返回后该值可能被替换成 135,因为满足所选条件的数据集里最接近的最小值是 135。这说明筛选下限会被数据分布「反写」,不是你填多少就锁多少。 筛选后信号 ID 组合框的可选项会明显变少,挑一个就能在图上跟踪它的属性变化。外汇与贵金属信号波动剧烈、杠杆风险高,跟单前务必在 MT5 里自行复核历史权益曲线。 下面这段 SQL 是筛选器的核心:四段查询分别按 Broker、AuthorLogin、Id 分组取列表,最后一段捞 Equity / Gain / Drawdown / Subscribers 的极大极小。注意绿色 WHERE 块在四个查询里完全复用,时间窗锁在 2019-11-01 到 2019-12-15,Currency='USD' 且 Gain 在 100–1399。
select `Broker` from `signals_mt5`.`metaquotes_demo__17273508` where `TimeInsert`>=&class="macro">#x27;class="num">2019-class="num">11-class="num">01 class="num">00:class="num">00:class="num">00&class="macro">#x27; AND `TimeInsert`<=&class="macro">#x27;class="num">2019-class="num">12-class="num">15 class="num">23:class="num">59:class="num">59&class="macro">#x27; AND `Currency`=&class="macro">#x27;USD&class="macro">#x27; AND `Gain`>=&class="macro">#x27;class="num">100&class="macro">#x27; AND `Gain`<=&class="macro">#x27;class="num">1399&class="macro">#x27; group by `Broker`; select `AuthorLogin` from `signals_mt5`.`metaquotes_demo__17273508` where `TimeInsert`>=&class="macro">#x27;class="num">2019-class="num">11-class="num">01 class="num">00:class="num">00:class="num">00&class="macro">#x27; AND `TimeInsert`<=&class="macro">#x27;class="num">2019-class="num">12-class="num">15 class="num">23:class="num">59:class="num">59&class="macro">#x27; AND `Currency`=&class="macro">#x27;USD&class="macro">#x27; AND `Gain`>=&class="macro">#x27;class="num">100&class="macro">#x27; AND `Gain`<=&class="macro">#x27;class="num">1399&class="macro">#x27; group by `AuthorLogin`; select `Id` from `signals_mt5`.`metaquotes_demo__17273508` where `TimeInsert`>=&class="macro">#x27;class="num">2019-class="num">11-class="num">01 class="num">00:class="num">00:class="num">00&class="macro">#x27; AND `TimeInsert`<=&class="macro">#x27;class="num">2019-class="num">12-class="num">15 class="num">23:class="num">59:class="num">59&class="macro">#x27; AND `Currency`=&class="macro">#x27;USD&class="macro">#x27; AND `Gain`>=&class="macro">#x27;class="num">100&class="macro">#x27; AND `Gain`<=&class="macro">#x27;class="num">1399&class="macro">#x27; group by `Id`; select Min(`Equity`) as EquityMin, Max(`Equity`) as EquityMax, Min(`Gain`) as GainMin, Max(`Gain`) as GainMax, Min(`Drawdown`) as DrawdownMin, Max(`Drawdown`) as DrawdownMax, Min(`Subscribers`) as SubscribersMin, Max(`Subscribers`) as SubscribersMax from `signals_mt5`.`metaquotes_demo__17273508` where `TimeInsert`>=&class="macro">#x27;class="num">2019-class="num">11-class="num">01 class="num">00:class="num">00:class="num">00&class="macro">#x27; AND `TimeInsert`<=&class="macro">#x27;class="num">2019-class="num">12-class="num">15 class="num">23:class="num">59:class="num">59&class="macro">#x27; AND `Currency`=&class="macro">#x27;USD&class="macro">#x27; AND `Gain`>=&class="macro">#x27;class="num">100&class="macro">#x27; AND `Gain`<=&class="macro">#x27;class="num">1399&class="macro">#x27;
让连接活过用户发呆的一分钟
看信号属性动态的程序不能每次查询都重连数据库。用户改一个受控参数,结果就回写一次库,若每次都建连再断连,纯属浪费握手开销。 持续连接模式的核心是把超时设成 60 秒:用户闲置超过 60 秒连接才断,期间只要操作频率高于每分钟一次,连接一直保活。抓包可见,一次查询后跟着一串 10 秒周期的 ping,满一分钟无操作才关闭。 ping 周期硬性设为 10 秒,而不是蹭服务器 3600 秒的 interactive_timeout。服务端超时太长,只有当下一条查询发出才发现链路已死;10 秒探活能几乎实时感知掉线。 代码里先判指针失效才新建事务对象,Config 最后一个参数 60000 即 60 秒超时,PingPeriod(10000) 锁死 10 秒心跳;下方 SQL 是拉服务端 interactive_timeout 值做核对。外汇贵金属行情中断连可能漏写关键信号数据,高波动时段建议本地先缓存再补写。
if(CheckPointer(mysqlt)==POINTER_INVALID) { mysqlt = new CMySQLTransaction; mysqlt.Config(m_mysql_server,m_mysql_port,m_mysql_login,m_mysql_password,class="num">60000); mysqlt.PingPeriod(class="num">10000); } show variables where `Variable_name`=&class="macro">#x27;interactive_timeout&class="macro">#x27;
「从多表查询里抠出下拉框与极值」
做信号属性面板时,最麻烦的不是画图,而是把数据库返回的多条结果正确灌进组合框和输入框。GetData() 就是干这个的:先拼一条含多个 SELECT 的批量查询,再在循环里按响应顺序逐个塞数据。 查询拼法很直接——Currency / Broker / Author 若当前选的是 All,就各自发一条 group by 查询;最后再补一条算 Equity、Gain、Drawdown、Subscribers 极值的汇总查询。Condition() 方法在别处统一收集过滤条件,避免各处硬编码。 响应回来后,mysqlt.Responses() 给出条数,循环里用 UpdateComboBox() 按字段名匹配:命中就写进列表并返回 true,同时把上一次选中的 set_value 设为当前索引;若新列表里没有旧值,则落到索引 1(即 Select... 之后那项)。文本输入框的极值由 UpdateTextEditRange() 单独刷。 m_duration 变量累计了从发 query 到收完响应的总耗时,并扔进状态栏。这个耗时是数据库优化的晴雨表——若它持续走高,往往意味着服务端索引或查询结构有瑕疵,值得回头查 SQL。外汇与贵金属信号数据波动大,这类远程查询延迟还可能受经纪商 API 抖动影响,实盘前请在 MT5 里先跑通再上量。
class="type">void CMain::GetData(class="type">void) { if(CheckPointer(mysqlt)==POINTER_INVALID) { mysqlt = new CMySQLTransaction; mysqlt.Config(m_mysql_server,m_mysql_port,m_mysql_login,m_mysql_password,class="num">60000); mysqlt.PingPeriod(class="num">10000); } class=class="str">"cmt">//--- Save signal id class="type">class="kw">string signal_id = SignalId(); if(signal_id=="Select...") signal_id=""; class=class="str">"cmt">//--- Make a query class="type">class="kw">string q = ""; if(Currency()=="All") { q+= "select `Currency` from `"+m_mysql_db+"`.`"+m_mysql_table+"` where "+Condition()+" group by `Currency`; "; } if(Broker()=="All") { q+= "select `Broker` from `"+m_mysql_db+"`.`"+m_mysql_table+"` where "+Condition()+" group by `Broker`; "; } if(Author()=="All") { q+= "select `AuthorLogin` from `"+m_mysql_db+"`.`"+m_mysql_table+"` where "+Condition()+" group by `AuthorLogin`; "; } q+= "select `Id` from `"+m_mysql_db+"`.`"+m_mysql_table+"` where "+Condition()+" group by `Id`; "; q+= "select Min(`Equity`) as EquityMin, Max(`Equity`) as EquityMax"; q+= ", Min(`Gain`) as GainMin, Max(`Gain`) as GainMax"; q+= ", Min(`Drawdown`) as DrawdownMin, Max(`Drawdown`) as DrawdownMax"; q+= ", Min(`Subscribers`) as SubscribersMin, Max(`Subscribers`) as SubscribersMax from `"+m_mysql_db+"`.`"+m_mysql_table+"` where "+Condition(); class=class="str">"cmt">//--- Display the transaction result in the status bar if(UpdateStatusBar(mysqlt.Query(q))==false) class="kw">return; class=class="str">"cmt">//--- Set accepted values in the combo box lists and extreme values of the class="kw">input fields class="type">uint responses = mysqlt.Responses(); for(class="type">uint j=class="num">0; j<responses; j++) { if(mysqlt.Response(j).Fields()<class="num">1) class="kw">continue; if(UpdateComboBox(m_currency,mysqlt.Response(j),"Currency")==true) class="kw">continue; if(UpdateComboBox(m_broker,mysqlt.Response(j),"Broker")==true) class="kw">continue;
◍ 筛选条件拼接到 SQL 字符串的落点
信号筛选面板的逻辑核心,是把 UI 控件状态翻译成一段 WHERE 子句。CMain::Condition() 先固定塞入时间区间:用 TimeFrom()/TimeTo() 格式化成 TimeInsert 的两个边界,这一步任何查询都跑不掉。
下拉框只在非「All」时才追加条件。注意 Broker 字段要做单引号转义——StringReplace(broker,"'","\\'") 漏掉这行,经纪商名里带撇号的(比如某些塞浦路斯牌照名)会直接让 SQL 语法崩掉。
数值区间控件靠 IsPressed() 判断是否启用。Equity、Gain、Drawdown、Subscribers 四个范围都是「大于等于下限 AND 小于等于上限」的闭区间拼法,GetValue() 取到的就是文本框里的字符串,没做类型校验,所以界面上填非数字会把这个脏值原样送进查询。
UpdateComboBox() 负责把下拉项刷新成查询结果。它用 p.Rows()+1 算总行数,和现有列表总数不等才重建——保留索引 0 的「All」占位,列表可视高度按 18*(min(total,16))+3 像素算,超过 16 行就截断滚动。开 MT5 把这段挂到自己的信号扫描 EA 上,改 Condition() 里的字段名就能直接复用。
if(UpdateComboBox(m_author,mysqlt.Response(j),"AuthorLogin")==true) class="kw">continue; if(UpdateComboBox(m_signal_id,mysqlt.Response(j),"Id",signal_id)==true) class="kw">continue; class=class="str">"cmt">// UpdateTextEditRange(m_equity_from,m_equity_to,mysqlt.Response(j),"Equity"); UpdateTextEditRange(m_gain_from,m_gain_to,mysqlt.Response(j),"Gain"); UpdateTextEditRange(m_drawdown_from,m_drawdown_to,mysqlt.Response(j),"Drawdown"); UpdateTextEditRange(m_subscribers_from,m_subscribers_to,mysqlt.Response(j),"Subscribers"); } GetSeries(); } class="type">class="kw">string CMain::Condition(class="type">void) { class=class="str">"cmt">//--- Add the time interval class="type">class="kw">string s = "`TimeInsert`>=&class="macro">#x27;"+time_from(TimeFrom())+"&class="macro">#x27; AND `TimeInsert`<=&class="macro">#x27;"+time_to(TimeTo())+"&class="macro">#x27; "; class=class="str">"cmt">//--- Add the remaining conditions if required class=class="str">"cmt">//--- For drop-down lists, the current value should not be equal to All if(Currency()!="All") s+= "AND `Currency`=&class="macro">#x27;"+Currency()+"&class="macro">#x27; "; if(Broker()!="All") { class="type">class="kw">string broker = Broker(); class=class="str">"cmt">//--- the names of some brokers contain characters that should be escaped StringReplace(broker,"&class="macro">#x27;","\\&class="macro">#x27;"); s+= "AND `Broker`=&class="macro">#x27;"+broker+"&class="macro">#x27; "; } if(Author()!="All") s+= "AND `AuthorLogin`=&class="macro">#x27;"+Author()+"&class="macro">#x27; "; class=class="str">"cmt">//--- A checkbox should be set for class="kw">input fields if(m_equity_from.IsPressed()==true) s+= "AND `Equity`>=&class="macro">#x27;"+m_equity_from.GetValue()+"&class="macro">#x27; AND `Equity`<=&class="macro">#x27;"+m_equity_to.GetValue()+"&class="macro">#x27; "; if(m_gain_from.IsPressed()==true) s+= "AND `Gain`>=&class="macro">#x27;"+m_gain_from.GetValue()+"&class="macro">#x27; AND `Gain`<=&class="macro">#x27;"+m_gain_to.GetValue()+"&class="macro">#x27; "; if(m_drawdown_from.IsPressed()==true) s+= "AND `Drawdown`>=&class="macro">#x27;"+m_drawdown_from.GetValue()+"&class="macro">#x27; AND `Drawdown`<=&class="macro">#x27;"+m_drawdown_to.GetValue()+"&class="macro">#x27; "; if(m_subscribers_from.IsPressed()==true) s+= "AND `Subscribers`>=&class="macro">#x27;"+m_subscribers_from.GetValue()+"&class="macro">#x27; AND `Subscribers`<=&class="macro">#x27;"+m_subscribers_to.GetValue()+"&class="macro">#x27; "; class="kw">return s; } class="type">bool CMain::UpdateComboBox(CComboBox &object, CMySQLResponse *p, class="type">class="kw">string name, class="type">class="kw">string set_value="") { class="type">int col_idx = p.Field(name); if(col_idx<class="num">0) class="kw">return false; class="type">uint total = p.Rows()+class="num">1; if(total!=object.GetListViewPointer().ItemsTotal()) { class="type">class="kw">string tmp = object.GetListViewPointer().GetValue(class="num">0); object.GetListViewPointer().Clear(); object.ItemsTotal(total); object.SetValue(class="num">0,tmp); object.GetListViewPointer().YSize(class="num">18*((total>class="num">16)?class="num">16:total)+class="num">3); }
从 MySQL 拉信号序列填图缓冲
GetSeries() 负责把数据库里某信号参数按时间升序取出来,灌进绘图用的 x_buf / y_buf。若 SignalId() 还是 "Select...",直接释放两个缓冲并刷新空图退出,避免无效查询。 连接层做了懒加载:CheckPointer(mysqlt)==POINTER_INVALID 时才 new 一个 CMySQLTransaction,Config 里超时设 60000 毫秒、PingPeriod 10000 毫秒保活。查询串按 Parameter() 选列,用 TimeFrom()/TimeTo() 转成的 time_from/time_to 卡时间窗,再按 Id 过滤。 响应回来后先判 Responses() 和 Rows() 都 ≥1,否则 return。关键一行 r.ColumnToArray(Parameter(),y_buf,false) 把列直接拷进 y_buf,第三个参数 false 表示不校验类型——若返回 <1 就放弃。X 轴用 ArrayResize(x_buf,rows) 后填 0..rows-1 整数索引,最后 UpdateSeries() 重绘。外汇与贵金属数据波动剧烈,这类远程序列可能因网络或库表结构异常中断,实盘前应在 MT5 策略测试器外单独跑一次 Query 看响应行数。
class="type">uint set_val_idx = class="num">0; for(class="type">uint i=class="num">1; i<total; i++) { class="type">class="kw">string value = p.Value(i-class="num">1,col_idx); object.SetValue(i,value); if(set_value!="" && value==set_value) set_val_idx = i; } class=class="str">"cmt">//--- if there is no specified value, but there are others, select the topmost one if(set_value!="" && set_val_idx==class="num">0 && total>class="num">1) set_val_idx=class="num">1; class=class="str">"cmt">//--- ComboSelectItem(object,set_val_idx); class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CMain::UpdateTextEditRange(CTextEdit &obj_from,CTextEdit &obj_to, CMySQLResponse *p, class="type">class="kw">string name) { if(p.Rows()<class="num">1) class="kw">return false; else class="kw">return SetTextEditRange(obj_from,obj_to,p.Value(class="num">0,name+"Min"),p.Value(class="num">0,name+"Max")); } class="type">void CMain::GetSeries(class="type">void) { if(SignalId()=="Select...") { class=class="str">"cmt">// if a signal is not selected ArrayFree(x_buf); ArrayFree(y_buf); UpdateSeries(); class="kw">return; } if(CheckPointer(mysqlt)==POINTER_INVALID) { mysqlt = new CMySQLTransaction; mysqlt.Config(m_mysql_server,m_mysql_port,m_mysql_login,m_mysql_password,class="num">60000); mysqlt.PingPeriod(class="num">10000); } class="type">class="kw">string q = "select `"+Parameter()+"` "; q+= "from `"+m_mysql_db+"`.`"+m_mysql_table+"` "; q+= "where `TimeInsert`>=&class="macro">#x27;"+time_from(TimeFrom())+"&class="macro">#x27; AND `TimeInsert`<=&class="macro">#x27;"+time_to(TimeTo())+"&class="macro">#x27; "; q+= "AND `Id`=&class="macro">#x27;"+SignalId()+"&class="macro">#x27; order by `TimeInsert` asc"; class=class="str">"cmt">//--- Send a query if(UpdateStatusBar(mysqlt.Query(q))==false) class="kw">return; class=class="str">"cmt">//--- Check the number of responses if(mysqlt.Responses()<class="num">1) class="kw">return; CMySQLResponse *r = mysqlt.Response(class="num">0); class="type">uint rows = r.Rows(); if(rows<class="num">1) class="kw">return; class=class="str">"cmt">//--- copy the column to the graph data buffer(false - do not check the types) if(r.ColumnToArray(Parameter(),y_buf,false)<class="num">1) class="kw">return; class=class="str">"cmt">//--- form X axis labels if(ArrayResize(x_buf,rows)!=rows) class="kw">return; for(class="type">uint i=class="num">0; i<rows; i++) x_buf[i] = i; class=class="str">"cmt">//--- Update the graph UpdateSeries(); }
「切换品种与信号源时的缓存清场」
EA 在运行时如果 broker 连接信息变动,或者交易者在界面上换了 SignalID 下拉框里的信号源,都必须把已有的持续时间计数清零,否则旧品种的 K 线节奏会污染新信号的逻辑。 上面两段 MQL5 处理函数做的就是这件事:OnChangeBroker 里把 m_duration 置 0 后调 GetData() 重新拉数据;OnChangeSignalId 里同样清零 m_duration,但走的是 GetSeries() 去重建序列。两者都只做轻量重置,不立即重算指标,等下一 tick 自然续上。 实盘里这类回调容易被忽略——比如服务器重连触发 OnChangeBroker,若不清 m_duration,原用于判断横盘延续的计时器可能直接带着旧值跑,导致信号误发。贵金属与外汇杠杆高,这类隐性 bug 会放大滑点风险,建议在 MT5 策略测试器里手动断网重连验证一次。
class="type">void CMain::OnChangeBroker(class="type">void) { m_duration=class="num">0; GetData(); } ... class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Handler of the value change event in the "SignalID" combo box | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CMain::OnChangeSignalId(class="type">void) { m_duration=class="num">0; GetSeries(); }
◍ 把这条线请下神坛
前面几篇把 MySQL 连通器、信号读写、GUI 库都拆完了,落到最后这一节其实就一句话:Socket 网络函数让 MQL5 摆脱 DLL 依赖成为可能,但它不是银弹。频繁查库时持续连接比短连合理,ping 保活能避开服务端主动断线——这是实盘跑服务该记下的点。
评论区里 leonerd 在 2022.02.19 14:28:59 日志曝出 array out of range in 'MySQLTransaction.mqh' (503,11),m_responses=1 但缓冲越界;Davi 在 2023.05.26 更新到 3759 版本后同样踩到 SocketRead 里的数组越界。说明无 DLL 方案在版本迭代中仍有边界 bug,复制代码前先对照自己 MT5 构建号。
外汇和贵金属波动剧烈、杠杆风险高,这类后台信号服务只解决数据通道,不替你过滤行情风险。开 MT5 把附件 MySQLTransaction.mqh 的 503 行附近数组长度判断改掉,比轻信「连通即稳」更有用。
class="num">2022.02.class="num">19 class="num">14:class="num">28:class="num">59.455 CMySQLTransaction::PacketDataHandler: m_responses=class="num">1 class="num">2022.02.class="num">19 class="num">14:class="num">28:class="num">59.455 CMySQLTransaction::PacketDataHandler: m_rbuf size=class="num">1 class="num">2022.02.class="num">19 class="num">14:class="num">28:class="num">59.455 array out of range in &class="macro">#x27;MySQLTransaction.mqh&class="macro">#x27; (class="num">503,class="num">11) class="type">bool CMySQLTransaction::Query(class="type">class="kw">string q) ENUM_TRANSACTION_STATE CMySQLTransaction::Incoming(class="type">uchar &data[], class="type">uint len) { class="type">int ptr=class="num">0; class=class="str">"cmt">// 数据缓冲区中当前字节的索引 ENUM_TRANSACTION_STATE result=MYSQL_TRANSACTION_IN_PROGRESS; class=class="str">"cmt">// 处理接收到的数据的结果 class="kw">while(len>class="num">0) { if(m_packet.total_length==class="num">0) { class=class="str">"cmt">//--- 如果数据包中的数据数量未知 class="kw">while(m_rcv_len<class="num">4 && len>class="num">0) { m_hdr[m_rcv_len] = data[ptr]; m_rcv_len++; ptr++; len--; } Transaction Error: MYSQL_ERR_AUTHORIZATION_TIMEOUT MySQL Server Error: class="num">1045 (Access denied for user &class="macro">#x27;abcd&class="macro">#x27;@&class="macro">#x27;localhost&class="macro">#x27; (class="kw">using password: YES))