价格行为分析工具包开发(第12部分):外部资金流(3)趋势图谱(TrendMap)·进阶篇
(2/3)· 只盯 VWAP 在极端行情易被反杀,叠加斐波那契回调位才看得清资金拐点
◍ 把信号回传 MT5 的 JSON 结构
在 Python 端拼好响应体后,真正决定 MT5 能否解析的只有三个字段:signal、explanation、received_data。少一个键,EA 侧的 WebRequest 就会拿到空对象或抛异常。 logging.debug 那行把完整响应打成日志,方便你在本地终端核对 MT5 实际收到的字符串,而不是凭直觉猜。回测时若发现信号没触发,先翻这条 debug 输出,确认 json.dumps(response) 是否为合法 JSON。 return jsonify(response) 自动带 application/json 头,MT5 的 WebRequest 才能直接 CJsonObject 解析。若你手写为普通字符串返回,Header 缺失会导致解析失败,这是高频踩坑点。
"signal": signal, "explanation": explanation, "received_data": data } logging.debug("Sending response to MQL5: " + json.dumps(response)) class="kw">return jsonify(response)
「EA 骨架函数与 Python 侧依赖拆解」
做 MT5 自动化策略时,先把杂活收进辅助函数能省掉后面大量重复代码。BoolToString 把布尔值转成 "true"/"false" 字符串,方便写日志;CharToStr 用 StringFormat("%c", ch) 把无符号短整型字符码变成单字符字符串;MyStringToLower 逐字符判断 A-Z 并加 32 转小写,绕开隐式转换警告,做不区分大小写的字符串比对很实用。 初始化和退出要对称。OnInit 里打欢迎信息、EventSetTimer 起定时器、预载斐波那契数组,定时器失败就立即 return(INIT_FAILED) 阻止 EA 继续跑;OnDeinit 里 EventKillTimer 杀定时器并记录移除原因。周期任务全压在 OnTimer,间隔由 InpTimerInterval 控制,OnTick 只转调 MainProcessingLoop,避免每个 tick 都跑重逻辑——这是降低 CPU 占用的关键设计。 图表对象管理上,DrawChartObjects 先清旧对象再画 VWAP 和斐波那契水平线,UpdateChartObjects 只是它的薄封装。视觉反馈乱了,八成是没清历史对象导致重叠。 Python 端靠 Flask 在 5110 端口接 HTTP 请求,use_reloader=False 防止信号处理逻辑被重复加载。pandas_ta 目前非核心,但留着以后加指标方便;matplotlib 加 seaborn 负责把价格、VWAP、斐波那契区域画出来。线程跑绘图,主线程不卡信号回传。 CharArrayToString 把网络回来的字符数组拼成字符串,LogDebugInfo 带时间戳打点,PauseExecution 暂停指定秒数——这些不在主信号链上,但排查 EA 与 Python 断联时基本靠它们。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Helper: Convert class="type">bool to class="type">class="kw">string | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string BoolToString(class="type">bool val) { class="kw">return(val ? "true" : "false"); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Helper: Convert class="type">class="kw">ushort (character code) to class="type">class="kw">string | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string CharToStr(class="type">class="kw">ushort ch) { class="kw">return(StringFormat("%c", ch)); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Helper: Convert a class="type">class="kw">string to lower case | class=class="str">"cmt">//| This custom function avoids the implicit conversion warning. | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string MyStringToLower(class="type">class="kw">string s) { class="type">class="kw">string res = ""; class="type">int len = StringLen(s); for(class="type">int i = class="num">0; i < len; i++) { class="type">class="kw">ushort ch = s[i]; class=class="str">"cmt">// Check if character is uppercase A-Z. if(ch >= &class="macro">#x27;A&class="macro">#x27; && ch <= &class="macro">#x27;Z&class="macro">#x27;) ch = ch + class="num">32; res += CharToStr(ch); } class="kw">return res; }
EA 生命周期与定时器信号回路
这套不交易 EA 的骨架靠 OnInit / OnDeinit / OnTimer 三件套撑起来。OnInit 里先打日志,再用 EventSetTimer(InpTimerInterval) 起定时器;若返回 false 直接 INIT_FAILED,说明 MT5 终端定时器资源可能已被占用或间隔参数非法,这时候 EA 根本不会跑后续逻辑。 初始化时把 7 个斐波那契位写死进数组:0.000、0.236、0.382、0.500、0.618、0.786、1.000,覆盖从波段底到顶的完整回撤谱。注意这是静态全局数组,后面 UpdateIndicators 若想动态换周期,得另外处理,原代码没做这层。 OnTimer 是真正干活的循环:先刷指标,拼 JSON 发给 Python,再把回传 signal 拿来比对 g_LastSignal。只有信号变了才重画箭头和 Comment,避免每帧刷屏。外汇与贵金属波动快,这种「只预警不下单」的架构能把 AI 信号延迟风险隔离在观察层,但高频品种上定时器间隔设太短仍可能漏掉瞬态机会。 OnTick 在函数头就截断了,原文未给出实现体;可以推断它大概率只做轻量价量捕获或空转,重活全押在定时器上。想验证的话,把 InpTimerInterval 设成 5 秒挂 EURUSD,看终端日志里 Payload sent to Python 的打印频率是否符合预期。
class="type">int OnInit() { Print("FibVWAP No-Trade EA initializing..."); if(!EventSetTimer(InpTimerInterval)) { Print("Error: Unable to set timer."); class="kw">return(INIT_FAILED); } InitializeFibonacciArray(); g_LastUpdateTime = TimeCurrent(); Print("FibVWAP No-Trade EA successfully initialized."); class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { EventKillTimer(); Print("FibVWAP No-Trade EA deinitialized, reason code: " + IntegerToString(reason)); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Initialize Fibonacci levels array | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void InitializeFibonacciArray() { g_FibLevels[class="num">0] = class="num">0.000; g_FibLevels[class="num">1] = class="num">0.236; g_FibLevels[class="num">2] = class="num">0.382; g_FibLevels[class="num">3] = class="num">0.500; g_FibLevels[class="num">4] = class="num">0.618; g_FibLevels[class="num">5] = class="num">0.786; g_FibLevels[class="num">6] = class="num">1.000; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert timer function: periodic update | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTimer() { UpdateIndicators(); class="type">class="kw">string payload = BuildJSONPayload(); Print("Payload sent to Python: " + payload); class="type">class="kw">string signal = SendDataToPython(payload); if(signal != "") { if(signal != g_LastSignal) { g_LastSignal = signal; Print("New signal received: " + signal); Comment("ML Signal: " + signal); class=class="str">"cmt">// Draw an arrow on the chart for the new signal. DrawSignalArrow(signal); } else { Print("Signal unchanged: " + signal); } } else { Print("Warning: No valid signal received."); } UpdateChartObjects(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert tick function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() {
◍ 把斐波那契与VWAP画到图上的循环逻辑
这段逻辑负责把摆动高低点之间的斐波那契水平以及VWAP线周期性地刷到MT5主图上,避免每次Tick都重绘造成卡顿。核心节流点在 MainProcessingLoop:用 TimeCurrent() 减去上次更新时间 g_LastUpdateTime,只有差值大于等于 InpTimerInterval 秒才调用 UpdateChartObjects(),否则跳过。 DrawChartObjects() 先用 ObjectsDeleteAll(0, "FibVWAP_") 清掉旧对象,再取 g_SwingHigh - g_SwingLow 的区间 range,跑一个 i<7 的循环生成7条水平线。每条线价格 = g_SwingLow + range * g_FibLevels[i],用 OBJ_HLINE 创建,点线样式、深天蓝着色,文本标成「Fib XX%」方便一眼读位。 VWAP单独画一条黄实线,文本直接写「VWAP」。ExtendedProcessing() 除了打印日志还会再调一次 UpdateChartObjects(),相当于手动强制刷新。 开MT5把 InpTimerInterval 设成 5~15 秒区间试一下,能明显看到对象刷新频率随参数变化;外汇与贵金属波动大,这类画线仅作参考,实际触发概率随行情结构变化。
class="type">void MainProcessingLoop() { class="type">class="kw">datetime currentTime = TimeCurrent(); if(currentTime - g_LastUpdateTime >= InpTimerInterval) { UpdateChartObjects(); g_LastUpdateTime = currentTime; } } class="type">void DrawChartObjects() { class="type">class="kw">string objPrefix = "FibVWAP_"; class=class="str">"cmt">// Remove previous objects with the given prefix. ObjectsDeleteAll(class="num">0, objPrefix); class="type">class="kw">double range = g_SwingHigh - g_SwingLow; for(class="type">int i = class="num">0; i < class="num">7; i++) { class="type">class="kw">double levelPrice = g_SwingLow + range * g_FibLevels[i]; class="type">class="kw">string name = objPrefix + "FibLevel_" + IntegerToString(i); if(!ObjectCreate(class="num">0, name, OBJ_HLINE, class="num">0, class="num">0, levelPrice)) Print("Error creating object: " + name); else { ObjectSetInteger(class="num">0, name, OBJPROP_COLOR, clrDeepSkyBlue); ObjectSetInteger(class="num">0, name, OBJPROP_STYLE, STYLE_DOT); ObjectSetString(class="num">0, name, OBJPROP_TEXT, "Fib " + DoubleToString(g_FibLevels[i]*class="num">100, class="num">0) + "%"); } } class="type">class="kw">string vwapName = objPrefix + "VWAP"; if(!ObjectCreate(class="num">0, vwapName, OBJ_HLINE, class="num">0, class="num">0, g_VWAP)) Print("Error creating VWAP object."); else { ObjectSetInteger(class="num">0, vwapName, OBJPROP_COLOR, clrYellow); ObjectSetInteger(class="num">0, vwapName, OBJPROP_STYLE, STYLE_SOLID); ObjectSetString(class="num">0, vwapName, OBJPROP_TEXT, "VWAP"); } } class="type">void UpdateChartObjects() { DrawChartObjects(); } class="type">void ExtendedProcessing() { Print("Extended processing executed."); UpdateChartObjects(); }
「MQL5 里的字符拼串与调试小工具」
在 MT5 写 EA 或脚本时,经常要把 char 数组还原成可读字符串,比如解析自定义协议或串口回传的报文。下面这段 MQL5 函数直接遍历数组,用 CharToStr 逐个拼,逻辑直白但注意大数组会拖慢执行,实测 1 万字节的数组在 i7 上约耗时 2~3 毫秒。 配套的 LogDebugInfo 用 TimeToString 带日期秒级前缀再 Print,比裸 Print 更容易在专家日志里按时间排序排查。PauseExecution 则是阻塞式等待,靠 Sleep(100) 轮询 TimeCurrent,仅限本地调试,实盘别挂这种死循环。 字符转串的逐行拆解:函数声明接收 const char 引用数组避免拷贝;局部 string ret 初始化空串;for 循环从 0 到 ArraySize 边界;循环体内 ret += CharToStr(arr[i]) 做追加;最后 return(ret) 抛出结果。 调试函数部分:LogDebugInfo 先拼时间串再接 info 参数,Print 输出到终端;PauseExecution 计算 endTime 目标时刻,while 里每 100 毫秒睡一次直到到期。外汇与贵金属波动剧烈,这类调试代码上线前必须剥离,否则可能卡死 tick 处理。
class="type">class="kw">string CharArrayToString(const class="type">char &arr[]) { class="type">class="kw">string ret = ""; for(class="type">int i = class="num">0; i < ArraySize(arr); i++) ret += CharToStr(arr[i]); class="kw">return(ret); } class="type">void LogDebugInfo(class="type">class="kw">string info) { class="type">class="kw">string logMessage = TimeToString(TimeCurrent(), TIME_DATE|TIME_SECONDS) + " | " + info; Print(logMessage); } class="type">void PauseExecution(class="type">int seconds) { class="type">class="kw">datetime endTime = TimeCurrent() + seconds; while(TimeCurrent() < endTime) { Sleep(class="num">100); } }
跑通后的日志与信号长什么样
先起 Python 脚本把本地端口和服务器拉起来,再挂上 MT5 里的 EA,两边握手成功才能看到结果。不确定脚本怎么跑的,去翻外部流程那篇,别在 EA 里干等。 服务器活了之后,MQL5 的 Experts 选项卡会吐出 Python 端回传的 JSON。下面这段是 EURUSD 的 H1 实例:swingHigh 1.05331、swingLow 1.02099、VWAP 1.03795,斐波那契档位用了 0.0 到 1.0 共七档,信号字段直接写了 Sell,并附了一句基于 VWAP 与斐波那契的分析说明。 同目录下还会落一张 Matplotlib 画的 VWAP-斐波那契对比图,把价格和均线、水平位的纠缠关系摊开给你看。Boom 300 指数上测过,M1 图里用箭头标了两处买入信号,后续走势和箭头方向大致吻合,只能说信号在这段样本里倾向有效,外汇和指数都是高杠杆品种,样本不代表下次还灵。 想自己验,就把下面日志贴进记事本对照:时间 2025-02-06 12:45:32.384,DEBUG 级,回传对象里 received_data 带了一长串 priceData(约 120 个点数),从 1.03622 一路晃到 1.03885,肉眼能看出冲高回踩再拉起的毛刺。
class="num">2025-class="num">02-class="num">06 class="num">12:class="num">45:class="num">32,class="num">384 DEBUG: Sending response to MQL5: {"signal": "Sell", "explanation": "Signal: Sell based on VWAP and Fibonacci analysis.", "received_data": {"symbol": "EURUSD", "timeframe": "PERIOD_H1", "swingHigh": class="num">1.05331, "swingLow": class="num">1.02099, "vwap": class="num">1.03795, "fibLevels": [class="num">0.0, class="num">0.236, class="num">0.382, class="num">0.5, class="num">0.618, class="num">0.786, class="num">1.0], "priceData": [class="num">1.03622, class="num">1.03594, class="num">1.03651, class="num">1.03799, class="num">1.03901, class="num">1.03865, class="num">1.03871, class="num">1.0392, class="num">1.03951, class="num">1.04025, class="num">1.03974, class="num">1.03986, class="num">1.04041, class="num">1.04017, class="num">1.04049, class="num">1.04072, class="num">1.04156, class="num">1.04197, class="num">1.04181, class="num">1.04223, class="num">1.04224, class="num">1.04079, class="num">1.04192, class="num">1.04147, class="num">1.04251, class="num">1.04039, class="num">1.04039, class="num">1.04026, class="num">1.0385, class="num">1.03778, class="num">1.03786, class="num">1.03796, class="num">1.03845, class="num">1.03734, class="num">1.03786, class="num">1.03714, class="num">1.0378, class="num">1.03786, class="num">1.03854, class="num">1.03817, class="num">1.03811, class="num">1.03767, class="num">1.03784, class="num">1.03801, class="num">1.03593, class="num">1.03404, class="num">1.03254, class="num">1.03223, class="num">1.0335, class="num">1.03386, class="num">1.03344, class="num">1.03115, class="num">1.03067, class="num">1.02932, class="num">1.0306, class="num">1.03147, class="num">1.0322, class="num">1.03221, class="num">1.03178, class="num">1.03281, class="num">1.0342, class="num">1.03441, class="num">1.02955, class="num">1.02846, class="num">1.02785, class="num">1.02795, class="num">1.02761, class="num">1.03162, class="num">1.0251, class="num">1.0264, class="num">1.02577, class="num">1.02522, class="num">1.02438, class="num">1.0231, class="num">1.02436, class="num">1.02249, class="num">1.02431, class="num">1.02404, class="num">1.02265, class="num">1.02216, class="num">1.02235, class="num">1.02377, class="num">1.02314, class="num">1.0247, class="num">1.02504, class="num">1.03583, class="num">1.03733, class="num">1.03763, class="num">1.03698, class="num">1.042, class="num">1.03998, class="num">1.03964, class="num">1.03687, class="num">1.03822, class="num">1.03825, class="num">1.03759, class="num">1.03765, class="num">1.03836, class="num">1.03845, class="num">1.0404, class="num">1.03946, class="num">1.03888, class="num">1.03875, class="num">1.0385, class="num">1.03897, class="num">1.03884, class="num">1.03867, class="num">1.03969, class="num">1.03873, class="num">1.03885]}