1. 回测需求 & 效果展示
以中国平安(601318.SH)为买卖对象,检验双均线策略在 2020-05-10 至 2021-01-01 的收益情况。效果如下:
2. 代码框架
2.1 对象
(1) 存储账户信息、回测信息
账户信息:现金,所持股票
回测信息:
- 开始/结束日期
- 当前日期
- 基准:一般会以一只股票或一个指数为基准,用于比较策略优劣性
- 开始至结束之间所有交易日的信息
class Context:
def __init__(self, cash, start_date, end_date):
# 账户信息
self.cash = cash # 现金
self.positions = {} # 持有的股票信息
# 回测信息
self.start_date = start_date
self.end_date = end_date
self.dt = start_date
self.benchmark = None
self.date_range = trade_cal[(trade_cal['is_open'] == '1') & \
(trade_cal['cal_date'] >= start_date) & \
(trade_cal['cal_date'] <= end_date)]['cal_date'].values
其中,trade_cal
存储了所有交易日的日期信息。可通过 tushare 包的 pro.trade_cal()
获取
(2) 存储其他全局变量
class G:
pass
2.2 函数
(0) 主体函数
def run():
initialize(context) # 初始化
# 创建一个DataFrame,储存画图所需数据
plt_df = pd.DataFrame(index=pd.to_datetime(context.date_range), columns=['value'])
# 存储股票的上一个交易日的价格信息,避免以为股票停牌而无法获取价格
last_prices = {}
# 回测开始时的股票价格,用于计算之后的价格变动曲线
initial_value = context.cash
for dt in context.date_range:
context.dt = dt # 更新当前时间
handle_data(context)
# 计算账户价值 = 现金 + 持仓股票
value = context.cash
for stock in context.positions.keys():
try:
data = get_today_data(stock)
last_prices[stock] = data['open']
except KeyError:
# 如果取不到,说明当日停牌,取上一个交易日的价格
price = last_prices[stock]
value += price * context.positions[stock].amount
plt_df.loc[dt, 'value'] = value
# 绘制策略
plt_df['ratio'] = (plt_df['value'] - initial_value) / initial_value
# 绘制基准
bm_df = attribute_daterange_history(context.benchmark, context.start_date, context.end_date)
bm_init = bm_df['open'][0]
plt_df['benchmark_ratio'] = (bm_df['open'] - bm_init) / bm_init
plt_df[['ratio', 'benchmark_ratio']].plot()
plt.show()
(1) 初始化函数
- 设置目标股票
- 设置基准
- 设置双均线信息(g.p1 & g.p2)
- 获取目标股票的历史数据:在以下代码中,
hist_1
获取的是回测开始日期前g.p2
天的数据,hist_2
获取的是回测开始至结束之间的数据。将两者合并后即可得到双均线回测所需要的所有股票价格数据
def initialize(context):
g.security = '601318.SH'
set_benchmark('601318.SH')
g.p1 = 5
g.p2 = 60
hist_1 = attribute_history(g.security, g.p2)
hist_2 = attribute_daterange_history(g.security, context.start_date, context.end_date)
g.hist = hist_1.append(hist_2)
(2) 每个交易日都需执行的函数
def handle_data(context):
hist = g.hist[:dateutil.parser.parse(context.dt)][-g.p2:]
ma5 = hist['close'][-g.p1:].mean()
ma60 = hist['close'].mean()
# 实现双均线策略:
# 如果短均线高于长均线,且股票不在持仓中,则买入
# 反之,且股票在持仓中,则卖出
if ma5 > ma60 and g.security not in context.positions.keys():
order_value(g.security, context.cash)
elif ma5 < ma60 and g.security in context.positions.keys():
order_target(g.security, 0)
(3) 下单函数
github 中上传的代码共包含四个下单函数:
-
order
: 购买一定的股票数量 -
order_value
购买一定的股票金额 -
order_target
购买到一定数量的股票 -
order_target_value
购买到一定金额的股票
而这四个下单函数均基于:
def _order(today_data, security, amount):
if today_data.empty: return
# 获取股票价格
price = today_data['open']
# 判断是否持有该股票
try:
test = context.positions[security]
except KeyError:
# 如果卖出操作,直接退出函数
if amount <= 0: return
# 如果买入操作,创建position
context.positions[security] = pd.Series(dtype=float)
context.positions[security]['amount'] = 0
# 买入/卖出操作时,必须以100的倍数购买,除非全部卖出
if (amount % 100 != 0) and (amount != -context.positions[security].amount):
amount = int(amount/100) * 100
# 更新持仓
context.positions[security].amount = context.positions[security].get('amount') + amount
if context.positions[security].amount == 0: # 如果持仓股数为0,删除
del context.positions[security]
# 更新现金
context.cash -= amount * price