从新手到专家:使用 MQL5 制作动画新闻标题(四) 本地托管 AI 模型市场洞察·进阶篇
给 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 权限。
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 切周期会残留不可见对象。外汇与贵金属行情受突发数据影响大,这类叠加层仅作信息辅助,实际下单仍应以价格结构为准,相关风险偏高。
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 通道会一直空等超时。
<span style="background-class="type">color:rgb(class="num">249, class="num">204, class="num">202);">llama_print_timings: load time = <span class="number">class="num">206235.75</span> ms llama_print_timings: sample time = <span class="number">class="num">58.01</span> ms / <span class="number">class="num">64</span> runs ( <span class="number">class="num">0.91</span> ms per token, <span class="number">class="num">1103.33</span> tokens per second) llama_print_timings: prompt <span class="built_in">eval</span> time = <span class="number">class="num">1487.17</span> ms / <span class="number">class="num">4</span> tokens( <span class="number">class="num">371.79</span> ms per token, <span class="number">class="num">2.69</span> tokens per second) llama_print_timings: <span class="built_in">eval</span> time = <span class="number">class="num">29555.55</span> ms / <span class="number">class="num">63</span> runs ( <span class="number">class="num">469.14</span> ms per token, <span class="number">class="num">2.13</span> tokens per second) llama_print_timings: total time = <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: <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: load time = <span class="number">class="num">206235.75</span> ms llama_print_timings: sample time = <span class="number">class="num">83.42</span> ms / <span class="number">class="num">64</span> runs ( <span class="number">class="num">1.30</span> ms per token, <span class="number">class="num">767.19</span> tokens per second) llama_print_timings: prompt <span class="built_in">eval</span> time = <span class="number">class="num">1890.97</span> ms / <span class="number">class="num">6</span> tokens( <span class="number">class="num">315.16</span> ms per token, <span class="number">class="num">3.17</span> tokens per second) llama_print_timings: <span class="built_in">eval</span> time = <span class="number">class="num">32868.44</span> ms / <span class="number">class="num">63</span> runs ( <span class="number">class="num">521.72</span> ms per token, <span class="number">class="num">1.92</span> tokens per second) llama_print_timings: total time = <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: <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: load time = <span class="number">class="num">206235.75</span> ms llama_print_timings: sample time = <span class="number">class="num">51.40</span> ms / <span class="number">class="num">64</span> runs ( <span class="number">class="num">0.80</span> ms per token, <span class="number">class="num">1245.21</span> tokens per second) llama_print_timings: prompt <span class="built_in">eval</span> time = <span class="number">class="num">1546.64</span> ms / <span class="number">class="num">4</span> tokens( <span class="number">class="num">386.66</span> ms per token, <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 辅助判断仅作概率参考,实盘仍属高风险。