精通日志记录(第七部分):如何在图表上显示日志·进阶篇
(2/3)· 还在专家标签页里翻报错?本篇教你把 Logify 日志直接渲染到图表上,实时盯盘不漏关键信息
接上篇,我们继续深挖 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 字内更稳。
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 只是把当前配置原样返回,方便别的模块读取或二次修改。整段没有收益承诺,外汇与贵金属波动剧烈、高风险,参数错了最多图表显示异常,不会替你下单。
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 行数靠谱。
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 验证。
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 {