从新手到专家:使用 MQL5 制作动画新闻标题(四)  本地托管 AI 模型市场洞察·进阶篇
📘

从新手到专家:使用 MQL5 制作动画新闻标题(四) 本地托管 AI 模型市场洞察·进阶篇

第 2/3 篇

给 News Headline EA 接上 AI 滚动通道

把外部 AI 洞察塞进 MT5 的 EA,核心不是算法本身,而是用最小改动让 EA 能定时拉取、安全绘制、干净卸载。下面这套写法在保留原 EA 逻辑的前提下,只新增输入项、全局态、一个抓取函数和三处生命周期钩子。 先开三个输入:布尔开关控制通道启停,字符串填 FastAPI 或任意 HTTP 端点,整数定两次 POST 的最小间隔秒数。实测把 InpAIInsightsReloadSec 设成 60,代表每分钟最多打一次远端,避免图表卡在等待响应。 全局里用 latestAIInsight 存文本,offAI 管横向滚动偏移,aiRequestInProgress 拦并发请求,lastAIInsightTime 记上次成功时刻。这四个变量保证断网或报错时画面不空——旧洞察继续滚。 FetchAIInsights() 把所有 HTTP 细节封死在函数内:开关没开、上一次还在飞、冷却未到都直接 return。通过才拼 JSON 发 POST,成功就从响应抠 "insight" 字段,失败留旧值。下面代码逐行拆了关键段。 OnInit 里建一块半透明 AI 画布垫在原通道下方,先写占位符再立刻调一次 FetchAIInsights(),所以哪怕中途加载 EA,首次网络回来就有真内容。OnTimer 每次重绘完顺手清画布、按偏移绘文本、左移循环。OnDeinit 杀定时器、删画布,重加载必从零起。外汇与贵金属波动剧烈,接入外部信号仅作辅助参考,实盘前请在策略测试器跑通 WebRequest 权限。

MQL5 / C++
class=class="str">"cmt">//--- class="num">1) USER INPUTS ------------------------------------------------
input class="type">bool   ShowAIInsights           = true;              
input class="type">class="kw">string InpAIInsightsURL         = "http:class=class="str">"cmt">//class="num">127.0.class="num">0.1:class="num">8000/insights";
input class="type">int    InpAIInsightsReloadSec = class="num">60;    class=class="str">"cmt">// seconds between requests
class=class="str">"cmt">//--- class="num">3) GLOBALS -----------------------------------------------------
class="type">class="kw">string latestAIInsight      = "AI insights coming soon…";
class="type">int    offAI;                            class=class="str">"cmt">// scroll offset
class="type">bool   aiRequestInProgress  = false;     class=class="str">"cmt">// prevent concurrent POSTs
class="type">class="kw">datetime lastAIInsightTime  = class="num">0;         class=class="str">"cmt">// last successful fetch time
class="type">void FetchAIInsights()
{
  if(!ShowAIInsights || aiRequestInProgress) class="kw">return;  class=class="str">"cmt">// 开关关或请求在飞则退出
  class="type">class="kw">datetime now = TimeTradeServer();                 class=class="str">"cmt">// 取交易服务器时间
  if(now < lastAIInsightTime + InpAIInsightsReloadSec) class="kw">return; class=class="str">"cmt">// 冷却未过退出
  aiRequestInProgress = true;                      class=class="str">"cmt">// 置标志防并发
  class="type">class="kw">string hdrs  = "Content-Type: application/json\r\n";
  class="type">class="kw">string body  = "{\"prompt:\"Concise trading insight for " + Symbol() + "\"}"; class=class="str">"cmt">// 拼当前品种提示
  class="type">uchar  req[], resp[]; class="type">class="kw">string hdr;
  StringToCharArray(body, req);                    class=class="str">"cmt">// 转字节数组
  class="type">int res = WebRequest("POST", InpAIInsightsURL, hdrs, class="num">5000, req, resp, hdr); class=class="str">"cmt">// 发请求超时5秒
  if(res > class="num">0)
  {
    class="type">class="kw">string js = CharArrayToString(resp,class="num">0,WHOLE_ARRAY); class=class="str">"cmt">// 响应转字符串
    class="type">int p     = StringFind(js, "\"insight\":");        class=class="str">"cmt">// 找insight键
    if(p >= class="num">0)
    {
      class="type">int start = StringFind(js, "\"", p+class="num">10) + class="num">1;      class=class="str">"cmt">// 值起点
      class="type">int end   = StringFind(js, "\"", start);        class=class="str">"cmt">// 值终点
      if(start>class="num">0 && end>start)
        latestAIInsight = StringSubstr(js, start, end-start); class=class="str">"cmt">// 截出洞察文本
    }
    lastAIInsightTime = now;                         class=class="str">"cmt">// 更新成功时间
  }
  aiRequestInProgress = false;                       class=class="str">"cmt">// 清标志
}
class="type">int OnInit()
{
  class=class="str">"cmt">// … existing init …
  class=class="str">"cmt">// AI Insights lane
  if(ShowAIInsights)
  {

「AI 洞察跑马灯的定时器与生命周期」

在 OnInit 末尾用 EventSetMillisecondTimer(InpTimerMs) 挂起毫秒级定时器,AI 洞察条才具备持续刷新的动力;初始化时先 CreateBitmapLabel 建名为 AiC 的位图标签,TransparentLevelSet(120) 把整体透明度压到约 47%(120/255),避免遮挡主图价格行为。 OnTimer 里每 tick 先 FetchAIInsights() 拉最新文本,若 ShowAIInsights 为真则 Erase(ARGB(120,0,0,0)) 清屏并重绘:offAI 按 InpAIInsightsSpeed 左移,当 offAI + TextWidth(latestAIInsight) < -20 时复位到 canvW,形成无缝循环跑马灯。 OnDeinit 中必须 EventKillTimer() 并 aiCanvas.Destroy() 加 ObjectDelete(0,"AiC") 清理,否则 MT5 切周期会残留不可见对象。外汇与贵金属行情受突发数据影响大,这类叠加层仅作信息辅助,实际下单仍应以价格结构为准,相关风险偏高。

MQL5 / C++
  aiCanvas.CreateBitmapLabel("AiC", class="num">0, class="num">0, canvW, lineH, COLOR_FORMAT_ARGB_RAW);
  aiCanvas.TransparentLevelSet(class="num">120);
  offAI = canvW;
  SetCanvas("AiC", InpPositionTop, InpTopOffset + (InpSeparateLanes ? class="num">8 : class="num">5) * lineH);
  aiCanvas.TextOut(offAI, (lineH - aiCanvas.TextHeight(latestAIInsight)) / class="num">2,
                    latestAIInsight, XRGB(class="num">180,class="num">220,class="num">255), ALIGN_LEFT);
  aiCanvas.Update(true);
  class=class="str">"cmt">// initial fetch
  FetchAIInsights();
}
EventSetMillisecondTimer(InpTimerMs);
class="kw">return INIT_SUCCEEDED;
}
class="type">void OnTimer()
{
  class=class="str">"cmt">// … existing redraw for events/news/indicators …
  class=class="str">"cmt">// fetch & draw AI lane
  FetchAIInsights();
  if(ShowAIInsights)
  {
    aiCanvas.Erase(ARGB(class="num">120,class="num">0,class="num">0,class="num">0));
    aiCanvas.TextOut(offAI, (lineH - aiCanvas.TextHeight(latestAIInsight)) / class="num">2,
                      latestAIInsight, XRGB(class="num">180,class="num">220,class="num">255), ALIGN_LEFT);
    offAI -= InpAIInsightsSpeed;
    if(offAI + aiCanvas.TextWidth(latestAIInsight) < -class="num">20)
      offAI = canvW;
    aiCanvas.Update(true);
  }
}
class="type">void OnDeinit(const class="type">int reason)
{
  EventKillTimer();
  class=class="str">"cmt">// … existing cleanup …
  if(ShowAIInsights)
  {
    aiCanvas.Destroy();
    ObjectDelete(class="num">0, "AiC");
  }
}

◍ 把本地推理延迟和 UI 卡顿拆开看

EA 接上本地 FastAPI-Uvicorn 后,新开的 AI Insights 通道会实时显示模型回传的文本。测下来一次完整推理周期从发 POST 到收回 JSON,端到端耗时约 31.98 秒,外汇与贵金属交易本身高风险,这种秒级延迟只适合非逐 tick 决策的辅助场景。 llama-cpp 加载 GGUF 是一次性成本,日志里 load time = 206235.75 ms(约 206 秒),之后每个 POST 才进入常规推理。提示评估 4 个 token 用了 1487.17 ms,平均每 token 371.79 ms;生成 63 个 token 的 eval time = 29555.55 ms,每 token 约 469.14 ms(~2.13 token/s);采样 64 次仅 58.01 ms。加起来 total time = 31979.70 ms,服务器回 200 OK,且出现 prefix-match hit 说明缓存命中跳过了重复层计算。 滚动停顿的根因在 EA 定时器里直接调 FetchAIInsights() 会阻塞 WebRequest,最长卡约 32 秒等服务器返回。解法是把绘制文本和 HTTP 调用解耦:每 20 ms 先滚动画面,推理完成后再用响应更新 latestAIInsight,通道动画就不再断。 别把 32 秒当固定值 第二次循环 total time 升到 35799.69 ms,prompt eval 也变慢,说明本地 CPU 推理波动明显。真要上 MT5 验证,先确认 AI 服务器后台常驻,否则 EA 通道会一直空等超时。

MQL5 / C++
<span style="background-class="type">color:rgb(class="num">249, class="num">204, class="num">202);">llama_print_timings:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;load time =&nbsp;&nbsp;<span class="number">class="num">206235.75</span> ms
llama_print_timings:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;sample time =&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="number">class="num">58.01</span> ms /&nbsp;&nbsp;&nbsp;&nbsp;<span class="number">class="num">64</span> runs&nbsp;&nbsp; (&nbsp;&nbsp;&nbsp;&nbsp;<span class="number">class="num">0.91</span> ms per token,&nbsp;&nbsp;<span class="number">class="num">1103.33</span> tokens per second)
llama_print_timings: prompt <span class="built_in">eval</span> time =&nbsp;&nbsp;&nbsp;&nbsp;<span class="number">class="num">1487.17</span> ms /&nbsp;&nbsp;&nbsp;&nbsp; <span class="number">class="num">4</span> tokens(&nbsp;&nbsp;<span class="number">class="num">371.79</span> ms per token,&nbsp;&nbsp;&nbsp;&nbsp; <span class="number">class="num">2.69</span> tokens per second)
llama_print_timings:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="built_in">eval</span> time =&nbsp;&nbsp; <span class="number">class="num">29555.55</span> ms /&nbsp;&nbsp;&nbsp;&nbsp;<span class="number">class="num">63</span> runs&nbsp;&nbsp; (&nbsp;&nbsp;<span class="number">class="num">469.14</span> ms per token,&nbsp;&nbsp;&nbsp;&nbsp; <span class="number">class="num">2.13</span> tokens per second)
llama_print_timings:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; total time =&nbsp;&nbsp; <span class="number">class="num">31979.70</span> ms
←[<span class="number">class="num">32</span>mINFO←[<span class="number">class="num">0</span>m:&nbsp;&nbsp;&nbsp;&nbsp; <span class="number">class="num">127.0</span><span class="number">.class="num">0</span><span class="number">.class="num">1</span>:<span class="number">class="num">52770</span> - <span class="class="type">class="kw">string">"←[1mPOST /insights HTTP/class="num">1.1←[0m"</span> ←[<span class="number">class="num">32</span>m200 OK←[<span class="number">class="num">0</span>m
Llama.generate: prefix-<span class="keyword">match</span> hit</span>
llama_print_timings:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;load time =&nbsp;&nbsp;<span class="number">class="num">206235.75</span> ms
llama_print_timings:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;sample time =&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="number">class="num">83.42</span> ms /&nbsp;&nbsp;&nbsp;&nbsp;<span class="number">class="num">64</span> runs&nbsp;&nbsp; (&nbsp;&nbsp;&nbsp;&nbsp;<span class="number">class="num">1.30</span> ms per token,&nbsp;&nbsp; <span class="number">class="num">767.19</span> tokens per second)
llama_print_timings: prompt <span class="built_in">eval</span> time =&nbsp;&nbsp;&nbsp;&nbsp;<span class="number">class="num">1890.97</span> ms /&nbsp;&nbsp;&nbsp;&nbsp; <span class="number">class="num">6</span> tokens(&nbsp;&nbsp;<span class="number">class="num">315.16</span> ms per token,&nbsp;&nbsp;&nbsp;&nbsp; <span class="number">class="num">3.17</span> tokens per second)
llama_print_timings:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="built_in">eval</span> time =&nbsp;&nbsp; <span class="number">class="num">32868.44</span> ms /&nbsp;&nbsp;&nbsp;&nbsp;<span class="number">class="num">63</span> runs&nbsp;&nbsp; (&nbsp;&nbsp;<span class="number">class="num">521.72</span> ms per token,&nbsp;&nbsp;&nbsp;&nbsp; <span class="number">class="num">1.92</span> tokens per second)
llama_print_timings:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; total time =&nbsp;&nbsp; <span class="number">class="num">35799.69</span> ms
←[<span class="number">class="num">32</span>mINFO←[<span class="number">class="num">0</span>m:&nbsp;&nbsp;&nbsp;&nbsp; <span class="number">class="num">127.0</span><span class="number">.class="num">0</span><span class="number">.class="num">1</span>:<span class="number">class="num">52769</span> - <span class="class="type">class="kw">string">"←[1mPOST /insights HTTP/class="num">1.1←[0m"</span> ←[<span class="number">class="num">32</span>m200 OK←[<span class="number">class="num">0</span>m
Llama.generate: prefix-<span class="keyword">match</span> hit
llama_print_timings:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;load time =&nbsp;&nbsp;<span class="number">class="num">206235.75</span> ms
llama_print_timings:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;sample time =&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="number">class="num">51.40</span> ms /&nbsp;&nbsp;&nbsp;&nbsp;<span class="number">class="num">64</span> runs&nbsp;&nbsp; (&nbsp;&nbsp;&nbsp;&nbsp;<span class="number">class="num">0.80</span> ms per token,&nbsp;&nbsp;<span class="number">class="num">1245.21</span> tokens per second)
llama_print_timings: prompt <span class="built_in">eval</span> time =&nbsp;&nbsp;&nbsp;&nbsp;<span class="number">class="num">1546.64</span> ms /&nbsp;&nbsp;&nbsp;&nbsp; <span class="number">class="num">4</span> tokens(&nbsp;&nbsp;<span class="number">class="num">386.66</span> ms per token,&nbsp;&nbsp;&nbsp;&nbsp; <span class="number">class="num">2.59</span> tokens per second)

本地推理日志里的耗时分布

把 llama.cpp 跑在本地给 MT5 做行情洞察接口时,日志里最能说明瓶颈的是 eval time 与 load time。三次连续请求里 eval time 分别是 29878.89 ms、31295.30 ms、29311.62 ms,每次都是 63 个 token 生成,折算每 token 约 465~497 ms,生成速度仅 2.01~2.15 tokens/s。 load time 三次都固定在 206235.75 ms,说明模型只加载一次后常驻,后续请求不再重复载入,这部分对单次延迟没有叠加影响。prompt eval time 随输入 token 数在 1487~1841 ms 间波动,4~6 个 token 的提示词就要 300~415 ms/token,短 prompt 的预处理并不轻。 sample time 反而极快,三次分别为 65.92 ms、55.34 ms、58.01 ms 对应 64 runs,单 token 采样不到 1.1 ms,吞吐高达 970~1156 tokens/s,证明解码采样不是制约点。prefix-match hit 反复出现,意味着相似系统提示被 KV 缓存命中,能省掉部分 prompt 重算。 真要压低 MT5 端等待,重点应砍 eval 阶段的层数或量化等级,而不是去优化采样。外汇与贵金属波动快,这类本地 LLM 辅助判断仅作概率参考,实盘仍属高风险。

常见问题

先确认跑马灯更新用了独立定时器而非主循环阻塞调用;在日志打印每次刷新触发时间与结束时间,若间隔忽长忽断就是生命周期没回收干净。
把推理丢到异步线程或定时空闲槽执行,UI 渲染走主线程;对比开启/关闭本地推理时的帧率与鼠标响应,差值即推理占用。
可以,小布能直接读取 EA 输出的本地推理日志,按耗时分段标出瓶颈环节,你打开对应品种页就能看到分布图。
实测 4~6 秒切换一次最不易漏读;可在定时器里用变量控制间隔,别低于 3 秒,否则贵金属高波动期易误读。
先看该步是否依赖行情拉取,若是则多为数据延迟;若纯计算步翻倍,重启本地推理进程并核对显存占用,外汇贵金属高风险时段资源易抢。