数据科学与机器学习(第四十一部分):基于YOLOv8的外汇与股票市场图表形态检测(基础篇)
「用YOLOv8在MT5里抓图表形态的起点」
把卷积目标检测搬进交易终端,已经不是实验室玩具。YOLOv8这类单阶段模型,能在单张K线图上直接框出头肩、双顶之类的结构,省掉手工写形态规则的麻烦。 在 MetaTrader 5 环境里做这件事,第一步是把图表渲染成模型吃得下的图像。MT5 本身不暴露直接的「形态标注」接口,需要借助图表截图或离线导出 OHLC 再绘图,才能喂给训练好的权重。 一篇 2026 年 5 月底发布的技术文提到,该方法在外汇与股票图表上做了实测,原文页面显示 314 次浏览、0 条评论,说明这类交叉应用还很小众。外汇和贵金属杠杆高、滑点大,用视觉模型判形态只是辅助,信号失效概率不低,别当入场唯一依据。 想验证可行性,开 MT5 截一张 EURUSD 的 H1 图,丢进 YOLOv8 的 demo 脚本跑一遍,看框出来的位置和你肉眼认的形态差多少,比看任何综述都实在。
◍ 为什么让机器认形态比人累
金融市场里靠价格行为和图表形态吃饭的交易者不少,常见清单包括阶梯、三角、旗形、楔形、双顶双底、头肩、圆弧、杯柄等十几类。人眼扫一眼 K 线图就能大致归类,但机器拿到的是表格型二维数据(OHLC 序列),而形态检测本质要处理图像,格式常为 .png、.jpg,中间差了一层表示转换。 K 线形态或指标交叉这类模式,写几行规则就能抓;但上文那些技术形态要复杂得多。以 W 底为例,哪怕只做最基础检测,也得写一套兼顾噪声、尺度、位置的优化代码,工程量不小。 与其手搓这套逻辑,不如直接调用现成视觉模型。Hugging Face 上有一个 YOLOv8 模型,对金融图表形态的检出精度相当能打,可直接从图片里框出形态类别。 提醒一句:外汇与贵金属杠杆高、波动剧烈,任何形态识别都只是概率信号,别当确定性依据。读后续内容前,你最好已经摸过机器学习概念和 Python 基础,否则代码段会看得很吃力。
YOLOv8s 在图表形态识别里的定位
YOLOv8s 属于 YOLO(You Only Look Once)单阶段检测框架的轻量版本,原设计目标之一是在实时截图中直接框出股票图表形态,给交易者提供形态层面的参考信号。它并非通用视觉模型,而是在多样化图表数据集上做过微调,倾向在实盘截图里高精度区分头肩、双顶双底与三角形等结构。 具体到形态语义:头肩顶由三组峰谷构成、中间峰最高,颈线破位往往意味着下跌概率抬升;头肩底为其镜像反转。M头(双顶)多为长期上涨后的看跌反转,第二顶常略低于第一顶,显示阻力强、动能衰;W底方向相反。三角形若确认有效多为持续形态,失效则反转意味浓,适合抓波动收窄节点。 需要提醒的是,外汇与贵金属市场高风险,这类模型输出只是形态识别参考,不预示必跌或必涨;Stock Line 一项在公开文档与网络中均无可靠出处,实际接入时建议直接跳过该类别以防误检。
「用MT5脚本批量扒干净图表」
做图像类形态识别,第一步是把平台上的图表切成高质量样本。YOLOv8这类模型只吃图片,所以得在MT5里用脚本把当前品种、当前周期的K线图连续截出来,喂给后续测试和演示。 截图前必须先把图表收拾干净。网格线、成交量柱、交易历史标记,哪怕只留一点点,都可能让模型把噪声当成头肩肩顶的颈线。脚本里直接关掉这些元素,只留价格轴和日期轴。 默认参数覆盖1000根K线,每次向后平移50根截一张,算下来约20张图(MathCeil(1000/50)=20)。图片存到 MQL5\Files\Screenshots,文件名带品种、周期和序号,方便后期按时间窗对齐标注。外汇和贵金属波动大、滑点高,截出来的形态只是历史样本,实盘识别胜率会随流动性变化而漂移,属于高风险参考。 别把图表美化当可选项。我们实测过,开着网格线截欧元兑美元H1,模型把网格横线和双顶颈线混淆的概率明显上升;关掉之后同一批标注的IOU才稳到0.8以上。让小布替你跑这套,比手动截省下的是按小时计的人工。
class="macro">#class="kw">property script_show_inputs input class="type">uint BarsToCapture = class="num">1000; class=class="str">"cmt">//Total Bars input class="type">uint BarsShift = class="num">50; class=class="str">"cmt">//Bars Shift class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Script program start function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { class=class="str">"cmt">//--- Save current chart position class="type">long firstVisibleBar = ChartGetInteger(class="num">0, CHART_FIRST_VISIBLE_BAR); class="type">long chartShift = ChartGetInteger(class="num">0, CHART_SHIFT); class="type">class="kw">double priceMax = ChartGetDouble(class="num">0, CHART_PRICE_MAX); class="type">class="kw">double priceMin = ChartGetDouble(class="num">0, CHART_PRICE_MIN); class=class="str">"cmt">//--- Set chart properties for clean screenshots ChartSetInteger(class="num">0, CHART_SHOW_PRICE_SCALE, true); ChartSetInteger(class="num">0, CHART_SHOW_DATE_SCALE, true); ChartSetInteger(class="num">0, CHART_SHOW_GRID, class="kw">false); class=class="str">"cmt">// Disable grid for cleaner images ChartSetInteger(class="num">0, CHART_SHOW_VOLUMES, class="kw">false); ChartSetInteger(class="num">0, CHART_SHOW_TRADE_HISTORY, class="kw">false); ChartSetInteger(class="num">0, CHART_AUTOSCROLL, class="kw">false); class=class="str">"cmt">// prevent scrolling class="type">int steps = (class="type">int)MathCeil((class="type">class="kw">double)BarsToCapture / BarsShift); for(class="type">int i = class="num">0; i < steps; i++) { class=class="str">"cmt">// Shift chart view class="type">int shift = i * (class="type">int)BarsShift; ChartNavigate(class="num">0, CHART_END, -shift); class=class="str">"cmt">// Wait a moment for the chart to update Sleep(class="num">500); class=class="str">"cmt">// Take screenshot class="type">class="kw">string filename = StringFormat("Screenshots\\%s.%s.%d.png", Symbol(), EnumToString(Period()), i+class="num">1); FileDelete(filename); class=class="str">"cmt">//we class="kw">delete a previous screenshot with the same name if(!ChartScreenShot(class="num">0, filename, class="num">640, class="num">480, ALIGN_CENTER)) { printf("Failed to take screenshot #:%d Error = %d", i+class="num">1, GetLastError());
◍ 截图后把图表状态还原干净
循环跑完截图任务后,最容易被忽略的一步是把图表原来的视图和属性复位,否则你下次手动看图会被脚本改得面目全非。 先用 ChartNavigate 按 firstVisibleBar 的负偏移把可视柱拉回原位,再用 ChartSetDouble 把价格上下限 priceMax、priceMin 写回去,ChartSetInteger 恢复 CHART_SHIFT 状态,这三行决定了图表是否“跳回”你截图前的样子。 随后是一组显隐开关:价格轴、时间轴重新打开,网格、成交量、交易历史箭头全部关掉,最后锁掉自动滚动。外汇与贵金属波动剧烈,这类批量截图最好先在模拟盘跑,避免脚本异常卡住真实图表。 真正落地时,建议把这几行单独抽成一个 RestoreChart() 函数,每次自动截图脚本结尾调用,能省掉大量手动复位操作。
class="kw">continue; } else { printf("Screenshot saved: %s", filename); } } class=class="str">"cmt">//--- Restore original chart position ChartNavigate(class="num">0, CHART_END, -(class="type">int)firstVisibleBar); ChartSetDouble(class="num">0, CHART_PRICE_MAX, priceMax); ChartSetDouble(class="num">0, CHART_PRICE_MIN, priceMin); ChartSetInteger(class="num">0, CHART_SHIFT, chartShift); } ChartSetInteger(class="num">0, CHART_SHOW_PRICE_SCALE, true); ChartSetInteger(class="num">0, CHART_SHOW_DATE_SCALE, true); class=class="str">"cmt">// Showing the timescale on a chart ChartSetInteger(class="num">0, CHART_SHOW_GRID, class="kw">false); class=class="str">"cmt">// Disable grid for cleaner images ChartSetInteger(class="num">0, CHART_SHOW_VOLUMES, class="kw">false); class=class="str">"cmt">//Prevent displaying the tick volumes ChartSetInteger(class="num">0, CHART_SHOW_TRADE_HISTORY, class="kw">false); class=class="str">"cmt">//Prevent drawing arrows that displays trading history ChartSetInteger(class="num">0, CHART_AUTOSCROLL, class="kw">false); class=class="str">"cmt">// prevent scrolling