ID:383995
本文基于华泰金工2020年8月研报《华泰证券金工量化资产配置7月月报:北向资金走向预示市场短期或震荡》,构建基于北向资金的股市择时策略,并进行历史回测。结果表明,北向资金对于判断沪深 300指数涨跌具有较好的预示作用。
一、调用相关库
tushare为数据来源,numpy、pandas为数据处理库,matplotlib、seaborn用于数据可视化,empyrical用于策略评价
import tushare as ts
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from secret import *
import empyrical
# secret.py中存放tstoken和图片保存路径save_path
sns.set()
pro = ts.pro_api(tstoken)
二、获取相关数据
# 获取交易日历,返回格式:array
def get_cal_date(start, end):
cal_date = pro.trade_cal(start_date=start, end_date=end)
cal_date = cal_date[cal_date.is_open == 1]
dates = cal_date.cal_date.values
return dates
def get_north_money(start, end):
dates = get_cal_date(start, end)
df = pd.DataFrame()
# 因为每次数据库只能返回300条数据,分段调取数据
for i in range(0, len(dates), 300):
if len(dates) - (i + 1) > 300:
d = pro.moneyflow_hsgt(start_date=dates[i], end_date=dates[i + 299])
d.sort_index(ascending=False, inplace=True)
d.reset_index(inplace=True, drop=True)
else: # 对于最后一段日期
d = pro.moneyflow_hsgt(start_date=dates[i], end_date=dates[-1])
d.sort_index(ascending=False, inplace=True)
d.reset_index(inplace=True, drop=True)
df = df.append(d, ignore_index=True)
df.trade_date = pd.to_datetime(df.trade_date)
return df
def get_index_data(code, start, end):
index_df = pro.index_daily(ts_code=code, start_date=start, end_date=end)
index_df.sort_index(ascending=False, inplace=True)
index_df.reset_index(inplace=True, drop=True)
index_df.trade_date = pd.to_datetime(index_df.trade_date)
return index_df
# code:指数代码
# start1:获取数据起始时间
# end1:获取数据终止时间
def get_total_data(code, start1, end1):
north_df = get_north_money(start1, end1)
index_df = get_index_data(code, start1, end1)
total_data = pd.merge(index_df, north_df, on='trade_date', how='left')
total_data.fillna(0, inplace=True) # 用0来替代
return total_data[['trade_date', 'open', 'high', 'close', 'low', 'north_money']]
三、 策略原理:采取布林带方法
当日北上资金流入规模突破上轨时,以第二天开盘价进行全仓买入
当日北上资金流入规模突破下轨时,以第二天开盘价进行全仓卖出
手续费:双边千分之三
# 输入参数:
# data:包含北向资金和指数价格数据
# window:移动窗口
# stdev:几倍标准差
# cost:手续费
def North_Strategy(df, window, stdev_n, cost):
# 布林带参数
# 中轨和标准差
df = df.copy()
df['mid'] = df['north_money'].rolling(window).mean()
stdev = df['north_money'].rolling(window).std()
# 上下轨
df['upper'] = df['mid'] + stdev_n * stdev
df['lower'] = df['mid'] - stdev_n * stdev
# 当天收益率
df['ret'] = df.close / df.close.shift(1) - 1
df.dropna(inplace=True)
# 设计买卖信号,1表示全仓,0表示空仓
df['signal'] = np.NaN
# 当日北向资金突破上轨线发出买入信号
df.loc[df['north_money'] > df['upper'], 'signal'] = 1
# 当日北向资金跌破下轨线发出卖出信号
df.loc[df['north_money'] < df['lower'], 'signal'] = 0
# 某日的position为前一日的signal
df['position'] = df['signal'].shift(1)
# 其余时间的position以上一个position进行填充
df.fillna(method='ffill', inplace=True)
# 由于布林带的rolling计算开始会有一部分空值,记为空仓
df['position'].fillna(0, inplace=True)
# 根据交易信号和仓位计算策略的每日收益率
df['capital_ret'] = np.NaN
# 初始收益率记为0
df.loc[df.index[0], 'capital_ret'] = 0
# 买卖按照成交金额扣除手续费
# 第二天开盘价买入
df.loc[df['position'] > df['position'].shift(1), 'capital_ret'] = \
(df.close - df.open - df.open * cost) / df.open
# 第二天开盘价卖出
df.loc[df['position'] < df['position'].shift(1), 'capital_ret'] = \
(df.open - df.close.shift(1) - df.open * cost) / df.close.shift(1)
# 当仓位不变时,当天的capital是当天的涨幅乘上持有头寸
df.loc[df['position'] == df['position'].shift(1), 'capital_ret'] = \
df['ret'] * df['position']
df.set_index('trade_date', inplace=True)
return df
四、策略回测
这里调用了empyrical库,年化收益率采取IRR复利年化
# df:经过处理后的策略数据
# start2:回测时间起点
# end2:回测时间终点
# 返回回测的数据
def strategy_evaluation(df, start2, end2):
# 选取所关注的时间范围
df2 = df.loc[(df.index > pd.to_datetime(start2)) &
(df.index < pd.to_datetime(end2)), :]
df2 = df2.copy()
# 计算策略、指数的净值
df2['strategy_net_value'] = (df2.capital_ret + 1.0).cumprod()
df2['index_net_value'] = (df2.ret + 1.0).cumprod()
# 导出数组
arr = df2['capital_ret'].values
arr_index = df2['ret'].values
index_r = empyrical.cum_returns_final(arr_index)
cum_r = empyrical.cum_returns_final(arr)
alpha, beta = empyrical.alpha_beta(arr, arr_index)
annualized_r = empyrical.annual_return(arr)
annualized_v = empyrical.annual_volatility(arr)
drawback = empyrical.max_drawdown(arr)
# 无风险收益,默认0
rf = 0
sharpe_r = empyrical.sharpe_ratio(arr, rf)
print('指数累计收益率为:%.2f%%' % (index_r * 100))
print('策略alpha=%.2f,beta=%.2f' % (alpha, beta))
print('策略累计收益率为:%.2f%%' % (cum_r * 100))
print('年化收益率为:%.2f%%' % (annualized_r * 100))
print('年化波动率为:%.2f%%' % (annualized_v * 100))
print('最大回撤为:%.2f%%' % (drawback * 100))
print('夏普比率为:%.2f' % sharpe_r)
print('收益回撤比为:%.2f' % (annualized_r / drawback))
return df2
五、策略可视化
def plot_strategy(d,sfpath):
plt.figure(figsize=(10, 6))
plt.plot(d['strategy_net_value'], 'b', label='Strategy')
plt.plot(d['index_net_value'], 'y', label='Index')
# 买卖点标记
plt.plot(d[d['position'] > d['position'].shift(1)].index_net_value, 'r.')
plt.plot(d[d['position'] < d['position'].shift(1)].index_net_value, 'k.')
plt.legend()
plt.title('North Money Strategy')
plt.savefig(sfpath)
plt.show()
六、主函数运行
if __name__ == '__main__':
'''
index_code:指数代码
start_time1:选取数据时间起点
end_time1:选取数据时间终点
start_time2:回测时间起点
end_time2:回测时间终点
save_fig_path:图片保存路径的文件夹
'''
index_code = '000300.SH'
start_time1 = '20150101'
end_time1 = '20210413'
start_time2 = '20200101'
end_time2 = '20210413'
# save_fig_path = save_path+'North_money_strategy.jpg'
data = get_total_data(index_code, start_time1, end_time1)
strategy_data = North_Strategy(data, 252, 1.5, 0.003)
print(strategy_data.tail())
print('回测结果为:')
strategy_data2 = strategy_evaluation(strategy_data, start_time2, end_time2)
print('\n\n红点为买,黑点为卖,策略净值可视化如图:')
print('回测时间范围:%s-%s' % (start_time2, end_time2))
plot_strategy(strategy_data2, save_fig_path)
七、回测结果
可以看出,相对沪深300指数有较好的择时效果