MQL5 Cookbook: 在MetaTrader 5策略测试器中分析仓位属性·进阶篇
📘

MQL5 Cookbook: 在MetaTrader 5策略测试器中分析仓位属性·进阶篇

第 2/2 篇

◍ 用前一根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() 会给出具体码,建议你在策略测试器里故意切到无数据的周期跑一遍,确认报错路径通畅再上实盘。

MQL5 / C++
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 或信号处理函数里改:

MQL5 / C++
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,能快速确认你的环境小数位设置没踩坑。

MQL5 / C++
  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 就能直接看到多仓/空仓的手续费、库存费、滑点后开盘价是否如预期刷新,外汇与贵金属品种点差跳动快,面板数值延迟可能暴露你的取价频率瓶颈。

MQL5 / C++
  "交易品种 :",
  "幻数 :",
  "注释:",
  "库存费 :",
  "手续费 :",
  "开盘价格 :",
  "当前价格 :",
  "利润 :",
  "交易量 :",
  "止损 :",
  "获利 :",
  "时间 :",
  "编号 :",
  "类型 :"
 };
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 历史数据里回放验证。

MQL5 / C++
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 换成自己的入场判断,就能避开绝大多数「同根柱反复算」的坑。外汇和贵金属杠杆高,这种基础节流不改好,后续加信号也只会在旧柱上叠错误单。 真要做价格行为分析,先让小布这类工具替你跑通新柱过滤,再谈形态识别才靠谱。

MQL5 / C++
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">//+------------------------------------------------------------------+

常见问题

取上一根K线的收盘价与开盘价比较:收高于开为阳线倾向做多,收低于开为阴线倾向做空,仅作信号参考,外汇贵金属高风险需结合其他过滤。
首次触发走开仓分支直接建仓;已有同方向仓位时走加仓分支,按既定步长补仓,注意控制总仓位上限避免爆仓。
可以,小布盯盘的AIGC已内置持仓属性诊断与信号提示,打开对应品种页即可看到实时面板,不用自己跑代码。
静态变量在初始化或重算时可能未重置导致时间比对错位,应在OnInit显式初始化并在历史回放时重算时间锚点。
减少每帧重绘次数,只在new_bar或持仓变动时重跑循环,并把文本对象复用而非反复创建,能明显降低资源占用。