用Prophet预测USDCNY走势--------仿照forecasting-stock-perfomance-with-prophet对美元人民币走势进行预测


        时间序列预测一直是一项挺复杂且结果挺玄学的领域。一次偶然的机会,我在微信上看到一篇用Prophet对股票表现进行预测的文章,于是对这款不用提取特征的简易时序预测工具产生了好奇,想着仿照该文章思路对Prophet库进行学习(先山寨一把,后面有时间再研究一下这个库的论文)。于是便有了下面这篇学习笔记(重要的事情这里说三遍:这篇文章的主体框架99.99%仿照链接1的原文框架,但是研究标的、细部的图表、部分代码作了细微调整。)
        由于原文所附源码有所残缺,辗转找了一下,发现原载英文网站无法打开(没翻墙的想法)。于是我又找了一下国内能打开的网站碰碰运气,终于在Github上面找到了这份源码,附链如下:


        链接卡片1为文章(所附源码有缺漏):


最新 | 使用Prophet预测股价并进行多策略交易(附代码)Prophet可以创建稳健的预测模型https://mp.weixin.qq.com/s/bf_CHcoZMjqP6Is4ebD58g链接卡片2为Github源码: 

prophet/Stock Price Forecast with Prophet.ipynb at master · gardnmi/prophet (github.com)https://github.com/gardnmi/prophet/blob/master/Stock%20Price%20Forecast%20with%20Prophet.ipynb


        跟着源码的步骤,本文仿照该思路用Prophet对USDCNY的市场走势进行了类似的分析。下面是操作的具体思路:

一、导入相关库,读取数据
    1.1 导入相关库
二、数据准备
    2.1 读取数据
    2.2 改列名,转类型,重排序
三、Prophet初步拟合
    3.1 预测模型实例化
    3.2 Prophet预测图形展示
    3.3 重建图表样式
四、Prophet仿真回测
    4.1 回测数据按月分块
    4.2 回测的逻辑
    4.3 回测的结果保存
五、简单的量化交易策略
    5.1 初始策略     
    5.2 初始策略效果解析   
    5.3 策略改进思路
    5.4 策略改进Debug
    5.5 策略改进优化--滑动时间窗口求阈值
    5.6 完美策略--时间旅行者策略

一、导入相关库,读取数据
1.1 导入相关库
#导入相关库,并初始化设置
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from fbprophet import Prophet
from functools import reduce
import warnings
warnings.filterwarnings('ignore')
%matplotlib inline


#让图表变得更美艳
plt.style.use('seaborn-deep')
pd.options.display.float_format="{:,.2f}".format

二、数据准备
2.1 读取数据

        Tushare或者Quandl都可以下载外汇的OHLC数据,但是有一定的氪金要求。对于只是用数据来验证一下prophet库的性能,个人觉得USDCNY的历史数据可以从下面的网站获取。:
https://cn.investing.com/currencies/usd-cny-historical-data/
        只需要花几分钟,注册一个账号,就可以一次过提取USDCNY长达十多年的open high low close的历史数据,赞!
        存入本地之后,直接pandas读取即可,代码如下:

#获取数据,由于本文只对收盘价进行预测,所以把open,high,low弃掉
cny_df=pd.read_csv("请输入你文档所在的本地路径",parse_dates=True).dropna()
cny_df.drop(['Open','High','Low'],axis=1,inplace=True) 


2.2 改列名,转类型,重排序
# 修改列名,才能在prophet模块内被顺利识别并建模预测。
# 同时由于原时间列格式并非datetime格式,故需要转化成datetime格式
# 对数据列 日期索引 进行升序重排序

cny_df.columns=['ds','y']
cny_df['ds']=pd.to_datetime(cny_df['ds'])
cny_df.sort_values(by=['ds'],ascending=True,inplace=True)

三、Prophet模型的初步拟合
3.1 预测模型实例化

        实例化Prophet模型,赋值给model。由于这初次实例化,涉及历史数据较多,usdcny的即期日内交易区间、交易时间等已经变更多次。如果按照默认的区间来寻找突变点并训练模型,可能无法用来对后续数据进行突变点的寻找。因此我在第一行语句内,把突变点的区间设为0.95,也就是让模型从所有历史数据的前95%内自动寻找突变点。
        change_prior_scale本文设为0.1,也就是对突变点的敏感性不会太高。
        第二行添加了月度的季节性,指定月度季节性的周期为21.75天,约为每个月的平均工作日。
        第三行语句,通知Prophet对usdcny的即期数据,开始进行训练;第四行语句生成需要预测的365天的日期数据框
        第五行语句,由于usdcny只能在交易日操作,我们需要按照原文的逻辑,用一句布尔值将周末的预测数据删除。(我们创建一个布尔表达式,如果一天不等于0-4,则返回False。“0 =星期一,6=星期六,等等)。理论上,需要导入usdcny的交易假期表,这样生成的数据框才最接近实际交易日。(这个后续有时间琢磨好了再分享)

model=Prophet(changepoint_range=0.95,changepoint_prior_scale=0.1,daily_seasonality=False)
model.add_seasonality(name='monthly', period=21.75, fourier_order=5) 
model.fit(cny_df)
future=model.make_future_dataframe(periods=365)

future_boolean=future['ds'].map(lambda x:True if x.weekday() in range(0,5) else False)
future=future[future_boolean]

forecast=model.predict(future)

3.2 Prophet预测图形展示
        第一张图是以2008年--2022年的数据为训练样本,以上述参数对模型进行训练后,得到的模型对历史上所有的交易日都生成按照模型所计算出的当日预测值。同时,Prophet还会按照在3.1里面的make_future_dataframe里的约定,给你外推365天生成预测值(yhat),并添加预测的上下(yhat_lower, yhat_upper)区间。
        可以看到随着预测天数的后移,上下区间会变得相当的宽,对于usdcny来说,预测一年后即期价格可能会处于6.2下方,但是上部区间可能在6.6,下方则可能在5.7左右,这对usdcny的即期交易来说,365天之后接近1万点(1pips=0.0001)的区间预测值,对交易基本没有指导意义。
        
在季节性的图表中,我们大致可以看出过去十多年以来,每年年初为结汇高峰,六七八月则为购汇高峰期。对于企业的结售汇敞口来说,可以据此选择适合自己的结售汇时间窗口。

fig1=model.plot(forecast)
fig2 = model.plot_components(forecast)

  
3.3 重建图表样式
        下面通过标准范式,对prophet自带的图表进行重构,以更好的辅助视觉分析,毕竟肉眼对不同颜色的线条规律更为敏感,更容易通过肉眼辅助观察,更好的把握和分析曲线走势。
        原文的图表重建相对简单,本文的图表自定义内容相对丰富些。大家可以按照自己的需求自行选择。
        画图过程搜索了不少matplotlib的画图知识点,发觉知乎的一篇文章(包括文章后面的问答)对卖魄力(matplotlib)画图非常有启发,分享如下:
matplotlib:先搞明白plt. /ax./ fig再画 - 知乎 (zhihu.com)https://zhuanlan.zhihu.com/p/93423829**************************************************************************************************************

#用matplotlib.pyplot优化画图效果,添加y轴的刻度,从而使得水平网格线能更好的区分价格的具体位置
from matplotlib.pyplot import MultipleLocator

#从预测数据框提取日期,预测值及预测上下限的数据;与初始的实际值数据进行拼合,生成新的数据框df1
cny_df_forecast1=forecast[['ds','yhat','yhat_lower','yhat_upper']]
df1=pd.merge(cny_df,cny_df_forecast1,on='ds',how='right')

#生成画布(画布实例化),把画布贴上fig3标签,该画布内的轴域贴上ax标签(目前只有一套轴域)
fig3,ax=plt.subplots(figsize=(12,8))

#在画布上显示图表名称
ax.set_title('USDCNY SPOT TREND')

#显示水平、垂直的主副网格线
ax.grid(which='both',axis='both')
ax.set_ylim(5.65,7.2)

#通过循环,分四次把四根趋势线画到画布上,并对4根趋势线使用不同颜色进行区分
result_list=['y','yhat','yhat_lower','yhat_upper']
color=['blue','green','orange','orange']
for num,result in enumerate(result_list):
    ax.plot(df1.set_index('ds')[result],color=color[num])


#下面两句如果放在上述循环语句前,则x轴标签的刻度就只能显示年份    
ax.legend(loc='best',labels=result_list)              #显示曲线样例
ax.yaxis.set_major_locator(MultipleLocator(0.10))     #设定y轴的刻度间隔0.1个点
ax.xaxis.set_major_locator(MultipleLocator(120))      #设定x轴的刻度间隔,120天
ax.xaxis.set_tick_params(rotation=70,labelsize=10,colors='darkblue')   #设定x轴刻度倾斜度

fig3.savefig("请输入你的本地路径/USDCNY 数据/New_picture_test3.jpg")

四、Prophe仿真回测
4.1 回测数据按月分块
        仿照原文给数据贴上年月日标签(本文把给数据贴标签的活计放在函数里面)。把原数据的日月年信息提取出来之后,再把月和年拼合再一起,通过merge函数,把所有的数据放在【月/年】索引的盒子内。由于【月/年】盒子是由index来排序,在后续的仿真回测时,这个index顺序可以用来为循环提供计数信息。

def month_labeling(data):
    data['dayname']=data['ds'].dt.day_name()
    data['month']=data['ds'].dt.month
    data['year']=data['ds'].dt.year
    data['month/year']=data['month'].map(str)+'/'+data['year'].map(str)
    data=pd.merge(data,data['month/year'].drop_duplicates().reset_index(drop=True).reset_index(),
                    on='month/year',
                    how='left')
    data=data.rename(columns={'index':'month/year_index'})
    return data

#添加年月星期名字的新数表
cny_df=month_labeling(cny_df)
cny_df.tail()
dsydaynamemonthyearmonth/yearmonth/year_index
33152022-02-086.37Tuesday220222/2022163
33162022-02-096.36Wednesday220222/2022163
33172022-02-106.35Thursday220222/2022163
33182022-02-116.35Friday220222/2022163
33192022-02-146.36Monday220222/2022163

4.2  回测的逻辑
        虽然我们在上面3.2创建的10年预测看起来挺有意思,但我们不想在没有使用交易策略对业绩进行回测的情况下就对其做出任何交易决定(引自原文)。
        翻成白话就是说,3.2的模型训练数据是基于十余年的数据,等于说你像一个时间旅行者,在攒了10年的市场经验之后,再把这些数据给拟合成一个prophet模型,基于这个模型再重新逐日生成历史当日的预测值。举个栗子,如果你在2015年1月9号收盘,想对第二天的市场(也就是2015年1月10日)做一个预测。理论上你应该只有2008年--2015年,总共7年的经验值帮助你进行预测。但是3.2的模型,其实是使用了2008年---2022年的数据进行拟合而成。等于凭空额外塞给你7年的经验来帮助你进行预测。
        实际交易中,我们当然可以基于过去十年,甚至二十年的数据来训练模型(如果数据存在的话)。但是这个模型你总得进行回测,观察它的有效性。进行回测的意思,意味着你需要假设每一段历史数据出现时,你就只能站在那段数据的尾部,对这个数据进行拟合并生成模型,然后用拟合的模型外推预测下一个月。随后再用外推的数据,跟当月的实际数据进行比对。这样,你才可以观察到这个模型的拟合有效性。
        下面的拟合需要花费较长时间,各位看官可以点杯奶茶慢慢等~~

loop_list=cny_df['month/year'].unique().tolist()
max_num=len(loop_list)-1
forecast_frames=[]
for num,item in enumerate(loop_list):
    if num==max_num:
        pass
    else:
        df=cny_df.set_index('ds')[
            cny_df[cny_df['month/year']==loop_list[0]]['ds'].min():cny_df[cny_df['month/year']==item]['ds'].max()
        ]
        #先用第一个月的数据训练,然后预测第二个月。把第二个月的预测值append如预测数据集(forecast_frames)
        #再用第一、第二个月的数据训练,预测第三个月,把第三个月的预测值append如预测数据集(forecast_frames)
        #继续用第一、第二、第三个月的数据训练,预测第四个月,把第四个月的预测值append如预测数据集(forecast_frames)
        #逻辑类似于你在某个小岛生活了一天,根据这一天的经验预测第二天晴雨的概率;生活了两天之后,预测第三天晴雨的概率;生活了三天之后,预测第四天晴雨的概率;以此类推。
        #个人觉得,随着生活时间的推移,你会对该岛晴雨的规律更为熟悉,从而能更好的预测该岛的晴雨走势。
        #但是从预测结果来看,前面部分的预测由于训练数据的缺少,预测的有效性会有所下降;
        #把下面几句打印语句加进去之后,就能看出本循环所作的预测模式
        # print(df.head(5))
        # print(df.tail(5))
        # print("{} 到 {}".format(cny_df2[cny_df2['month/year']==loop_list[0]]['ds'].min(),cny_df2[cny_df2['month/year']==item]['ds'].max()))


        df=df.reset_index()[['ds','y']]
        
        model=Prophet(changepoint_prior_scale=0.2,daily_seasonality=False)
        model.fit(df)
        
        future=cny_df[cny_df['month/year_index']==(num+1)][['ds']]
        forecast=model.predict(future)
        forecast_frames.append(forecast)
        print(num)

4.3 回测结果保存
        由于回测所需时间比较长,因此把结果保存到csv文档内,以备后面分析之用。这样每一次需要对回测结果进行观察分析时,就无需重新运行该程序。这能节省大量时间。

cny_df_forecast=reduce(lambda top,bottom:pd.concat([top,bottom],sort=False),forecast_frames)
cny_df_forecast=cny_df_forecast[['ds','yhat','yhat_lower','yhat_upper']]
cny_df_forecast.to_csv('cny_df_forecast2.csv',index=False)   

        仿照3.3,重建图表样式。由结果的回测图可以看到,对USDCNY即期的预测效果并不是特别理想。

cny_df_forecast=pd.read_csv('请输入你的本地路径\\cny_df2_forecast.csv',parse_dates=['ds'])
df2=pd.merge(cny_df[['ds','y','month/year_index']],cny_df_forecast,on='ds')
df2['Percent Change']=df2['y'].pct_change()

fig4,ax=plt.subplots(figsize=(12,8))

#要求画图时显示图表名称
ax.set_title('USDCNY SPOT BACKTESTING TREND')
#显示水平、垂直的主副网格线
ax.grid(which='both',axis='both')
ax.set_ylim(5.65,8.1)

#通过循环,分四次把四根趋势线画到画布上,并对4根趋势线使用不同颜色进行区分
result_list=['y','yhat','yhat_lower','yhat_upper']
color=['blue','green','orange','orange']
for num,result in enumerate(result_list):
    ax.plot(df2.set_index('ds')[result],color=color[num])

#下面两句如果放在上述循环语句前,则x轴标签的刻度就只能显示年份    
ax.legend(loc='best',labels=result_list)              #显示曲线样例
ax.yaxis.set_major_locator(MultipleLocator(0.10))     #设定y轴的刻度间隔0.1个点
ax.xaxis.set_major_locator(MultipleLocator(100))      #设定x轴的刻度间隔,120天
ax.xaxis.set_tick_params(rotation=90,labelsize=10,colors='darkblue')   #设定x轴刻度倾斜度

fig4.savefig("请输入你的本地路径/New_picture_test4.jpg")

 五、简单的量化交易策略
5.1 初始策略
        仿照原文,本文也对USDCNY的买卖进行的四种交易策略的收益回测计算。只需要把原文的股票改成我们的USDCNY即期收盘价即可。对于Seasonality策略,原文是8月退出市场,10月重新进入。本文根据观察得到的5—8月份购汇高峰,以及11-12月购汇小峰对季节性的策略进行了简单的调整。我们1、2,9,10,12月均清空所持头寸,余下月份根据信号买入。

Hold:这是一种买入并持有的策略。也就是说,我们买股票(USDCNY即期)并持有到最后一段时间。

Prophet:这种策略是当我们的预测显示下跌趋势时卖出,当我的预测显示上涨趋势时买进

Prophet Thresh:这个策略是只有当股票(USDCNY即期)价格跌破我们的yhat_lower边界时才卖出。

Seasonality:这一策略是在8月退出市场,重新进入Ocober。这是基于上面的季节性图表。

#交易算法。原文在对股价建模时,并没有取对数收益率。因此在构建交易算法时,用了cumprod而不是cumsum.
df2['Hold']=(df2['Percent Change']+1).cumprod()
df2['Prophet']=((df2['yhat'].shift(-1)>df2['yhat']).shift(1)*(df2['Percent Change'])+1).cumprod()
df2['Prophet Thresh']=((df2['y']>df2['yhat_lower']).shift(1)*(df2['Percent Change'])+1).cumprod()
# ~df表示逻辑非,在pandas里面不能用not表示,而是用了~这个符号表示。因此下面的意思就是,当交易月份不在12,1,2,9,10月里面的时候,按照原买卖方向交易并计算收益。
df2['Seasonality']=((~df2['ds'].dt.month.isin([12,1,2,9,10])).shift(1)*(df2['Percent Change'])+1).cumprod()

(df2.dropna().set_index('ds')[['Hold','Prophet','Prophet Thresh','Seasonality']]*1000).plot(figsize=(16,8),grid=True,color=['r','g','y','b'])
print(f"Hold={df2['Hold'].iloc[-1]*1000:,.0f}")
print(f"Prophet={df2['Prophet'].iloc[-1]*1000:,.0f}")
print(f"Prophet Thresh={df2['Prophet Thresh'].iloc[-1]*1000:,.0f}")
print(f"Seasonality={df2['Seasonality'].iloc[-1]*1000:,.0f}")

Hold=926
Prophet=1,059
Prophet Thresh=1,091
Seasonality=1,063

5.2 初始策略效果解析
        从上面打印出来的结果可以看出,2008-2018年期间,Prophet策略的效果一直是最好的。Prophet Thresh在最后4年收益爬升。总体收益排行第一。
        季节性策略自2019年左右开始发力,总体收益排行第二。因此我们可以据此猜测,季节性在2019年对市场的影响有所加深,至今的影响力依然较大。

5.3 策略改进思路
        原文希望通过调整阈值,来尝试优化Prophet Thresh这个策略。也就是yhat这个预测值,大于yhat_lower的某个比例的时候进行买卖。本文仿照该思路,也对阈值进行了循环测算,发现在对于USDCNY即期来说,对于thresh的最佳阈值是当前yhat_lower值的101.03%。('Best Yhat=1.0103')。改进效果其实并不是太明显。

performance={}
for x in np.linspace(0.8,0.99,30):
    y=((df2['y']>df2['yhat_lower']*x).shift(1)*(df2['Percent Change'])+1).cumprod()
    performance[x]=y

#某个阈值产生的收益时间序列,先用max()找出期间的最大值。再有由最大值反过来寻找该最大值所在的index,也就是阈值的vallue
best_yhat=pd.DataFrame(performance).astype(float).max().idxmax()  
pd.DataFrame(performance).plot(figsize=(16,8),grid=True)

f'Best Yhat={best_yhat:,.4f}'

df2['Optimized Prophet Thresh']=((df2['y']>df2['yhat_lower']*best_yhat).shift(1)*
                                 (df2['Percent Change'])+1).cumprod()
(df2.dropna().set_index('ds')[['Hold','Prophet','Prophet Thresh',
                               'Seasonality','Optimized Prophet Thresh']]*1000).plot(figsize=(16,8),grid=True)
print(f"Hold={df2['Hold'].iloc[-1]*1000:,.0f}")
print(f"Prophet={df2['Prophet'].iloc[-1]*1000:,.0f}")
print(f"Prophet Thresh={df2['Prophet Thresh'].iloc[-1]*1000:,.0f}")
print(f"Seasonality={df2['Seasonality'].iloc[-1]*1000:,.0f}")
print(f"Optimized Prophet Thresh={df2['Optimized Prophet Thresh'].iloc[-1]*1000:,.0f}")
Hold=926
Prophet=1,059
Prophet Thresh=1,091
Seasonality=1,063
Optimized Prophet Thresh=1,078

 5.4 策略改进Debug 
        参考4.2的回测逻辑,我们稍微琢磨一下5.3的阈值优化操作,发现这个寻找阈值改进Prophet Thresh交易收益的思路,其实是存在bug的。归根结底还是用了超前的数据拟合模型,用这个携带超前数据得模型来分析交易策略的有效性时,存在很大的信息优势。从而可能夸大了这个阈值的有效性。
        因此我们需要仿照4.2的思路,对阈值的优化操作进行调整。通过循环来模拟每一次阈值查找操作都只会站在该时间段末尾,而避免引入该时间段后面的额外数据进行模型拟合。
        代码里面,我把打印语句注释掉了。启用该打印语句,我们可以看到本次的优化,是基于以下数据框<第一个数据日期,到当前数据日期( 【'month/year_index' 】所在的数据日期最后一天)>,对处于0-0.99之内的阈值进行迭代,循环找出最优阈值。

1 batch, date1 is 2008-08-01 00:00:00, date2 is 2008-08-29 00:00:00
2 batch, date1 is 2008-08-01 00:00:00, date2 is 2008-09-26 00:00:00
3 batch, date1 is 2008-08-01 00:00:00, date2 is 2008-10-31 00:00:00
4 batch, date1 is 2008-08-01 00:00:00, date2 is 2008-11-28 00:00:00
5 batch, date1 is 2008-08-01 00:00:00, date2 is 2008-12-31 00:00:00
6 batch, date1 is 2008-08-01 00:00:00, date2 is 2009-01-30 00:00:00

循环计算最优阈值具体代码如下:

fcst_thresh={}

for num,index in enumerate(df2['month/year_index'].unique()):
    temp_df=df2.set_index('ds')[
        df2[df2['month/year_index']==df2['month/year_index'].unique()[0]]['ds'].min():
                df2[df2['month/year_index']==index]['ds'].max()
               ]
    #print("{} batch, date 1 is {}, date 2 is {}".format(num+1,df2[df2['month/year_index']==df2['month/year_index'].unique()[0]]['ds'].min(),df2[df2['month/year_index']==index]['ds'].max()))
    
    performance={}
    for thresh in np.linspace(0,0.99,100):
        percent=((temp_df['y']>temp_df['yhat_lower']*thresh).shift(1)*(temp_df['Percent Change'])+1).cumprod()
        performance[thresh]=percent
        
    best_thresh=pd.DataFrame(performance).astype(float).max().idxmax()
    fcst_thresh[df2['month/year_index'].unique()[num]]=best_thresh

fcst_thresh=pd.DataFrame([fcst_thresh]).T.reset_index().rename(columns={'index':'month/year_index',0:'Fcst Thresh'})
fcst_thresh['Fcst Thresh'].plot(figsize=(16,8),grid=True)


结果展示&打印代码如下::

df2['yhat_optimized']=pd.merge(df2,fcst_thresh,
                               on='month/year_index',
                               how='left')['Fcst Thresh'].shift(1)*df2['yhat_lower']
df2['Prophet Fcst Thresh']=((df2['y']>df2['yhat_optimized']).shift(1)*(df2['Percent Change'])+1).cumprod()
(df2.dropna().set_index('ds')[['Hold','Prophet','Prophet Thresh',
                               'Prophet Fcst Thresh']]*1000).plot(figsize=(16,8),grid=True)
print(f"Hold={df2['Hold'].iloc[-1]*1000:,.0f}")
print(f"Prophet={df2['Prophet'].iloc[-1]*1000:,.0f}")
print(f"Prophet Thresh={df2['Prophet Thresh'].iloc[-1]*1000:,.0f}")
print(f"Prophet Fcst Thresh={df2['Prophet Fcst Thresh'].iloc[-1]*1000:,.0f}")
                                          

结果如下:
Hold=926
Prophet=1,059
Prophet Thresh=1,091
Prophet Fcst Thresh=1,067


 5.5 策略改进优化——滑动时间窗口求阈值
        5.4对5.3的策略改进进行了debug,把超前的信息优势抹掉后重新弄计算最优的阈值。但是每一次计算最优阈值的数据框<第一个数据日期,到当前数据日期( 【'month/year_index' 】所在的数据日期最后一天)> 都是固定的。理论上,我们可以通过滑动或长或短的时间窗口,对这一数据框的数据进行循环计算,找出更为有效的阈值。
        基于上述思路,原文提出了rolling_thresh这一阈值计算思路。下面是本文稍作调整的代码。添加了几行画图代码。本文选择画出第151次循环中各个循环中存放的数据框图示。(之所以选择第151次,主要是对于本次即期时序分析中,151次循环的图示能比较好的展示各个嵌套中的情况。大家可以根据自己想看的某次循环调整代码)。

rolling_thresh={}

#为了让大家看清楚下面几个嵌套循环所作的计算逻辑,我添加了下面的画图语句,大家可以根据自己的需要开启或关闭
fig,(ax1,ax2,ax3)=plt.subplots(3,1,figsize=(26,18))

for num,index in enumerate(df2['month/year_index'].unique()):
    rolling_performance={}

    #print("This's the {} round ".format(num))
    
    
    for roll in range(10,55,10):
        temp_df=df2.set_index('ds')[
            df2[df2['month/year_index']==index]['ds'].min()-pd.DateOffset(months=roll):\
            df2[df2['month/year_index']==index]['ds'].max()]
        performance={}

        for thresh in np.linspace(0,0.99,50):
            percent=((temp_df['y']>temp_df['yhat_lower']*thresh).shift(1)*(temp_df['Percent Change'])+1).cumprod()
            performance[thresh]=percent
            
       #开启下列语句,可以看到第151次循环中,滑动窗口为距离150个month/year_index 40个月前的50次循环计算
       #所得出的同一滑动窗口内,所有的收益曲线(选择下面9行语句,Ctrl+?)
        # test_num1=index
        # test_num2=roll
        # while test_num1==150 and test_num2==40:
        #     ax1.legend(str('%.4f'%thresh),loc=2)
        #     ax1.set_title("Same Roll Different Thresh (0,0.99)")
        #     print(pd.DataFrame(performance))
        #     ax1.plot(pd.DataFrame(performance))
        #     test_num1=10
        #     test_num2=30
            
            
        per_df=pd.DataFrame(performance)
        best_thresh=per_df.iloc[[-1]].astype(float).max().idxmax()
        percents=per_df[best_thresh]
        rolling_performance[best_thresh]=percents
        
       #开启下列语句,可以看到第151次循环中,所得出的不同长度的滑动窗口内,最优收益曲线
       #曲线长短不一,因为它们是来自于不同的时间滑动窗口。(选择下面8行语句,Ctrl+?)
        # test_num3=index
        # while test_num3==150:
        #     print("test_num is {}, let's plot".format(test_num3))
        #     print("This's {} 's best rolling performance: ".format(roll),'\n',rolling_performance[best_thresh])
        #     ax2.legend(str('%.4f'%best_thresh),loc=2)
        #     ax2.set_title("Different Roll Best Thresh")
        #     ax2.plot(pd.DataFrame(rolling_performance))
        #     test_num3=10

    
        
    per_df=pd.DataFrame(rolling_performance)
    best_rolling_thresh=per_df.iloc[[-1]].astype(float).max().idxmax()
    rolling_thresh[df2['month/year_index'].unique()[num]]=best_rolling_thresh
    
   #开启下列语句,可以看到第151次循环所得出的最优滑动窗口收益曲线(选择下面8行语句,Ctrl+?)
    # test_num4=index
    # while test_num4==150:
    #     print("test_num4 is {}, let's plot the big chart".format(test_num4))
    #     print("This's best rolling performance: ",'\n',per_df[best_rolling_thresh])
    #     ax3.legend(str('%.4f'%best_rolling_thresh),loc=4)
    #     ax3.set_title("Best Rolling Thresh Thresh")
    #     ax3.plot(per_df[best_rolling_thresh],'bo')
    #     test_num4=10
    
rolling_thresh=pd.DataFrame([rolling_thresh]).T.reset_index().rename(columns={'index':'month/year_index',0:'Fcst Thresh'})
rolling_thresh[['Fcst Thresh']].plot(figsize=(16,8),grid=True)

结果展示&打印代码如下:

df2['yhat_optimized']=pd.merge(df2,rolling_thresh,
                               on='month/year_index',
                               how='left')['Fcst Thresh'].fillna(1).shift(1)*df2['yhat_lower']
df2['Prophet Rolling Thresh']=((df2['y']>df2['yhat_optimized']).shift(1)*(df2['Percent Change'])+1).cumprod()
(df2.dropna().set_index('ds')[['Hold','Prophet','Prophet Thresh','Prophet Fcst Thresh','Prophet Rolling Thresh']]*1000).plot(figsize=(16,8),grid=True)

print(f"Hold={df2['Hold'].iloc[-1]*1000:,.0f}")
print(f"Prophet={df2['Prophet'].iloc[-1]*1000:,.0f}")
print(f"Prophet Thresh={df2['Prophet Thresh'].iloc[-1]*1000:,.0f}")
print(f"Prophet Fcst Thresh={df2['Prophet Fcst Thresh'].iloc[-1]*1000:,.0f}")
print(f"Prophet Rolling Thresh={df2['Prophet Rolling Thresh'].iloc[-1]*1000:,.0f}")

结果如下:

Hold=926
Prophet=1,059
Prophet Thresh=1,091
Prophet Fcst Thresh=1,067
Prophet Rolling Thresh=1,080


 5.6 完美策略--时间旅行者策略
Time Traveler这个策略,只是把Prophet Thresh这个策略调整了一下,原语句为:
df2['Prophet Thresh']=((df2['y']>df2['yhat_lower']).shift(1)*(df2['Percent Change'])+1).cumprod()
而Time Traveler则为:
df2['Time Traveler']=((df2['y'].shift(-1)>df2['yhat']).shift(1)*(df2['Percent Change'])+1).cumprod()

df2['Time Traveler']=((df2['y'].shift(-1)>df2['yhat']).shift(1)*(df2['Percent Change'])+1).cumprod()
(df2.dropna().set_index('ds')[['Hold','Prophet','Prophet Thresh','Prophet Fcst Thresh','Prophet Rolling Thresh','Time Traveler']]*1000).plot(figsize=(16,8),grid=True)

print(f"Hold={df2['Hold'].iloc[-1]*1000:,.0f}")
print(f"Prophet={df2['Prophet'].iloc[-1]*1000:,.0f}")
print(f"Prophet Thresh={df2['Prophet Thresh'].iloc[-1]*1000:,.0f}")
print(f"Prophet Fcst Thresh={df2['Prophet Fcst Thresh'].iloc[-1]*1000:,.0f}")
print(f"Prophet Rolling Thresh={df2['Prophet Rolling Thresh'].iloc[-1]*1000:,.0f}")
print(f"Time Traveler={df2['Time Traveler'].iloc[-1]*1000:,.0f}")
Hold=926
Prophet=1,059
Prophet Thresh=1,091
Prophet Fcst Thresh=1,067
Prophet Rolling Thresh=1,080
Time Traveler=1,369

这个策略的收益显然很完美,但实际中,你是没办法用这个逻辑来交易的。试想,基于今天及过去一年的历史数据,prophet模型算出来明天的即期预测值yhat在6.5,你今天面对着市场在6.4,你是否会一定会去买入呢?答案因人而异。如果你对模型非常有信心,当然可以尝试根据这一预测值买入spot。但是如果这个模型的预测效力并不是特别强。你很可能并不会根据这个信息来调整仓位。而Time Traveler能直接看到后天的spot,并把它shift(-1)之后,跟明天的6.5比较。当后天的实际即期高于prophet告诉你的明天预测值6.5时,你才去买入。这种策略当然必须完美,因为你直接预知了后天的实际市场价格,以此来辅助实际交易,效果自然是显著的。

小结:
        Prophet这个库,目测对USDCNY即期的预测效果并不是特别理想。但是对USDCNY即期的周期发现还是有一定的参考意义。
        由于本文是初次使用的学习笔记,正如本文开篇所说,时序预测是相当玄学的,调参是一门很耗精力的炼丹术。本文的很多地方只是仿照原文的思路和参数,还没有很细致且有针对性的对USDCNY即期交易进行深入分析,如果大家有好的调参思路,请多多分享。

  • 3
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

外汇量化__炼丹房

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值