来自专业程序员的提示(第三部分):日志。 连接到 Seq 日志收集和分析系统·进阶篇
📘

来自专业程序员的提示(第三部分):日志。 连接到 Seq 日志收集和分析系统·进阶篇

第 2/2 篇

◍ 给EA接一个能发到Seq的日志类

在MT5里跑EA,最怕出问题时只看到一句『array out of range』却不知道是哪个周期、哪个函数炸的。下面这段Logger类把日志按模块、发送者、品种、周期打上标签,再按CLEF格式推到Seq日志服务,复盘时能直接过滤。 类声明里先列了几个私有辅助函数:Log()负责落盘或转发,TimeToStr()把datetime压成ISO8601,PeriodToStr()把ENUM_TIMEFRAMES砍掉前7个字符只留PERIOD_M5这类尾巴,Quote()和Level()做转义与级别文本。对外暴露Debug/Info/Warning/Error/Fatal五个宏式内联方法,分别调Log并传不同级别常量。 构造函数只接appName和可选urlSeq,后者为空就不往Seq发。SetSender(module, sender)把.mq5后缀抹掉,只留模块名与函数名,调用栈一眼可辨。 TimeToStr里有个细节:用GetTickCount64()%1000取毫秒,同一秒内的多笔日志能靠这三位区分先后,回测时若一秒触发多次订单信号不会全挤成一条。PeriodToStr从EnumToString结果第7位起截,比如PERIOD_H1变H1,省得日志里全是长枚举名。 SendToSeq先把换行和制表符替换成空格,避免破坏CLEF的JSON单行结构;再用StringFormat拼出『符号, 周期 / 模块 / 函数 / 消息』的扩展字段。外汇与贵金属波动快、滑点随机,这类结构化日志只能帮你定位问题,不代表策略本身盈利概率高,实盘前务必在MT5策略测试器跑通。

MQL5 / C++
class="type">void Log(class="type">class="kw">string level, class="type">class="kw">string message);
class="type">class="kw">string TimeToStr(class="type">class="kw">datetime value);
class="type">class="kw">string PeriodToStr(ENUM_TIMEFRAMES value);
class="type">class="kw">string Quote(class="type">class="kw">string value);
class="type">class="kw">string Level();
class="type">void SendToSeq();
class="kw">public:
    Logger(class="type">class="kw">string appName, class="type">class="kw">string urlSeq);
    class="type">void SetSender(class="type">class="kw">string module, class="type">class="kw">string sender);
    class="type">void Debug(class="type">class="kw">string message) { Log(LEV_DEBUG, message); };
    class="type">void Info(class="type">class="kw">string message) { Log(LEV_INFO, message); };
    class="type">void Warning(class="type">class="kw">string message) { Log(LEV_WARNING, message); };
    class="type">void Error(class="type">class="kw">string message) { Log(LEV_ERROR, message); };
    class="type">void Fatal(class="type">class="kw">string message) { Log(LEV_FATAL, message); };
};
class="kw">extern Logger *gLog; class=class="str">"cmt">// logger instance
class=class="str">"cmt">// -----------------------------------------------------------------------------
class=class="str">"cmt">// Constructor
class=class="str">"cmt">// -----------------------------------------------------------------------------
Logger::Logger(class="type">class="kw">string appName, class="type">class="kw">string urlSeq = "") {
    m_appName = appName;
    m_urlSeq = urlSeq;
}
class=class="str">"cmt">// -----------------------------------------------------------------------------
class=class="str">"cmt">// Set the message sender          
class=class="str">"cmt">// -----------------------------------------------------------------------------
class="type">void Logger::SetSender(class="type">class="kw">string module, class="type">class="kw">string sender) {
    m_module = module; class=class="str">"cmt">// module or file name
    m_sender = sender; class=class="str">"cmt">// function name
    StringReplace(m_module, ".mq5", "");
}
class=class="str">"cmt">// -----------------------------------------------------------------------------
class=class="str">"cmt">// Convert time to the ISO8601 format for Seq
class=class="str">"cmt">// -----------------------------------------------------------------------------
class="type">class="kw">string Logger::TimeToStr(class="type">class="kw">datetime value) {
    class="type">MqlDateTime mdt;
    TimeToStruct(value, mdt);
    class="type">class="kw">ulong msec = GetTickCount64() % class="num">1000; class=class="str">"cmt">// for comparison
    class="kw">return StringFormat("%4i-%02i-%02iT%02i:%02i:%02i.%03iZ",
        mdt.year, mdt.mon, mdt.day, mdt.hour, mdt.min, mdt.sec, msec);
}
class=class="str">"cmt">// -----------------------------------------------------------------------------
class=class="str">"cmt">// Convert period to class="type">class="kw">string
class=class="str">"cmt">// -----------------------------------------------------------------------------
class="type">class="kw">string Logger::PeriodToStr(ENUM_TIMEFRAMES value) {
    class="kw">return StringSubstr(EnumToString(value), class="num">7);
}
class=class="str">"cmt">// -----------------------------------------------------------------------------
class=class="str">"cmt">// Send message to Seq via http
class=class="str">"cmt">// -----------------------------------------------------------------------------
class="type">void Logger::SendToSeq() {
    class=class="str">"cmt">// replace illegal characters
    StringReplace(m_message, "\n", " ");
    StringReplace(m_message, "\t", " ");
    
    class=class="str">"cmt">// prepare a class="type">class="kw">string in the CLEF(Compact Logging Event Format) format
    class="type">class="kw">string speriod = PeriodToStr(_Period);
    class="type">class="kw">string extended_message = StringFormat("%s, %s / %s / %s / %s",
        _Symbol, speriod, m_module, m_sender, m_message);
    class="type">class="kw">string clef = "{" +

正文

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="string">"\"@t\":"</span> + Quote(TimeToStr(<span class="functions">TimeCurrent</span>())) + <span class="comment">// event time</span> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="string">",\"AppName\":"</span> + Quote(m_appName) + <span class="comment"> // application name (Cayman)</span> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="string">",\"Symbol\":"</span> + Quote(<span class="predefines">_Symbol</span>) + <span class="comment"> // symbol (EURUSD)</span> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="string">",\"Period\":"</span> + Quote(speriod) + <span class="comment">// period (H4)</span> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="string">",\"Module\":"</span> + Quote(m_module) + <span class="comment"> // module name (__FILE__)</span> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="string">",\"Sender\":"</span> + Quote(m_sender) + <span class="comment">// sender name (__FUNCTION__)</span> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="string">",\"Level\":"</span> + Quote(m_level) + <span class="comment"> // level abbreviation (INF)</span> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="string">",\"@l\":"</span> + Quote(Level()) + <span class="comment"> // level details (Information)</span> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&

「跑一遍 Logger 类看日志怎么落盘」

在 MT5 里挂上 TestLogger 脚本,核心目的是验证 Logger 类能否把不同级别的消息准确带出调用方标识。宏 LOG_DEBUG / LOG_INFO / LOG_WARNING / LOG_ERROR / LOG_FATAL 被分散写在 OnStart、普通函数 TestFunc 和类构造函数里,方便观察发送者字段是否如实反映位置。 日志输出印证了这一点:2022.02.16 20:17:21 这段里,USDJPY H1 图表上的脚本依次打出 DBG/INF/WRN/ERR/FTL 五级,后面紧跟 OnStart、TestFunc、TestClass::TestClass 等所有者名。级别前缀和发送者没有错位,说明宏定义确实在编译期把函数上下文织进了日志。 除了 MT5 智能系统日志,消息还能推到 Notepad++ 直接看明文,或发往本地 Seq 服务端(代码里 url 为 http://seqlocal.net:5341/api/events/raw?clef)做结构化检索。外汇与贵金属行情波动剧烈、杠杆风险高,这类诊断脚本仅用于本地验证,切勿直接挂实盘。 下面这段是脚本主干,注意 gLog 在 OnStart 末尾被 delete,TestClass 对象也手动释放,避免 MT5 脚本退出后留堆残骸。

MQL5 / C++
class="macro">#include <Cayman/Logger.mqh>
class TestClass {
  class="type">int m_id;
class="kw">public:
  TestClass(class="type">int id) { 
    m_id = id;
    LOG_DEBUG(StringFormat("create object with id = %i", id));
  };
};
class="type">void TestFunc() {
  LOG_INFO("info message from inner function");
}
class="type">void OnStart() {
  class="type">class="kw">string urlSeq = "http:class=class="str">"cmt">//seqlocal.net:class="num">5341/api/events/raw?clef";
  gLog = new Logger("TestLogger", urlSeq);
  
  LOG_DEBUG("debug message");
  LOG_INFO("info message");
  LOG_WARNING("warning message");
  LOG_ERROR("error message");
  LOG_FATAL("fatal message");
  
  class=class="str">"cmt">// call function
  TestFunc();
  
  class=class="str">"cmt">// create object
  TestClass *testObj = new TestClass(class="num">101);
  class=class="str">"cmt">// free memory
  class="kw">delete testObj;
  class="kw">delete gLog;
}
class="num">2022.02.class="num">16 class="num">20:class="num">17:class="num">21.048 TestLogger(USDJPY,H1)  DBG: OnStart debug message
class="num">2022.02.class="num">16 class="num">20:class="num">17:class="num">21.291 TestLogger(USDJPY,H1)  INF: OnStart info message
class="num">2022.02.class="num">16 class="num">20:class="num">17:class="num">21.299 TestLogger(USDJPY,H1)  WRN: OnStart warning message
class="num">2022.02.class="num">16 class="num">20:class="num">17:class="num">21.303 TestLogger(USDJPY,H1)  ERR: OnStart error message
class="num">2022.02.class="num">16 class="num">20:class="num">17:class="num">21.323 TestLogger(USDJPY,H1)  FTL: OnStart fatal message
class="num">2022.02.class="num">16 class="num">20:class="num">17:class="num">21.328 TestLogger(USDJPY,H1)  INF: TestFunc info message from inner function
class="num">2022.02.class="num">16 class="num">20:class="num">17:class="num">21.332 TestLogger(USDJPY,H1)  DBG: TestClass::TestClass create object with id = class="num">101

◍ 用 Python 把 MT5 流水账丢进 Seq

想看 MT5 智能系统的运行轨迹,光在文本编辑器里翻 MQL5/Logs/yyyymmdd.log 效率太低。有个轻量思路:用 Python 写个桥接包,把日志按行喂给 Seq,没有级别标注的消息统一归为 INF,方便后续在界面里筛选。 如果你是接外包的开发者,让客户发一份 EA 日志比远程查盘省事得多。Seq 里能用 SQL 语法查日志,常用的复杂查询存成名称,点一下就跑,比在记事本里 Ctrl+F 强出几个量级。 调用方式就一行命令,pathLog 指向具体的某天日志文件即可: py log2seq Cayman d:/Project/MQL5/Logs/20211028.log 上面例子里 Cayman 是应用在 Seq 里的标识名,20211028 对应 MT5 生成的那天日志。外汇与贵金属 EA 跑这套日志分析也有平台断线、滑点等高风险干扰,结论只作排查参考。

MQL5 / C++
  Run: py log2seq appName pathLog
  where log2seq - package name
        appName - application name to identify events in Seq
        pathLog - MetaTrader class="num">5 log path
  Example: py log2seq Cayman d:/Project/MQL5/Logs/class="num">20211028.log

把这条线请下神坛

Logger 类真正落地的价值,是把 MT5 里零散的 Print 输出换成带严重性级别的结构化消息,再借 log2seq 这个 8.32 KB 的 Python 包导进 Seq 做采样和图表化观测。附件里 Logger.mqh 占 15.58 KB、TestLogger.mq5 仅 2.83 KB,直接拖进 MT5 的 MQL5/Include 和 Scripts 就能跑通。 外汇与贵金属自动化日志分析属于高风险辅助手段,Seq 里的信号再漂亮也不构成进场依据,它只帮你看见自己 EA 在极端滑点下的真实行为。 下次写 EA 别再把调试信息注释掉,让小布替你把这层日志接到可视化里,哪段逻辑在伦敦盘反常抛错一目了然。

常见问题

给EA接一个能发到Seq的日志类,把关键变量和报错位置都写进日志,打开Seq按时间线过滤就能定位。
跑一遍Logger类让每笔操作落盘,再用Python把流水账丢进Seq,按品种和订单类型筛一下就清楚。
小布可对接你的日志流,自动聚合并高亮异常模式,你只看它推的摘要就行。
在Logger类里统一转成UTC存储,Seq面板配上时区偏移,显示就一致了。
高风险品种建议保留心跳日志但降频,用Seq的保留策略自动清理空段,别全关以免漏真异常。