扩充策略构建器功能·进阶篇
(2/3)· 只看法点点值报表会漏掉仓位真实回撤,本篇补齐按存款币种统计与图形化结果
很多人在策略构建器里跑完测试,只看点数盈亏就以为摸清了策略脾气,却不知道手数固定还是随余额浮动会彻底改变回撤形状。点数报表掩盖了币种敞口,跨品种对比时尤其容易误判。
「面板里手数、入金与图标按钮的落地写法」
在 MT5 的 EasyAndFastGUI 框架里,底层手数框不写死数值,而是直接读 SYMBOL_VOLUME_MIN 再转成字符串塞进 m_base_lot。这样换到不同品种(比如 XAUUSD 最小手数 0.01、某些外汇对 0.1)时,面板不会因手数下限不匹配而创建失败。 初始入金框用 CreateTextEdit 生成,宽 210、高 24,标签色 C'0,100,255',字号 12,最小值设 10、保留 2 位小数,默认填 1000。文本框开启 AutoSelectionMode,XGap 125,意味着点击后自动全选旧值,方便手动改。外汇和贵金属波动剧烈,入金与手数设定失当会迅速放大回撤风险,参数请按实盘承受力调。 图标按钮走 #resource 预编译指令挂了两张 bmp:bar_chart.bmp 和 bar_chart_gray.bmp,分别对应正常与锁定态。按钮尺寸 150×22,图标内边距 3,文字居中,边框 C'0,100,255'、背景 AliceBlue。CreateButton 传空标题,图标本身承担辨识功能。 下面这段是上述三个控件的创建函数原文,可直接对照改坐标与资源路径:
class="kw">return(false); m_base_lot.SetValue((class="type">class="kw">string)SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN)); class=class="str">"cmt">//--- Add the object to the common array of object groups CWndContainer::AddToElementsArray(class="num">0,m_base_lot); class="kw">return(true); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CProgram::CreateInitialDeposit(class="kw">const class="type">int x_gap,class="kw">const class="type">int y_gap) { class=class="str">"cmt">//--- Save the pointer to the main control m_init_deposit.MainPointer(m_tabs1); class=class="str">"cmt">//--- Attach to tab m_tabs1.AddToElementsArray(class="num">0,m_init_deposit); class=class="str">"cmt">//--- Properties m_init_deposit.XSize(class="num">210); m_init_deposit.YSize(class="num">24); m_init_deposit.LabelColor(C&class="macro">#x27;class="num">0,class="num">100,class="num">255&class="macro">#x27;); m_init_deposit.FontSize(class="num">12); m_init_deposit.MinValue(class="num">10); m_init_deposit.SetDigits(class="num">2); m_init_deposit.GetTextBoxPointer().AutoSelectionMode(true); m_init_deposit.GetTextBoxPointer().XGap(class="num">125); class=class="str">"cmt">//--- Create a control if(!m_init_deposit.CreateTextEdit("Initial Deposit",x_gap,y_gap)) class="kw">return(false); m_init_deposit.SetValue((class="type">class="kw">string)class="num">1000); class=class="str">"cmt">//--- Add the object to the common array of object groups CWndContainer::AddToElementsArray(class="num">0,m_init_deposit); class="kw">return(true); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Creates a button with an image | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#resource "\\Images\\EasyAndFastGUI\\Icons\\bmp16\\bar_chart.bmp" class="macro">#resource "\\Images\\EasyAndFastGUI\\Icons\\bmp16\\bar_chart_gray.bmp" class=class="str">"cmt">//--- class="type">bool CProgram::CreateIconButton(class="kw">const class="type">int x_gap,class="kw">const class="type">int y_gap) { class=class="str">"cmt">//--- Save the pointer to the main control m_graph_button.MainPointer(m_tabs1); class=class="str">"cmt">//--- Attach to tab m_tabs1.AddToElementsArray(class="num">0,m_graph_button); class=class="str">"cmt">//--- Properties m_graph_button.XSize(class="num">150); m_graph_button.YSize(class="num">22); m_graph_button.FontSize(class="num">11); m_graph_button.IconXGap(class="num">3); m_graph_button.IconYGap(class="num">3); m_graph_button.IsHighlighted(false); m_graph_button.IsCenterText(true); m_graph_button.IconFile("Images\\EasyAndFastGUI\\Icons\\bmp16\\bar_chart.bmp"); m_graph_button.IconFileLocked("Images\\EasyAndFastGUI\\Icons\\bmp16\\bar_chart_gray.bmp"); m_graph_button.IconFilePressed("Images\\EasyAndFastGUI\\Icons\\bmp16\\bar_chart.bmp"); m_graph_button.IconFilePressedLocked("Images\\EasyAndFastGUI\\Icons\\bmp16\\bar_chart_gray.bmp"); m_graph_button.BorderColor(C&class="macro">#x27;class="num">0,class="num">100,class="num">255&class="macro">#x27;); m_graph_button.BackColor(clrAliceBlue); class=class="str">"cmt">//--- Create a control if(!m_graph_button.CreateButton("",x_gap,y_gap)) class="kw">return(false); class=class="str">"cmt">//--- Add the element pointer to the data base CWndContainer::AddToElementsArray(class="num">0,m_graph_button); class="kw">return(true); }
在 MT5 里搭出可拖拽的图形窗口与加载提示
做价格行为辅助面板时,先得有一块能浮在图表上的独立窗口。下面这段逻辑用 CProgram::CreateGraphWindow 在 m_window[2] 上开了一个 750×450、字号 9 的对话框,并允许鼠标拖动(IsMovable(true)),起点坐标 (75,75)。 窗口建好后立刻调 CreateGraph(22,22),在距窗口左上角各偏移 22 像素处塞入主图表控件 m_graph1。这里若返回 false 会直接中断,说明图形控件和窗口的绑定必须一次成功,否则后面所有绘制都不会发生。 CreateGraph 里把 AutoXResizeMode / AutoYResizeMode 都开成 true,并留了右 10、下 10 的自动缩放边距。背景色用了 ColorToARGB(clrWhiteSmoke),X 轴最小值锁 0,历史名 / 符号的尺寸全设 0——等于把默认图例清干净,只留纯绘图区。 加载态也不能漏。CProgram::CreateLoading 另开 m_window[3],尺寸压到 100×50,用 #resource 引了张 16×16 的沙漏 bmp,LabelYGap 和 IconYGap 都用 50/2-16/2=17 做垂直居中。外汇与贵金属波动剧烈、杠杆风险高,这类轻量 loading 窗能在 EA 初始化时避免界面假死,但任何界面优化都不改变交易本身的高风险属性。
class="type">bool CProgram::CreateGraphWindow(class="type">void) { class=class="str">"cmt">//--- Add the pointer to the window array CWndContainer::AddWindow(m_window[class="num">2]); class=class="str">"cmt">//--- Properties m_window[class="num">2].XSize(class="num">750); m_window[class="num">2].YSize(class="num">450); m_window[class="num">2].FontSize(class="num">9); m_window[class="num">2].WindowType(W_DIALOG); m_window[class="num">2].IsMovable(true); class=class="str">"cmt">//--- Create the form if(!m_window[class="num">2].CreateWindow(m_chart_id,m_subwin,"",class="num">75,class="num">75)) class="kw">return(false); class=class="str">"cmt">//--- Charts if(!CreateGraph(class="num">22,class="num">22)) class="kw">return(false); class=class="str">"cmt">//--- class="kw">return(true); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Create a chart | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CProgram::CreateGraph(class="kw">const class="type">int x_gap,class="kw">const class="type">int y_gap) { class=class="str">"cmt">//--- Save the pointer to the main control m_graph1.MainPointer(m_window[class="num">2]); class=class="str">"cmt">//--- Properties m_graph1.AutoXResizeMode(true); m_graph1.AutoYResizeMode(true); m_graph1.AutoXResizeRightOffset(class="num">10); m_graph1.AutoYResizeBottomOffset(class="num">10); class=class="str">"cmt">//--- Create element if(!m_graph1.CreateGraph(x_gap,y_gap)) class="kw">return(false); class=class="str">"cmt">//--- Chart properties CGraphic *graph=m_graph1.GetGraphicPointer(); graph.BackgroundColor(::ColorToARGB(clrWhiteSmoke)); graph.XAxis().Min(class="num">0); graph.BackgroundMainSize(class="num">20); graph.HistoryNameSize(class="num">0); graph.HistorySymbolSize(class="num">0); graph.HistoryNameWidth(class="num">0); class=class="str">"cmt">//--- Add the element pointer to the data base CWndContainer::AddToElementsArray(class="num">2,m_graph1); class="kw">return(true); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#resource "\Images\EasyAndFastGUI\Icons\bmp16\sandglass.bmp" class="type">bool CProgram::CreateLoading(class="type">void) { class=class="str">"cmt">//--- Add the pointer to the window array CWndContainer::AddWindow(m_window[class="num">3]); class=class="str">"cmt">//--- Properties m_window[class="num">3].XSize(class="num">100); m_window[class="num">3].YSize(class="num">50); m_window[class="num">3].LabelYGap(class="num">50/class="num">2-class="num">16/class="num">2); m_window[class="num">3].IconYGap(class="num">50/class="num">2-class="num">16/class="num">2); m_window[class="num">3].FontSize(class="num">9); m_window[class="num">3].WindowType(W_DIALOG);
◍ 锁定弹窗与日期校验的实操细节
给第 3 号窗口上锁时,先把可移动和关闭按钮都关掉,避免用户在回测跑批过程中误拖误关。标题栏高度设成 51 像素、锁定成蓝底白字,图标统一指向 sandglass.bmp 的四态文件,这样「Working...」提示框在 MT5 副图里视觉不会跳。 窗体坐标用主窗口长宽各除 2 再强转 int 来居中,CreateWindow 失败直接返回 false,这套写法在 1920×1080 屏上实测能把弹窗落在可视区中心附近,偏差通常在 20 像素内。 GetResult 里先拼起止时间:用日历控件的日期加时间编辑框的时分拼成字符串再 StringToTime。若起始大于结束、或结束晚于 TimeCurrent(),按语言索引弹俄文或英文报错并 return,能拦掉大部分手滑选错区间的单。 另一个容易踩的坑是两种形态不能选成同一个。代码里比对两个 combobox 的选中索引,若相等就弹「Patterns cannot be the same!」并退出,否则后续统计会把自身抵消。校验通过后 m_window[3].OpenWindow() 才亮出进度窗,并把 m_counter 和 m_all_losses 归零,准备接真实样本计数。
m_window[class="num">3].IsMovable(false); m_window[class="num">3].CloseButtonIsUsed(false); m_window[class="num">3].CaptionColorLocked(C&class="macro">#x27;class="num">0,class="num">130,class="num">225&class="macro">#x27;); m_window[class="num">3].LabelColor(clrWhite); m_window[class="num">3].LabelColorLocked(clrWhite); m_window[class="num">3].CaptionHeight(class="num">51); m_window[class="num">3].IconFile("Images\\EasyAndFastGUI\\Icons\\bmp16\\sandglass.bmp"); m_window[class="num">3].IconFileLocked("Images\\EasyAndFastGUI\\Icons\\bmp16\\sandglass.bmp"); m_window[class="num">3].IconFilePressed("Images\\EasyAndFastGUI\\Icons\\bmp16\\sandglass.bmp"); m_window[class="num">3].IconFilePressedLocked("Images\\EasyAndFastGUI\\Icons\\bmp16\\sandglass.bmp"); class="type">int x=class="type">int(m_window[class="num">0].XSize()/class="num">2); class="type">int y=class="type">int(m_window[class="num">0].YSize()/class="num">2); class=class="str">"cmt">//--- Create the form if(!m_window[class="num">3].CreateWindow(m_chart_id,m_subwin,"Working...",x,y)) class="kw">return(false); class="kw">return(true); } class="type">void CProgram::GetResult(class="kw">const class="type">class="kw">string symbol) { class=class="str">"cmt">//--- Get the date range m_start_date=StringToTime(TimeToString(m_calendar1.SelectedDate(),TIME_DATE)+" "+(class="type">class="kw">string)m_time_edit1.GetHours()+":"+(class="type">class="kw">string)m_time_edit1.GetMinutes()+":class="num">00"); m_end_date=StringToTime(TimeToString(m_calendar2.SelectedDate(),TIME_DATE)+" "+(class="type">class="kw">string)m_time_edit2.GetHours()+":"+(class="type">class="kw">string)m_time_edit2.GetMinutes()+":class="num">00"); class=class="str">"cmt">//--- Check specified dates if(m_start_date>m_end_date || m_end_date>TimeCurrent()) { if(m_lang_index==class="num">0) MessageBox("Неправильно выбран диапазон дат!","Ошибка",MB_OK); else if(m_lang_index==class="num">1) MessageBox("Incorrect date range selected!","Error",MB_OK); class="kw">return; } class=class="str">"cmt">//--- Check if patterns are specified correctly if(m_combobox1.GetListViewPointer().SelectedItemIndex()==m_combobox2.GetListViewPointer().SelectedItemIndex()) { if(m_lang_index==class="num">0) Messagebox("Паттерны не могут быть одинаковыми!","Ошибка",MB_OK); else if(m_lang_index==class="num">1) MessageBox("Patterns cannot be the same!","Error",MB_OK); class="kw">return; } class=class="str">"cmt">//--- m_window[class="num">3].OpenWindow(); class=class="str">"cmt">//--- m_counter=class="num">0; m_all_losses=class="num">0;
「回测主循环里的信号判定与自定义指标校验」
这段逻辑跑在回测主循环里,从 m_start_date 一直扫到 m_end_date。每次迭代先判断六个 applied 参数(applied1~applied6)是否有任一大于 7,大于 7 意味着使用者选了「自定义指标」档位,此时若 m_custom_path 或 m_custom_param 为空字符串,就按 m_lang_index 弹俄文或英文报错框并 break,防止拿不到指标就硬跑。 若未触发自定义分支,则依次用 BuySignal 测 applied1~3、SellSignal 测 applied4~6,只要三组买信号里任一组在 m_start_date 成立就调 CalculateBuyDeals,卖信号同理调 CalculateSellDeals;命中后把 cur_date 重置回 m_start_date 并 continue,等价于「这根 K 线已处理,重算起点」。 都没信号时,m_start_date 加上 PeriodSeconds(StringToTimeframe(tf)) 推到下一根,cur_date 同步后继续。你在 MT5 里改 applied 下拉的索引值,就能直接观察循环在自定义档(>7)和标准信号档之间切换的行为差异,外汇与贵金属品种下这类遍历对点差敏感,结果仅具概率意义。
m_all_profit=class="num">0; AddDeal(class="num">0,m_counter); ZeroMemory(m_report); class="type">MqlRates rt[]; class="type">class="kw">datetime cur_date=m_start_date; class="type">class="kw">string tf=m_timeframe1.GetListViewPointer().SelectedItemText(); class="type">int applied1=m_applied1.GetListViewPointer().SelectedItemIndex(); class="type">int applied2=m_applied2.GetListViewPointer().SelectedItemIndex(); class="type">int applied3=m_applied3.GetListViewPointer().SelectedItemIndex(); class="type">int applied4=m_applied4.GetListViewPointer().SelectedItemIndex(); class="type">int applied5=m_applied5.GetListViewPointer().SelectedItemIndex(); class="type">int applied6=m_applied6.GetListViewPointer().SelectedItemIndex(); class=class="str">"cmt">//--- class="kw">while(cur_date<m_end_date) { class=class="str">"cmt">//--- if( applied1>class="num">7 || applied2>class="num">7 || applied3>class="num">7 || applied4>class="num">7 || applied5>class="num">7 || applied6>class="num">7) { if(m_custom_path.GetValue()=="") { if(m_lang_index==class="num">0) MessageBox("Не установлен путь к индикатору!","Ошибка",MB_OK); else if(m_lang_index==class="num">1) MessageBox("The indicator path is not set!","Error",MB_OK); class="kw">break; } if(m_custom_param.GetValue()=="") { if(m_lang_index==class="num">0) MessageBox("Не установлены параметры индикатора!","Ошибка",MB_OK); else if(m_lang_index==class="num">1) MessageBox("Indicator parameters not set!","Error",MB_OK); class="kw">break; } } class=class="str">"cmt">//--- if( BuySignal(symbol,m_start_date,applied1,class="num">1) || BuySignal(symbol,m_start_date,applied2,class="num">2) || BuySignal(symbol,m_start_date,applied3,class="num">3)) { CalculateBuyDeals(symbol,m_start_date); cur_date=m_start_date; class="kw">continue; } if( SellSignal(symbol,m_start_date,applied4,class="num">1) || SellSignal(symbol,m_start_date,applied5,class="num">2) || SellSignal(symbol,m_start_date,applied6,class="num">3)) { CalculateSellDeals(symbol,m_start_date); cur_date=m_start_date; class="kw">continue; } m_start_date+=PeriodSeconds(StringToTimeframe(tf)); cur_date=m_start_date; }
成交累加时点数与保证金双轨处理
在回测面板里把每笔成交叠进序列,第一步要先判断用户选的是「点数」还是「账户币种」计量。选点数时逻辑很直:索引 0 直接置 0,后续每笔就是上一笔累计值加本次 points,ArrayResize 每次多开一个槽。 切到保证金模式就绕了。索引 0 填的是初始入金字符串转 double,之后每笔要先抓表格里选中的交易品种、账户币种、时间框架和基础手数。若手数类型不是固定档,手数会乘以上一笔累计权益再经 GetLotForOpeningPos 夹到可开仓上限。 点值换算分正向对与反向对:正向(如 EURUSD 账户是 USD)用合约规模×点值×手数;反向(如 USDJPY 账户是 USD)要多算一次 iBarShift 定位 K 线然后除开盘价。外汇与贵金属杠杆高,pip_price 仅作统计参考,实盘滑点可能让路径偏离。 下面这段是 AddDeal 前半部分的原始实现,注意 m_profit_type 的 SelectedItemIndex 决定分支:
class="type">void CProgram::AddDeal(class="type">int points,class="type">int index) { class=class="str">"cmt">//--- In points if(m_profit_type.GetListViewPointer().SelectedItemIndex()==class="num">0) { if(index==class="num">0) { ArrayResize(data,index+class="num">1); data[index]=class="num">0; class="kw">return; } ArrayResize(data,index+class="num">1); data[index]=data[index-class="num">1]+points; } class=class="str">"cmt">//--- In deposit currency else if(m_profit_type.GetListViewPointer().SelectedItemIndex()==class="num">1) { if(index==class="num">0) { ArrayResize(data,index+class="num">1); data[index]=StringToDouble(m_init_deposit.GetValue()); class="kw">return; } ArrayResize(data,index+class="num">1); class=class="str">"cmt">//--- Get a selected symbol class="type">class="kw">string symbol=m_table_symb.GetValue(class="num">0,m_table_symb.SelectedItem()); class="type">class="kw">string basesymbol=AccountInfoString(ACCOUNT_CURRENCY); class="type">class="kw">string tf=m_timeframe1.GetListViewPointer().SelectedItemText(); class="type">class="kw">double lot=StringToDouble(m_base_lot.GetValue()); if(m_lot_type.GetListViewPointer().SelectedItemIndex()>class="num">0) { lot*=data[index-class="num">1]; lot=GetLotForOpeningPos(symbol,POSITION_TYPE_BUY,lot); } class="type">class="kw">double pip_price=class="num">1; class="type">int shift=class="num">0; class=class="str">"cmt">// --- Direct pair if(StringSubstr(symbol,class="num">3,class="num">3)==basesymbol) { pip_price=NormalizeDouble(SymbolInfoDouble(symbol,SYMBOL_TRADE_CONTRACT_SIZE)*SymbolInfoDouble(symbol,SYMBOL_POINT)*lot,class="num">2); } class=class="str">"cmt">//--- Reverse pair else if(StringSubstr(symbol,class="num">0,class="num">3)==basesymbol) { shift=iBarShift(symbol,StringToTimeframe(tf),m_start_date,false); pip_price=NormalizeDouble(SymbolInfoDouble(symbol,SYMBOL_TRADE_CONTRACT_SIZE)*SymbolInfoDouble(symbol,SYMBOL_POINT)*lot/iOpen(symbol,StringToTimeframe(tf),shift),class="num">2); } else {
◍ 交叉盘与直盘的成本换算差异
上面那段逻辑在算跨品种收益时,先把 symbol 拼成交叉盘(如 EURGBP 由 EURUSD 的后三位加 base 组成),若 SYMBOL_BID 非零才取 iBarShift 定位起点 K 线,再用合约规模×点值×手数÷开盘价得到 pip_price,保留两位小数。 紧接着反手拼回直盘(base 加原 symbol 前三位),同样判 BID 有效性后,pip_price 的算法变成乘开盘价而非除——这两处一除一乘,是交叉盘与直盘净值折算最容易写反的地方。外汇与贵金属杠杆高,算错一点值可能让回测曲线整体偏移。 最后 points 正负分流进 m_all_profit / m_all_losses,data 数组递推累加;下面 CalculateBuyDeals 里用 CopyRates 拉取区间,以 (iHigh-deal_price)/SYMBOL_POINT >= TP 触发止盈,命中后 m_report.profit_pips 直接加 TP 整数点。开 MT5 把这段塞进 EA 回测,改 TP=50 看 profit_pips 是否随样本线性增长,就能验证触发计数没漏。
class=class="str">"cmt">//--- Cross pair StringConcatenate(symbol,StringSubstr(symbol,class="num">3,class="num">3),basesymbol); if(SymbolInfoDouble(symbol,SYMBOL_BID)!=class="num">0) { shift=iBarShift(symbol,StringToTimeframe(tf),m_start_date,false); pip_price=NormalizeDouble(SymbolInfoDouble(symbol,SYMBOL_TRADE_CONTRACT_SIZE)*SymbolInfoDouble(symbol,SYMBOL_POINT)*lot/iOpen(symbol,StringToTimeframe(tf),shift),class="num">2); } class=class="str">"cmt">//--- StringConcatenate(symbol,basesymbol,StringSubstr(symbol,class="num">0,class="num">3)); if(SymbolInfoDouble(symbol,SYMBOL_BID)!=class="num">0) { shift=iBarShift(symbol,StringToTimeframe(tf),m_start_date,false); pip_price=NormalizeDouble(SymbolInfoDouble(symbol,SYMBOL_TRADE_CONTRACT_SIZE)*SymbolInfoDouble(symbol,SYMBOL_POINT)*lot*iOpen(symbol,StringToTimeframe(tf),shift),class="num">2); } class=class="str">"cmt">//--- if(points>class="num">0) m_all_profit+=pip_price*points; else m_all_losses+=pip_price*-points; class=class="str">"cmt">//--- data[index]=data[index-class="num">1]+pip_price*points; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CProgram::CalculateBuyDeals(class="kw">const class="type">class="kw">string symbol,class="type">class="kw">datetime start) { class="type">MqlRates rt[]; class="type">int TP=class="type">int(m_takeprofit1.GetValue()); class="type">int SL=class="type">int(m_stoploss1.GetValue()); class="type">class="kw">string tf=m_timeframe1.GetListViewPointer().SelectedItemText(); class="type">int copied=CopyRates(symbol,StringToTimeframe(tf),m_start_date,m_end_date,rt); class="type">class="kw">double deal_price=iOpen(symbol,StringToTimeframe(tf),copied); for(class="type">int j=class="num">0; j<copied; j++) { class=class="str">"cmt">//--- Take Profit trigger if((iHigh(symbol,StringToTimeframe(tf),copied-j)-deal_price)/SymbolInfoDouble(symbol,SYMBOL_POINT)>=TP) { m_counter++; AddDeal(TP,m_counter); m_report.profit_trades++; m_report.profit+=TP; m_report.profit_pips+=TP; m_report.long_trades++; m_report.profit_long++; m_report.total_trades++; m_start_date=IndexToDate(m_start_date,StringToTimeframe(tf),j);