本文讲述对于动量策略的一个实例。动量策略是最驰名的定量长短期股票策略之一。自从 Jegadeesh 和 Titman(1993)首次提出这个概念以来,它已宽泛呈现在学术研究和销售方面的著述中。投资者在动量策略中置信,个股中,过来的赢家将超过过来的输家。
最罕用的动量因素是股票除去最近一个月在过来 12 个月的收益。在学术出版物中,动量策略通常是一个月调整一次且持有期也是一个月。在本例中,咱们每天从新均衡咱们的投资组合的 1 / 21,并持有新份额 21 天。为简略起见,咱们不思考交易成本。
步骤 1:加载股票交易数据,对数据进行荡涤和筛选,而后为每个公司股票构建一个过来 12 个月疏忽最近一个月的动量信号。
def load_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.PRC
USstocks['cumretIndex'] = (USstocks + 1)['RET'].groupby('PERMNO', lazy=True).cumprod()
USstocks['signal'] = (USstocks.shift(21) / USstocks.shift(252) - 1).groupby('PERMNO', lazy=True)['cumretIndex'].transform()
return USstocks
df = orca.read_csv('C:/DolphinDB/Orca/databases/USstocks.csv')
price_data = load_price_data(df)
留神:以上代码应用了 Orca 的两个扩大性能。
- Orca 反对在条件过滤语句中应用逗号代替 &,在某些场景下会更高效,参见教程。
- Orca 的
groupby
函数提供了 lazy 参数,配合transform
函数应用可能实现 DolphinDB 的 context by 性能,参见教程。
步骤 2:为动量策略生成投资组合。
首先,抉择满足以下条件的流通股:动量信号值无缺失、当日是正交易量、市值超过 1 亿美金、以及每股价格超过 5 美元。
def gen_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')
return USstocks
tradables = gen_trade_tables(price_data)
其次,依据动量信号,制订 10 组可交易股票。只保留 2 个最极其的群体(赢家和输家)。假如咱们总是想在 21 天内,每天多头 1 美元和空头 $1,所以咱们每天在赢家组多头 $1/21,在输家组每天空头 $1/21。在每组中,咱们能够应用等权重或值权重,来计算投资组合造成日期上每个股票的权重。
def form_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.0
ports_rank_eq_0 = (ports['rank'] == 0)
ports_rank_eq_groups_sub_1 = (ports['rank'] == groups-1)
if 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)
)
elif wt_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)
return ports
start_date, end_date = orca.Timestamp("1996.01.01"), orca.Timestamp("2017.01.01")
holding_days = 5
groups = 10
ports = 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 天敞开股票。
def calc_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()
for age in range(1, holding_days+1):
dates_after_age_i = dates.copy()
dates_after_age_i['age'] = age
dates_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.0
pos['pnl'] = 0.0
# use set_index to make it easy to equal join two Frames
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)
return pos
last_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 按索引列进行 left join,而后将后果中的对应列赋值。如果间接执行脚本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
函数会将整个 DataFrame 所对应的 DolphinDB 表下载到客户端,而后对齐绘图。在应用时该当留神数据量,防止大量的网络传输带来的性能问题。参见教程。
点此查看残缺的代码。对于如何用 DolphinDB 脚本实现这个策略,请参考官网范例。