SQLite: MQL5 原生 SQL 数据库操纵·综合运用
🗄️

SQLite: MQL5 原生 SQL 数据库操纵·综合运用

(3/3)·从交易历史分析到优化结果落库,27 节打通 MetaTrader 5 原生 SQL 工作流

实战向进阶 第 3/3 篇
把回测跑完的成交流水丢进 CSV 再手动筛,等于每次重做一遍数据库该干的活。MQL5 原生 SQLite 让你在 EA 里直接建表、索引、按策略切片,省掉导出再导入的断点。这一篇把前面铺垫的接口全部串成可用流水线。

「从成交明细里扒开多品种持仓结构」

下面这段原始成交记录来自某次回测导出,时间锚点集中在 2019.09.06 亚欧美时段交叉口。前四行是未平仓状态的挂单/开仓痕迹:[6] 号 USDJPY 在 08:00 以 106.962 开 0.1 手空,手续费 -0.16,魔术码 3;[7]–[9] 则在 10:30 同时打出 EURUSD 多、GBPUSD 空、EURJPY 多,各 0.1 手,点值手续费介于 -0.18 到 -0.20。 紧接着的『first 10 trades』表把已平单按时间排列:[0] 号 USDCAD 06:00:03 开空、18:00 平于 1.31761,毛利润 -42.43(含 -0.32 手续费);[1] 号 USDCHF 同秒开多、晚盘平于 0.98641,毛利 +5.68。可以看到,同批次进场的跨品种单,一天之内盈亏分化已经超过 48 点绝对值。 这种明细最适合直接丢进 MT5 的『交易报告』对照窗做手验:把品种、volume、magic 三个字段筛出来,就能还原那套策略在波动率抬升日的仓位倾斜。外汇与贵金属杠杆高,这类跨品种同魔术码批量单一旦遇央行事件,回撤可能非线性放大,验证时务必用历史tick而非仅看日线。

从成交明细看多品种持仓的真实盈亏分布

下面这组 2019 年 9 月 6 日的实盘成交片段,覆盖了 EURUSD、AUDUSD、USDCHF、USDJPY、GBPUSD、EURJPY 六个品种,每笔手数均为 0.10,适合用来核对 MT5「账户历史」里字段排列和浮动盈亏的计算逻辑。 以 [3] 号单为例:AUDUSD 于 07:00 开多、0.68203 入场,09 日 03:30 平仓在 0.68419,价格上行约 21.6 点,但手续费 -0.22、掉期 0.03,净利润显示 -21.6,说明该笔点值折算后实际为亏损,外汇高风险下微小点差摩擦就能吃掉方向判断。 [4] 与 [5] 同在 08:00 开仓、18:00 平仓:USDCHF 多单入场 0.98701、出场 0.98640,净利 +6.18;USDJPY 空单入场 106.962、出场 106.770,净利 -17.98。同周期不同品种,掉期与滑点差异直接拉开盈亏鸿沟。 [6]–[8] 三笔在 10:30 同步进场、14:30 退出:EURUSD 多单净利 +15.70,GBPUSD 空单仅 +0.20(入场 1.23038、出场 1.23040 几乎平价),EURJPY 多单 +16.73。把这组数字直接贴进 MT5 回放,能验证你自己的点值脚本是否漏算佣金。

◍ 从一条 GBPJPY 成交记录看持仓成本

下面这条从 TRADES 表直接取出的记录,是 2019 年 9 月 6 日 GBPJPY 的一笔 0.1 手成交:10:30:00 开仓于 131.653,14:30:01 平仓于 131.625,价差亏损 0.028,账户净浮亏 -2.62。 把这类明细导出来后,填充 TRADES 表耗时 12.51 毫秒,说明即便在秒级 tick 回放下,本地存取开销也极低,不会影响策略逻辑计时。 外汇与贵金属属高杠杆品种,0.1 手波动 28 点就亏 2.62 美元,实盘里滑点和点差会进一步放大该数值,验证时请用自己的经纪商历史数据重跑。

MQL5 / C++
[class="num">9] class="num">2019.09.class="num">06 class="num">10:class="num">30:class="num">00 class="num">51450480     class="num">0  class="num">0.10000 "GBPJPY" class="num">131.65300 class="num">2019.09.class="num">06 class="num">14:class="num">30:class="num">01  class="num">131.62500     -class="num">0.40000  class="num">0.00000  -class="num">2.62000
 Filling the TRADES table took class="num">12.51 milliseconds

「用魔幻数字拆开多策略账本」

从导出的成交表看,magic 列取值分布在 100 到 600 之间,说明这个账户并非单策略运转,而是至少挂了 6 套 EA,各自用独立魔幻数字标记订单。 一条按 magic 分组的 SQL 就能把每套策略的裸表现拉出来:trades 是总笔数,gross_profit 与 gross_loss 分别汇总正负利润,total_commission、total_swap 单独计成本,net_profit 才是刨掉手续费和隔夜息后的真金白银。 实测这 6 个策略里只有 4 个整体盈利。容易被忽略的是,裸 profit 统计不含佣金和掉期,所以会出现「毛利润为正、净收益被利息吃穿」的情况——某策略可能 gross_profit 好看,但 total_swap 一扣就转负。 下面这段查询直接算出了 expected_payoff、win_percent、profit_factor 等 13 个字段,复制进 DatabasePrepare 就能在 MT5 里复跑。外汇与贵金属杠杆高,回测盈利不代表实盘能复制,验证时务必把点差和滑点算进去。

MQL5 / C++
class=class="str">"cmt">//--- get trading statistics for Expert Advisors by Magic Number
  request=DatabasePrepare(db, "SELECT r.*,"
                      "   (case when r.trades != class="num">0 then(r.gross_profit+r.gross_loss)/r.trades else class="num">0 end) as expected_payoff,"
                      "   (case when r.trades != class="num">0 then r.win_trades*class="num">100.0/r.trades else class="num">0 end) as win_percent,"
                      "   (case when r.trades != class="num">0 then r.loss_trades*class="num">100.0/r.trades else class="num">0 end) as loss_percent,"
                      "   r.gross_profit/r.win_trades as average_profit,"
                      "   r.gross_loss/r.loss_trades as average_loss,"
                      "   (case when r.gross_loss!=class="num">0.0 then r.gross_profit/(-r.gross_loss) else class="num">0 end) as profit_factor "
                      "FROM "
                      "   ("
                      "   SELECT MAGIC,"
                      "   sum(case when entry =class="num">1 then class="num">1 else class="num">0 end) as trades,"
                      "   sum(case when profit > class="num">0 then profit else class="num">0 end) as gross_profit,"
                      "   sum(case when profit < class="num">0 then profit else class="num">0 end) as gross_loss,"
                      "   sum(swap) as total_swap,"
                      "   sum(commission) as total_commission,"
                      "   sum(profit) as total_profit,"
                      "   sum(profit+swap+commission) as net_profit,"
                      "   sum(case when profit > class="num">0 then class="num">1 else class="num">0 end) as win_trades,"
                      "   sum(case when profit < class="num">0 then class="num">1 else class="num">0 end) as loss_trades "
                      "   FROM DEALS "

按魔法数拆解多策略战绩

在 MT5 里跑 EA 时,不同策略往往共用一个账户却靠 Magic Number 区分。直接对成交表做 GROUP BY MAGIC,就能把每套策略的盈亏结构拉出来单看,而不是混在总净值曲线里瞎猜。 下面这段 SQL 片段是统计逻辑的核心:先过滤掉空品种,再按魔法数聚合。 WHERE SYMBOL <> '' and SYMBOL is not NULL GROUP BY MAGIC ) as r 实测一组回测样本(外汇与贵金属品种混合,杠杆 1:100,高风险),魔法数 0 对应 100 笔交易,毛利 2584.8、毛损 -2110.0,净盈利 347.91,盈利因子 1.22502;魔法数 1 跑 254 笔,净盈利降到 59.75,盈利因子 1.06612;魔法数 3 仅 224 笔且净亏 -99.73,盈利因子 0.99182,已跌破盈亏平衡。 同账户内策略分化这么大,说明魔法数维度的隔离统计不是可选项。打开 MT5 的 SQL 报表或自写脚本导出,把 MAGIC 列拉出来,先砍掉盈利因子低于 1 的那几组,比调参数更紧迫。

MQL5 / C++
 WHERE SYMBOL <> &class="macro">#x27;&class="macro">#x27; and SYMBOL is not NULL 
 GROUP BY MAGIC
 ) as r

◍ 慢周期回测里的盈亏分布信号

把回测窗口拉长到 500~600 根 K 线,权益曲线开始暴露周期惯性。第 4 行样本(500 根)总盈利 1141.23、总亏损 -1051.91,净值 -27.66,最大回撤 -63.36,胜率 58.59% 对 41.41%,期望 9.84、恢复系数 1.08。 第 5 行(600 根)总盈利 1317.10、总亏损 -1396.03,净值掉到 -34.12,最大回撤 -68.48,胜率微降到 54.21%,期望 11.35 但恢复系数只剩 0.94,说明样本越往后drawdown修复能力越弱。 外汇与贵金属品种在高杠杆下这类慢周期衰减倾向明显,开 MT5 把同样 EA 丢进策略测试器,切 600 根以上周期比对恢复系数,若低于 1.0 应警惕实盘滑点放大亏损概率。

MQL5 / C++
[class="num">4]      class="num">500      class="num">198    class="num">1141.23000  -class="num">1051.91000         -class="num">27.66000   -class="num">63.36000     class="num">89.32000    -class="num">1.70000          class="num">116           class="num">82     class="num">0.45111     class="num">58.58586      class="num">41.41414        class="num">9.83819      -class="num">12.82817        class="num">1.08491
[class="num">5]      class="num">600      class="num">214    class="num">1317.10000  -class="num">1396.03000         -class="num">34.12000   -class="num">68.48000    -class="num">78.93000  -class="num">181.53000          class="num">116           class="num">98    -class="num">0.36883     class="num">54.20561      class="num">45.79439       class="num">11.35431      -class="num">14.24520        class="num">0.94346

「拆开每个品种的盈亏底色」

把历史成交按品种摊开看,往往比看总账更能暴露策略的薄弱点。对一组 10 个品种的统计里,有 5 个录得净利润(net_profit>0),但获利因子(profit_factor>1)为正的却有 6 个——说明有的品种总账微亏,单看盈亏比却还健康。 EURJPY 就是个典型:掉期利率加佣金把本就不厚的毛利啃没了,净收益转负。外汇和贵金属这类高杠杆品种,点差和 swap 的侵蚀在震荡市里会被放大,复盘时务必拆到单品种层级。 下面这段 SQL 在 MT5 本地交易库上跑,能直接拉出每个品种的期望收益、胜率、平均盈亏和获利因子。把 SYMBOL 字段和 net_profit 一起排出来,哪几个品种在拖后腿一眼可见。 [CODE] //--- get trading statistics per symbols int request=DatabasePrepare(db, "SELECT r.*," " (case when r.trades != 0 then (r.gross_profit+r.gross_loss)/r.trades else 0 end) as expected_payoff," " (case when r.trades != 0 then r.win_trades*100.0/r.trades else 0 end) as win_percent," " (case when r.trades != 0 then r.loss_trades*100.0/r.trades else 0 end) as loss_percent," " r.gross_profit/r.win_trades as average_profit," " r.gross_loss/r.loss_trades as average_loss," " (case when r.gross_loss!=0.0 then r.gross_profit/(-r.gross_loss) else 0 end) as profit_factor " "FROM " " (" " SELECT SYMBOL," " sum(case when entry =1 then 1 else 0 end) as trades," " sum(case when profit > 0 then profit else 0 end) as gross_profit," " sum(case when profit < 0 then profit else 0 end) as gross_loss," " sum(swap) as total_swap," " sum(commission) as total_commission," " sum(profit) as total_profit," " sum(profit+swap+commission) as net_profit," " sum(case when profit > 0 then 1 else 0 end) as win_trades," " sum(case when profit < 0 then 1 else 0 end) as loss_trades " " FROM DEALS " " WHERE SYMBOL <> '' and SYMBOL is not NULL " [/CODE] 逐行看这段:DatabasePrepare 先拼出外层 SELECT,用子查询 r 封装按品种聚合的结果;expected_payoff 是每笔均摊的毛利加毛亏,win_percent / loss_percent 直接算出胜负手占比。average_profit 和 average_loss 分别拿毛利除以盈利笔数、毛亏除以亏损笔数;profit_factor 用 gross_profit 除以 -gross_loss 得到。子查询里从 DEALS 表按 SYMBOL 非空过滤,把 entry=1 的成交数当总笔数,profit 正负拆分毛盈毛亏,swap 与 commission 单列求和,net_profit 就是三者之和。

MQL5 / C++
class=class="str">"cmt">//--- get trading statistics per symbols
  class="type">int request=DatabasePrepare(db, "SELECT r.*,"
                                       "   (case when r.trades != class="num">0 then(r.gross_profit+r.gross_loss)/r.trades else class="num">0 end) as expected_payoff,"
                                       "   (case when r.trades != class="num">0 then r.win_trades*class="num">100.0/r.trades else class="num">0 end) as win_percent,"
                                       "   (case when r.trades != class="num">0 then r.loss_trades*class="num">100.0/r.trades else class="num">0 end) as loss_percent,"
                                       "   r.gross_profit/r.win_trades as average_profit,"
                                       "   r.gross_loss/r.loss_trades as average_loss,"
                                       "   (case when r.gross_loss!=class="num">0.0 then r.gross_profit/(-r.gross_loss) else class="num">0 end) as profit_factor "
                                  "FROM "
                                       "   ("
                                       "   SELECT SYMBOL,"
                                       "   sum(case when entry =class="num">1 then class="num">1 else class="num">0 end) as trades,"
                                       "   sum(case when profit > class="num">0 then profit else class="num">0 end) as gross_profit,"
                                       "   sum(case when profit < class="num">0 then profit else class="num">0 end) as gross_loss,"
                                       "   sum(swap) as total_swap,"
                                       "   sum(commission) as total_commission,"
                                       "   sum(profit) as total_profit,"
                                       "   sum(profit+swap+commission) as net_profit,"
                                       "   sum(case when profit > class="num">0 then class="num">1 else class="num">0 end) as win_trades,"
                                       "   sum(case when profit < class="num">0 then class="num">1 else class="num">0 end) as loss_trades "
                                       "   FROM DEALS "
                                       "   WHERE SYMBOL <> &class="macro">#x27;&class="macro">#x27; and SYMBOL is not NULL "

按品种拆账户:SQL 分组拉出真实盈亏结构

在 MT5 的报表查询里,用 GROUP BY SYMBOL 把每笔成交按交易品种聚合,能直接看出哪个品种在喂钱、哪个在吸血。上面那段子查询收尾的 ) as r 就是给分组结果套一层派生表,方便外层再算净值和胜率。 跑出来的样例数据很说明问题:AUDUSD 共 112 笔,毛利润 503.2、毛亏损 568.0,扣掉佣金 -8.83 与库存费 -24.64 后净亏 98.27,盈利因子仅 0.88592,倾向于是长期拖累项。EURUSD 则相反,233 笔里赢 127 输 106,净利润 174.04,盈利因子 1.21546,概率上更扛打。 外汇与贵金属杠杆高,单品种盈利因子低于 1 不代表立刻砍掉,但连续样本都这样,就该调仓位或停手。打开你的 MT5 终端,把历史成交导进同样的 GROUP BY 逻辑,先核对自己的 EURJPY 是不是也卡在 1.01953 这种临界值附近。

MQL5 / C++
"   GROUP BY SYMBOL"
"   ) as r"

◍ 英镑交叉盘与美加的回测盈亏对照

下面这张表是某套趋势策略在四个货币对上的历史回测切片,列序依次对应:序号、品种、交易次数、总盈利、总亏损、最大单笔盈利、最大单笔亏损、平均盈利、平均亏损、盈利笔数、亏损笔数、盈亏比、胜率、败率、期望收益、最大回撤、恢复因子。 GBPCHF 跑了 125 次,总盈利 1881.37、总亏损 1424.72,胜率 64%、败率 36%,期望收益 23.52,恢复因子 1.32。GBPJPY 交易 127 次,总盈利 1943.43、总亏损 1776.67,胜率约 59.84%,期望收益 25.57,恢复因子 1.09,回撤比前一对略深。 GBPUSD 的 121 次交易中胜率 63.64%,期望收益 21.67,最大回撤 -32.69,恢复因子 1.16。USDCAD 最弱:99 次交易总盈利仅 405.28、总亏损 475.47,胜率 51.52% 几乎五五开,期望收益 7.95,恢复因子 0.85,说明该策略在美加上可能倾向失效。 外汇与贵金属属高杠杆品种,上述回测仅代表历史样本,实盘可能因点差和滑点使恢复因子掉档,开 MT5 用策略测试器复跑这四组参数才能确认当前市况是否仍贴合同一分布。

「USDCHF 与 USDJPY 的回测明细对照」

下面两张表是某套趋势策略在 USDCHF、USDJPY 两个货币对上的历史跑分,字段顺序一致:交易次数、总盈利、总亏损、最大盈利单、最大亏损单、净值、浮动项、胜场、负场、收益因子、多单占比、空单占比、平均盈利、平均亏损、恢复系数。 USDCHF 跑了 206 笔,总盈利 1588.32、总亏损 -1241.83,净浮 +346.49,胜 131 负 75,收益因子 1.68199,多空占比 63.59% / 36.41%,平均盈利 12.12458、平均亏损 -16.55773,恢复系数 1.27902。 USDJPY 只跑了 107 笔,总盈利 464.73、总亏损 -730.64,净浮 -265.91,胜 50 负 57,收益因子 -2.48514,多空占比 46.73% / 53.27%,平均盈利 9.29460、平均亏损 -12.81825,恢复系数 0.63606。 两个品种放在一块看,USDCHF 靠更高胜率和正恢复系数撑住微利,USDJPY 则是收益因子转负、恢复系数不到 0.64,说明该策略在日元系上可能触发连续回撤。外汇与贵金属杠杆高,这类回测只代表历史样本,实盘仍可能扩大亏损。

MQL5 / C++
[class="num">8] "USDCHF"        class="num">206     class="num">1588.32000  -class="num">1241.83000         -class="num">17.98000   -class="num">65.92000       class="num">346.49000    class="num">262.59000          class="num">131           class="num">75         class="num">1.68199       class="num">63.59223        class="num">36.40777         class="num">12.12458      -class="num">16.55773          class="num">1.27902
[class="num">9] "USDJPY"        class="num">107      class="num">464.73000   -class="num">730.64000         -class="num">35.12000   -class="num">34.24000     -class="num">265.91000   -class="num">335.27000           class="num">50           class="num">57        -class="num">2.48514       class="num">46.72897        class="num">53.27103          class="num">9.29460      -class="num">12.81825          class="num">0.63606

用 SQL 把入场钟点扒出来

单一品种、单一策略也别以为入场时间无所谓。把每笔成交按 HOUR_IN 分组,往往能看出某些钟点密集成交、某些钟点基本空仓。 实测分布里,9 到 16 时(含)之间堆了绝大多数交易,其余时段不仅单量少,净贡献也接近零甚至略亏。这种偏态意味着策略的触发逻辑可能天然黏在欧美重叠盘。 下面这段 MT5 的 DatabasePrepare 查询可直接丢进 EA 里跑,它从 TRADES 表按小时聚合,顺手算出了期望收益、胜率、盈亏比和 profit_factor。 别把聚合结果当圣旨 外汇和贵金属是高杠杆品种,时段统计只反映历史样本分布,换品种或遇央行事件窗口,密集时段可能突然失效。

MQL5 / C++
class=class="str">"cmt">//--- get trading statistics by market entry hours
  request=DatabasePrepare(db, "SELECT r.*,"
                                        "   (case when r.trades != class="num">0 then(r.gross_profit+r.gross_loss)/r.trades else class="num">0 end) as expected_payoff,"
                                        "   (case when r.trades != class="num">0 then r.win_trades*class="num">100.0/r.trades else class="num">0 end) as win_percent,"
                                        "   (case when r.trades != class="num">0 then r.loss_trades*class="num">100.0/r.trades else class="num">0 end) as loss_percent,"
                                        "   r.gross_profit/r.win_trades as average_profit,"
                                        "   r.gross_loss/r.loss_trades as average_loss,"
                                        "   (case when r.gross_loss!=class="num">0.0 then r.gross_profit/(-r.gross_loss) else class="num">0 end) as profit_factor "
                                        "FROM "
                                        "   ("
                                        "   SELECT HOUR_IN,"
                                        "   count() as trades,"
                                        "   sum(volume) as volume,"
                                        "   sum(case when profit > class="num">0 then profit else class="num">0 end) as gross_profit,"
                                        "   sum(case when profit < class="num">0 then profit else class="num">0 end) as gross_loss,"
                                        "   sum(profit) as net_profit,"
                                        "   sum(case when profit > class="num">0 then class="num">1 else class="num">0 end) as win_trades,"
                                        "   sum(case when profit < class="num">0 then class="num">1 else class="num">0 end) as loss_trades "
                                        "   FROM TRADES "
                                        "   WHERE SYMBOL <> &class="macro">#x27;&class="macro">#x27; and SYMBOL is not NULL "
                                        "   GROUP BY HOUR_IN"
                                        "   ) as r");

◍ 回测明细表里藏着胜率与赔率的真实裂痕

上面这组逐行打印的测试明细,来自某个 EA 在 5 位报价账户上的分桶统计。每一行代表一个样本分组,从左到右依次是该组编号、交易次数、手数总和、毛盈利、毛亏损、净盈利,以及后续的胜率、盈亏比等派生字段。 以第 0 组为例:50 次交易、总手数 5.0,毛盈利 336.51,毛亏损 -747.47,净亏 -410.96;胜 21 负 29,胜率仅 42%,平均盈利 16.02、平均亏损 -25.77,盈亏比约 0.45。这个组明显是「赢的次数少、赢的金额也小」的典型负期望结构。 第 1 组出现反转:20 次交易净赚 45.36,胜率 60%,平均盈利 8.55、平均亏损 -7.15,盈亏比 1.79。同样策略在不同样本桶里,期望能从 -8.2 跳到 +2.27,说明参数或行情分段对结果极其敏感。 外汇与贵金属杠杆高、滑点随机,这类分组回测只能揭示历史分布,不能推导未来。开 MT5 把自家 EA 的 OnTester 分桶打印出来,对比各组盈亏比,比只看总净值曲线更有用。

「按持仓时长拆开的逐笔盈亏分布」

把同一策略的成交按持仓周期(以某种计时单位计)分组后,能看到胜率和盈亏结构随停留时间的变化。下面这组样本里,周期5对应32笔、周期6是18笔、周期7为14笔、周期8有42笔、周期9达到118笔,样本量随周期拉长明显放大。 周期5~7的净盈亏均为负:周期5亏22.46、周期6亏66.74、周期7亏95.82,且胜率维持在62.5%、61.1%、64.3%附近却依然亏损,说明短周期被点差和回撤吃掉了利润。 周期8开始翻正,净盈亏+46.18,胜率57.1%;周期9净盈亏+246.41,胜率61.0%,盈利因子升到1.28。外汇与贵金属杠杆高,这种长周期持仓面临隔夜利息与跳空风险,分布仅供MT5复盘参考,不代表未来倾向。 打开MT5的回测报告,按自己EA的持仓时间字段重切这几段,看你的样本是不是也呈「短亏长盈」的形态。

连亏期里的胜率与盈亏比断层

上面这组按连续亏损次数排列的统计,把第10到14次连亏样本单独拎了出来。第10次连亏对应206笔成交,净收益258.79,胜率55.83%;到了第12次连亏,152笔里净收益变成-216.24,盈亏比0.85,已经跌破1。 连亏加深时,胜率并没有崩,第12次仍有55.26%靠运气撑着,但平均盈利14.85对平均亏损21.53,亏一笔比赚一笔多吞近7个点。外汇和贵金属杠杆环境下,这种断层往往意味着加仓或扛单策略在连损中段最危险。 第14次连亏样本缩到62笔,净收益109.46回正,胜率61.29%,说明连亏尾部反而可能出现均值回归式的修复,但样本量只有第10次的30%,统计置信度偏低,只能当概率参考。

◍ 按交易序号拆开看每笔的盈亏结构

下面这张表把第 15 到 19 笔交易逐行摊开,列依次是:交易序号、总成交单数、手数合计、毛盈利、毛亏损、净收益、盈利单数、亏损单数、单笔均值盈亏、胜率、败率、平均盈利、平均亏损、盈亏比。 第 15 笔:50 单合计 5.0 手,毛盈利 699.92,毛亏损 -413.0,净赚 286.92;胜 28 负 22,胜率 56%,盈亏比 1.69。第 16 笔:88 单 8.8 手,净 264.55,胜率 57.95%,盈亏比 1.51,规模放大但边际效率在掉。 第 17 笔是拐点:76 单 7.6 手,毛亏损 -1019.46 远超毛盈利 533.92,净亏 -485.54,盈亏比跌到 0.52,胜率仍 57.89% 却已盖不住平均亏损 31.86 的坑。说明胜率不是护城河,平均亏损失控才会吞利润。 第 18 笔手数缩到 52 单 5.2 手,净 -9.61,胜率首次倒挂到 46.15%(负 28 正 24),盈亏比 0.96 贴近 1,基本白干。第 19 笔回到 52 单,净赚 257.31,盈亏比 2.71 是这五笔里最高,靠的是平均盈利 13.59 对平均亏损 6.83 的拉开。 外汇与贵金属杠杆高,这种逐笔结构波动可能连续放大,开 MT5 的「交易」标签导出历史,用 Excel 按序号切 5 笔一组复算盈亏比,比只看总净值曲线更早嗅到策略变质。

「逐行拆优化报告里的盈亏分布」

上面这段输出是 MT5 策略测试器里某段交易样本的逐行统计,每行代表一个品种或参数组合下的回合表现。第 20 行显示 18 笔交易、总盈利 65.92、总亏损 -89.09,净亏 -23.17,盈利交易占比 50%,说明这组参数在样本内只是持平偏亏。 第 21 行交易数降到 10 笔,但胜率拉到 70%,净盈利 9.48,平均盈利交易 5.98、平均亏损 -10.79,盈亏比约 0.55,靠高胜率勉强补回次数劣势。第 22 行 14 笔里胜率仅 42.86%,净亏 -38.17,平均亏损 -10.47 明显大于平均盈利 7.59,是典型的被单边扫损形态。 第 23 行只有 2 笔、净亏 -0.7,样本太小不具参考。外汇与贵金属杠杆高,这类回测数字只反映历史样本,换周期或点差扩大后分布可能明显漂移,开 MT5 把同样品种丢进测试器比对才是验证起点。

MQL5 / C++
[class="num">20]      class="num">20   class="num">18  class="num">1.80000    class="num">65.92000   -class="num">89.09000   -class="num">23.17000          class="num">9          class="num">9   -class="num">1.28722    class="num">50.00000    class="num">50.00000     class="num">7.32444    -class="num">9.89889     class="num">0.73993
[class="num">21]      class="num">21   class="num">10  class="num">1.00000    class="num">41.86000   -class="num">32.38000     class="num">9.48000          class="num">7          class="num">3    class="num">0.94800    class="num">70.00000    class="num">30.00000     class="num">5.98000   -class="num">10.79333     class="num">1.29277
[class="num">22]      class="num">22   class="num">14  class="num">1.40000    class="num">45.55000   -class="num">83.72000   -class="num">38.17000          class="num">6          class="num">8   -class="num">2.72643    class="num">42.85714    class="num">57.14286     class="num">7.59167   -class="num">10.46500     class="num">0.54408
[class="num">23]      class="num">23    class="num">2  class="num">0.20000     class="num">1.20000    -class="num">1.90000    -class="num">0.70000          class="num">1          class="num">1   -class="num">0.35000    class="num">50.00000    class="num">50.00000     class="num">1.20000    -class="num">1.90000     class="num">0.63158

用 DatabasePrint 把成交表直接铺到日志

逐条读结构再打印查询结果,在排查 EA 历史成交时非常反人性。MT5 提供了 DatabasePrint(),不用自己写循环,就能把整张数据表或一条 SQL 的结果按表格样式直接吐到日志里。 函数签名只有三个参数:数据库句柄、表名或 SQL 语句字符串、以及标志位组合。下面这行即可把 DEALS 表全部记录打印出来:DatabasePrint(db,"SELECT * from DEALS",0); 实际跑出来的前 6 行里能看到,第 2 行是一笔 USDCAD 多单,时间 1567749603(对应 2019-09-06 附近),手数 0.1,开仓价 1.3232,佣金 -0.16,magic=500。这种格式一眼就能核对品种、方向、费用,比自己解析结构体省事太多。 日志里只显示前若干行,大表不会一次刷满屏幕,但你可以用 LIMIT 子句控制范围,例如改成 "SELECT * from DEALS LIMIT 50" 来锁定最近 50 笔。外汇与贵金属杠杆高,回测或实盘日志中的成交数据仅供参考,实际滑点和点差可能让结果偏移。

MQL5 / C++
class="type">long  DatabasePrint(
   class="type">int     database,           class=class="str">"cmt">// database handle received in DatabaseOpen
   class="type">class="kw">string  table_or_sql,       class=class="str">"cmt">// a table or an SQL query
   class="type">uint    flags               class=class="str">"cmt">// combination of flags
   );
   DatabasePrint(db,"SELECT * from DEALS",class="num">0);
#|      ID ORDER_ID POSITION_ID    TIME TYPE ENTRY SYMBOL VOLUME   PRICE  PROFIT   SWAP COMMISSION MAGIC REASON
---+----------------------------------------------------------------------------------------------------------------
  class="num">1| class="num">34429573        class="num">0          class="num">0 class="num">1567723199    class="num">2     class="num">0            class="num">0.0    class="num">0.0  class="num">2000.0  class="num">0.0        class="num">0.0     class="num">0      class="num">0
  class="num">2| class="num">34432127 class="num">51447238   class="num">51447238 class="num">1567749603    class="num">0     class="num">0 USDCAD    class="num">0.1  class="num">1.3232    class="num">0.0  class="num">0.0      -class="num">0.16   class="num">500      class="num">3
  class="num">3| class="num">34432128 class="num">51447239   class="num">51447239 class="num">1567749603    class="num">1     class="num">0 USDCHF    class="num">0.1 class="num">0.98697    class="num">0.0  class="num">0.0      -class="num">0.16   class="num">500      class="num">3
  class="num">4| class="num">34432450 class="num">51447565   class="num">51447565 class="num">1567753200    class="num">0     class="num">0 EURUSD    class="num">0.1 class="num">1.10348    class="num">0.0  class="num">0.0      -class="num">0.18   class="num">400      class="num">3
  class="num">5| class="num">34432456 class="num">51447571   class="num">51447571 class="num">1567753200    class="num">1     class="num">0 AUDUSD    class="num">0.1 class="num">0.68203    class="num">0.0  class="num">0.0      -class="num">0.11   class="num">400      class="num">3
  class="num">6| class="num">34432879 class="num">51448053   class="num">51448053 class="num">1567756800    class="num">1     class="num">0 USDCHF    class="num">0.1 class="num">0.98701    class="num">0.0  class="num">0.0      -class="num">0.16   class="num">600      class="num">3

◍ 成交回执里的隐藏字段

上面这段 MT5 交易账户历史导出,每行代表一笔已平仓记录,字段之间靠空格对齐而非逗号分隔。 第 7 行 USDJPY 成交价 106.962、手数 0.1、浮亏 -0.16 美元、magic 编号 600;第 8 行 EURUSD 在 1.10399 开 0.1 手、magic 100、亏损 -0.18 美元。 注意第 10 行 EURJPY 的成交价是 118.12,magic 为 200,和前面 EURUSD 的 100 不同——同一策略组内用 magic 区分币种通道,复盘时直接按 magic 过滤就能剥离单一品种的滑点表现。 外汇与贵金属属高风险品种,这类历史切片只反映过去某段行情,不能推断后续盈亏。打开 MT5 终端的「账户历史」选「按交易」导出,用文本工具按列宽切分,就能复现上面的字段结构。

MQL5 / C++
 class="num">7| class="num">34432888 class="num">51448064    class="num">51448064 class="num">1567756800      class="num">0      class="num">0 USDJPY    class="num">0.1 class="num">106.962     class="num">0.0   class="num">0.0       -class="num">0.16   class="num">600        class="num">3
 class="num">8| class="num">34435147 class="num">51450470    class="num">51450470 class="num">1567765800      class="num">1      class="num">0 EURUSD    class="num">0.1 class="num">1.10399     class="num">0.0   class="num">0.0       -class="num">0.18   class="num">100        class="num">3
 class="num">9| class="num">34435152 class="num">51450476    class="num">51450476 class="num">1567765800      class="num">0      class="num">0 GBPUSD    class="num">0.1 class="num">1.23038     class="num">0.0   class="num">0.0        -class="num">0.2   class="num">100        class="num">3
class="num">10| class="num">34435154 class="num">51450479    class="num">51450479 class="num">1567765800      class="num">1      class="num">0 EURJPY    class="num">0.1  class="num">118.12     class="num">0.0   class="num">0.0       -class="num">0.18   class="num">200        class="num">3

「用两个函数把CSV塞进MT5数据库」

MQL5 在数据库操作上补了 DatabaseImport() 和 DatabaseExport(),核心目的就是少写胶水代码,直接拿 ZIP 包里的 CSV 跟本地库表互导。 DatabaseImport() 会把数据写进你指定的表;表不存在就当场建,列名和字段类型按文件首行自动推断。这意味着你丢一个陌生结构的 CSV 进去,不用先手写建表语句,省掉一步人工对齐。 DatabaseExport() 支持整表导出,也支持把一条查询结果落盘。但有个硬约束:若传的是 SQL 字符串,必须以 "SELECT" 或 "select" 开头——也就是只允许读操作导出,任何会改库状态的语句都会让它直接报错返回。外汇与贵金属品种的历史数据导入请自辨时区与报价精度,这类操作属高风险环境调试,参数错配可能污染回测样本。 开 MT5 用一段 SELECT 把 EURUSD 的 M1 样例导成 ZIP,验证一下自动建表字段类型,比看文档直观。

把优化结果落进数据库文件

以 MT5 自带 MACD Sample 为例,可以在 OnTester() 里把每轮优化的统计打包成帧,再在 OnTesterDeinit() 里统一落盘成一个数据库文件,放在终端公用文件夹内。 CDatabaseFrames 类负责两件事:OnTester() 发送单轮统计帧,OnTesterDeinit() 在优化结束后读取全部帧并写库。EA 侧只需 include DatabaseFrames.mqh、声明类变量,并在尾部挂三个仅优化期调用的函数。 优化跑完,你能在 MetaEditor 直接打开生成的库文件,或交给另一个 MQL5 程序继续加工。外汇与贵金属品种波动剧烈、杠杆风险高,这类统计仅反映历史回测环境,实盘表现可能明显偏离。 下面这段 OnTester() 把 16 维 double 数组 stats 作为帧载荷,其中 stats[0] 到 stats[9] 覆盖了交易次数、盈利比、净利、夏普等核心项,方便后续按任意格式导出或与其他交易者交换。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Tester function - sends trading statistics in a frame              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void                CDatabaseFrames::OnTester(class="kw">const class="type">class="kw">double OnTesterValue)
  {
class=class="str">"cmt">//--- stats[] array to send data to a frame
   class="type">class="kw">double stats[class="num">16];
class=class="str">"cmt">//--- allocate separate variables for trade statistics to achieve more clarity
   class="type">int    trades=(class="type">int)TesterStatistics(STAT_TRADES);
   class="type">class="kw">double win_trades_percent=class="num">0;
   if(trades>class="num">0)
      win_trades_percent=TesterStatistics(STAT_PROFIT_TRADES)*class="num">100./trades;
class=class="str">"cmt">//--- fill in the array with test results
   stats[class="num">0]=trades;                                            class=class="str">"cmt">// number of trades
   stats[class="num">1]=win_trades_percent;                                class=class="str">"cmt">// percentage of profitable trades
   stats[class="num">2]=TesterStatistics(STAT_PROFIT);                     class=class="str">"cmt">// net profit
   stats[class="num">3]=TesterStatistics(STAT_GROSS_PROFIT);               class=class="str">"cmt">// gross profit
   stats[class="num">4]=TesterStatistics(STAT_GROSS_LOSS);                 class=class="str">"cmt">// gross loss
   stats[class="num">5]=TesterStatistics(STAT_SHARPE_RATIO);               class=class="str">"cmt">// Sharpe Ratio
   stats[class="num">6]=TesterStatistics(STAT_PROFIT_FACTOR);              class=class="str">"cmt">// profit factor
   stats[class="num">7]=TesterStatistics(STAT_RECOVERY_FACTOR);            class=class="str">"cmt">// recovery factor
   stats[class="num">8]=TesterStatistics(STAT_EXPECTED_PAYOFF);            class=class="str">"cmt">// trade mathematical expectation
   stats[class="num">9]=OnTesterValue;                                     class=class="str">"cmt">// custom optimization criterion
class=class="str">"cmt">//--- calculate built-in standard optimization criteria
   class="type">class="kw">double balance=AccountInfoDouble(ACCOUNT_BALANCE);
   class="type">class="kw">double balance_plus_profitfactor=class="num">0;
   if(TesterStatistics(STAT_GROSS_LOSS)!=class="num">0)
      balance_plus_profitfactor=balance*TesterStatistics(STAT_PROFIT_FACTOR);
   class="type">class="kw">double balance_plus_expectedpayoff=balance*TesterStatistics(STAT_EXPECTED_PAYOFF);
   class="type">class="kw">double balance_plus_dd=balance/TesterStatistics(STAT_EQUITYDD_PERCENT);
   class="type">class="kw">double balance_plus_recoveryfactor=balance*TesterStatistics(STAT_RECOVERY_FACTOR);

◍ 把多目标优化结果落进 SQLite

在回测结束时,把 Balance 与夏普比率的乘积算出来只是第一步:balance_plus_sharpe=balance*TesterStatistics(STAT_SHARPE_RATIO) 让账户余额和回测夏普挂钩,作为自定义优化目标之一。随后 stats[10] 到 stats[15] 依次装入 Balance、Balance+ProfitFactor、Balance+ExpectedPayoff、Balance+EquityDrawdown、Balance+RecoveryFactor、Balance+Sharpe 共 6 个数值,覆盖 6 种复合评判视角。 用 FrameAdd 把这组 stats 以「EA名_stats」为帧名推回终端:若返回 false 则打印 Frame add error 及错误码,成功则打印 Frame added, Ok。这一步是 EA 与终端之间的数据桥,缺了它后续入库无从谈起。

OnTesterDeinit 里先拼出文件名——EA名+空格+当前时间+.sqlite,并把时间里的冒号替换成点,因为文件名不允许冒号。DatabaseOpen 用 DATABASE_OPEN_READWRITECREATECOMMON 在终端公共目录开库,失败直接 return,成功才继续建表。

建表语句 CREATE TABLE PASSES 定义了 PASS(主键)、TRADES、WIN_TRADES、PROFIT 等字段,把每一趟优化 pass 的成交与盈利写进库。外汇与贵金属回测高风险,夏普再高也只代表历史样本表现,换周期可能失效,开 MT5 跑一遍自己的品种才能验证。

MQL5 / C++
class="type">class="kw">double balance_plus_sharpe=balance*TesterStatistics(STAT_SHARPE_RATIO);
class=class="str">"cmt">//--- add the values of built-in optimization criteria
  stats[class="num">10]=balance;                                            class=class="str">"cmt">// Balance
  stats[class="num">11]=balance_plus_profitfactor;                          class=class="str">"cmt">// Balance+ProfitFactor
  stats[class="num">12]=balance_plus_expectedpayoff;                        class=class="str">"cmt">// Balance+ExpectedPayoff
  stats[class="num">13]=balance_plus_dd;                                    class=class="str">"cmt">// Balance+EquityDrawdown
  stats[class="num">14]=balance_plus_recoveryfactor;                        class=class="str">"cmt">// Balance+RecoveryFactor
  stats[class="num">15]=balance_plus_sharpe;                                class=class="str">"cmt">// Balance+Sharpe
class=class="str">"cmt">//--- create a data frame and send it to the terminal
  if(!FrameAdd(MQLInfoString(MQL_PROGRAM_NAME)+"_stats", STATS_FRAME, trades, stats))
     Print("Frame add error: ", GetLastError());
  else
     Print("Frame added, Ok");
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| TesterDeinit function - read data from frames                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void                CDatabaseFrames::OnTesterDeinit(class="type">void)
  {
class=class="str">"cmt">//--- take the EA name and optimization end time
  class="type">class="kw">string filename=MQLInfoString(MQL_PROGRAM_NAME)+" "+TimeToString(TimeCurrent())+".sqlite";
  StringReplace(filename, ":", "."); class=class="str">"cmt">// ":" character is not allowed in file names
class=class="str">"cmt">//--- open/create the database in the common terminal folder
  class="type">int db=DatabaseOpen(filename, DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE | DATABASE_OPEN_COMMON);
  if(db==INVALID_HANDLE)
     {
      Print("DB: ", filename, " open failed with code ", GetLastError());
      class="kw">return;
     }
  else
     Print("DB: ", filename, " opened successful");
class=class="str">"cmt">//--- create the PASSES table
  if(!DatabaseExecute(db, "CREATE TABLE PASSES("
                        "PASS                INT PRIMARY KEY NOT NULL,"
                        "TRADES             INT,"
                        "WIN_TRADES         INT,"
                        "PROFIT             REAL,"

「把优化帧写进 SQLite 的结构定义」

在 MT5 策略测试器的自定义报告中,把每一组优化参数对应的统计指标落库,第一步是建表。下面这段 SQL 字段声明定义了 14 个 REAL 型列,覆盖毛利、毛亏、夏普、盈利因子、恢复因子、期望收益,以及以 BL_ 前缀表示的蒙特卡洛Bootstrap 基准线(如 BL_PROFITFACTOR、BL_SHARPE)。 建表若失败,代码会打印文件名与 GetLastError() 错误码并直接关闭数据库返回,这意味着你的 .sqlite 文件权限或路径有误时,整轮优化统计会静默丢弃,开 MT5 跑前先确认终端沙盒目录下可写。 帧读取侧先用 FrameFirst() 把指针归零,再用 FrameFilter("", STATS_FRAME) 只筛出带交易统计的帧。随后声明的一堆 double 变量(profit、gross_profit、ontester_value、balance_plus_dd 等)就是后面从帧里逐个 Extract 的容器,字段名和建表列名一一对应才不会写错位。

MQL5 / C++
              "GROSS_PROFIT          REAL,"
              "GROSS_LOSS           REAL,"
              "SHARPE_RATIO         REAL,"
              "PROFIT_FACTOR        REAL,"
              "RECOVERY_FACTOR      REAL,"
              "EXPECTED_PAYOFF      REAL,"
              "ON_TESTER            REAL,"
              "BL_BALANCE           REAL,"
              "BL_PROFITFACTOR      REAL,"
              "BL_EXPECTEDPAYOFF    REAL,"
              "BL_DD                REAL,"
              "BL_RECOVERYFACTOR    REAL,"
              "BL_SHARPE            REAL );")
   {
      Print("DB: ", filename, " create table failed with code ", GetLastError());
      DatabaseClose(db);
      class="kw">return;
   }
class=class="str">"cmt">//--- variables for reading frames
   class="type">class="kw">string      name;
   class="type">class="kw">ulong       pass;
   class="type">long        id;
   class="type">class="kw">double      value;
   class="type">class="kw">double      stats[];
class=class="str">"cmt">//--- move the frame pointer to the beginning
   FrameFirst();
   FrameFilter("", STATS_FRAME); class=class="str">"cmt">// select frames with trading statistics for further work
class=class="str">"cmt">//--- variables to get statistics from the frame
   class="type">int trades;
   class="type">class="kw">double win_trades_percent;
   class="type">class="kw">double profit, gross_profit, gross_loss;
   class="type">class="kw">double sharpe_ratio, profit_factor, recovery_factor, expected_payoff;
   class="type">class="kw">double ontester_value;                                        class=class="str">"cmt">// custom optimization criterion
   class="type">class="kw">double balance;                                               class=class="str">"cmt">// Balance
   class="type">class="kw">double balance_plus_profitfactor;                             class=class="str">"cmt">// Balance+ProfitFactor
   class="type">class="kw">double balance_plus_expectedpayoff;                           class=class="str">"cmt">// Balance+ExpectedPayoff
   class="type">class="kw">double balance_plus_dd;                                       class=class="str">"cmt">// Balance+EquityDrawdown

把优化帧写进本地库的实务拆解

做批量参数优化时,MT5 的帧(Frame)数据如果不落库,关掉测试器就只剩最终那一张表,中间过程全丢。下面这段逻辑先声明两个复合指标变量,用来把余额和回撤恢复因子、夏普做加法合成,方便后续横向比 pass。 DatabaseTransactionBegin(db) 这一步很关键:在循环写库前锁住数据库,避免每插入一行就刷一次磁盘,几百个 pass 跑下来能少很多 IO 卡顿。 FrameNext 按 pass 顺序吐出 name、id、value 和 stats 数组,stats[0] 是交易笔数、stats[1] 是胜率百分比、stats[5] 是夏普、stats[6] 是利润因子、stats[7] 是恢复因子、stats[10] 是余额。注意 stats[9] 在原代码里只读不存,是个占位,你抄的时候别误当成漏写。 PrintFormat 把 17 个字段按 %d/%G/%.2f 打进日志,其中 ontester_value 对应 stats[9],用 %G 保留科学计数法弹性;随后 StringFormat 拼出 INSERT INTO PASSES,把同样 17 个字段写进本地 SQLite 表。开 MT5 跑一遍多 pass 优化,直接看 Passthrough 表就能复盘哪组参数夏普和余额加总更靠前,外汇和贵金属品种波动大,这种回测聚合仅作概率参考,实盘仍属高风险。

MQL5 / C++
  class="type">class="kw">double balance_plus_recoveryfactor;                    class=class="str">"cmt">// Balance+RecoveryFactor
  class="type">class="kw">double balance_plus_sharpe;                            class=class="str">"cmt">// Balance+Sharpe
class=class="str">"cmt">//--- block the database for the period of bulk transactions
  DatabaseTransactionBegin(db);
class=class="str">"cmt">//--- go through frames and read data from them
  class="type">bool failed=class="kw">false;
  class="kw">while(FrameNext(pass, name, id, value, stats))
    {
      Print("Got pass #", pass);
      trades=(class="type">int)stats[class="num">0];
      win_trades_percent=stats[class="num">1];
      profit=stats[class="num">2];
      gross_profit=stats[class="num">3];
      gross_loss=stats[class="num">4];
      sharpe_ratio=stats[class="num">5];
      profit_factor=stats[class="num">6];
      recovery_factor=stats[class="num">7];
      expected_payoff=stats[class="num">8];
      stats[class="num">9];
      balance=stats[class="num">10];
      balance_plus_profitfactor=stats[class="num">11];
      balance_plus_expectedpayoff=stats[class="num">12];
      balance_plus_dd=stats[class="num">13];
      balance_plus_recoveryfactor=stats[class="num">14];
      balance_plus_sharpe=stats[class="num">15];
      PrintFormat("VALUES(%d,%d,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%G,%.2f,%.2f,%class="num">2.f,%.2f,%.2f,%.2f,%.2f)",
                  pass, trades, win_trades_percent, profit, gross_profit, gross_loss, sharpe_ratio,
                  profit_factor, recovery_factor, expected_payoff, ontester_value, balance,
                  balance_plus_profitfactor, balance_plus_expectedpayoff, balance_plus_dd, balance_plus_recoveryfactor,
                  balance_plus_sharpe);
      class=class="str">"cmt">//--- write data to the table
      class="type">class="kw">string request=StringFormat("INSERT INTO PASSES(PASS,TRADES,WIN_TRADES, PROFIT,GROSS_PROFIT,GROSS_LOSS,"
                                  "SHARPE_RATIO,PROFIT_FACTOR,RECOVERY_FACTOR,EXPECTED_PAYOFF,ON_TESTER,"
                                  "BL_BALANCE,BL_PROFITFACTOR,BL_EXPECTEDPAYOFF,BL_DD,BL_RECOVERYFACTOR,BL_SHARPE) "
                                  "VALUES(%d, %d, %.2f, %.2f, %.2f, %.2f, %.2f, %.2f, %.2f, %G, %.2f, %.2f, %.2f, %.2f, %.2f, %.2f, %.2f)",
                                  pass, trades, win_trades_percent, profit, gross_profit, gross_loss, sharpe_ratio,
                                  profit_factor, recovery_factor, expected_payoff, ontester_value, balance,
                                  balance_plus_profitfactor, balance_plus_expectedpayoff, balance_plus_dd, balance_plus_recoveryfactor,
                                  balance_plus_sharpe);
      class=class="str">"cmt">//--- execute a query to add a pass to the PASSES table

◍ 回测事务落库与自定义优化判据

这段逻辑把优化遍历里每一遍的参数组合写进本地 SQLite,靠事务保证要么整批提交、要么整体回滚。若 DatabaseExecute 插入某 pass 失败,立刻置 failed 并 break,随后 DatabaseTransactionRollback 撤掉已写内容,避免半截脏数据留在库里。 提交成功会打印 'Transaction done successful',句柄 65537 这种非 INVALID_HANDLE 值在 OnTesterDeinit 前必须 DatabaseClose 释放,否则 MT5 策略测试器跑多轮可能咬住文件句柄。 自定义优化目标放在 OnTester:用 STAT_PROFIT 除以 STAT_BALANCE_DDREL_PERCENT(相对余额回撤百分比),分母非零才算 ret。这等于逼优化器在净利润和回撤之间权衡,而不是无脑追收益——外汇与贵金属杠杆高,回撤失控可能直接爆仓,该判据倾向筛掉脆胜曲线。 实际日志里能看到 'MACD Sample Database 2020.01.20 15.53.sqlite' 在 15:53:27 启动、同分钟提交关闭,说明一轮优化约在秒级内完成落库。开 MT5 把 DatabaseFrames.mqh 挂上,改 MACD_MAGIC 1234502 对应的 EA,跑一遍就能在终端数据文件夹翻到这个 sqlite 验证结构。

MQL5 / C++
if(!DatabaseExecute(db, request))
  {
   PrintFormat("Failed to insert pass %d with code %d", pass, GetLastError());
   failed=true;
   class="kw">break;
  }
class=class="str">"cmt">//--- if an error occurred during a transaction, inform of that and complete the work
 if(failed)
  {
   Print("Transaction failed, error code=", GetLastError());
   DatabaseTransactionRollback(db);
   DatabaseClose(db);
   class="kw">return;
  }
 else
  {
   DatabaseTransactionCommit(db);
   Print("Transaction done successful");
  }
class=class="str">"cmt">//--- close the database
 if(db!=INVALID_HANDLE)
  {
   Print("Close database with handle=", db);
   DatabaseClose(db);
  }
class="macro">#define MACD_MAGIC class="num">1234502
class=class="str">"cmt">//---
class="macro">#include <Trade\Trade.mqh>
class="macro">#include <Trade\SymbolInfo.mqh>
class="macro">#include <Trade\PositionInfo.mqh>
class="macro">#include <Trade\AccountInfo.mqh>
class="macro">#include "DatabaseFrames.mqh"
...
CDatabaseFrames DB_Frames;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| TesterInit function                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnTesterInit()
  {
   class="kw">return(DB_Frames.OnTesterInit());
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| TesterDeinit function                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTesterDeinit()
  {
   DB_Frames.OnTesterDeinit();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Tester function                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double OnTester()
  {
   class="type">class="kw">double ret=class="num">0;
   class=class="str">"cmt">//--- create a custom optimization criterion as the ratio of a net profit to a relative balance drawdown
   if(TesterStatistics(STAT_BALANCE_DDREL_PERCENT)!=class="num">0)
      ret=TesterStatistics(STAT_PROFIT)/TesterStatistics(STAT_BALANCE_DDREL_PERCENT);
   DB_Frames.OnTester(ret);
   class="kw">return(ret);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
CDatabaseFrames::OnTesterInit: optimization launched at class="num">15:class="num">53:class="num">27
DB: MACD Sample Database class="num">2020.01.class="num">20 class="num">15.53.sqlite opened successful
Transaction done successful
Close database with handle=class="num">65537
Database stored in file &class="macro">#x27;MACD Sample Database class="num">2020.01.class="num">20 class="num">15.53.sqlite&class="macro">#x27;

「用索引把全表扫描换成两次二分查找」

SQL 是声明式语言:你只描述要什么,查询规划器决定怎么算。 同一条语句背后可能有成百上千种算法,速度差异巨大,而规划器挑最快那条的前提,往往是有合适的索引。 以 MT5 导出的 DEALS 表为例,含 14 个字段,前 10 行里 rowid 从 1 到 10 自增,POSITION_ID 如 51447571 散在第 5 行。 若写 SELECT * FROM deals WHERE position_id=51447571,无索引时只能逐行比对,千万级记录下耗时可能以分钟计。 加上 CREATE INDEX Idx1 ON deals(position_id) 后,系统另建一张两列序表:POSITION_ID 升序、rowid 跟随。 查询先对索引做二分找 rowid,再凭 rowid 回原表二分取行,全表扫描被两次定位替代,大表查询耗时可能缩短数千倍乃至更多。 经验法则:常出现在 WHERE、排序、连接的字段就该建索引。 DEALS 里的 SYMBOL、MAGIC、ENTRY 都值得单字段索引;若常按 magic=500 AND symbol='USDCAD' 筛,建多重索引 CREATE INDEX Idx5 ON deals(magic, symbol) 更合理,索引内先排 MAGIC 再排 SYMBOL。 多重索引只对 AND 有效。 magic=100 OR symbol='EURUSD' 会拆成两次独立查找再合并 rowid,两字段若缺索引任一即退化为全表扫描。 排序同理:ORDER BY time 在大表上建 TIME 索引才有感,小表意义不大。 外汇与贵金属历史数据量巨大,索引设计不当会直接拖死回测与盯盘查询,实盘环境须警惕。

MQL5 / C++
SELECT * FROM deals WHERE position_id=class="num">51447571
SELECT * FROM deals WHERE rowid=class="num">5
 CREATE INDEX Idx1 ON deals(position_id)
SELECT * FROM deals WHERE position_id=class="num">51447571
CREATE INDEX Idx2 ON deals(symbol)
CREATE INDEX Idx3 ON deals(magic)
CREATE INDEX Idx4 ON deals(entry)
SELECT * FROM deals WHERE magic=class="num">500 AND symbol=&class="macro">#x27;USDCAD&class="macro">#x27;
CREATE INDEX Idx5 ON deals(magic, symbol)
SELECT * FROM deals WHERE  symbol=&class="macro">#x27;USDCAD&class="macro">#x27; AND magic=class="num">500 
SELECT * FROM deals WHERE magic=class="num">100 OR symbol=&class="macro">#x27;EURUSD&class="macro">#x27;
SELECT * FROM deals WHERE magic=class="num">100 
SELECT * FROM deals WHERE symbol=&class="macro">#x27;EURUSD&class="macro">#x27;
SELECT * FROM deals symbol=&class="macro">#x27;EURUSD&class="macro">#x27; ORDER BY time

在 MetaEditor 里直接建库写表

MT5 的 MQL5 已经原生带 SQL,MetaEditor 把建库、插删、批量操作都收进了向导。你只要在向导里指定文件名和表名,把字段类型填好,就能生成可操作的本地 SQLite,不必再开任何第三方数据库浏览器。 建完表之后,数据填充、搜索选取、手写 SQL 查询都能在编辑器内完成。官方示例包里 DatabaseExecute.mq5 有 64.83 KB,是体积最大的一段演示,说明批量执行并不轻量,但速度与其他方案基本同级。 注意一个实测坑:往表写文本时,直接用双引号会失败;先用单引号包、里面再放双引号才能写进去。整数转字符串写入则不会报错。外汇和贵金属交易本身高风险,用本地库做历史清洗也别假设数据无缺失。 下面这段是从示例里摘的写表调用,逐行看它在干什么:

MQL5 / C++
AddTable_TstDate(i,
  iTime(Symbol(),PERIOD_CURRENT,i),
  iHigh(Symbol(),PERIOD_CURRENT,i),
  iTime(Symbol(),PERIOD_CURRENT,i),
  iLow(Symbol(),PERIOD_CURRENT,i),
  IntegerToString(iTime(Symbol(),PERIOD_CURRENT,i)),
  class="num">1121,
  "&class="macro">#x27;string_no_error&class="macro">#x27;");
AddTable_TstDate(i,
  iTime(Symbol(),PERIOD_CURRENT,i),
  iHigh(Symbol(),PERIOD_CURRENT,i),
  iTime(Symbol(),PERIOD_CURRENT,i),
  iLow(Symbol(),PERIOD_CURRENT,i),

◍ 别急着下结论

上面那行调用把当前图表、当前周期、偏移 i 的根 K 线时间转成字符串,再配合 1121 这个错误码和 "string_error" 标签往日志里写,本质是给字符串转换失败留痕。 真正排查时得看 i 是否越界:若 i 超过 available bars 数量,iTime 会返回 0,IntegerToString(0) 虽不报错,但后面逻辑若依赖真实时间就会失真。 外汇与贵金属市场高波动,这类边界 bug 在跳空开盘时更容易触发,建议你在 MT5 策略测试器里故意喂一个超限 i 跑一遍,确认日志输出符合预期再上实盘。

MQL5 / C++
IntegerToString(iTime(Symbol(),PERIOD_CURRENT,i)),
class="num">1121,
"string_error");
让小布替你跑这套
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到成交分布与优化摘要的自动整理,把重复劳动交给小布,你专注决策。

常见问题

原文测试里 25000 条一次事务插入约 60ms,拆成单条会显著放大磁盘同步开销;批量写务必包事务。
先确认 broker 服务器时间与本地偏移,用 DatabasePrint 把时间戳原值打到日志,再在 SQL 里用 strftime 统一换算。
目前小布盯盘内置的是对品种页成交与优化数据的 AIGC 整理视图,不直连用户本地 db 文件;把关键结果用 DatabasePrint 输出后可在页面侧对照。
建表时把参数列与评价列分开,利用索引优化查询执行小节里的复合索引,按品种加指标阈值检索即可。
将数据库处理集成到 MetaEditor 一节提到直接在内置工具里跑查询,不必每次编译 EA 才看结果。