帧分析器(Frames Analyzer)工具带来的时间片交易魔法·综合运用
(3/3)·从数据库读取到智能系统剔除无盈利片,三步把测试器里的魔法搬进实盘
很多交易者跑完优化只盯着最终参数,却忽略了一段段被隐藏的时间片里藏着回撤与机会。帧分析器把每一帧余额曲线摊开,可大多数人连 MQD 文件和数据库都没打开过,更别说用被移除的片去修正 EA。
◍ 从本地库捞回收藏的回测通过号
在 MT5 里做批量优化后,把看中的参数组存进 SQLite 是常见做法。下面这段逻辑就是从外部数据库文件读出 FAVORITE_RESULTS 表里被标记过的 Pass 编号,方便后续只对这些编号做精细重验。 日志里能看到实际按下筛选按钮的时刻是 16:34:54,而允许触发的时间窗被限定在 15:30:00 至 17:39:59 之间;系统预设的五个盯盘时段字符串分别是 "00:45:00-01:20:01" "08:55:00-09:25:01" "12:55:00-13:50:01" "15:30:00-17:39:59" "20:10:00-21:05:00",说明筛选动作本身也受会话段约束。 GetPassNumbersOfFavoriteResults 先以读写并可创建的方式打开库,失败就打印错误码并退出;随后用 SELECT (Pass) FROM FAVORITE_RESULTS 取数,逐行把整数列塞进 ulong 数组。另一函数 GetFavoriteRemovedIntervalsByPassNumber 则按传入的 pass_number 反查 RemovedIntervals 字段,拿到该组被剔除的时间区间。 外汇与贵金属品种用这套机制重跑历史,属于高杠杆高风险操作,回测通过不代表实盘概率占优,仅可作参数缩圈用。
class="type">void CApp::GetPassNumbersOfFavoriteResults(class="kw">const class="type">class="kw">string db_filename, class="type">class="kw">ulong &passes[]) { ::ResetLastError(); class="type">uint flags = DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE; class="type">int db_handle = ::DatabaseOpen(db_filename, flags); if(db_handle == INVALID_HANDLE) { ::Print(__FUNCTION__, " > DB: ", db_filename, " open failed with code: ", ::GetLastError()); class="kw">return; } class="type">class="kw">string command = "SELECT(Pass) FROM FAVORITE_RESULTS;"; class="type">int db_query = ::DatabasePrepare(db_handle, command); if(db_query == INVALID_HANDLE) { ::Print(__FUNCTION__, " > DB: ", db_filename, " request failed with code ", ::GetLastError()); ::DatabaseClose(db_handle); class="kw">return; } for(class="type">int i = class="num">0; ::DatabaseRead(db_query); i++) { class="type">int pass_number; ::ResetLastError(); if(::DatabaseColumnInteger(db_query, class="num">0, pass_number)) { class="type">int prev_size = ::ArraySize(passes); ::ArrayResize(passes, prev_size + class="num">1); passes[prev_size] = (class="type">class="kw">ulong)pass_number; } else { ::Print(__FUNCTION__, " > DatabaseColumnInteger() error: ", ::GetLastError()); } } class=class="str">"cmt">//--- Finish working with the database ::DatabaseFinalize(db_query); ::DatabaseClose(db_handle); } class="type">class="kw">string CApp::GetFavoriteRemovedIntervalsByPassNumber(class="kw">const class="type">class="kw">ulong pass_number) { ::ResetLastError(); class="type">uint flags = DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE; class="type">int db_handle = ::DatabaseOpen(m_db_filename, flags); if(db_handle == INVALID_HANDLE) { ::Print(__FUNCTION__, " > DB: ", m_db_filename, " open failed with code: ", ::GetLastError()); class="kw">return(NULL); } class="type">class="kw">string command = "SELECT(RemovedIntervals) FROM FAVORITE_RESULTS WHERE Pass=" + (class="type">class="kw">string)pass_number + ";"; class="type">int db_query = ::DatabasePrepare(db_handle, command); if(db_query == INVALID_HANDLE) { ::Print(__FUNCTION__, " > DB: ", m_db_filename, " request failed with code ", ::GetLastError()); ::DatabaseClose(db_handle); class="kw">return(NULL); } class="type">class="kw">string time_ranges = ""; if(::DatabaseRead(db_query)) {
「从 SQLite 回读被剔除的时段并灌进时间轴控件」
这段逻辑干的事很直接:点开本地 .db 文件,把优化器跑完后被剔除的时段读出来,再塞进 Combobox 绑定的 ListView 和时间轴对象里。外汇与贵金属回测里,被剔除的时段往往对应流动性极差或滑点爆炸的窗口,高杠杆下直接交易这些区间可能放大亏损概率。 OnClickOpenDB 用 FileSelectDialog 限制后缀为 .db / .sqlite,选完把路径存进 m_db_filename,随后调用 GetPassNumbersOfFavoriteResults 拿到该库里所有 favorite 的 pass 编号。若 passes_total > 0,就清空旧列表、按索引逐个 AddItem,并默认选中第 0 项刷新控件。 SetTimeRange 负责把选中的 pass 对应的剔除区间字符串(用 '|' 分隔)拆成数组,再把 ' - ' 替换成 '-' 以适配时间轴格式。最后 m_time_ranges1.SetTimeRanges 接手渲染。打开 MT5 把这段接进你的面板类,改掉 folder="DB" 就能直接指到自己的回测库验证。 别让默认选中项骗了你 代码里 m_combobox1.SelectItem(0) 永远挑第一个 pass,若你的 favorite 表里 0 号是空跑,时间轴会静默空白。接好后建议先 ArrayPrint(passes) 确认编号顺序再决定默认项。
::ResetLastError(); if(!::DatabaseColumnText(db_query, class="num">0, time_ranges)) { ::Print(__FUNCTION__, " > DatabaseColumnText() error: ", ::GetLastError()); } } class=class="str">"cmt">//--- Finish working with the database ::DatabaseFinalize(db_query); ::DatabaseClose(db_handle); class="kw">return(time_ranges); } class="type">bool CApp::OnClickOpenDB(class="kw">const class="type">int id) { if(m_button1.Id() != id) { class="kw">return(class="kw">false); } class="type">class="kw">string filenames[]; class="type">class="kw">string folder = "DB"; class="type">class="kw">string filter = "DB files(*.db)|*.db|SQLite files(*.sqlite)|*.sqlite|All files(*.*)|*.*"; if(::FileSelectDialog("Select database file to upload", folder, filter, FSD_FILE_MUST_EXIST, filenames) > class="num">0) { class="type">int filenames_total = ::ArraySize(filenames); if(filenames_total < class="num">1) { class="kw">return(class="kw">false); } m_db_filename = filenames[class="num">0]; ::Print(__FUNCTION__, " > m_db_filename: ", m_db_filename); class="type">class="kw">ulong passes[]; GetPassNumbersOfFavoriteResults(m_db_filename, passes); ::ArrayPrint(passes); CListView *list_view = m_combobox1.GetListViewPointer(); class="type">int passes_total = ::ArraySize(passes); if(passes_total > class="num">0) { list_view.Clear(); for(class="type">int i = class="num">0; i < passes_total; i++) { list_view.AddItem(i, (class="type">class="kw">string)passes[i]); } m_combobox1.SelectItem(class="num">0); m_combobox1.GetButtonPointer().Update(true); list_view.Update(true); } } class="kw">return(true); } class="type">void CApp::SetTimeRange(class="type">void) { CListView *list_view = m_combobox1.GetListViewPointer(); class="type">int index = list_view.SelectedItemIndex(); class="type">class="kw">ulong pass_number = (class="type">int)list_view.GetValue(index); class="type">class="kw">string removed_intervals = GetFavoriteRemovedIntervalsByPassNumber(pass_number); ::ArrayFree(m_time_ranges_str); ::StringSplit(removed_intervals, ::StringGetCharacter("|", class="num">0), m_time_ranges_str); class="type">int ranges_total = ::ArraySize(m_time_ranges_str); for(class="type">int i = class="num">0; i < ranges_total; i++) { ::StringReplace(m_time_ranges_str[i], " - ", "-"); } ::ArrayPrint(m_time_ranges_str); m_time_ranges1.SetTimeRanges(m_time_ranges_str); m_time_ranges1.Update(); } class="kw">struct TimeRange { class="type">MqlDateTime start; class="type">MqlDateTime end; }; class="type">void CApp::GetTimeRanges(TimeRange &ranges[]) { ::ArrayFree(ranges); class="type">int ranges_total = ::ArraySize(m_time_ranges_str); for(class="type">int i = class="num">0; i < ranges_total; i++) {
把字符串时段转成可比对的时间结构
时段配置常以 "HH:MM:SS-HH:MM:SS" 这样的字符串传入,核心是先按 '-' 拆开再转成秒数做比较。下面这段把字符串数组 tr 拆出起止,并用 HourSeconds/MinuteSeconds 把时分秒压成当日偏移秒,写进 ranges 结构体数组。 string tr[]; ::StringSplit(m_time_ranges_str[i], ::StringGetCharacter("-", 0), tr); int size = ::ArraySize(ranges); ::ArrayResize(ranges, size + 1); MqlDateTime start, end; ::TimeToStruct(::StringToTime(tr[0]), start); ::TimeToStruct(::StringToTime(tr[1]), end); datetime time1 = HourSeconds(start.hour) + MinuteSeconds(start.min) + start.sec; datetime time2 = HourSeconds(end.hour) + MinuteSeconds(end.min) + end.sec; ::TimeToStruct(time1, ranges[size].start); ::TimeToStruct(time2, ranges[size].end); 在 CApp 里 HourSeconds 就是 hour*60*60、MinuteSeconds 是 minute*60,两个辅助函数把人工可读时间变成整数秒,方便后面 CheckTradeTime 里用 time_c 与 time_s/time_e 直接比大小。 按钮或下拉框事件触发后调 SetTimeRange 和 GetTimeRanges 刷新 m_time_ranges,之后每笔订单只在当前秒数落在区间内才放行。外汇与贵金属波动受时段流动性影响明显,这类过滤只降低非活跃段介入概率,不消除高风险。
class="type">class="kw">string tr[]; ::StringSplit(m_time_ranges_str[i], ::StringGetCharacter("-", class="num">0), tr); class="type">int size = ::ArraySize(ranges); ::ArrayResize(ranges, size + class="num">1); class="type">MqlDateTime start, end; ::TimeToStruct(::StringToTime(tr[class="num">0]), start); ::TimeToStruct(::StringToTime(tr[class="num">1]), end); class="type">class="kw">datetime time1 = HourSeconds(start.hour) + MinuteSeconds(start.min) + start.sec; class="type">class="kw">datetime time2 = HourSeconds(end.hour) + MinuteSeconds(end.min) + end.sec; ::TimeToStruct(time1, ranges[size].start); ::TimeToStruct(time2, ranges[size].end);
◍ 用秒数比较圈定禁交易时段
这段逻辑把起止时间换算成当日秒数后做区间判断,比直接比对时分针更不容易在跨小时边界出错。 time_e 由 HourSeconds(end.hour) + MinuteSeconds(end.min) + end.sec 拼出结束时刻的秒数,time_c 落在 [time_s, time_e] 内就置 is_trade_time=false 并 break,说明当前属于禁交易窗口。 外层 if(CheckTradeTime()) 成立时仅 Print 一条函数名加 ' > trade time!' 的日志,实盘里你可以把这句换成具体的下单拦截或报警,先在 MT5 策略测试器里跑一遍看日志是否符合预期。 外汇与贵金属杠杆高、滑点突发,任何时间过滤都只是概率层面的风控辅助,不保证避开极端行情。
class="type">class="kw">datetime time_e = HourSeconds(end.hour) + MinuteSeconds(end.min) + end.sec; if(time_c >= time_s && time_c <= time_e) { is_trade_time = class="kw">false; class="kw">break; } } class="kw">return(is_trade_time); } if(CheckTradeTime()) { Print(__FUNCTION__, " > trade time!"); }
「优化完别只盯着单测」
在策略测试器里跑完参数优化,传统做法是逐个做单测去核对每组成果,这套流程费时且容易漏看整体分布。 帧分析器这类插件能在优化结束后直接读 MQD 文件或数据库,把前 100 个结果按盈利、交易次数、回撤、盈利因子、恢复因子等维度同时图形化呈现,省去手工翻页的麻烦。 它内置的“最佳片段”模块会标出统计上无盈利的时间片,剔除后你能看到反事实下的资金曲线——只保留被统计证明较安全的时间窗口。喜欢的结果可存进数据库,也能导出带 EA 外部参数的 set 文件,删除的时间片同样留档,实盘里按需规避即可。 外汇与贵金属杠杆高、滑点跳空频繁,这类工具降低的是筛选成本,不抵消品种本身的高风险,上实盘前仍建议用历史数据自分桶验证时间片划分是否过拟合。