帧分析器(Frames Analyzer)工具带来的时间片交易魔法·进阶篇
(2/3)· 多数交易者优化完 EA 就直接实盘,却从没看过那些拖垮曲线的无盈利时间片
很多人在 MetaTrader 5 测试器跑完优化,导出参数便匆忙上实盘,忽略了结果里藏着的长期横盘与回撤片段。帧分析器提供的选卡与数据库视图,正是把这些隐性损耗摊开来看的入口。本篇接着基础篇,把余额、前100、收藏与数据库联动,以及被移除时间片如何写回智能系统逐一拆开。
余额选卡里的两种曲线对照
余额选卡并排给了两张图。左边只画初始余额,以及把亏损交易序列整段删掉后、每一步中间改进出来的余额曲线;一眼能看出,砍掉无盈利片段后资金曲线被拉直了多少。 右边放的是左边挑出的最优改进结果作为终态余额,再叠上每个交易周期里跑出来的全部余额轨迹,方便比对「单周期真实回撤」和「剔亏后理想路径」差多远。 两张图都跟随数据表里高亮行实时刷新,所以你点选某一行,就能看到那一笔或那一段被排除前后,余额从多少变到多少。 实操上,开 MT5 把回测报告按周期高亮,观察左图删亏后余额相较初始抬升的幅度,若多数周期高亮后左图曲线仍频繁下破右图终值,说明该策略剔亏后的稳定性可能仍偏弱,外汇与贵金属品种杠杆高、滑点敏感,此类结论仅作概率参考。
「剔除无盈利片后的前百结果对比」
工具界面左图展示原始回测里前 100 个盈利结果,右图则按 Profit、Trades、DD、RF、BI_Profit、BI_Trades、BI_DD、BI_RF 八个准则筛选出的前 100 个结果。表中高亮行会实时驱动图形刷新——比如左图黑色余额曲线对应的高亮行,在右图里就变成了排除一段连续亏损交易后的改进曲线。 用户可以指定从历史中剔除若干「无盈利时间片」来重算结果。实测中,即便原本看起来不赚钱的 EA,在剥掉这些片之后,前 100 个样本的最终余额和回撤表现都可能明显好转。 把看中的参数组合点 Add to favorites 存进收藏表,整张结果表里这些行会用不同颜色标出,方便后续横向比对。外汇与贵金属市场杠杆高、滑点跳空频繁,这类样本外改善只代表历史概率倾向,实盘仍需在小周期上手动复核。
◍ 把筛选结果落盘到收藏与数据库
收藏选卡的结构和 Results 选卡一致,差异只在能把挑出来的验算结果同时写进文件和本地库。窗口上方三个按钮直接控制生命周期:Delete selected 删左表选中项并清库,Delete all 清空左表与库,Save parameters 负责落盘。 点 Save parameters 后,选中结果会进库里的 FAVORITE_RESULTS 表;若 MQD 文件是读取模式,就写到 MQL5/Files/Reports/FramesA/[当前时间],FRAME_MODE(优化后立刻存)则写到 MQL5/Files/Reports/[当前时间][EA名]。每个验算编号单独成文件夹,前缀统一挂在文件名前,例如 422462.set 存 EA 外部参数,422462_balance_sub_bi.png 是剔掉无盈利时间片后的余额图,422462_bi_table.png 是对应终值表截图,422462_gui.png 为完整界面。 外汇与贵金属优化结果高度依赖样本区间,回测漂亮不等于实盘能跑,杠杆品种随时可能击穿止损。下面这段 set 是实际落盘样例,MACD 快线周期 15、慢线 25、信号线 25,止盈 270 点、止损 115 点,参数范围都标了 Y 允许优化。
; <span class="keyword">this</span> file contains last used <span class="keyword">input</span> parameters <span class="keyword">for</span> testing/optimizing FA expert advisor ; Experts\Advisors\ExpertMACD.ex5 ; Inp_Expert_Title=ExpertMACD||<span class="number">class="num">0.0</span>||<span class="number">class="num">0.0</span>||<span class="number">class="num">0.0</span>||N Inp_Signal_MACD_PeriodFast=<span class="number">class="num">15</span>||<span class="number">class="num">5.0</span>||<span class="number">class="num">5.0</span>||<span class="number">class="num">30.0</span>||Y Inp_Signal_MACD_PeriodSlow=<span class="number">class="num">25</span>||<span class="number">class="num">5.0</span>||<span class="number">class="num">5.0</span>||<span class="number">class="num">30.0</span>||Y Inp_Signal_MACD_PeriodSignal=<span class="number">class="num">25</span>||<span class="number">class="num">5.0</span>||<span class="number">class="num">5.0</span>||<span class="number">class="num">30.0</span>||Y Inp_Signal_MACD_TakeProfit=<span class="number">class="num">270</span>||<span class="number">class="num">30.0</span>||<span class="number">class="num">5.0</span>||<span class="number">class="num">300.0</span>||Y Inp_Signal_MACD_StopLoss=<span class="number">class="num">115</span>||<span class="number">class="num">20.0</span>||<span class="number">class="num">5.0</span>||<span class="number">class="num">200.0</span>||Y
优化后数据库落盘到哪了
在测试器跑完一轮参数优化,帧分析器会马上把全部结果写进本地数据库,而不是只留在界面里。每次优化都会在终端目录 MQL5/Files/DB 下新建一个 .db 文件,文件名由当前时间和 EA 名拼成:[CURRENT_TIME] [EXPERT_NAME].db,所以同一 EA 多轮优化不会互相覆盖。
| 库里实际有三张表。OPTIMIZATION_RESULTS 存每遍验算的核心指标:Pass 索引、Profit 盈利、Trades 笔数、PF 盈利因子、DD 回撤、RF 恢复因子,以及所有外部参数列和 Deals 交易历史。EXPERT_PARAMETERS 只有 Parameter 和 Value 两列,记录参数名与值;以标准版 ExpertMACD 为例,INPUT_n 的 Value 会用 | 隔开外部参数名和优化参数名。 |
|---|
FАVORITE_RESULTS 只留三列:FavoriteID、Pass、RemovedIntervals,专门存被标收藏的验算,若真实交易时段被禁,就在 RemovedIntervals 记掉。这样优化加筛选完,后续 EA 实盘要用的东西基本都能直接从库里捞,外汇和贵金属品种波动大、滑点跳空频繁,拿这些历史 Pass 做样本前先确认回测与真实环境偏差。
「把帧分析器接进自己的EA」
帧分析器本身只是个EA,直接挂进终端跑,图形和表格全是空的——它不吃实时行情,只认优化结果。想立刻看效果,得去把附带的优化数据库放进 MQL5/Files/DB,然后在EA界面的 Frames 选卡点 Open DB,弹窗里挑对应文件就能载入,我本地那个目录常备好几个不同品种的库。 要让它自动存你自己的优化数据,得把它当函数库引到EA主文件里。下载完后在 MQL5/Experts/Market 能找到 FramesA.ex5,用 #import 指过去,再把几个事件函数挂到EA原有的回调上就行,代码量极小。 如果你EA本来就用了 EasyAndFastGUI v2.0 做界面,那什么都不用额外调,帧分析器内置的同款库会自己协同,它跑在 FRAME_MODE 下不抢事件。没用这个GUI库也没关系,分析器照样收数据。文末给的 ExpertMACD 示例已经焊好了模块,拆开照抄最快。
class="macro">#class="kw">import "..\Experts\Market\FramesA.ex5" class="type">void OnEvent(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam); class="type">void OnTesterEvent(class="type">void); class="type">void OnTesterInitEvent(class="type">void); class="type">void OnTesterPassEvent(class="type">void); class="type">void OnTesterDeinitEvent(class="type">void); class="macro">#class="kw">import class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ChartEvent function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnChartEvent(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam) { FramesA::OnEvent(id, lparam, dparam, sparam); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Test completion event handler | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double OnTester(class="type">void) { FramesA::OnTesterEvent(); class="kw">return(class="num">0.0); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TesterInit function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTesterInit(class="type">void) { FramesA::OnTesterInitEvent(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TesterPass function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTesterPass(class="type">void) { FramesA::OnTesterPassEvent(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TesterDeinit function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTesterDeinit(class="type">void) { FramesA::OnTesterDeinitEvent(); } class=class="str">"cmt">//+------------------------------------------------------------------+
◍ 把删掉的时段接回EA交易闸门
帧分析器跑完优化后,你手头会有一个存满验算结果的数据库,以及一张自己挑出来的收藏表(FAVORITE_RESULTS),表里对应的时间片已经被你主动移除。下一步不是看报表,而是让EA在实盘里真的避开这些时段。 用 EasyAndFastGUI 2.0 里的 CTimeRanges 元素能直接在窗体上画一条日内时间标尺,虚线标当前时间,鼠标悬停拉出竖线和标签。点击标尺会抛出 ON_CLICK_TIME_RANGE 自定义事件,dparam 是当天逝去的秒数,sparam 是点中的被删区间字符串(空白代表没命中任何范围)。 下面这段 CreateGUI 演示了最少代码量搭出界面:窗体、Open DB 按钮、验算下拉框、以及写死的一组禁交易时段。注意背景高亮的那几行,time_ranges 数组里塞了 5 个具体区间,例如 00:45:00-01:20:01、15:30:00-17:39:59,SetTimeRanges 之后标尺立刻可视化。 打开数据库后调 GetPassNumbersOfFavoriteResults() 从收藏表抽验算索引,再经 GetFavoriteRemovedIntervalsByPassNumber() 拿到对应删除时段,填进全局数组并刷给 CTimeRanges。真正的闸门在 CheckTradeTime():每次 OnTick 先问一句现在是不是落在禁交易串里,命中就直接不下单。外汇与贵金属杠杆高,时段过滤只降概率不消风险,参数自己回测再定。 事件拦截那段,先判 id 再核 lparam 的元素ID,命中就把 dparam/sparam 打到 Experts 日志,顺手 GetTimeRanges() 把数组 ArrayPrint 出来,方便你核对图形和底层数据是否一致。
class="type">void CApp::CreateGUI(class="type">void) { class=class="str">"cmt">//--- Form m_window1.ResizeMode(true); m_window1.ThemeButtonIsUsed(true); CCoreCreate::CreateWindow(m_window1, "TIME TRADE RANGES", class="num">1, class="num">1, class="num">350, class="num">100, true, true, true, true); class=class="str">"cmt">//--- Button CCoreCreate::CreateButton(m_button1, m_window1, class="num">0, "Open DB...", class="num">10, class="num">30, class="num">100); class=class="str">"cmt">//--- Combobox class="type">class="kw">string items1[] = {"class="num">12345", "class="num">19876", "class="num">45678", "class="num">23456", "class="num">67890"}; CCoreCreate::CreateCombobox(m_combobox1, m_window1, class="num">0, "Passes: ", class="num">120, class="num">30, class="num">135, class="num">90, items1, class="num">103, class="num">0); class=class="str">"cmt">//--- Time ranges class="type">class="kw">string time_ranges[] = { "class="num">00:class="num">45:class="num">00 - class="num">01:class="num">20:class="num">01", "class="num">08:class="num">55:class="num">00 - class="num">09:class="num">25:class="num">01", "class="num">12:class="num">55:class="num">00 - class="num">13:class="num">50:class="num">01", "class="num">15:class="num">30:class="num">00 - class="num">17:class="num">39:class="num">59", "class="num">20:class="num">10:class="num">00 - class="num">21:class="num">05:class="num">00" }; m_time_ranges1.SetTimeRanges(time_ranges); m_time_ranges1.AutoXResizeMode(true); m_time_ranges1.AutoXResizeRightOffset(class="num">5); CCoreCreate::CreateTimeRanges(m_time_ranges1, m_window1, class="num">0, class="num">5, class="num">60, class="num">390); } class="type">void CApp::OnEvent(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam) { if(id == CHARTEVENT_CUSTOM + ON_CLICK_TIME_RANGE) { if(m_time_ranges1.Id() == lparam) { Print(__FUNCTION__, " > pressed time: ", ::TimeToString((class="type">class="kw">datetime)dparam, TIME_MINUTES|TIME_SECONDS), "; pressed time range: ", sparam); class="type">class="kw">string time_ranges[]; m_time_ranges1.GetTimeRanges(time_ranges); ArrayPrint(time_ranges); class="kw">return; } } }