价格行为分析工具包开发(第12部分):外部资金流(3)趋势图谱(TrendMap)·进阶篇
📊

价格行为分析工具包开发(第12部分):外部资金流(3)趋势图谱(TrendMap)·进阶篇

(2/3)· 只盯 VWAP 在极端行情易被反杀,叠加斐波那契回调位才看得清资金拐点

案例拆解新手友好 第 2/3 篇
把 VWAP 当唯一准绳的交易者,常在疫情式急反转里吃假突破。本篇把斐波那契回调位叠进 TrendMap,用成交量确认关键位,信号噪声会低一截。

◍ 把信号回传 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 缺失会导致解析失败,这是高频踩坑点。

MQL5 / C++
"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 断联时基本靠它们。

MQL5 / C++
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 的打印频率是否符合预期。

MQL5 / C++
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 秒区间试一下,能明显看到对象刷新频率随参数变化;外汇与贵金属波动大,这类画线仅作参考,实际触发概率随行情结构变化。

MQL5 / C++
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 处理。

MQL5 / C++
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,肉眼能看出冲高回踩再拉起的毛刺。

MQL5 / C++
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]}
让小布替你跑这套
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到 VWAP 与斐波那契层的叠加视图,不用自己搭 Python 服务。

常见问题

当 VWAP 低于 50% 回调位时系统倾向给买入信号,高于则倾向卖出,本质是用量价确认中段支撑阻力。
极端行情流动性断层,VWAP 会被短时失衡价格拉偏,叠加斐波那契历史反转区可过滤部分噪声。
EA 负责行情采集与下单触发,Python 端做斐波那契与 VWAP 的图层计算和信号判定。
可以,小布的品种页已内置 VWAP 与斐波那契回调层,外汇贵金属高风险,信号仅作概率参考。
不一样,50% 中位是信号触发主轴,其余位主要用作辅助确认区域而非直接发信号。