这篇文章描述了一个动量策略的例子。动量策略是最著名的量化长期和短期股票策略之一。自从Jegadeesh和Titman(1993)首次提出这个概念以来,它就广泛出现在学术研究和销售文献中。动量策略的投资者认为,个股过去的赢家将超过过去的输家。最常用的动量因子是股票在过去12个月的回报减去最近一个月的回报。在学术刊物上,动量策略通常每月调整一次,并持有一个月。在这个例子中,我们每天重新平衡1/21的投资组合,并持有新股21天。为简单起见,我们不考虑交易成本。第1步:加载股票交易数据,清洗和过滤数据,然后为忽略最近一个月的每只公司股票构建过去12个月的动量信号。defload_price_data(df):USstocks=df[df.date.dt.weekday.between(0,4),df.PRC.notnull(),df.VOL.notnull()][['PERMNO','date','PRC','VOL','RET','SHROUT']].sort_values(by=['PERMNO','date'])USstocks['PRC']=USstocks.PRC.abs()USstocks['MV']=USstocks.SHROUT*USstocks.PRCUSstocks['cumretIndex']=(USstocks+1)['RET'].groupby('PERMNO',lazy=True).cumprod()USstocks['signal']=(USstocks.shift(21)/USstocks.shift(252)-1).groupby('PERMNO',lazy=True)['cumretIndex'].transform()返回USstocksdf=orca.read_csv('C:/DolphinDB/Orca/databases/USstocks.csv')price_data=load_price_data(df)注:以上代码使用了Orca的两个扩展函数。Orca支持在条件过滤语句中使用逗号代替&,这在某些场景下会更高效,参见教程。Orca的groupby函数提供了惰性参数,可以配合transform函数使用,实现DolphinDB的contextby函数,详见教程。第2步:为动量策略生成投资组合。首先,选择符合以下条件的流通股:无缺失动量信号值、当日成交量为正、市值超过1亿美元、每股价格超过5美元。defgen_trade_tables(df):USstocks=df[(df.PRC>5),(df.MV>100000),(df.VOL>0),(df.signal.notnull())]USstocks=USstocks[['date','PERMNO','MV','signal']].sort_values(by='date')returnUSstockstradables=gen_trade_tables(price_data)其次,根据动量信号,制定10组流通股票。只保留2个最极端的组(赢家和输家)。假设我们总是想在21天内每天做多1美元和做空1美元,那么我们在赢家组每天做多1/21美元,在输家组每天做空1/21美元。在每个组内,我们可以使用相等的权重或价值权重来计算每只股票在投资组合形成日的权重。defform_portfolio(start_date,end_date,tradables,holding_days,groups,wt_scheme):ports=tradables[tradables.date.between(start_date,end_date)].groupby('date').filter('count(PERMNO)>=100')ports['rank']=ports.groupby('date')['signal'].transform('rank{{,true,{groups}}}'.format(groups=groups))ports['wt']=0.0ports_rank_eq_0=(ports['rank']==0)ports_rank_eq_groups_sub_1=(ports['rank']==groups-1)如果wt_scheme==1:ports.loc[ports_rank_eq_0,'wt']=ports[ports_rank_eq_0].groupby(['date'])['PERMNO'].transform(r'(PERMNO->-1count(PERMNO){holding_days})'.format(holding_days=holding_days))ports.loc[ports_rank_eq_groups_sub_1,'wt']=ports[ports_rank_eq_groups_sub_1].groupby(['date'])['PERMNO'].transform(r'(PERMNO->1count(PERMNO){holding_days})'.format(holding_days=holding_days))elifwt_scheme==2:ports.loc[ports_rank_eq_0,'wt']=ports[ports_rank_eq_0].groupby(['date'])['MV'].transform(r'(MV->-MVsum(MV){holding_days})'.format(holding_days=holding_days))ports.loc[ports_rank_eq_groups_sub_1,'wt']=ports[ports_rank_eq_groups_sub_1].groupby(['date'])['MV'].transform(r'(MV->MVsum(MV){holding_days})'.format(holding_days=holding_days))ports=ports.loc[ports.wt!=0,['PERMNO','date','wt']].sort_values(by=['PERMNO','date'])ports.rename(columns={'date':'tranche'},inplace=True)返回portsstart_date,end_date=orca.Timestamp("1996.01.01"),orca.Timestamp("2017.01.01")holding_days=5groups=10ports=form_portfolio(start_date,end_date,tradables,holding_days,groups,2)daily_rtn=price_data.loc[price_data.date.between(start_date,end_date),['date','PERMNO','RET']]注:以上代码使用了Orca的扩展函数,即允许filter、transform等高阶函数,接受一个代表DolphinDB的函数或脚本字符串见教程。第3步:计算我们投资组合中每只股票随后的21天损益。股票在投资组合形成日期后21天关闭。defcalc_stock_pnl(ports,daily_rtn,holding_days,end_date,last_days):dates=ports[['tranche']].drop_duplicates().sort_values(by='tranche')dates_after_ages=orca.DataFrame()年龄在范围内(1,holding_days+1):dates_after_age_i=dates.copy()dates_after_age_i['age']=agedates_after_age_i['date_after_age']=dates_after_age_i['tranche'].shift(-age)dates_after_ages.append(dates_after_age_i,inplace=True)pos=ports.merge(dates_after_ages,on='tranche')pos=pos.join(last_days,on='PERMNO')pos=pos.loc[(pos.date_after_age.notnull()&(pos.date_after_age<=pos.last_day.clip(upper=end_date))),['date_after_age','PERMNO','tranche','age','wt']]pos=pos.compute()pos.rename(columns={'date_after_age':'date','wt':'expr'},inplace=True)pos['ret']=0.0pos['pnl']=0.0#使用set_index可以很容易地相等连接两个帧daily_rtn.set_index(['date','PERMNO'],inplace=True)pos.set_index(['date','PERMNO'],inplace=True)pos['ret']=daily_rtn['RET']pos.reset_index(inplace=True)pos['expr']=(pos.expr*(1+pos.ret).cumprod()).groupby(['PERMNO','tranche'],lazy=True).transform()pos['pnl']=pos.expr*pos.ret/(1+pos.ret)returnposlast_days=price_data.groupby('PERMNO')['date'].max()last_days.rename("last_day",inplace=True)stock_pnl=calc_stock_pnl(ports,daily_rtn,holding_days,end_date,last_days)注意:上面的代码有一个pos.compute()语句,它直接计算了一个中间表达式(带条件过滤的DataFrame)的结果,因为我们只是是有必要对过滤后的结果执行赋值操作。另外,上面的代码在两个DataFrame上调用了set_index函数,然后将其中一个DataFrame的列赋值给另一个,类似于按索引列左连接两个DataFrame,然后在结果中赋值对应的列。如果直接执行脚本pos['ret']=pos.merge(daily_rtn,on=['date','PERMNO'])['RET'],计算时会进行一次join,再进行一次join在分配期间,导致不必要的计算。第4步:计算投资组合的损益并绘制动量策略随时间的累积回报。port_pnl=stock_pnl.groupby('date')['pnl'].sum()cumulative_return=port_pnl.cumsum()cumulative_return.plot()plt.show()注:plot函数会下载整个对应的DolphinDB表DataFrame传给客户端,并对齐绘图。使用时要注意数据量,避免大量网络传输带来的性能问题。见教程。单击此处查看完整代码。如何使用DolphinDB脚本实现该策略,请参考官方示例。
