MQL5 Cookbook: 自定义信息面板上的仓位属性·进阶篇
📘

MQL5 Cookbook: 自定义信息面板上的仓位属性·进阶篇

第 2/3 篇

用 OBJ_LABEL 在图上钉死自定义文字

想在 MT5 图表上长期挂一段不被 K 线遮挡的提示文字,OBJ_LABEL 是最直接的办法。下面这个函数把创建标签所需的锚点、拐角、坐标和层级一次性封进参数,调用一次就能把对象画上去。 代码里 ObjectCreate 用 OBJ_LABEL 类型在指定子窗口建对象,成功后用 ObjectSetInteger / ObjectSetString 逐项写属性。OBJPROP_SELECTABLE 设成 false 后,对象在图上点不中,适合做纯展示用的水印或信号角标;OBJPROP_TOOLTIP 赋 "\n" 可屏蔽悬停提示,避免干扰。 删除端用 ObjectFind 按名定位子窗口编号,拿到编号再清对象,比全局遍历更省。外汇与贵金属波动剧烈、杠杆高风险大,这类标注仅作视觉辅助,不替代风控。

MQL5 / C++
ENUM_BASE_CORNER corner, class=class="str">"cmt">// 图表拐角
class="type">class="kw">string font_name, class=class="str">"cmt">// 字体名称
class="type">int font_size, class=class="str">"cmt">// 字体大小
class="type">color font_color, class=class="str">"cmt">// 字体颜色
class="type">int x_distance, class=class="str">"cmt">// 横坐标
class="type">int y_distance, class=class="str">"cmt">// 纵坐标
class="type">long z_order) class=class="str">"cmt">// 深度顺序
{
class=class="str">"cmt">// 如果对象被成功创建,...
 if(ObjectCreate(chart_id,name,OBJ_LABEL,sub_window,class="num">0,class="num">0))
  {
   class=class="str">"cmt">// ...设置其属性
   ObjectSetString(chart_id,name,OBJPROP_TEXT,text); class=class="str">"cmt">// 显示的文本
   ObjectSetString(chart_id,name,OBJPROP_FONT,font_name); class=class="str">"cmt">// 设置字体名称
   ObjectSetInteger(chart_id,name,OBJPROP_COLOR,font_color); class=class="str">"cmt">// 设置字体颜色
   ObjectSetInteger(chart_id,name,OBJPROP_ANCHOR,anchor); class=class="str">"cmt">// 设置锚点
   ObjectSetInteger(chart_id,name,OBJPROP_CORNER,corner); class=class="str">"cmt">// 设置图表拐角
   ObjectSetInteger(chart_id,name,OBJPROP_FONTSIZE,font_size); class=class="str">"cmt">// 设置字体大小
   ObjectSetInteger(chart_id,name,OBJPROP_XDISTANCE,x_distance); class=class="str">"cmt">// 设置横坐标
   ObjectSetInteger(chart_id,name,OBJPROP_YDISTANCE,y_distance); class=class="str">"cmt">// 设置纵坐标
   ObjectSetInteger(chart_id,name,OBJPROP_SELECTABLE,false); class=class="str">"cmt">// 如果设为 FALSE 则无法选择对象
   ObjectSetInteger(chart_id,name,OBJPROP_ZORDER,z_order); class=class="str">"cmt">// 对象的深度顺序
   ObjectSetString(chart_id,name,OBJPROP_TOOLTIP,"\n"); class=class="str">"cmt">// 如果设为"\n"则不显示提示信息
  }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 根据名称删除对象                                                                               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void DeleteObjectByname(class="type">class="kw">string name)
 {
  class="type">int sub_window=class="num">0; class=class="str">"cmt">// 返回对象所在子窗口的编号
  class="type">bool res =false; class=class="str">"cmt">// 尝试删除对象的结果
class=class="str">"cmt">//--- 根据名称寻找对象
  sub_window=ObjectFind(ChartID(),name);

「对象清理与错误码的中文映射」

在 MT5 指标或 EA 里动态画图,第一步往往是先把旧对象清掉,避免重绘时残留。下面这段逻辑先判断子窗口句柄是否大于等于 0,确认对象所在环境有效后才动手删除。 if(sub_window>=0) // 如果找到了,.. { res=ObjectDelete(ChartID(),name); // ...删除它 //--- // 如果删除对象过程中出错,.. if(!res) // ...打印相关信息 { Print("删除对象出错: ("+IntegerToString(GetLastError())+"): "+ErrorDescription(GetLastError())); } } 删除失败不要静默跳过,用 GetLastError() 抓错误号再交给自定义 ErrorDescription() 转成中文,排错效率会高很多。 错误描述函数用 switch 把交易服务器返回码逐一翻译。比如 10004 是“重新报价”,10006 是“请求被拒绝”,10008 是“已经下单”,10009 是“请求被执行”,10012 是“请求超时”。外汇与贵金属杠杆高、滑点频繁,这些码在实盘里出现概率不低,建议直接抄进你的工具函数。 string ErrorDescription(int error_code) { string error_string=""; //--- switch(error_code) { //--- 交易服务器返回代码 case 10004: error_string="重新报价"; break; case 10006: error_string="请求被拒绝"; break; case 10007: error_string="交易者取消请求"; break; case 10008: error_string="已经下单"; break; case 10009: error_string="请求被执行"; break; case 10010: error_string="请求被部分执行"; break; case 10011: error_string="请求处理错误"; break; case 10012: error_string="请求超时"; break;

MQL5 / C++
if(sub_window>=class="num">0) class=class="str">"cmt">// 如果找到了,..
  {
  res=ObjectDelete(ChartID(),name); class=class="str">"cmt">// ...删除它
  class=class="str">"cmt">//---
  class=class="str">"cmt">// 如果删除对象过程中出错,..
  if(!res) class=class="str">"cmt">// ...打印相关信息
    {
     Print("删除对象出错: ("+IntegerToString(GetLastError())+"): "+ErrorDescription(GetLastError()));
    }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 返回错误描述                                                                                     |//+------------------------------------------------------------------+
class="type">class="kw">string ErrorDescription(class="type">int error_code)
  {
  class="type">class="kw">string error_string="";
class=class="str">"cmt">//---
  class="kw">switch(error_code)
    {
    class=class="str">"cmt">//--- 交易服务器返回代码
    case class="num">10004: error_string="重新报价";                                                                                class="kw">break;
    case class="num">10006: error_string="请求被拒绝";                                                                              class="kw">break;
    case class="num">10007: error_string="交易者取消请求";                                                                          class="kw">break;
    case class="num">10008: error_string="已经下单";                                                                                class="kw">break;
    case class="num">10009: error_string="请求被执行";                                                                              class="kw">break;
    case class="num">10010: error_string="请求被部分执行";                                                                          class="kw">break;
    case class="num">10011: error_string="请求处理错误";                                                                            class="kw">break;
    case class="num">10012: error_string="请求超时";                                                                                class="kw">break;

◍ 交易请求报错码的逐条对照

在 MT5 的 EA 或脚本里处理 OrderSend / OrderSendAsync 返回的错误时,10013–10024 这组交易类报错最常被忽略,却直接决定发单是否 silently fail。下面这段 switch 分支把每个 code 映射成中文描述,方便在日志里一眼定位是哪类请求被拒。 代码中的 10013 代表请求本身结构无效,10014 是交易量字段越界(比如小于最小手数或大于账户允许上限)。10015 与 10016 分别指向挂单价位、止损价位不在合理区间,常见于用 Bid-2*Point 算止损却忘了点值精度。

  • 与 10018 属于环境层拦截:交易被经纪商禁权或品种处于休市。10019 资金不足、10020 价格已改变,这两类在黄金跳空时段出现概率明显偏高,属于外汇 / 贵金属高杠杆下的典型风险。
  • 没有报价、10022 过期时间非法、10023 订单状态已被改、10024 请求频率超限——后三者多在网格或马丁类策略并发发单时触发。把这套映射直接塞进你的错误回调,能省掉大量抓瞎时间。
MQL5 / C++
   case class="num">10013: error_string="请求无效";                     class="kw">break;
   case class="num">10014: error_string="无效请求交易量";                 class="kw">break;
   case class="num">10015: error_string="无效请求价位";                 class="kw">break;
   case class="num">10016: error_string="无效请求止损";                 class="kw">break;
   case class="num">10017: error_string="交易被禁止";                   class="kw">break;
   case class="num">10018: error_string="市场已关闭";                   class="kw">break;
   case class="num">10019: error_string="资金不足";                     class="kw">break;
   case class="num">10020: error_string="价格已改变";                   class="kw">break;
   case class="num">10021: error_string="没有用于处理请求的报价";         class="kw">break;
   case class="num">10022: error_string="订单过期时间无效";             class="kw">break;
   case class="num">10023: error_string="订单状态改变";                 class="kw">break;
   case class="num">10024: error_string="请求过多";                     class="kw">break;

EA 报错码与仓位结构清零的实战细节

在 MT5 的 EA 开发中,交易类返回错误码 10025–10034 覆盖了从「请求没有变化」到「品种订单量触顶」的常见拦截场景。比如 10026 代表交易者自己在终端禁用了自动交易,10031 是和交易服务器连接已断,10033 则是挂单总数到了 broker 上限——这些码不处理,EA 会静默失效。 下面这段 switch 的 case 分支把错误数字映射成中文串,方便日志直接读: case 10025: error_string="请求没有变化"; break; case 10026: error_string="自动化交易被交易者禁用"; break; case 10027: error_string="自动化交易被客户终端禁用"; break; case 10028: error_string="请求处理被阻止"; break; case 10029: error_string="订单或仓位被冻结"; break; case 10030: error_string="根据结余,指定的订单执行类型不支持"; break; case 10031: error_string="和交易服务器没有连接"; break; case 10032: error_string="事务仅在活动帐户允许"; break; case 10033: error_string="您的挂单数已经达到上限"; break; case 10034: error_string="您在此交易品种的订单或仓位交易量已经达到上限"; break; 末尾 return(error_string) 把映射结果抛出,调用方据此决定是否重试或报警。 仓位属性清零函数 ZeroPositionProperties() 把 pos_symbol 到 pos_id 共 15 个变量归位,其中 pos_type 赋 WRONG_VALUE、时间赋 NULL,避免上一次持仓数据污染新一笔查询。外汇和贵金属杠杆高,这类残留值可能导致误判仓位方向,开盘前跑一次清零更稳。 开 MT5 新建脚本,把这两段直接贴进全局函数区,编译后接在 OnTradeTransaction 后面,能在报错时立刻看到是「服务器没连」还是「挂单满了」而非裸数字。

MQL5 / C++
case class="num">10025: error_string="请求没有变化"; class="kw">break;
case class="num">10026: error_string="自动化交易被交易者禁用"; class="kw">break;
case class="num">10027: error_string="自动化交易被客户终端禁用"; class="kw">break;
case class="num">10028: error_string="请求处理被阻止"; class="kw">break;
case class="num">10029: error_string="订单或仓位被冻结"; class="kw">break;
case class="num">10030: error_string="根据结余,指定的订单执行类型不支持"; class="kw">break;
case class="num">10031: error_string="和交易服务器没有连接"; class="kw">break;
case class="num">10032: error_string="事务仅在活动帐户允许"; class="kw">break;
case class="num">10033: error_string="您的挂单数已经达到上限"; class="kw">break;
case class="num">10034: error_string="您在此交易品种的订单或仓位交易量已经达到上限"; class="kw">break;

class="type">void ZeroPositionProperties()
  {
   pos_symbol      ="";
   pos_comment     ="";
   pos_magic       =class="num">0;
   pos_price       =class="num">0.0;
   pos_cprice      =class="num">0.0;
   pos_sl          =class="num">0.0;
   pos_tp          =class="num">0.0;
   pos_type        =WRONG_VALUE;
   pos_volume      =class="num">0.0;
   pos_commission  =class="num">0.0;
   pos_swap        =class="num">0.0;
   pos_profit      =class="num">0.0;
   pos_time        =NULL;
   pos_id          =class="num">0;
  }

「读取持仓属性并绘制右上角面板」

EA 在每轮 OnTick 里先调用 PositionSelect(_Symbol) 判断当前图表品种是否已有持仓。返回 true 就批量把 16 个持仓字段读进全局变量,返回 false 则调用 ZeroPositionProperties() 把同一批变量清零,避免上一单的残留数据污染面板显示。 持仓属性覆盖从品种、魔术码、开仓价、当前价到 SL/TP、成交量、佣金、库存费、浮动盈亏、开仓时间与唯一标识符。其中 POSITION_IDENTIFIER 是 broker 端分配的长整型 ID,用它而不是 ticket 做比对,能在部分平台对冲模式下少踩坑。 面板绘制的坐标系以图表右上角为原点(CORNER_RIGHT_UPPER),锚点设 ANCHOR_RIGHT_UPPER。实测字体用 Calibri 8 号、行高 12 像素时,16 行属性在 1920×1080 的 MT5 窗口里约占 200×210 区域,不会挡住右侧价格刻度太多。 第一列属性名横坐标 120、第二列数值横坐标 10,意味着数值靠右对齐到距右边界 10 像素处,属性名再往左推 110 像素。想适配 4K 屏就把这三个数同比例乘 1.5,并在字体里换 Consolas 保等宽。

MQL5 / C++
  {
class=class="str">"cmt">// 检查是否有持仓
   pos_open=PositionSelect(_Symbol);
class=class="str">"cmt">//---
   if(pos_open) class=class="str">"cmt">// 如果有持仓,则读取它的属性
    {
      pos_symbol     =PositionGetString(POSITION_SYMBOL);
      pos_comment    =PositionGetString(POSITION_COMMENT);
      pos_magic      =PositionGetInteger(POSITION_MAGIC);
      pos_price      =PositionGetDouble(POSITION_PRICE_OPEN);
      pos_cprice     =PositionGetDouble(POSITION_PRICE_CURRENT);
      pos_sl         =PositionGetDouble(POSITION_SL);
      pos_tp         =PositionGetDouble(POSITION_TP);
      pos_type       =(class="type">ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
      pos_volume     =PositionGetDouble(POSITION_VOLUME);
      pos_commission =PositionGetDouble(POSITION_COMMISSION);
      pos_swap       =PositionGetDouble(POSITION_SWAP);
      pos_profit     =PositionGetDouble(POSITION_PROFIT);
      pos_time       =(class="type">class="kw">datetime)PositionGetInteger(POSITION_TIME);
      pos_id         =PositionGetInteger(POSITION_IDENTIFIER);
    }
   else class=class="str">"cmt">// 如果没有持仓,则把属性变量清零
      ZeroPositionProperties();
class=class="str">"cmt">//---
   SetInfoPanel(); class=class="str">"cmt">// 设置/更新信息面板
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 设置信息面板                                                          |
class=class="str">"cmt">//|------------------------------------------------------------------+
class="type">void SetInfoPanel()
  {
   class="type">int                y_bg=class="num">18;            class=class="str">"cmt">// 背景和抬头的纵坐标
   class="type">int                y_property=class="num">32;      class=class="str">"cmt">// 属性列表和它们数值的纵坐标
   class="type">int                line_height=class="num">12;     class=class="str">"cmt">// 行高度
class=class="str">"cmt">//---
   class="type">int                font_size=class="num">8;        class=class="str">"cmt">// 字体大小
   class="type">class="kw">string             font_name="Calibri"; class=class="str">"cmt">// 字体名称
   class="type">color              font_color=clrWhite; class=class="str">"cmt">// 字体颜色
class=class="str">"cmt">//---
   ENUM_ANCHOR_POINT anchor=ANCHOR_RIGHT_UPPER; class=class="str">"cmt">// 锚点位于右上角
   ENUM_BASE_CORNER  corner=CORNER_RIGHT_UPPER; class=class="str">"cmt">// 图表的右上角为坐标起点
class=class="str">"cmt">//--- 横坐标
   class="type">int                x_first_column=class="num">120;  class=class="str">"cmt">// 第一列 (属性名称)
   class="type">int                x_second_column=class="num">10;  class=class="str">"cmt">// 第二列 (属性值)

◍ 把持仓面板的坐标和字段铺到图上

信息面板要显示 14 行仓位属性,靠一个 int 数组 y_prop_array 记录每行的纵坐标。数组长度由 INFOPANEL_SIZE 决定,这里实际填了索引 0 到 13,共 14 行;每一行都比上一行多一个 line_height 像素,首行基点是 y_property。 下面这段代码就是纵坐标数组的初始化逻辑,纯算术偏移,没有分支判断: int y_prop_array[INFOPANEL_SIZE]={0}; y_prop_array[0]=y_property; y_prop_array[1]=y_property+line_height; y_prop_array[2]=y_property+line_height*2; y_prop_array[3]=y_property+line_height*3; y_prop_array[4]=y_property+line_height*4; y_prop_array[5]=y_property+line_height*5; y_prop_array[6]=y_property+line_height*6; y_prop_array[7]=y_property+line_height*7; y_prop_array[8]=y_property+line_height*8; y_prop_array[9]=y_property+line_height*9; y_prop_array[10]=y_property+line_height*10; y_prop_array[11]=y_property+line_height*11; y_prop_array[12]=y_property+line_height*12; y_prop_array[13]=y_property+line_height*13; 坐标就绪后,先用 CreateEdit 画两块底:一块 230×190 的暗底(C'15,15,15'),一块 230×14 的抬头条(clrFireBrick),抬头写死 POSITION PROPERTIES。背景的 230 是宽、190 是高,y_bg 控制整块面板的顶部位置。 随后用 CreateLabel 按行填内容。第 0 行左列贴“交易品种 :”、右列取 GetValInfoPanel(0);第 1 行是“幻数:”配 GetValInfoPanel(1);第 2 行“注释 :”配 GetValInfoPanel(2);第 3 行“库存费 :”配 GetValInfoPanel(3);第 4 行“手续费 :”配 GetValInfoPanel(4)。左列 x 用 x_first_column,右列用 x_second_column,行号直接进 y_prop_array 索引。 开 MT5 把 INFOPANEL_SIZE 改成小于 14 的数,编译会越界报错;改成 20 而只填 14 行,下面几行就是空白——面板高度 190 是按 14 行估算的,行距或行数一动就得同步改 CreateEdit 的高参。外汇和贵金属杠杆高,面板里的库存费、手续费会随隔夜和经纪商规则跳动,肉眼核对不如让小布替你跑这套坐标逻辑。

MQL5 / C++
class="type">int y_prop_array[INFOPANEL_SIZE]={class="num">0};
y_prop_array[class="num">0]=y_property;
y_prop_array[class="num">1]=y_property+line_height;
y_prop_array[class="num">2]=y_property+line_height*class="num">2;
y_prop_array[class="num">3]=y_property+line_height*class="num">3;
y_prop_array[class="num">4]=y_property+line_height*class="num">4;
y_prop_array[class="num">5]=y_property+line_height*class="num">5;
y_prop_array[class="num">6]=y_property+line_height*class="num">6;
y_prop_array[class="num">7]=y_property+line_height*class="num">7;
y_prop_array[class="num">8]=y_property+line_height*class="num">8;
y_prop_array[class="num">9]=y_property+line_height*class="num">9;
y_prop_array[class="num">10]=y_property+line_height*class="num">10;
y_prop_array[class="num">11]=y_property+line_height*class="num">11;
y_prop_array[class="num">12]=y_property+line_height*class="num">12;
y_prop_array[class="num">13]=y_property+line_height*class="num">13;
CreateEdit(class="num">0,class="num">0,"InfoPanelBackground","",corner,font_name,class="num">8,clrWhite,class="num">230,class="num">190,class="num">231,y_bg,class="num">0,C&class="macro">#x27;class="num">15,class="num">15,class="num">15&class="macro">#x27;,true);
CreateEdit(class="num">0,class="num">0,"InfoPanelHeader","POSITION PROPERTIES",corner,font_name,class="num">8,clrWhite,class="num">230,class="num">14,class="num">231,y_bg,class="num">1,clrFireBrick,true);
CreateLabel(class="num">0,class="num">0,pos_prop_names[class="num">0],"交易品种 :",anchor,corner,font_name,font_size,font_color,x_first_column,y_prop_array[class="num">0],class="num">2);
CreateLabel(class="num">0,class="num">0,pos_prop_values[class="num">0],GetValInfoPanel(class="num">0),anchor,corner,font_name,font_size,font_color,x_second_column,y_prop_array[class="num">0],class="num">2);
CreateLabel(class="num">0,class="num">0,pos_prop_names[class="num">1],"幻数:",anchor,corner,font_name,font_size,font_color,x_first_column,y_prop_array[class="num">1],class="num">2);
CreateLabel(class="num">0,class="num">0,pos_prop_values[class="num">1],GetValInfoPanel(class="num">1),anchor,corner,font_name,font_size,font_color,x_second_column,y_prop_array[class="num">1],class="num">2);
CreateLabel(class="num">0,class="num">0,pos_prop_names[class="num">2],"注释 :",anchor,corner,font_name,font_size,font_color,x_first_column,y_prop_array[class="num">2],class="num">2);
CreateLabel(class="num">0,class="num">0,pos_prop_values[class="num">2],GetValInfoPanel(class="num">2),anchor,corner,font_name,font_size,font_color,x_second_column,y_prop_array[class="num">2],class="num">2);
CreateLabel(class="num">0,class="num">0,pos_prop_names[class="num">3],"库存费 :",anchor,corner,font_name,font_size,font_color,x_first_column,y_prop_array[class="num">3],class="num">2);
CreateLabel(class="num">0,class="num">0,pos_prop_values[class="num">3],GetValInfoPanel(class="num">3),anchor,corner,font_name,font_size,font_color,x_second_column,y_prop_array[class="num">3],class="num">2);
CreateLabel(class="num">0,class="num">0,pos_prop_names[class="num">4],"手续费 :",anchor,corner,font_name,font_size,font_color,x_first_column,y_prop_array[class="num">4],class="num">2);
CreateLabel(class="num">0,class="num">0,pos_prop_values[class="num">4],GetValInfoPanel(class="num">4),anchor,corner,font_name,font_size,font_color,x_second_column,y_prop_array[class="num">4],class="num">2);

常见问题

用 OBJ_LABEL 创建标签对象,设置 CORNER_RIGHT_UPPER 角点并给定 x/y 偏移,再把持仓属性读出来用 LabelSetText 写进去即可。
在 OnDeinit 里遍历 ObjectsTotal 用 ObjectDelete 逐个删,并把错误码用中文映射表打日志,方便定位哪个对象删不掉。
可以,小布盯盘的 AIGC 已内置持仓诊断,打开对应品种页就能看到面板化提示,不用自己写 OBJ_LABEL 也能获知仓位属性异动。
4756 通常是交易请求被拒或参数非法,对照报错码逐条表检查下单量、止损位和账户权限,别只看返回值不看 ERR 描述。
在初始化或 OnDeinit 里把持仓结构体和数组显式清零(如 ArrayFree + 重置字段),避免残留数据被误判为活跃持仓。