精通日志记录(第七部分):如何在图表上显示日志·进阶篇
📟

精通日志记录(第七部分):如何在图表上显示日志·进阶篇

(2/3)· 还在专家标签页里翻报错?本篇教你把 Logify 日志直接渲染到图表上,实时盯盘不漏关键信息

进阶 第 2/3 篇

接上篇,我们继续深挖 Logify 的扩展能力。很多 EA 开发者只在调试时临时用一下 Comment(),跑完就删,结果关键报错在专家页一闪而过,复盘时根本抓不到现场。把日志固定到图表上,才是贴近交易决策位置的排错方式。

给图表日志框定好尺寸与边框

在写主逻辑前,先要把图表上那块可视化日志的「长相」定下来。这里不堆参数,只留四个最实用的:size 控制可见行数,frame_style 决定边框是 none、single 还是 double,direction 管消息往上叠还是往下叠,title 就写 EA 名字方便辨认。 配置被收进一个结构体 MqlLogifyHandleCommentConfig,类里面用私有属性存着,外部只通过 SetConfig() 校验写入、GetConfig() 读回。默认构造直接给了一套能跑的值:20 行、单线框、向上生长、标题叫 LOGIFY。 校验逻辑里有两个硬限制值得注意:size 小于等于 0 时强制拉回 20 并报错;title 超过 40 字符直接判无效。开 MT5 把下面代码丢进 include,改 title 测一下超长字符串,就能看到 ValidateConfig 把错误吐出来。 别让标题吃掉边框空间 title 上限 40 字符不是拍脑袋,双线边框加长标题在 1920 宽屏上也可能挤歪。EA 名加周期缩写控制在 20 字内更稳。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| ENUMS                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
enum ENUM_LOG_FRAME_STYLE
  {
   LOG_FRAME_STYLE_NONE = class="num">0,           class=class="str">"cmt">// No rotation
   LOG_FRAME_STYLE_SINGLE,             class=class="str">"cmt">// Rotate based on date
   LOG_FRAME_STYLE_DOUBLE,             class=class="str">"cmt">// Rotate based on file size
  };
enum ENUM_LOG_DIRECTION
  {
   LOG_DIRECTION_UP = class="num">0,               class=class="str">"cmt">// Up
   LOG_DIRECTION_DOWN,                 class=class="str">"cmt">// Down
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Struct: MqlLogifyHandleComment                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">struct MqlLogifyHandleCommentConfig
  {
   class="type">int size;                           class=class="str">"cmt">// Space in lines that it will occupy
   ENUM_LOG_FRAME_STYLE frame_style;   class=class="str">"cmt">// Display grid
   ENUM_LOG_DIRECTION direction;       class=class="str">"cmt">// Direction
   class="type">class="kw">string title;                       class=class="str">"cmt">// log title

   class=class="str">"cmt">//--- Default constructor
   MqlLogifyHandleCommentConfig(class="type">void)
     {
      size = class="num">20;
      frame_style = LOG_FRAME_STYLE_SINGLE;
      direction = LOG_DIRECTION_UP;
      title = "LOGIFY";
     }

   class=class="str">"cmt">//--- Destructor
   ~MqlLogifyHandleCommentConfig(class="type">void)
     {
     }
   class=class="str">"cmt">//--- Validate configuration
   class="type">bool ValidateConfig(class="type">class="kw">string &error_message)
     {
      class=class="str">"cmt">//--- Saves the class="kw">return value
      class="type">bool is_valid = true;

      class=class="str">"cmt">//--- Check if size is greater than class="num">0
      if(size <= class="num">0)
        {
         size = class="num">20;
         error_message = "Size must be greater than class="num">0.";
         is_valid = false;
        }

      class=class="str">"cmt">//--- Check len
      if(StringLen(title) > class="num">40)
        {
         error_message = "Title is too class="type">long for frame. Max class="num">40 chars.";
         is_valid = false;
        }

      class=class="str">"cmt">//--- No errors found
      class="kw">return(is_valid);

◍ 图表注释处理类的配置与构造

在 MT5 里想把日志直接打到图表上,而不是挤在「专家」标签页,就靠这个从 CLogifyHandler 派生的注释处理器。它内部只持有一个 MqlLogifyHandleCommentConfig 类型的配置对象,构造时把处理器名字写死成 "comment",析构留空,轻量无副作用。 SetConfig 是核心入口:先把外部传进来的配置拷给成员,再调 m_config.ValidateConfig 做校验。若校验没过,会带当前时间戳 Print 一条 [ERROR] 提示,方便你当场发现参数问题。 校验通过后立刻 ArrayResize(m_logs, m_config.size) 把日志缓冲区拉到配置指定长度。这个 size 直接决定图表上能保留几条记录,设太小老信息会被覆盖,设太大则占更多内存——实盘前建议在策略测试器里改几个值看看刷新节奏。 GetConfig 只是把当前配置原样返回,方便别的模块读取或二次修改。整段没有收益承诺,外汇与贵金属波动剧烈、高风险,参数错了最多图表显示异常,不会替你下单。

MQL5 / C++
class CLogifyHandlerComment : class="kw">public CLogifyHandler
  {
class="kw">private:
  MqlLogifyHandleCommentConfig m_config;
class="kw">public:
                    CLogifyHandlerComment(class="type">void);
                   ~CLogifyHandlerComment(class="type">void);
  class=class="str">"cmt">//--- Configuration management
  class="type">void              SetConfig(MqlLogifyHandleCommentConfig &config);
  MqlLogifyHandleCommentConfig GetConfig(class="type">void);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Constructor                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
CLogifyHandlerComment::CLogifyHandlerComment(class="type">void)
  {
  m_name = "comment";
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Destructor                                                       |
class=class="str">"cmt">//+------------------------------------------------------------------+
CLogifyHandlerComment::~CLogifyHandlerComment(class="type">void)
  {
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Set configuration                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CLogifyHandlerComment::SetConfig(MqlLogifyHandleCommentConfig &config)
  {
  m_config = config;
  class=class="str">"cmt">//--- Validade config
  class="type">class="kw">string err_msg = "";
  if(!m_config.ValidateConfig(err_msg))
    {
      Print("[ERROR] ["+TimeToString(TimeCurrent())+"] Log system error: "+err_msg);
    }
  class=class="str">"cmt">//--- Resize
  ArrayResize(m_logs, m_config.size);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Get configuration                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
MqlLogifyHandleCommentConfig CLogifyHandlerComment::GetConfig(class="type">void)
  {
  class="kw">return(m_config);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

「图表日志的级联移位怎么落地」

MT5 图表上用 Comment() 打日志,空间不是无限的。假设只留 10 行可见区,新消息进来的时候如果直接覆盖,前一条就丢了;如果无限堆,文本会重叠出屏。要 newest-at-top、oldest-向上漂走,就得在固定数组里做元素前移——计算机科学里叫循环缓冲或滑动队列,这里叫「瀑布」位移。 机制很直白:维护 m_logs[] 定长数组(例如 10 元素)。新日志要插在位置 0,于是把 i 从 size-1 降到 1,逐个 m_logs[i] = m_logs[i-1],腾出 0 号位再写新数据;满了之后队尾那行自然被下一次移位挤掉,不占内存也不占屏。 Emit() 把这套逻辑跑通:先按 level 过滤,未达阈值直接 return;再跑移位循环;最后拼 header、行内容、footer 交给 Comment()。辅助函数管边框样式——单线给 │ 和 ┌─┐,双线给 ║ 和 ╔═╗,无边框返回空串。 开 MT5 新建 EA 把下面代码贴进类里,调 m_config.size 到 5 看看老消息是不是第 6 条进来就掉队,比肉眼数 Comment 行数靠谱。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Processes a log message and sends it to the specified destination|
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CLogifyHandlerComment::Emit(MqlLogifyModel &data)
  {
   class=class="str">"cmt">//--- Check if log level is allowed
   if(data.level < this.GetLevel())
     {
      class="kw">return;
     }
   class=class="str">"cmt">//--- Shift logs to maintain history
   for(class="type">int i = m_config.size-class="num">1; i > class="num">0; i--)
     {
      m_logs[i] = m_logs[i-class="num">1];
     }
   m_logs[class="num">0] = data;
   class=class="str">"cmt">//--- Build the complete comment
   class="type">class="kw">string comment = BuildHeader();
   comment += FormatLogLines();
   comment += BuildFooter();
   class=class="str">"cmt">//--- Display on chart
   Comment(comment);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Returns the side border character based on frame style            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">string CLogifyHandlerComment::GetSideBorder()
  {
   if(m_config.frame_style == LOG_FRAME_STYLE_SINGLE) class="kw">return "│";
   if(m_config.frame_style == LOG_FRAME_STYLE_DOUBLE) class="kw">return "║";
   class="kw">return "";
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Returns the top border based on frame style                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">string CLogifyHandlerComment::GetBorderTop()
  {
   if(m_config.frame_style == LOG_FRAME_STYLE_SINGLE)
      class="kw">return "┌───────────────────────────────────────────┐\n";
   if(m_config.frame_style == LOG_FRAME_STYLE_DOUBLE)
      class="kw">return "╔═══════════════════════════════════════════╗\n";
   class="kw">return "";
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Returns the middle separator based on frame style                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">string CLogifyHandlerComment::GetBorderMiddle()
  {
   if(m_config.frame_style == LOG_FRAME_STYLE_SINGLE)
      class="kw">return "├───────────────────────────────────────────┤\n";
   if(m_config.frame_style == LOG_FRAME_STYLE_DOUBLE)
      class="kw">return "╠═══════════════════════════════════════════╣\n";
   class="kw">return "";
  }

日志边框与方向的拼装逻辑

想在 MT5 图表上把日志显示成带框的样式,核心就在 CLogifyHandlerComment 的几个私有方法里。GetBorderBottom 按 frame_style 返回单线、双线或无边框的底边,单线用 47 个 ─ 拼接,双线用 47 个 ═ 拼接,实测字符宽度与 GetBorderTop 完全对齐,否则右上角会错位。 BuildHeader 负责拼标题区:无边框时只打一行标题加一条 45 字符的横线;有边框时先调 GetBorderTop 再拼侧边和标题,最后接 GetBorderMiddle 做分隔。若标题为空且非无边框模式,仅输出顶边,不浪费图表空间。 BuildFooter 很简单,非无边框才补底边。FormatLogLines 则按 direction 决定遍历顺序:UP 模式从 m_config.size-1 倒序到 0,每条非空日志前加侧边符和空格,空行只补侧边,这样最新消息贴着顶框。外汇与贵金属图表叠加此类注释时,注意字符宽度占用可能遮挡右侧价格轴,属高风险界面的可读性问题,调 size 参数前先开 MT5 验证。

MQL5 / C++
class="type">class="kw">string CLogifyHandlerComment::GetBorderBottom()
  {
   if(m_config.frame_style == LOG_FRAME_STYLE_SINGLE)
      class="kw">return "└───────────────────────────────────────────┘\n";
   if(m_config.frame_style == LOG_FRAME_STYLE_DOUBLE)
      class="kw">return "╚═══════════════════════════════════════════╝\n";
   class="kw">return "";
  }
class="type">class="kw">string CLogifyHandlerComment::BuildHeader()
  {
   class="type">class="kw">string header = "";
   if(m_config.title != "" && m_config.title != NULL)
     {
      if(m_config.frame_style == LOG_FRAME_STYLE_NONE)
        {
         header += " " + m_config.title + "\n";
         header += "─────────────────────────────────────────────\n";
        }
      else
        {
         header += GetBorderTop();
         header += GetSideBorder() + " " + m_config.title + "\n";
         header += GetBorderMiddle();
        }
     }
   else
     {
      if(m_config.frame_style != LOG_FRAME_STYLE_NONE)
        {
         header += GetBorderTop();
        }
     }
   class="kw">return header;
  }
class="type">class="kw">string CLogifyHandlerComment::BuildFooter()
  {
   if(m_config.frame_style != LOG_FRAME_STYLE_NONE)
      class="kw">return GetBorderBottom();
   class="kw">return "";
  }
class="type">class="kw">string CLogifyHandlerComment::FormatLogLines()
  {
   class="type">class="kw">string result = "";
   class="type">class="kw">string side = GetSideBorder();
   if(m_config.direction == LOG_DIRECTION_UP)
     {
      for(class="type">int i = m_config.size-class="num">1; i >= class="num">0; i--)
        {
         class="type">class="kw">string line = m_logs[i].formated;
         if(line != "")
           {
            result += side + " " + line + "\n";
           }
         else
           {
            result += side + "\n";
           }
        }
     }
   else class=class="str">"cmt">// LOG_DIRECTION_DOWN
     {
让小布替你跑这套
这些诊断逻辑小布盯盘已内置,打开对应品种页即可看到实时状态流,把重复劳动交给小布,你专注决策。

常见问题

放在 Logify 库的 handlers 文件夹下,作为独立处理程序集中管理图表渲染逻辑,不污染其他模块。
会互相覆盖,因此处理程序需统一接管图表文本输出,避免多个 Comment 调用抢同一块显示区。
小布盯盘内置了类似实时状态流的诊断视图,无需自己写处理程序即可在品种页看到关键运行信息,适合不想改代码的交易者。
通过固定行高与缓冲队列控制重绘节奏,让旧行稳定上移、新行底部追加,降低图表重绘带来的视觉抖动。