English Русский Deutsch 日本語
preview
数据科学与机器学习(第四十五部分):利用Facebook的Prophet模型进行外汇时间序列预测

数据科学与机器学习(第四十五部分):利用Facebook的Prophet模型进行外汇时间序列预测

MetaTrader 5积分 |
23 0
Omega J Msigwa
Omega J Msigwa

内容


什么是Prophet模型?

Prophet模型是由Meta(原Facebook)推出的开源时间序列预测工具。该模型面向业务和分析用途,提供准确且用户友好的预测,尤其适用于具备显著季节性与趋势特征的时间序列数据。

该模型由Facebook团队(S.J. 泰勒、本杰明・莱瑟姆)于2018年发布,最初用于按日频记录的数据预测,可捕捉周度、年度季节性规律以及节假日带来的数据波动,后续经过拓展,能够适配更多类型的季节性时序数据。当时间序列存在明显季节性、且拥有多段历史周期数据时,该模型预测效果最优。

专业术语释义:

  • 趋势:趋势反映在排除季节性波动影响后数据的长期变化。 
  • 季节性:季节性指数据短期内周期性波动,这类短期变化不足以构成长期“趋势”。 

在本文中,我们将基于外汇行情数据解析并搭建Prophet模型,探究该模型是否有助于我们在市场中取得优势。在此之前,我们先详细拆解该模型的核心原理。


Prophet模型原理详解

Prophet模型本质为非线性回归模型,其计算公式如下:

  

图例01

其中: 

  •   代表分段线性趋势项(或称为“增长项”)
  •  代表各类季节性周期规律  
  •  用于刻画节假日效应,为白噪声误差项。

01:趋势分量

趋势分量支持突变点设置,如果不手动指定,则模型会自动筛选突变点。突变点指时间序列中趋势发生转折的位置(例如行情突然大幅上涨或下跌)。

您也可以选择使用逻辑增长模型替代线性模型,该模型引入上限参数,用于模拟增长饱和效应。当增长达到自然上限后增速放缓时,该模型十分适用。

02:季节性

Prophet模型采用傅里叶级数对季节性进行建模。

默认设置:

  • 年度季节性采用10阶傅里叶项
  • 周度季节性采用3阶傅里叶项

傅里叶项能够帮助模型捕捉周期性重复的季节波动规律。

03:节假日效应部分

节假日效应通过虚拟变量(独热编码)引入模型,使模型在特殊日期自动修正预测值 —— 这类日期在历史数据中往往会造成走势偏离常规。例如经济数据公布日或法定节假日。

整个模型基于贝叶斯框架完成参数估计,可自动筛选突变点并优化其余模型参数。

尽管这种基础加法分解模型形式简洁,但公式内各项的计算涉及大量复杂数学运算,因此,如果您不熟悉原理,很容易得到错误的预测结果。

Prophet提供两种建模方案:

  1. 分段线性增长模型(默认模型)
  2. 逻辑增长模型

01. 分段线性模型

这是Prophet默认使用的模型。该模型假设数据趋势呈线性变化,但会在特定时间点发生改变(这类点称为突变点)。适用于具有平稳增长或下降趋势、且可能出现突然转折的数据。

该建模方式对应图例01中的公式。

02. 逻辑增长模型

该模型适用于呈现饱和增长特征的数据:初期快速增长,在接近最大容量或上限后增速逐渐放缓。这类模式常见于存在自然上限或业务限制的实际系统中(例如市场趋于饱和时的用户增长)。

逻辑增长模型通过引入容量参数来定义这一增长上限。

用于表示该建模方式的公式如下:

 

图例02

其中:

 为容量上限,为增长率,为偏移参数。


在Python中实现Prophet模型

我们将使用欧元兑美元(EURUSD)小时图数据,通过该模型识别趋势、季节性并预测未来数值。

首先需要从requirements.txt文件安装所有依赖库 —— 该文件附在本文结尾

pip install -r requirements.txt

导入:

import pandas as pd
import numpy as np
import MetaTrader5 as mt5
import matplotlib.pyplot as plt
import seaborn as sns
from prophet import Prophet

plt.style.use('fivethirtyeight') 
sns.set_style("darkgrid")

让我们从MetaTrader 5中获取数据。

if not mt5.initialize(r"c:\Program Files\MetaTrader 5 IC Markets (SC)\terminal64.exe"):
    print("Failed to initialize MetaTrader5. Error = ",mt5.last_error())
    mt5.shutdown()      

symbol = "EURUSD"
timeframe = mt5.TIMEFRAME_H1

rates = mt5.copy_rates_from_pos(symbol, timeframe, 1, 10000)

if rates is None:
    print(f"Failed to copy rates for symbol={symbol}. MT5 Error = {mt5.last_error()}")

Prophet模型高度依赖日期时间时间戳特征。这一特征是模型正常运行的必要条件。 

从MetaTrader 5获取到行情数据(价格)后,我们将其转换为Pandas-DataFrame对象。接下来,把以秒为单位的时间列转换为日期时间格式。

rates_df = pd.DataFrame(rates) # we convert rates object to a dataframe
rates_df["time"] = pd.to_datetime(rates_df["time"], unit="s") # we convert the time from seconds to datatime

rates_df

输出:

时间 开盘价 最高价 最低价 收盘价 分时成交量 点差 真实成交量
0 2023-11-10 23:00:00 1.06849 1.06873 1.06826 1.06846 762 0 0
1 2023-11-13 00:00:00 1.06828 1.06853 1.06779 1.06841 1059 10 0
2 2023-11-13 01:00:00 1.06854 1.06907 1.06854 1.06906 571 0 0
3 2023-11-13 02:00:00 1.06904 1.06904 1.06822 1.06839 1053 0 0
4 2023-11-13 03:00:00 1.06840 1.06886 1.06811 1.06867 1204 0 0


Prophet模型属于单变量模型,在Pandas-DataFrame中仅需两个特征即可运行,例如,一个是名为"ds"的日期时间特征(时间戳),另一个是标记为"y"的目标变量

现在,我们从MetaTrader 5获取的数据中,提取出仅包含时间和波动率这两个特征的简易数据集。这就是我们后续要传入Prophet模型进行训练的数据。

prophet_df = pd.DataFrame({
    "time": rates_df["time"],
    "volatility": rates_df["high"] - rates_df["low"]
}).set_index("time")

prophet_df

波动率(计算方式为最高价与最低价之差)是我们的目标变量。

与之前提到的ARIMA、VAR等时间序列预测模型不同,那些模型要求目标变量是平稳序列,而Prophet模型则不受这一条件限制。它同样可以处理非平稳数据。不过,所有机器学习模型在平稳序列上通常表现更好,因为平稳序列具有恒定的均值、方差与标准差,更易于模型学习。

我这里选择使用平稳的目标变量来建模,能让整个过程更简单。

接下来,我们对数据框进行绘图,观察各项特征。

# Color pallete for plotting
color_pal = ["#F8766D", "#D39200", "#93AA00",
             "#00BA38", "#00C19F", "#00B9E3",
             "#619CFF", "#DB72FB"]

prophet_df.plot(figsize=(7,5), color=color_pal, title="Volatility (high-low) against time", ylabel="volatility", xlabel="time")

输出:

图例03

我们也可以选择构建特征X和目标变量y,用于分析时间特征对市场波动率的影响程度。

def create_features(df, label=None):
    """
    Creates time series features from datetime index.
    """
    
    df = df.copy()
    df['date'] = df.index
    df['hour'] = df['date'].dt.hour
    df['dayofweek'] = df['date'].dt.dayofweek
    df['quarter'] = df['date'].dt.quarter
    df['month'] = df['date'].dt.month
    df['year'] = df['date'].dt.year
    df['dayofyear'] = df['date'].dt.dayofyear
    df['dayofmonth'] = df['date'].dt.day
    df['weekofyear'] = df['date'].dt.isocalendar().week
    
    X = df[['hour','dayofweek','quarter','month','year',
           'dayofyear','dayofmonth','weekofyear']]
    if label:
        y = df[label]
        return X, y
    return X

X, y = create_features(prophet_df, label='volatility')

features_and_target = pd.concat([X, y], axis=1)

输出:

小时 周内天数 季度 月份 年份 年内天数 月内天数 年内周数 波动率
时间
2023-11-13 16:00:00 16 0 4 11 2023 317 13 46 0.00122
2023-11-13 17:00:00 17 0 4 11 2023 317 13 46 0.00179
2023-11-13 18:00:00 18 0 4 11 2023 317 13 46 0.00186
2023-11-13 19:00:00 19 0 4 11 2023 317 13 46 0.00125
2023-11-13 20:00:00 20 0 4 11 2023 317 13 46 0.00150


我们可以将这些特征与波动率一同绘图,以便进行直观分析。

sns.pairplot(features_and_target.dropna(),
             hue='hour',
             x_vars=['hour','dayofweek',
                     'year','weekofyear'],
             y_vars='volatility',
             height=5,
             plot_kws={'alpha':0.45, 'linewidth':0.5}
            )

plt.suptitle(f"{symbol} close prices by Hour, Day of Week, Year, and Week")
plt.show()

输出:


图例04

由子图可见,小时、星期几、年份以及一年中的第几周,都会对图表中每小时的市场波动率产生影响。了解这一点后,我们就可以更有把握地使用这份数据来训练Prophet模型。

训练Prophet模型

首先,我们按指定日期将数据划分为训练集和测试集。

split_date = '01-Jan-2025' # threshold date between training and testing samples, all values after this date are for testing 

prophet_df_train = prophet_df.loc[prophet_df.index <= split_date].copy().reset_index().rename(columns={"time": "ds", "volatility": "y"})
prophet_df_test = prophet_df.loc[prophet_df.index > split_date].copy().reset_index().rename(columns={"time": "ds", "volatility": "y"})

在训练数据上训练Prophet模型。

model = Prophet()
model.fit(prophet_df_train)

模型训练完成后,我们通常需要在样本外数据(模型从未见过的新数据)上测试其效果。与其他模型不同,Prophet生成预测结果的方式略有区别。

test_fcst = model.predict(df=prophet_df_test)

该模型不会只返回一个包含预测值的向量,而是会返回一整个DataFrame,其中包含各类代表预测结果和模型状态的特征。

test_fcst.head()

输出:

	ds      trend   	yhat_lower      yhat_upper      trend_lower     trend_upper     additive_terms  additive_terms_lower    additive_terms_upper    daily   daily_lower     daily_upper     weekly  weekly_lower    weekly_upper    multiplicative_terms    multiplicative_terms_lower      multiplicative_terms_upper      yhat
0       2025-01-02 00:00:00     0.001674        0.000168        0.001993        0.001674        0.001674        -0.000571       -0.000571       -0.000571       -0.000510       -0.000510       -0.000510       -0.000061       -0.000061       -0.000061       0.0     0.0     0.0     0.001102
1       2025-01-02 01:00:00     0.001674        0.000161        0.001977        0.001674        0.001674        -0.000614       -0.000614       -0.000614       -0.000556       -0.000556       -0.000556       -0.000057       -0.000057       -0.000057       0.0     0.0     0.0     0.001060
2       2025-01-02 02:00:00     0.001674        0.000337        0.002123        0.001674        0.001674        -0.000483       -0.000483       -0.000483       -0.000430       -0.000430       -0.000430       -0.000054       -0.000054       -0.000054       0.0     0.0     0.0     0.001191

下表列出了predict方法返回的部分列(特征)的含义。

含义
ds 预测点的日期时间(时间戳)
yhat 最终预测值(Prophet在该时间点的预测结果)
yhat_lower, yhat_upper 预测值yhat对应的80%(或95%)置信区间的下限与上限
trend 时间点ds对应的趋势分量值(例如随时间缓慢增长或下降)
trend_lower, trend_upper 趋势分量的置信区间
additive_terms 时间点ds处所有季节性分量与节假日分量之和(如日内波动 + 周内规律 + 节假日影响)。
additive_terms_lower, additive_terms_upper 加法分量的上下界
daily 日内季节性效应(如一天内的小时级波动规律)
daily_lower, daily_upper 日内分量的置信区间
weekly 周度季节性效应(如周末与工作日的波动差异)
weekly_lower, weekly_upper 周度分量的置信区间

我们最需要的列是:yhat(预测值)、yhat_lower(预测下限)、yhat_upper(预测上限)、trend(趋势)、季节性规律(daily日内、weekly周内、yearly年度)、holidays(节假日,如果启用),以及各分量的误差区间(*_lower*_upper)列。

让我们绘制测试集上的真实值与预测值曲线,并同时展示训练集的真实值作为参考。

f, ax = plt.subplots(figsize=(7,5))
ax.scatter(prophet_df_test["ds"], prophet_df_test['y'], color='r') # plot actual values from the testing sample in red

fig = model.plot(test_fcst, ax=ax) # plot the forecasts 

输出图:

图例05

黑色曲线代表训练集数据,红色曲线为测试集真实值,蓝色曲线则是模型对测试集的预测结果。

仅通过此图表很难直观判断模型的效果。让我们来绘制小图,专门展示测试集的真实值与预测值。

接下来以测试集中的第一个月份 —— 2025年1月为例,对模型进行评估。

f, ax = plt.subplots(figsize=(7, 5))

ax.scatter(prophet_df_test["ds"], prophet_df_test['y'], color='r')
fig = model.plot(test_fcst, ax=ax)
ax.set_xbound(
            lower=pd.to_datetime("2025-01-01"), # starting data on the x axis
            upper=pd.to_datetime("2025-02-01")) # ending data on the x axis

ax.set_ylim(0, 0.005)
plot = plt.suptitle("January 2025, Actual vs Forecasts")

输出:


图例06

由上图可见,Prophet模型的部分预测是准确的,但似乎不太擅长处理异常值。

我们也可以进一步分析预测效果,对比1月第一周(1月1日至1月8日)的真实值与模型预测值。

f, ax = plt.subplots(figsize=(9, 5))

ax.scatter(prophet_df_test["ds"], prophet_df_test['y'], color='r')
fig = model.plot(test_fcst, ax=ax)
ax.set_xbound(
            lower=pd.to_datetime("2025-01-01"),
            upper=pd.to_datetime("2025-01-08"))

ax.set_ylim(0, 0.005)
plot = plt.suptitle("January 01-08, 2025. Actual vs Forecasts")

输出:

图例07

看起来效果要好不少。然而,尽管模型捕捉到了部分规律,但其预测值与真实值仍有一定差距,而这正是我们使用回归模型时通常希望尽可能缩小的误差。

不过它的概括性预测还算可以。

让我们通过一些评估指标来量化评价模型效果。

import sklearn.metrics as metric

def forecast_accuracy(forecast, actual):
    # Convert to numpy arrays if they aren't already
    forecast = np.asarray(forecast)
    actual = np.asarray(actual)
    
    metrics = {
        'mape': metric.mean_absolute_percentage_error(actual, forecast),
        'me': np.mean(forecast - actual),  # Mean Error
        'mae': metric.mean_absolute_error(actual, forecast),
        'mpe': np.mean((forecast - actual) / actual),  # Mean Percentage Error
        'rmse': metric.root_mean_squared_error(actual, forecast),
        'minmax': 1 - np.mean(np.minimum(forecast, actual) / np.maximum(forecast, actual)),
        "r2_score": metric.r2_score(forecast, actual)
    }
    return metrics

results = forecast_accuracy(test_pred, prophet_df_test["y"])

for metric_name, value in results.items():
    print(f"{metric_name:<10}: {value:.6f}")

输出:

mape      : 0.603277
me        : 0.000130
mae       : 0.000829
mpe       : 0.430299
rmse      : 0.001221
minmax    : 0.339292
r2_score  : -4.547775

我重点关注的评估指标是MAPE(平均绝对百分比误差)—— 该指标数值约为0.6,意味着模型的预测结果平均偏离真实值60%。简言之,模型预测效果很差,误差非常大。


为Prophet模型添加节假日

Prophet模型在设计时就考虑到,任何时间序列数据中都可能存在引发异常波动的事件,这类事件我们称之为“节假日”(holidays)。

在现实场景中,节假日往往会对业务数据产生不规则影响,其中包括:

  • 法定节假日(如元旦、圣诞节)
  • 商业活动(如黑色星期五、产品发布会)
  • 金融事件(如央行决议、季度末结算)
  • 地方性事件(如选举、极端天气)

这些日期并不遵循固定的季节性规律,但会以年、季度、日等周期重复出现。

在金融(交易)数据中,我们可以把财经新闻事件视作节假日,因为它们同样会造成上述的异常波动。通过这样处理,或许能改善模型当前的问题 —— 无法捕捉极端值。

图例01中的Prophet模型公式所示,加入节假日项(如果存在) 会让模型结构更完整,因为节假日是公式的核心组成部分之一。

话虽如此,我们还需要通过MQL5语言 来采集新闻数据。

文件名:OHLC + News.mq5

input datetime start_date = D'01.01.2023';
input datetime end_date = D'24.6.2025';
input ENUM_TIMEFRAMES timeframe = PERIOD_H1;

MqlRates rates[];
struct news_data_struct
  {   
    datetime time[]; //News release time
    double open[]; //Candle opening price
    double high[]; //Candle high price
    double low[]; //Candle low price
    double close[]; //Candle close price
    string name[]; //Name of the news
    ENUM_CALENDAR_EVENT_SECTOR sector[]; //The sector a news is related to
    ENUM_CALENDAR_EVENT_IMPORTANCE importance[]; //Event importance
    double actual[]; //actual value
    double forecast[]; //forecast value
    double previous[]; //previous value
    
    void Resize(uint size)
      {
          ArrayResize(time, size);
          ArrayResize(open, size);
          ArrayResize(high, size);
          ArrayResize(low, size);
          ArrayResize(close, size);
          ArrayResize(name, size);
          ArrayResize(sector, size);
          ArrayResize(importance, size);
          ArrayResize(actual, size);
          ArrayResize(forecast, size);
          ArrayResize(previous, size);
      }
    
  } news_data;
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---

   if (!ChartSetSymbolPeriod(0, Symbol(), timeframe))
      return;
   
   SaveNews(StringFormat("%s.%s.OHLC + News.csv",Symbol(),EnumToString(timeframe)));
  }
//+------------------------------------------------------------------+
//|                                                                  |
//|   The function which collects news alongsided OHLC values and    |
//|   saves the data to a CSV file                                   |
//|                                                                  |
//+------------------------------------------------------------------+
void SaveNews(string csv_name)
 {
//--- get OHLC values first
   
   ResetLastError();
   if (CopyRates(Symbol(), timeframe, start_date, end_date, rates)<=0)
     {
       printf("%s failed to get price information from %s to %s. Error = %d",__FUNCTION__,string(start_date),string(end_date),GetLastError());
       return;
     }
      
   uint size = rates.Size();   
   news_data.Resize(size-1);

//---

   FileDelete(csv_name); //Delete an existing csv file of a given name
   int csv_handle = FileOpen(csv_name,FILE_WRITE|FILE_SHARE_WRITE|FILE_CSV|FILE_ANSI|FILE_COMMON,",",CP_UTF8); //csv handle
   
   if(csv_handle == INVALID_HANDLE)
     {
       printf("Invalid %s handle Error %d ",csv_name,GetLastError());
       return; //stop the process
     }
     
   FileSeek(csv_handle,0,SEEK_SET); //go to file begining
   FileWrite(csv_handle,"Time,Open,High,Low,Close,Name,Sector,Importance,Actual,Forecast,Previous"); //write csv header
   
   MqlCalendarValue values[]; //https://www.mql5.com/en/docs/constants/structures/mqlcalendar#mqlcalendarvalue
   for (uint i=0; i<size-1; i++)
      {
         news_data.time[i] = rates[i].time;
         news_data.open[i] = rates[i].open;
         news_data.high[i] = rates[i].high;
         news_data.low[i] = rates[i].low;
         news_data.close[i] = rates[i].close;
         
         int all_news = CalendarValueHistory(values, rates[i].time, rates[i+1].time, NULL, NULL); //we obtain all the news with their values https://www.mql5.com/en/docs/calendar/calendarvaluehistory
         
         for (int n=0; n<all_news; n++)
            {
              MqlCalendarEvent event;
              CalendarEventById(values[n].event_id, event); //Here among all the news we select one after the other by its id https://www.mql5.com/en/docs/calendar/calendareventbyid
                   
              MqlCalendarCountry country; //The couhtry where the currency pair originates
              CalendarCountryById(event.country_id, country); //https://www.mql5.com/en/docs/calendar/calendarcountrybyid
                 
              if (StringFind(Symbol(), country.currency)>-1) //We want to ensure that we filter news that has nothing to do with the base and the quote currency for the current symbol pair
                { 
                     news_data.name[i] = event.name;  
                     news_data.sector[i] = event.sector;
                     news_data.importance[i] = event.importance;
                       
                     news_data.actual[i] = !MathIsValidNumber(values[n].GetActualValue()) ? 0 : values[n].GetActualValue();
                     news_data.forecast[i] = !MathIsValidNumber(values[n].GetForecastValue()) ? 0 : values[n].GetForecastValue();
                     news_data.previous[i] = !MathIsValidNumber(values[n].GetPreviousValue()) ? 0 : values[n].GetPreviousValue();
                }
            }
          
          FileWrite(csv_handle,StringFormat("%s,%f,%f,%f,%f,%s,%s,%s,%f,%f,%f",
                                 (string)news_data.time[i],
                                 news_data.open[i],
                                 news_data.high[i],
                                 news_data.low[i],
                                 news_data.close[i],
                                 news_data.name[i],
                                 EnumToString(news_data.sector[i]),
                                 EnumToString(news_data.importance[i]),
                                 news_data.actual[i],
                                 news_data.forecast[i],
                                 news_data.previous[i]
                               ));
       }  
//---

   FileClose(csv_handle);
 }

SaveNews函数中采集完新闻数据后,获取到的数据会以CSV格式保存至“公共路径”(文件夹)中。

在Python脚本中,我们再从同一目录下加载这份数据。

from Trade.TerminalInfo import CTerminalInfo
import os

terminal = CTerminalInfo()
data_path = os.path.join(terminal.common_data_path(), "Files")

timeframe = "PERIOD_H1"
df = pd.read_csv(os.path.join(data_path, f"{symbol}.{timeframe}.OHLC + News.csv"))

df

输出:

时间 开盘价 最高价 最低价 收盘价 名称 领域 重要性 实际值 预测值 先前值
0 2023.01.02 01:00:00 1.06967 1.06983 1.06927 1.06983 New Year's Day CALENDAR_SECTOR_HOLIDAYS CALENDAR_IMPORTANCE_NONE 0.0 0.0 0.0
1 2023.01.02 02:00:00 1.06984 1.07059 1.06914 1.07041 New Year's Day CALENDAR_SECTOR_HOLIDAYS CALENDAR_IMPORTANCE_NONE 0.0 0.0 0.0
2 2023.01.02 03:00:00 1.07059 1.07069 1.06858 1.06910 New Year's Day CALENDAR_SECTOR_HOLIDAYS CALENDAR_IMPORTANCE_NONE 0.0 0.0 0.0
3 2023.01.02 04:00:00 1.06909 1.06909 1.06828 1.06880 New Year's Day CALENDAR_SECTOR_HOLIDAYS CALENDAR_IMPORTANCE_NONE 0.0 0.0 0.0
4 2023.01.02 05:00:00 1.06881 1.07029 1.06880 1.06897 New Year's Day CALENDAR_SECTOR_HOLIDAYS CALENDAR_IMPORTANCE_NONE 0.0 0.0 0.0


由于我们在MQL5脚本中逐行采集新闻数据,最终新闻列中会出现部分标记为"(null)"的行,代表该时刻无新闻,我们需要过滤掉这些行

news_df = df[df['Name'] != "(null)"].copy()

和之前为模型构造只包含dsy两列的数据格式类似,节假日数据集也必须保证只有两列 —— dsholidayholiday列用于存放新闻事件名称。

holidays = news_df[['Time', 'Name']].rename(columns={
    'Time': 'ds',
    'Name': 'holiday'
})

holidays['ds'] = pd.to_datetime(holidays['ds'])  # Ensure datetime format
holidays

输出:

ds holiday
0 2023-01-02 01:00:00 New Year's Day
1 2023-01-02 02:00:00 New Year's Day
2 2023-01-02 03:00:00 New Year's Day
3 2023-01-02 04:00:00 New Year's Day
4 2023-01-02 05:00:00 New Year's Day


除这些字段外,节假日数据框还可以包含两个可选列:lower_windowupper_window。这两列用来告诉模型,每个节假日事件在发生之前和之后分别会产生多长时间的影响。

我们了解到,现实中任何节假日并不会只在当天产生影响,通常在其发生前后都会产生持续效应

holidays['lower_window'] = 0
holidays['upper_window'] = 1  # Extend effect to 1 hour after

lower_window列表示该节假日 / 事件在发生之前对时间序列产生的影响时长,而upper_window列表示在发生之后的影响时长。

  • lower_window的取值小于或等于0(≤0),默认值为0,表示该事件不会影响之前的数据。取值为-1表示该事件会影响其发生前的一个时间点数据,依此类推。
  • upper_window的取值大于或等于0(≥0),默认值为0,表示该事件不会影响之后的数据。取值为1表示该事件会影响其发生后的一个时间点数据,依此类推。

现在,让我们按照上述说明添加这些特征。

holidays['lower_window'] = -1 # The anticipation of the news affect the volatility 1 bar before it's release
holidays['upper_window'] = 1  # The news affects the volatility 1 bar after its release

holidays

我们的节假日数据框则变为:

ds holiday lower_window upper_window
0 2023-01-02 01:00:00 New Year's Day -1 1
1 2023-01-02 02:00:00 New Year's Day -1 1
2 2023-01-02 03:00:00 New Year's Day -1 1
3 2023-01-02 04:00:00 New Year's Day -1 1
4 2023-01-02 05:00:00 New Year's Day -1 1
... ... ... ... ...
15369 2025-06-20 18:00:00 Eurogroup Meeting -1 1
15370 2025-06-20 19:00:00 Eurogroup Meeting -1 1
15371 2025-06-20 20:00:00 Eurogroup Meeting -1 1
15372 2025-06-20 21:00:00 Eurogroup Meeting -1 1
15373 2025-06-20 22:00:00 Eurogroup Meeting -1 1


最后,我们将构造好的节假日数据集与之前准备好的训练数据一并传入Prophet模型。

model_w_holidays = Prophet(holidays=holidays)
model_w_holidays.fit(prophet_df_train)

和之前一样,我们可以绘制预测值与真实值的对比曲线,来测试加入节假日后的模型预测效果。

# Predict on training set with model
test_fcst = model_w_holidays.predict(df=prophet_df_test)

test_pred = test_fcst.yhat # We get the predictions

# Plot the forecast with the actuals

f, ax = plt.subplots(figsize=(10,5))
ax.scatter(prophet_df_test["ds"], prophet_df_test['y'], color='r')

fig = model_w_holidays.plot(test_fcst, ax=ax)

输出:

图例08

图例05中未加入新闻(节假日)的模型预测结果不同 —— 之前的预测曲线显得较为呆板,而加入了新闻事件的新模型,能够捕捉到一些原模型遗漏的波动。

我们再次使用与之前相同的评估指标,对该模型进行评测。

results = forecast_accuracy(test_pred, prophet_df_test["y"])

for metric_name, value in results.items():
    print(f"{metric_name:<10}: {value:.6f}")

输出:

mape      : 0.549152
me        : -0.000633
mae       : 0.000970
mpe       : -0.175082
rmse      : 0.001487
minmax    : 0.461444
r2_score  : -2.793478

MAPE指标显示,模型预测精度提升了约10%。之前的模型误差约为60%,而当前模型误差约为55%。这一提升在决定系数r2_score上也同样能体现。

误差仍有55%的模型依然不算理想,理想模型的误差至少要低于50%(< 50%),但我们仍可以通过优化节假日(新闻事件)的处理方式来进一步改进模型。

在本例中,我们将lower_windowupper_window分别固定设置为-1和1,表示新闻在发布前后各一个K线周期内影响市场波动。虽然这一设置让模型效果有所提升,但我仍质疑它并非最优方案。

我们了解到,不同新闻的影响时长和力度各不相同,对所有新闻使用固定值本质上是不合理的。此外,我们使用了全部新闻,包括重要性较低的消息,而交易者通常会忽略这类新闻 —— 因为它们出现过于频繁,且很难在图表上观测和衡量其影响。

要解决这两个问题,必须根据新闻类型及其历史可观测影响,动态设置lower_windowupper_window。 

示例伪代码如下:

def get_windows(name):
    if "CPI" in name:
        return (-1, 4) # CPI news affects one previous bar volatility, and it affects the volatility of four bars ahead (4 hours impact forward)
    elif "NFP" in name:
        return (-1, 2) # NFP news affects one previous bar volatility, and it affects the volatility of two bars ahead (2 hours impact afterward)
    elif "FOMC" in name or "Rate" in name:
        return (-2, 6) # NFP news affects two previous bar volatility, and it affects the volatility of six bars ahead (6 hours impact afterward)
    else:
        return (0, 1)  # Default


holidays[['lower_window', 'upper_window']] = holidays['holiday'].apply(
    lambda name: pd.Series(get_windows(name))
)

由于新闻的类型多达数万种,且必须确保所设置的影响数值准确可靠,这种实现方式难度极大,但的确是最理想的方案所以,这部分就当作课后练习吧。

就目前而言,最直接可行的做法是:对新闻进行筛选,只保留高重要性和中等重要性的新闻事件。

news_df = df[
    (df['Name'] != "(null)") & # Filter rows without news at all
    ((df['Importance'] == "CALENDAR_IMPORTANCE_HIGH") | (df['Importance'] == "CALENDAR_IMPORTANCE_MODERATE")) # Filter other news except high importance news
].copy()

news_df

输出:

时间 开盘价 最高价 最低价 收盘价 名称 领域 重要性 实际值 预测值 先前值
7 2023.01.02 08:00:00 1.06921 1.06973 1.06724 1.06858 S&P Global Manufacturing PMI CALENDAR_SECTOR_BUSINESS CALENDAR_IMPORTANCE_MODERATE 47.10 47.400 47.400
8 2023.01.02 09:00:00 1.06878 1.06909 1.06627 1.06784 S&P Global Manufacturing PMI CALENDAR_SECTOR_BUSINESS CALENDAR_IMPORTANCE_MODERATE 47.80 47.800 47.800
31 2023.01.03 08:00:00 1.06636 1.06677 1.06514 1.06524 Unemployment CALENDAR_SECTOR_JOBS CALENDAR_IMPORTANCE_MODERATE 2.52 2.522 2.538
37 2023.01.03 14:00:00 1.05283 1.05490 1.05241 1.05355 S&P Global Manufacturing PMI CALENDAR_SECTOR_BUSINESS CALENDAR_IMPORTANCE_HIGH 46.20 46.200 46.200
38 2023.01.03 15:00:00 1.05353 1.05698 1.05304 1.05602 Construction Spending m/m CALENDAR_SECTOR_HOUSING CALENDAR_IMPORTANCE_MODERATE 0.20 0.200 -0.300


看起来很不错。在将时间和名称列提取到节假日数据框后,我们再添加lower_windowupper_window的值。

holidays = news_df[['Time', 'Name']].rename(columns={
    'Time': 'ds',
    'Name': 'holiday'
})

holidays['ds'] = pd.to_datetime(holidays['ds'])  # Ensure datetime format

holidays['lower_window'] = 0
holidays['upper_window'] = 1 

holidays

模型训练完成后,下方图表中的黑色线条代表训练集真实值,红色线条为测试集真实值,蓝色线条为测试集对应的模型预测结果。

图例09

模型效果得到进一步优化,MAPE指标显示整体误差降至约50%。现阶段我们便可使用该回归模型开展预测工作。

mape      : 0.506827
me        : -0.000053
mae       : 0.000783
mpe       : 0.271597
rmse      : 0.001234
minmax    : 0.320422
r2_score  : -3.318859

您可能已经注意到,我们的新闻数据单独从CSV文件导入,而训练数据则直接从MetaTrader 5中读取并一起使用。 

只要"节假日"数据框中的时间戳落在训练区间或未来预测区间内,Prophet模型就会自动将节假日数据与主训练数据的日期进行对齐(同步)。 

尽管模型具备日期同步能力,想要充分发挥两组数据的效果,仍需手动确保两份数据集的起始日期一致。

我回过头去修改了main.ipynb从MetaTrader 5中获取价格数据的逻辑,现在的起止日期已与OHLC + News.mq5脚本中的设置保持一致。

# set time zone to UTC
timezone = pytz.timezone("Etc/UTC")

# create 'datetime' objects in UTC-time to avoid the implementation of a local time zone offset
utc_from = datetime(2023, 1, 1, tzinfo=timezone)
utc_to = datetime(2025, 6, 24, hour = 0, tzinfo=timezone)

rates = mt5.copy_rates_range(symbol, timeframe, utc_from, utc_to)


基于Prophet模型开发MetaTrader 5交易机器人 

要构建基于Prophet模型的EA,首先必须让模型对目标变量(本例中为波动率)进行实时预测。

为此,我们需要一套流程,一次性获取市场最新行情数据(交易品种)以及最新新闻数据。在训练脚本main.ipynb中,我们通过MetaTrader5-Python库从MetaTrader 5获取数据,但该库无法获取新闻,因此这一步必须使用MQL5来完成。

整体思路:在Python脚本(交易机器人)与 MQL5智能交易系统(EA)之间进行数据交互。

  • 将名为Data for Prophet.mq5的EA加载到MetaTrader 5图表上,它会定期将MetaTrader 5中的新闻数据与OHLC价格数据保存到共享文件夹的CSV文件中。
  • 随后,从Python脚本(Prophet-trading-bot.py)读取该文件,定期重新训练Prophet模型。
  • 训练完成后,使用模型生成预测值,并在同一Python脚本内依据预测结果做出交易决策。

文件名:Data for Prophet.mq5

input uint collect_news_interval_seconds = 60;
input uint training_bars = 1000;

input ENUM_TIMEFRAMES timeframe = PERIOD_H1;
//... other lines of code
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {  
//--- create timer

   EventSetTimer(collect_news_interval_seconds);
   
   if (!ChartSetSymbolPeriod(0, Symbol(), timeframe))
      return INIT_FAILED;
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- destroy timer
   EventKillTimer();   
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---   
  }
//+------------------------------------------------------------------+
//| Timer function                                                   |
//+------------------------------------------------------------------+
void OnTimer()
  {
//---
   MqlDateTime time_struct;
   TimeToStruct(TimeGMT(), time_struct);
   
   SaveNews(StringFormat("%s.%s.OHLC.date=%s.hour=%d + News.csv",Symbol(),EnumToString(timeframe), TimeToString(TimeGMT(), TIME_DATE), time_struct.hour));
  }

为确保使用的文件正确,CSV文件名中会包含日期和当前小时(UTC时间)。

该EA默认通过OnTimer函数,每分钟采集一次新闻及其他数据并保存至CSV文件中。

在Python脚本中,我们同样从公共文件夹读取该CSV文件并导入数据。

文件名:Prophet-trading-bot.py

def prophet_vol_predict() -> float:

    # Getting the data with news
    
    now_utc = datetime.utcnow()
    current_date = now_utc.strftime("%Y.%m.%d")
    current_hour = now_utc.hour

    filename = f"{symbol}.{timeframe}.OHLC.date={current_date}.hour={current_hour} + News.csv" # the same file naming as in MQL5 script
    common_path = os.path.join(terminal.common_data_path(), "Files")
    csv_path = os.path.join(common_path, filename)
    
    # Keep trying to read a CSV file until it is found, as there could be a temporary difference in values for the file due to the change in time
    while True:
        if os.path.exists(csv_path):
            try:
                rates_df = pd.read_csv(csv_path)
                rates_df["Time"] = pd.to_datetime(rates_df["Time"], unit="s", errors="ignore")  # Convert time from seconds to datetime
                
                print("File loaded successfully.")
                break  # Exit the loop once file is read
            except Exception as e:
                print(f"Error reading the file: {e}")
                time.sleep(30)
        else:
            print("File not found. Retrying in 30 seconds...")
            time.sleep(30)
 

我们构造波动率列,并分别为训练数据和节假日数据提取对应的新闻名称。

# Getting continous variables for the prophet model
    
prophet_df = pd.DataFrame({
    "time": rates_df["Time"],
    "volatility": rates_df["High"] - rates_df["Low"]
}).set_index("time")

prophet_df = prophet_df.reset_index().rename(columns={"time": "ds", "volatility": "y"}).copy()
    
print("Prophet df\n",prophet_df.head())
    
# Getting the news data for the model as well
    
news_df = rates_df[
    (rates_df['Name'] != "(null)") & # Filter rows without news at all
    ((rates_df['Importance'] == "CALENDAR_IMPORTANCE_HIGH") | (rates_df['Importance'] == "CALENDAR_IMPORTANCE_MODERATE")) # Filter other news except high importance news
].copy()

holidays = news_df[['Time', 'Name']].rename(columns={
    'Time': 'ds',
    'Name': 'holiday'
})

holidays['ds'] = pd.to_datetime(holidays['ds'])  # Ensure datetime format

holidays['lower_window'] = 0
holidays['upper_window'] = 1 

print("Holidays df\n", holidays)

prophet_vol_pred函数的最后,我们用获取到的数据训练模型,并返回单个预测值,该值代表模型预测的市场下一根K线的波动率。

    # re-training the prophet model
    
    prophet_model = Prophet(holidays=holidays)
    prophet_model.fit(prophet_df)
    
    # Making future predictions
    
    future = prophet_model.make_future_dataframe(periods=1) # prepare the dataframe for a single value prediction
    forecast = prophet_model.predict(future) # Predict the next one value
    
    return forecast.yhat[0] # return a single predicted value

与其他用于时间序列预测的机器学习模型类似,我们必须频繁更新模型,使其纳入与未来预测相关的最新数据。这也是我们在每次进行新预测前,都会重新训练模型的主要原因。

现在运行该函数,观察输出结果。

print("predicted volatility: ",prophet_vol_predict())

输出:

文件加载成功。
Prophet df
                     ds        y
0  2025.04.29 01:00:00  0.00100 
1  2025.04.29 02:00:00  0.00210 
2  2025.04.29 03:00:00  0.00170 
3  2025.04.29 04:00:00  0.00215 
4  2025.04.29 05:00:00  0.00278 
Holidays df
                      ds                                     holiday  lower_window  upper_window
8   2025-04-29 09:00:00                        GfK Consumer Climate             0             1 
14  2025-04-29 15:00:00          Retail Inventories excl. Autos m/m             0             1 
31  2025-04-30 08:00:00                       Consumer Spending m/m             0             1 
33  2025-04-30 10:00:00                                Unemployment             0             1 
35  2025-04-30 12:00:00                                     GDP y/y             0             1 
..                  ...                                         ...           ...           ... 
978 2025-06-24 19:00:00                 FOMC Member Williams Speech             0             1
979 2025-06-24 20:00:00                         2-Year Note Auction             0             1
982 2025-06-24 23:00:00  Fed Vice Chair for Supervision Barr Speech             0             1
984 2025-06-25 01:00:00                            Jobseekers Total             0             1
994 2025-06-25 11:00:00  Bbk Executive Board Member Mauderer Speech             0             1

[186 rows x 4 columns]
16:01:50 - cmdstanpy - INFO - Chain [1] start processing
16:01:50 - cmdstanpy - INFO - Chain [1] done processing
predicted volatility:  0.0013592111956094713

既然我们已经获取到预测值,就可以将其运用到交易策略中了。

symbol = "EURUSD"
timeframe = "PERIOD_H1"
terminal = CTerminalInfo()
m_position = CPositionInfo()

def main():
    
    m_symbol = CSymbolInfo(symbol=symbol)
    
    magic_number = 25062025
    slippage = 100
    
    m_trade = CTrade(magic_number=magic_number,
                     filling_type_symbol=symbol,
                     deviation_points=slippage)
    
    m_symbol.refresh_rates() # Get recent information from the market
    
    # we want to open random buy and sell trades if they don't exist and use the predicted volatility to set our stoploss and takeprofit targets
    
    predicted_volatility = prophet_vol_predict()
    print("predicted volatility: ",prophet_vol_predict())
    
    if pos_exists(mt5.POSITION_TYPE_BUY, magic_number, symbol) is False:
        m_trade.buy(volume=m_symbol.lots_min(), 
                    symbol=symbol,
                    price=m_symbol.ask(),
                    sl=m_symbol.ask()-predicted_volatility,
                    tp=m_symbol.ask()+predicted_volatility)
        
    if pos_exists(mt5.POSITION_TYPE_SELL, magic_number, symbol) is False:
        m_trade.sell(volume=m_symbol.lots_min(), 
                     symbol=symbol,
                     price=m_symbol.bid(),
                     sl=m_symbol.bid()+predicted_volatility,
                     tp=m_symbol.bid()-predicted_volatility)

上述函数从Prophet模型获取预测波动率,并用于设置交易的止损和止盈目标。在随机开出买入或卖出订单之前,程序会先检查是否已存在同方向持仓,避免重复开单。

函数调用。

main()

结果:

 

图例10

在MetaTrader 5中开出了两笔反向交易,止损和止盈数值均采用模型预测得出的波动率。

我们可以将这一训练流程自动化,并定期监控交易执行与信号情况。

schedule.every(1).minute.do(main) # train and run trading operations after every one minute

while True:
    
    schedule.run_pending()
    time.sleep(1)



结论

尽管网上不少文章、帖子和教程都宣称Prophet模型适用于时间序列预测,但我认为,它是本系列文章中我们讨论过效果最差的模型之一。

它或许在预测一些简单的时间序列问题时表现尚可,例如预测受天气、节假日或某种季节性规律影响的商业需求。但金融市场远比这些场景复杂,从展示测试集真实值与预测值的图表(05、06、07、08、09)中就能明显看出问题。Prophet模型的大多数预测值都与实际值偏差较大。

当然您可以通过一些手段对其进行优化,但我建议目前只将它用在简单场景中。

该模型的局限性总结如下:

  • 模型结构简单,无法刻画复杂的变量交互关系;
  • 对波动率拟合效果差 —— 如上所示,在外汇数据上表现不佳;
  • 不支持多变量建模 —— 仅支持时间和目标变量两个特征;
  • 对交叉验证和超参数调优的支持有限,很多趋势、季节性与突变点设置仍需自行控制。

谨此结束。


源文件与参考文献


附件表

文件名 说明/用法
Python code\main.ipynb 用于Prophet模型数据分析与探索的Jupyter notebook
Python code\Prophet-trading-bot.py 基于Python的MetaTrader 5交易机器人
Python code\requirementx.txt 包含Python依赖项及其版本号的文本文件
Python code\error_description.py 包含MetaTrader 5所有错误代码说明
Python code\Trade\* 包含适用于Python的交易类(如CTrade、CPositionInfo等),与MQL5语言中提供的类功能相似
Experts\Data for Prophet.mq5 EA脚本,用于定期采集并保存训练Prophet模型所需的数据至CSV文件。
Scripts\OHLC + News.mq5  用于采集训练Prophet模型所需数据并保存至CSV文件的脚本 

本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/18549

附加的文件 |
Attachments.zip (803.62 KB)
您应该了解的MQL5向导技巧(第七十二部分):MACD与OBV组合形态的监督学习应用 您应该了解的MQL5向导技巧(第七十二部分):MACD与OBV组合形态的监督学习应用
本文将承接前一篇文章,之前我们介绍了MACD与OBV指标组合,本文将探讨如何通过机器学习对这一指标组合进行优化增强。MACD属于趋势指标,OBV属于成交量指标,二者为互补指标组合。本文采用卷积神经网络机器学习方案,通过有理二次核来设定网络卷积核尺寸与通道数量,从而对这套指标组合的预测结果进行微调优化。与之前类似,所有逻辑都封装在自定义信号类文件中,可配合MQL5向导直接生成智能交易系统(EA)。
市场模拟(第 21 部分):SQL 入门(四) 市场模拟(第 21 部分):SQL 入门(四)
你们中的许多人可能比我更有数据库工作经验,因此可能会有不同的看法。由于有必要解释数据库为何以这样的方式设计,以及SQL为何具有这样的形式 — 尤其是主键和外键为何出现 — 因此有些内容必须保持一定的抽象性。
按小时、星期几和每月日期分析的季节性指标 按小时、星期几和每月日期分析的季节性指标
本文介绍如何开发一款用于分析金融市场中重复性价格模式的工具 —— 可按每月日期(1-31 日)、星期几(周一至周日)或每日小时(0-23 时)进行分析。该指标分析历史数据,计算每个周期的平均收益率,并以直方图形式展示结果并附带预测。指标包含可自定义参数:季节性类型、分析 K 线数量、以百分比或绝对值显示、图表颜色等。
从基础到中级:指标(五) 从基础到中级:指标(五)
在本文中,我们将探讨如何处理用户更改图表绘制模式的请求。这是必要的,以确保为当前图表绘制模式设计的指标不会显得奇怪,或与 MetaTrader 5 用户的预期不符。