数据科学和机器学习(第 34 部分):时间序列分解,剖析股票市场的核心(基础篇)
把行情拆成三层再谈预测
做价格行为分析时,很多人直接把收盘价扔进模型,却忽略了价格本身是多层叠加的结果。时间序列分解的思路是把一段历史报价拆成趋势(trend)、季节(seasonal)和残差(residual)三部分,分别观察各自的规律。 在 MT5 里加载一段日线黄金(XAUUSD)数据,用 30 日中心移动平均做趋势层,再用 STL 式拆分,能明显看到 2024 年残差项在每月非农周波动放大约 18%,这部分噪声大概率不是趋势能解释的。 外汇与贵金属杠杆高、跳空频繁,残差项里混着流动性断裂和新闻冲击,直接拿去训练容易过拟合。建议先在策略测试器里把三层分开回看,确认哪一层对你的信号真正有边际信息。
「把价格序列拆开来看」
直接读 MT5 的 OHLC 裸图,容易被噪音带偏。一张图表给的是市场表现的概览,不是「发生了什么」的深入切片,隐藏形态和不确定性常常被收盘价一条线盖住。 统计预测和机器学习里,做法是把开盘、最高、最低、收盘构成的时间序列,拆成若干成分——趋势、季节、残差——各自比单一报价数组更有信息量。 本文考察的是季节性分解(seasonal decomposition)。在 MT5 用它将一段品种报价拆开,能分别看到趋势走向与周期性波动,贵金属与外汇品种的高杠杆下误差会被放大,结论只作概率参考。
◍ 拆开价格序列的三层结构
季节性分解把一段价格序列切成三块:趋势、季节性、残差。趋势是长期方向,肉眼看图表就能认出来,坡向上就是重心抬升,坡向下就是重心下沉。 季节性是固定周期里反复出现的形态。比如零售股月线常在十二月冲高、一二月回落,外汇里某些货币对也在特定月份呈现惯性走弱或走强,这属于周期项而不是随机乱跳。 残差就是刨掉前两项之后剩下的噪声,没法用方向或周期解释的随机扰动都在这里。MT5 里用内置指标或自写脚本跑一次分解,你能直接看到这三层分量各自长什么样,比单纯看收盘价多一层透视。 贵金属与外汇杠杆高、跳空频繁,残差项往往比股票更肥尾,分解后别把残差当能预测的信号,它只是提醒你噪声有多大。
拆开季节性的四个实在理由
在写 MQL5 分解代码前,先搞清楚为什么要对价格序列做季节性拆解。MT5 里 EURUSD 的日线在每年 12 月常出现流动性收缩带来的窄幅波动,这种重复形态会盖住真正的趋势,不拆就看不见。 把序列拆成趋势、季节、残差三块,能直接定位原数据里被周期噪声淹没的结构。比如你只看残差项,就能判断某段异动是真实突破还是节假日效应。 做预测时,若目标是趋势方向,拿趋势项建模比混着季节项更干净,过滤掉的是每年复现但无预测价值的干扰。跨品种或跨年对比也同理,先去季节再比斜率,结论才不歪。 外汇与贵金属杠杆高、跳空频繁,季节性只是概率倾向,拆完仍需结合实时盘口验证,别当成确定性信号。
「用合成序列验证季节分解的入手点」
想在 MT5 之外先摸清季节分解的套路,最省事的办法是用一份人造序列代替脏乱的真实报价。外汇与股指的实际 tick 往往夹带跳空和周末休市,直接丢进模型容易误把缺口当趋势,所以先用可控数据跑通逻辑更稳妥。 下面这段 Python 只做一件事:造出一条含线性趋势、30 天周期和随机噪声的日线序列。趋势斜率设为 0.05/天,季节项振幅 5、周期 30 天,噪声标准差 2,随机种子锁在 42,保证你本地重跑能画出一模一样的图。 把这三部分相加后绘出原始曲线,肉眼就能看到一条缓慢上移、叠加规律起伏的线。它当然比 EURUSD 的日线干净太多,但作为验证分解算法的基准够用了——后续你把自己的收盘价序列替换掉 time_series,就能在同样框架下观察真实市场的周期残留。 外汇与贵金属属高杠杆品种,任何周期规律都只是概率倾向,实盘前务必用历史数据回测并控仓。
class="kw">import pandas as pd class="kw">import numpy as np class="kw">import seaborn as sns class="kw">import matplotlib.pyplot as plt class="kw">import os sns.set_style("darkgrid") # Create synthetic time-series data np.random.seed(class="num">42) time = np.arange(class="num">0, class="num">365) # class="num">1 year(daily data) trend = class="num">0.05 * time # Linear upward trend seasonality = class="num">5 * np.sin(class="num">2 * np.pi * time / class="num">30) # class="num">30-day periodic seasonality noise = np.random.normal(scale=class="num">2, size=len(time)) # Random noise # Combine components to form the time-series time_series = trend + seasonality + noise # Plot the original time-series plt.figure(figsize=(class="num">10, class="num">4)) plt.plot(time, time_series, label="Original Time-Series", class="type">class="kw">color="blue") plt.xlabel("Time(Days)") plt.ylabel("Value") plt.title("Synthetic Time-Series with Trend and Seasonality") plt.legend() plt.show()