MQL5 Cookbook: 在MetaTrader 5策略测试器中分析仓位属性·进阶篇
◍ 用前一根K线实体方向做信号
在 MT5 的 EA 骨架里,先把最近两根 K 线的开收盘价拉进数组,是后续所有判断的底座。下面这段把 close_price 和 open_price 设为时间序列(倒序:3 2 1 0),只取 2 根,够判一根完结柱就够了。 读取时用 CopyClose / CopyOpen 从 _Symbol 与当前 Period() 抓数据,若实际复制条数少于请求的 amount(此处为 2),就打印错误码与描述,方便你直接在终端排错。外汇与贵金属波动大,这类基础读取失败常发生在品种休市或离线图,属高风险环境下的常见坑。 信号函数只看已关闭的那根(索引 1):收盘大于开盘返回 0(倾向买入),小于返回 1(倾向卖出),相等返回 3 无信号。交易模块预设了 start_lot=0.1、comment 和 signal 变量,为反向加仓或首单留接口。 别把复制失败当偶发 复制条数不足时 GetLastError() 会给出具体码,建议你在策略测试器里故意切到无数据的周期跑一遍,确认报错路径通畅再上实盘。
class="type">void GetBarsData() { class=class="str">"cmt">//--- 读取数组中数据的柱的数量 class="type">int amount=class="num">2; class=class="str">"cmt">//--- 像时间序列一样倒序 ... class="num">3 class="num">2 class="num">1 class="num">0 ArraySetAsSeries(close_price,true); ArraySetAsSeries(open_price,true); class=class="str">"cmt">//--- 读取柱的收盘价 class=class="str">"cmt">// 如果获取数值的数量少于所请求的,打印相关信息 if(CopyClose(_Symbol,Period(),class="num">0,amount,close_price)<amount) { Print("复制数据到收盘价数组失败 (" +_Symbol+", "+TimeframeToString(Period())+")!" "错误 "+IntegerToString(GetLastError())+": "+ErrorDescription(GetLastError())); } class=class="str">"cmt">//--- 读取柱的开盘价 class=class="str">"cmt">// 如果获取数值的数量少于所请求的,打印相关信息 if(CopyOpen(_Symbol,Period(),class="num">0,amount,open_price)<amount) { Print("复制数据到开盘价数组失败 (" +_Symbol+", "+TimeframeToString(Period())+") !" "错误 "+IntegerToString(GetLastError())+": "+ErrorDescription(GetLastError())); } } class="type">int GetTradingSignal() { class=class="str">"cmt">//--- 买入信号 (class="num">0) : if(close_price[class="num">1]>open_price[class="num">1]) class="kw">return(class="num">0); class=class="str">"cmt">//--- 卖出信号 (class="num">1) : if(close_price[class="num">1]<open_price[class="num">1]) class="kw">return(class="num">1); class=class="str">"cmt">//--- 没有信号 (class="num">3): class="kw">return(class="num">3); } class="type">void TradingBlock() { class="type">int signal=-class="num">1; class=class="str">"cmt">// 取得信号的变量 class="type">class="kw">string comment="hello :)"; class=class="str">"cmt">// 仓位注释 class="type">class="kw">double start_lot=class="num">0.1; class=class="str">"cmt">// 仓位初始交易量 class="type">class="kw">double lot=class="num">0.0; class=class="str">"cmt">// 反向持仓情况下计算仓位的交易量
「信号触发后的开仓与加仓分支」
这段逻辑处理的是交易信号落地环节:当自定义函数 GetTradingSignal() 返回 0 视为买入信号、返回 1 视为卖出信号,先通过 PositionSelect(_Symbol) 判断当前图表品种是否已有持仓,再决定是首开还是加仓。 买入信号下,先用 NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK),_Digits) 取精确买价。若无持仓直接以 start_lot 开多;若已持有卖仓,则读取原仓位量 pos_volume,用 NormalizeDouble(pos_volume+start_lot,2) 把新开量四舍五入到 2 位小数后加仓多单。 卖出信号对称处理:取 SYMBOL_BID 卖价,无持仓以 start_lot 开空,有持仓分支原文在此截断,但结构同样是按已有仓位类型决定加仓方向。外汇与贵金属杠杆高,这类自动加仓逻辑可能放大回撤,实盘前务必在 MT5 策略测试器用历史数据验证信号与仓位控制。 下面把核心片段逐行拆开,方便你直接拷进 EA 的 OnTick 或信号处理函数里改:
class="type">class="kw">double ask=class="num">0.0; class=class="str">"cmt">// 买价 class="type">class="kw">double bid=class="num">0.0; class=class="str">"cmt">// 卖价 class=class="str">"cmt">//--- 取得信号 signal=GetTradingSignal(); class=class="str">"cmt">//--- 查找已有持仓 pos_open=PositionSelect(_Symbol); class=class="str">"cmt">//--- 如果是买入信号 if(signal==class="num">0) { class=class="str">"cmt">//--- 取得买价 ask=NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK),_Digits); class=class="str">"cmt">//--- 如果没有持仓 if(!pos_open) { class=class="str">"cmt">//--- 开启一个仓位。如果开启仓位失败,打印相关信息 if(!trade.PositionOpen(_Symbol,ORDER_TYPE_BUY,start_lot,ask,class="num">0,class="num">0,comment)) { Print("开启买入仓位失败: ",GetLastError()," - ",ErrorDescription(GetLastError())); } } class=class="str">"cmt">//--- 如果已有持仓 else { class=class="str">"cmt">//--- 读取仓位类型 pos_type=(class="type">ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); class=class="str">"cmt">//--- 如果是卖出仓位 if(pos_type==POSITION_TYPE_SELL) { class=class="str">"cmt">//--- 取得仓位交易量 pos_volume=PositionGetDouble(POSITION_VOLUME); class=class="str">"cmt">//--- 调整交易量 lot=NormalizeDouble(pos_volume+start_lot,class="num">2); class=class="str">"cmt">//--- 开启一个仓位。如果开启仓位失败,打印相关信息 if(!trade.PositionOpen(_Symbol,ORDER_TYPE_BUY,lot,ask,class="num">0,class="num">0,comment)) { Print("Error opening a SELL position: ",GetLastError()," - ",ErrorDescription(GetLastError())); } } } class=class="str">"cmt">//--- class="kw">return; } class=class="str">"cmt">//--- 如果是卖出信号 if(signal==class="num">1) { class=class="str">"cmt">//-- 取得卖价 bid=NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID),_Digits); class=class="str">"cmt">//--- 如果没有持仓 if(!pos_open) { class=class="str">"cmt">//--- 开启一个仓位。如果开启仓位失败,打印相关信息 if(!trade.PositionOpen(_Symbol,ORDER_TYPE_SELL,start_lot,bid,class="num">0,class="num">0,comment)) { Print("Error opening a SELL position: ",GetLastError()," - ",ErrorDescription(GetLastError())); } } class=class="str">"cmt">//--- 如果已有持仓 else {
逆加仓的开仓与生命周期钩子
这段逻辑展示了在已有 BUY 仓位时如何逆向下单:先读持仓类型,命中后取原仓位交易量,把 start_lot 加上去再规范到 2 位小数,然后以市价 SELL 开新仓。若 trade.PositionOpen 返回失败,会把错误码和描述打到日志,方便你直接在 MT5 终端找原因。 EA 的骨架靠几个系统函数串起来:OnInit 里调 CheckNewBar 和 GetPositionProperties 做初始化;OnDeinit 在图表移除时清掉信息面板对象;OnTick 只在「新柱」成立时跑取数与交易块,否则直接 return,避免每跳都刷逻辑。 OnTrade 钩子专门在成交事件后刷新面板数值,和 OnTick 末尾的 GetPositionProperties 形成双保险。外汇与贵金属杠杆高,逆加仓会放大回撤,实盘前务必在策略测试器用历史数据验证 lot 计算与报错路径。 让小布替你跑这套 把这段 SELL 开仓块单独抽进脚本,用 Print 输出 lot 值,观察当 pos_volume=0.10、start_lot=0.01 时 NormalizeDouble 结果是否为 0.11,能快速确认你的环境小数位设置没踩坑。
class=class="str">"cmt">//--- 读取仓位类型 pos_type=(class="type">ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); class=class="str">"cmt">//--- 如果是买入仓位 if(pos_type==POSITION_TYPE_BUY) { class=class="str">"cmt">//--- 取得仓位交易量 pos_volume=PositionGetDouble(POSITION_VOLUME); class=class="str">"cmt">//--- 调整交易量 lot=NormalizeDouble(pos_volume+start_lot,class="num">2); class=class="str">"cmt">//--- 开启一个仓位。如果开启仓位失败,打印相关信息 if(!trade.PositionOpen(_Symbol,ORDER_TYPE_SELL,lot,bid,class="num">0,class="num">0,comment)) { Print("Error opening a SELL position: ",GetLastError()," - ",ErrorDescription(GetLastError())); } } } class=class="str">"cmt">//--- class="kw">return; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| EA初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- 初始化新柱 CheckNewBar(); class=class="str">"cmt">//--- 取得仓位信息并在面板上更新它们的值 GetPositionProperties(); class=class="str">"cmt">//--- class="kw">return(class="num">0); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| EA去初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { class=class="str">"cmt">//--- 在日志中打印去初始化原因 Print(GetDeinitReasonText(reason)); class=class="str">"cmt">//--- 当从图表上移除时 if(reason==REASON_REMOVE) class=class="str">"cmt">//--- 从图表上删除所有有关信息面板的对象 DeleteInfoPanel(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| EA订单函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { class=class="str">"cmt">//--- 如果柱不是新的,退出 if(!CheckNewBar()) class="kw">return; class=class="str">"cmt">//--- 如果有新柱 else { GetBarsData(); class=class="str">"cmt">// 取得柱数据 TradingBlock(); class=class="str">"cmt">// 检查条件并交易 } class=class="str">"cmt">//--- 读取属性并更新面板上的数值 GetPositionProperties(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 交易事件 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTrade() { class=class="str">"cmt">//--- 取得仓位信息并在面板上更新它们的值 GetPositionProperties(); } class=class="str">"cmt">// 仓位属性名称数组 class="type">class="kw">string pos_prop_texts[INFOPANEL_SIZE]=
◍ 把持仓属性铺到面板上的循环
这段逻辑干的事很直白:先定义一组中文标签,从「交易品种」到「类型」共 16 项,对应持仓的关键属性名。 随后用一个 for 循环把名称和数值分别画到两列。INFOPANEL_SIZE 控制循环次数,i 从 0 跑到尺寸上限,每次调用 CreateLabel 两次——一次写属性名(第一列 x_first_column),一次写 GetPropertyValue(i) 取到的实时值(第二列 x_second_column),纵向坐标由 y_prop_array[i] 决定,行距在数组里预设。 值得注意的是可视化测试分支:当 MQL5InfoInteger(MQL5_VISUAL_MODE) 为真,也就是在策略测试器的可视模式下跑,背景 y_bg 被压到 2、属性起始 y_property 设为 16,明显是为小窗体让出顶部空间。实盘或正常图表里这两个值不走这套,面板布局会按另一套坐标走。 开 MT5 把这段塞进 EA 的 OnTick 或独立指标,改 INFOPANEL_SIZE 和 y_prop_array 就能直接看到多仓/空仓的手续费、库存费、滑点后开盘价是否如预期刷新,外汇与贵金属品种点差跳动快,面板数值延迟可能暴露你的取价频率瓶颈。
"交易品种 :", "幻数 :", "注释:", "库存费 :", "手续费 :", "开盘价格 :", "当前价格 :", "利润 :", "交易量 :", "止损 :", "获利 :", "时间 :", "编号 :", "类型 :" }; class=class="str">"cmt">//--- 仓位属性名称和数值的列表 for(class="type">int i=class="num">0; i<INFOPANEL_SIZE; i++) { class=class="str">"cmt">//--- 属性名称 CreateLabel(class="num">0,class="num">0,pos_prop_names[i],pos_prop_texts[i],anchor,corner,font_name,font_size,font_color,x_first_column,y_prop_array[i],class="num">2); class=class="str">"cmt">//--- 属性值 CreateLabel(class="num">0,class="num">0,pos_prop_values[i],GetPropertyValue(i),anchor,corner,font_name,font_size,font_color,x_second_column,y_prop_array[i],class="num">2); } class=class="str">"cmt">//--- 在可视模式下测试 if(MQL5InfoInteger(MQL5_VISUAL_MODE)) { y_bg=class="num">2; y_property=class="num">16; }
「静态变量在 new_bar 检测里的坑」
上面这段 CheckNewBar 曾在社区里引发过一连串追问:调用时第一行把 static datetime new_bar=NULL 重新赋值,逻辑还能不能成立。结论很直白——静态变量跨调用保留值,但每次进函数都会执行那行初始化赋值,所以它不是「只初始化一次」,而是每次都被写上 NULL 再往下判断。 Vladimir Karputov 在 2018-08-09 的回帖里点了一句:变量必须始终初始化,这是不成文规定,不遵守迟早出难查的错。换言之,写 NULL 本身不算错,真正要清楚的是「赋值」和「保留上次值」发生的先后顺序。 顺带一提,原文附带可下载的 positionpropertiestesteren.mq5(约 90.22 KB),里面就用了这套 new_bar 判定来在策略测试器里抓新 K 线。外汇与贵金属行情跳空频繁,这类基于开盘时间的判断在极端点差下可能漏判,实盘前务必在 MT5 历史数据里回放验证。
class="type">bool CheckNewBar() { class=class="str">"cmt">//--- 当前条形图打开时间变量 class="kw">static class="type">class="kw">datetime new_bar=NULL; class=class="str">"cmt">//--- 数组用于获取当前条形图的开盘时间 class="kw">static class="type">class="kw">datetime time_last_bar[class="num">1]={class="num">0}; class=class="str">"cmt">//--- 获取当前条形图的开盘时间 class=class="str">"cmt">// 如果在接收过程中出现错误,我们将报告该错误 if(CopyTime(_Symbol,Period(),class="num">0,class="num">1,time_last_bar)==-class="num">1) { Print(__FUNCTION__,":复制条形图打开时间出错:"+IntegerToString(GetLastError())+""); } class=class="str">"cmt">//--- 如果这是第一次函数调用 if(new_bar==NULL) { class=class="str">"cmt">// 设置时间 new_bar=time_last_bar[class="num">0]; Print(__FUNCTION__,":初始化 ["+_Symbol+"][TF: "+TimeframeToString(Period())+"][" +TimeToString(time_last_bar[class="num">0],TIME_DATE|TIME_MINUTES|TIME_SECONDS)+"]"); class="kw">return(false); class=class="str">"cmt">// 返回 false 并退出 } class=class="str">"cmt">//--- 如果时间不同 if(new_bar!=time_last_bar[class="num">0]) { new_bar=time_last_bar[class="num">0]; class=class="str">"cmt">// 设置时间并退出 class="kw">return(true); class=class="str">"cmt">// 记住时间并返回 true } class=class="str">"cmt">//--- 我们到了这一步--所以条形图不是新的,让我们返回 false class="kw">return(false); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//|测试 EA。mq5 class=class="str">"cmt">//|版权 © class="num">2018, 弗拉基米尔-卡尔普托夫 || class=class="str">"cmt">//|http://wmua.ru/slesar/ | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "版权 © class="num">2018 年,弗拉基米尔-卡尔普托夫"
画得少,看得清
把 EA 逻辑压缩到只认新 K 线,是剥掉噪音最直接的一刀。上面这段 MT5 代码用 static datetime 记住上一根柱的诞生时间,OnTick 里只要时间没变就 return,变了才打印 New bar,日志里 2018.01.08 00:00:00 到 00:02:00 的回显显示每根 1 分钟柱只触发一次提示。 你开 MT5 新建脚本粘进去,把 Print 换成自己的入场判断,就能避开绝大多数「同根柱反复算」的坑。外汇和贵金属杠杆高,这种基础节流不改好,后续加信号也只会在旧柱上叠错误单。 真要做价格行为分析,先让小布这类工具替你跑通新柱过滤,再谈形态识别才靠谱。
class="macro">#class="kw">property link "http:class=class="str">"cmt">//wmua.ru/slesar/" class="macro">#class="kw">property version "class="num">1.000" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 专家初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 专家去初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { class=class="str">"cmt">//--- } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 专家勾选功能 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { class=class="str">"cmt">//--- 我们只在新栏诞生时工作 class="kw">static class="type">class="kw">datetime PrevBars=class="num">0; Print("PrevBars ",PrevBars); class="type">class="kw">datetime time_0=iTime(Symbol(),Period(),class="num">0); if(time_0==PrevBars) class="kw">return; PrevBars=time_0; Print("New bar. PrevBars ",PrevBars); } class=class="str">"cmt">//+------------------------------------------------------------------+