数据科学与机器学习(第24部分):使用常规AI模型进行外汇时间序列预测(基础篇)
◍ 时间序列预测在外汇里的位置
时间序列预测,本质是用过去按时间排序的观测值去推断后续走势。放到外汇和贵金属市场,它常被用来处理分钟级或日线级的报价序列,这类品种波动受宏观与流动性叠加影响,高杠杆下试错成本极高,任何模型输出都只能当作概率参考。 原文给出的系列目录显示,从扩展迪基-福勒检验、LightGBM 回归与分类,到导出 ONNX 再接 EA 回测,是一条完整链路。其中明确提到在策略测试器中验证模型,说明离线训练与 MT5 实盘仿真必须分开走,不能直接信训练集表现。 对读者来说,第一步能做的不是调模型,而是打开 MT5 自带的 EURUSD 小时图,手动拉一段 2024 年数据,先确认自己能不能复现「平稳性检验」这一步,再谈后面的 LightGBM。
「时间序列预测里那两个绕不开的变量」
时间序列预测说白了,就是拿按时间排好的历史数据去推下一步可能的值。序列之所以叫时间序列,是因为每个样本都绑在一个时间点上,顺序不能乱。 做这类分析,特征可以堆很多,但有两个变量缺一个都不行:一个是时间,作为自变量标记观测发生的时刻;另一个是目标变量,也就是你想根据过往去估的那个值,比如日线收盘价、每小时温度或每分钟访问量。 它的用处不只在猜未来。借历史里的模式和趋势,你还能反推过去行为、做基于过往的规划,以及回头评估当下结果。外汇与贵金属行情属高风险序列,历史模式失效概率不低,拿来预测前先在 MT5 历史数据里跑一遍再说。
时间序列模型为何更贴外汇行情
线性回归、SVM、神经网络这类模型在拟合时把样本当作相互独立的特征组合,靠学到变量间关系去外推;时间序列模型反过来,直接用过去观测到的价格序列本身去推下一步,顺序和时滞才是它的输入。对外汇、贵金属这种高频连续品种,5分钟前EURUSD的一条新闻冲击会直接写进当前K线,传统模型若不做特征工程就会丢掉这种因果链,而ARIMA、LSTM天生带着时间依赖性。 拿具体能力比对:ARIMA、SARIMA内建趋势与季节性分量,LSTM能显式建模自相关,且序列模型支持新数据增量更新,不用全量重训;传统模型要捕捉同样结构,得手动造滞后项、滚动统计特征,复杂度陡增。下表式差异里最硬的一点是——时间序列法在时间相关任务上预测表现通常更好,而传统法容易翻车。 不过LightGBM、XGBoost、CatBoost并非不能碰时间序列,只是默认不擅长。关键在操作端:你把lag特征、滚动均值、时间分箱喂足,它们也能跑。外汇与贵金属杠杆高、跳空频繁,任何模型输出都只是概率倾向,开MT5接真实tick回测前别当真理。
◍ 把K线拆成可喂模型的滞后与滚动特征
做时间序列预测,核心不是堆指标,而是把价格序列里藏着的趋势、季节、自相关先抽出来变成列。原始 OHLC 只描述当前 bar,模型看不到「前一天发生了什么」,所以第一步通常是引滞后项。 下面这段 MQL5 把日线_open/high/low/close 各取了 lag1~lag3 三档:从 start_bar+1 开始拷贝,得到的向量比主序列早一个 bar,1000 根日线就能白捡前 3 天信息。注意 start_bar 输入设为 2,注释里写明必须 ≥1,否则越界拷贝会直接报错。 //--- getting Open, high, low and close prices ohlc_struct OHLC; OHLC.AddCopyRates(Symbol(), timeframe, start_bar, bars); time_vector.CopyRates(Symbol(), timeframe, COPY_RATES_TIME, start_bar, bars); //--- Getting the lagged values of Open, High, low and close prices ohlc_struct lag_1; lag_1.AddCopyRates(Symbol(), timeframe, start_bar+1, bars); ohlc_struct lag_2; lag_2.AddCopyRates(Symbol(), timeframe, start_bar+2, bars); ohlc_struct lag_3; lag_3.AddCopyRates(Symbol(), timeframe, start_bar+3, bars); input int bars = 1000; input ENUM_TIMEFRAMES timeframe = PERIOD_D1; input uint start_bar = 2; //StartBar|Must be >= 1 struct ohlc_struct { vector open; vector high; vector low; vector close; matrix MATRIX; //this stores all the vectors all-together void AddCopyRates(string symbol, ENUM_TIMEFRAMES tf, ulong start, ulong size) { open.CopyRates(symbol, tf, COPY_RATES_OPEN, start, size); high.CopyRates(symbol, tf, COPY_RATES_HIGH, start, size); low.CopyRates(symbol, tf, COPY_RATES_LOW, start, size); close.CopyRates(symbol, tf, COPY_RATES_CLOSE, start, size); this.MATRIX.Resize(open.Size(), 4); //we resize it to match one of the vector since all vectors are of the same size this.MATRIX.Col(open, 0); this.MATRIX.Col(high, 1); this.MATRIX.Col(low, 2); this.MATRIX.Col(close, 3); } }; int ma_handle = iMA(Symbol(),timeframe,30,0,MODE_SMA,PRICE_WEIGHTED); //The Moving averaege for 30 days int stddev = iStdDev(Symbol(), timeframe, 7,0,MODE_SMA,PRICE_WEIGHTED); //The standard deviation for 7 days vector SMA_BUFF, STDDEV_BUFF; SMA_BUFF.CopyIndicatorBuffer(ma_handle,0,start_bar, bars); 逐行拆一下:OHLC.AddCopyRates 用 CopyRates 拉主序列;lag_1 到 lag_3 各自偏移 1~3 个 bar 拷贝,形成滞后特征。ohlc_struct 里用 matrix 把四个向量按列拼起来,Resize(open.Size(),4) 保证行数一致。后面 iMA(...,30,...) 是 30 日加权 SMA,iStdDev(...,7,...) 是 7 日标准差,CopyIndicatorBuffer 把均线缓冲读进 SMA_BUFF——这就是滚动统计特征,概括近期波动与方向。 滞后项抓邻近依赖,滚动统计(如 30 日 SMA、7 日 StdDev)补长期波动画像,两者叠加后示例数据集已到 26 个自变量。外汇和贵金属杠杆高、跳空频繁,这类特征只在历史样本里「可能」提升拟合,实盘前务必用 MT5 策略测试器回测验证。
class=class="str">"cmt">//--- getting Open, high, low and close prices ohlc_struct OHLC; OHLC.AddCopyRates(Symbol(), timeframe, start_bar, bars); time_vector.CopyRates(Symbol(), timeframe, COPY_RATES_TIME, start_bar, bars); class=class="str">"cmt">//--- Getting the lagged values of Open, High, low and close prices ohlc_struct lag_1; lag_1.AddCopyRates(Symbol(), timeframe, start_bar+class="num">1, bars); ohlc_struct lag_2; lag_2.AddCopyRates(Symbol(), timeframe, start_bar+class="num">2, bars); ohlc_struct lag_3; lag_3.AddCopyRates(Symbol(), timeframe, start_bar+class="num">3, bars); input class="type">int bars = class="num">1000; input ENUM_TIMEFRAMES timeframe = PERIOD_D1; input class="type">uint start_bar = class="num">2; class=class="str">"cmt">//StartBar|Must be >= class="num">1 class="kw">struct ohlc_struct { vector open; vector high; vector low; vector close; matrix MATRIX; class=class="str">"cmt">//this stores all the vectors all-together class="type">void AddCopyRates(class="type">class="kw">string symbol, ENUM_TIMEFRAMES tf, class="type">ulong start, class="type">ulong size) { open.CopyRates(symbol, tf, COPY_RATES_OPEN, start, size); high.CopyRates(symbol, tf, COPY_RATES_HIGH, start, size); low.CopyRates(symbol, tf, COPY_RATES_LOW, start, size); close.CopyRates(symbol, tf, COPY_RATES_CLOSE, start, size); this.MATRIX.Resize(open.Size(), class="num">4); class=class="str">"cmt">//we resize it to match one of the vector since all vectors are of the same size this.MATRIX.Col(open, class="num">0); this.MATRIX.Col(high, class="num">1); this.MATRIX.Col(low, class="num">2); this.MATRIX.Col(close, class="num">3); } }; class="type">int ma_handle = iMA(Symbol(),timeframe,class="num">30,class="num">0,MODE_SMA,PRICE_WEIGHTED); class=class="str">"cmt">//The Moving averaege for class="num">30 days class="type">int stddev = iStdDev(Symbol(), timeframe, class="num">7,class="num">0,MODE_SMA,PRICE_WEIGHTED); class=class="str">"cmt">//The standard deviation for class="num">7 days vector SMA_BUFF, STDDEV_BUFF; SMA_BUFF.CopyIndicatorBuffer(ma_handle,class="num">0,start_bar, bars);
「把K线时间拆成可训练的日历特征」
做完标准差与OHLC向量的拷贝后,真正让模型吃到“时间维度”的,是把每根Bar的秒级时间转成结构化日历字段。下面这段逻辑直接塞进vector,后面拼特征矩阵时能当独立列用。 先通过CopyRates拉时间向量,注意用的是COPY_RATES_TIME,拿到的是自1970年以来的秒数;再循环把秒数转datetime、转string、再StringToTime回datetime交给TimeToStruct,拆出day / day_of_week / day_of_year / mon四个分量。绕这一圈是因为MQL5里vector不直接吃MqlDateTime,只能借字符串做桥。 [CODE] STDDEV_BUFF.CopyIndicatorBuffer(stddev, 0, start_bar, bars); vector time_vector; //we want to add time vector time_vector.CopyRates(Symbol(), timeframe, COPY_RATES_TIME, start_bar, bars); //copy the time in seconds ulong size = time_vector.Size(); vector DAY(size), DAYOFWEEK(size), DAYOFYEAR(size), MONTH(size); MqlDateTime time_struct; string time = ""; for (ulong i=0; i<size; i++) { time = (string)datetime(time_vector[i]); //converting the data from seconds to date then to string TimeToStruct((datetime)StringToTime(time), time_struct); //convering the string time to date then assigning them to a structure DAY[i] = time_struct.day; DAYOFWEEK[i] = time_struct.day_of_week; DAYOFYEAR[i] = time_struct.day_of_year; MONTH[i] = time_struct.mon; } vector diff_lag_1_open = OHLC.open - lag_1.open; vector diff_lag_1_high = OHLC.high - lag_1.high; vector diff_lag_1_low = OHLC.low - lag_1.low; vector diff_lag_1_close = OHLC.close - lag_1.close; vector TARGET_CLOSE; TARGET_CLOSE.CopyRates(Symbol(), timeframe, COPY_RATES_CLOSE, start_bar-1, bars); //one bar forward [/CODE] 滞后一Bar的价格差分(open/high/low/close相对前一根)和前向一根的收盘TARGET_CLOSE也一并向量化:TARGET_CLOSE的起始位是start_bar-1、长度bars,等于把“下一根收盘”当标签列。外汇与贵金属杠杆高、滑点跳空频繁,这种标签在实盘外推时胜率可能明显低于样本内,开MT5用策略测试器跑一遍EURUSD的M15才能看清偏差。 日历特征加滞后差分这套拼法,本质是把非平稳序列转成近似平稳的输入;若你换到XAUUSD,day_of_week在周一圈跳空段的权重倾向异常,建议单独mask掉再训练。
STDDEV_BUFF.CopyIndicatorBuffer(stddev, class="num">0, start_bar, bars); vector time_vector; class=class="str">"cmt">//we want to add time vector time_vector.CopyRates(Symbol(), timeframe, COPY_RATES_TIME, start_bar, bars); class=class="str">"cmt">//copy the time in seconds class="type">ulong size = time_vector.Size(); vector DAY(size), DAYOFWEEK(size), DAYOFYEAR(size), MONTH(size); class="type">MqlDateTime time_struct; class="type">class="kw">string time = ""; for (class="type">ulong i=class="num">0; i<size; i++) { time = (class="type">class="kw">string)class="type">class="kw">datetime(time_vector[i]); class=class="str">"cmt">//converting the data from seconds to date then to class="type">class="kw">string TimeToStruct((class="type">class="kw">datetime)StringToTime(time), time_struct); class=class="str">"cmt">//convering the class="type">class="kw">string time to date then assigning them to a structure DAY[i] = time_struct.day; DAYOFWEEK[i] = time_struct.day_of_week; DAYOFYEAR[i] = time_struct.day_of_year; MONTH[i] = time_struct.mon; } vector diff_lag_1_open = OHLC.open - lag_1.open; vector diff_lag_1_high = OHLC.high - lag_1.high; vector diff_lag_1_low = OHLC.low - lag_1.low; vector diff_lag_1_close = OHLC.close - lag_1.close; vector TARGET_CLOSE; TARGET_CLOSE.CopyRates(Symbol(), timeframe, COPY_RATES_CLOSE, start_bar-class="num">1, bars); class=class="str">"cmt">//one bar forward