上篇已经介绍了data的获取,此篇介绍ma5与ma10的双均线策略具体实现。双均线策略是一个趋势策略,基本思路是金叉买入,死叉卖出,也就是当ma5向上穿过ma10时,则买入,向下穿过ma10时,则卖出。
主要实现在Strategy类中,输入的变量格式如下:code_list = ['002415', '002416', '000333'],init_cash = 100000,starttime = '2014-01-01',endtime = '2017-11-30',其他几个重要成员变量如下:cash为还剩余的现金;capital_market_value为持仓的市值,按每天的收盘价计算;limit_cash为每只股票分得的仓位,双均线策略中,codelist中的股票平分持仓;position_list为持仓的情况,类型为dict,股票的code为key,持仓的数量为value;data_range为一个回测的日期list,pd.period_range的freq参数选择为B,即工作日;benchmark赋值为sh,即上证指数,以便比对策略结果。
#coding=utf-8
from trade import Trade
from data_repository import DataRepository
import tushare as ts
import pandas as pd
import numpy as np
import math
class Strategy(object):
def __init__(self, code_list, init_cash, start_time, end_time):
self.start_time = start_time
self.end_time = end_time
self.data_repository = DataRepository.get_instance(code_list, self.start_time, self.end_time)
self.code_list = code_list
self.benchmark_code = 'sh'
self.init_cash = init_cash
self.cash = init_cash
self.limited_cash = init_cash/len(code_list)
self.position_list = {}
for code in self.code_list:
self.position_list[code] = 0
#存储trade对象
self.trade = Trade()
d = list(pd.period_range(start=start_time, end=end_time, freq='B'))
self.date_range = list(map(str, d))
self.res_df = pd.DataFrame()
self.res_df['date'] = self.date_range
self.capital_market_value = []
实际的回测定义在run_simulation函数中,主循环框架为按天循环,通过每天的开盘价判断是否买入或者卖出(
先判断卖出信号,再判断买入信号,具体判断在后面介绍),如果卖出成功,那么将卖出的所得加入到cash变量,对应的position变量调整为0(这个策略为初级策略,都是在每只股票的额度内全买全卖,后续策略会加入仓位控制),并将操作记录下到trade变量中。买入的操作也是类似的。每天买卖判断完成后,再按收盘价计算下持仓的市值,记录到capital_market_value中,以便统计每天的涨跌,计算sharp比率等统计指标。
所有天数回测结束后,将结果记录到res_df中,该变量为pandas类型,以便分析结果。res_df中数据有每天capital_market_value,每天的涨跌幅,每天对应的benchmark的值(因为回测的date_range已经剔除了周末,节假日等情况会存在停市,故capital_market_value与benchmark的值均使用之前第一个值填充),benchmark采用先bfill,然后还可能存在最开头的值是空的情况,在ffill填充,而capital_market_value则是使用df[df['date'] <= date].tail(1)['close']获得。
def run_simulation(self):
#按天循环
for date in self.date_range:
for code in self.code_list:
sell_signal, sell_open_price = self.get_sell_signal(code, date)
direction = -1
if sell_signal == 1:
amount = self.get_sell_amount(code)
if amount > 0:
commission = self.cal_cost_function(sell_open_price, amount)
#更改现金
self.cash += sell_open_price*amount
self.cash -= commission
#更改持仓
self.position_list[code] -= amount
#加入trade记录
self.trade.add_trade(code, sell_open_price, amount, date, direction, commission)
for code in self.code_list:
buy_signal, buy_open_price = self.get_buy_signal(code, date)
direction = 1
if buy_signal == 1:
amount = self.get_buy_amount(code, buy_open_price)
if amount > 0:
commission = self.cal_cost_function(buy_open_price, amount)
#更改现金
self.cash -= buy_open_price*amount
self.cash -= commission
#更改持仓
self.position_list[code] += amount
#加入trade记录
self.trade.add_trade(code, buy_open_price, amount, date, direction, commission)
#计算每天的市值
self.capital_market_value.append(self.get_market_value(date))
#所有天循环结束后,加入到res_df
self.res_df['capital_market_value'] = pd.Series(self.capital_market_value)
self.res_df['profolio_daily_return'] = round((self.res_df['capital_market_value']/\
self.res_df['capital_market_value'].shift(1)-1),4)
self.res_df['benchmark'] = self.get_benchmark_index()
self.res_df['benchmark'].fillna(method='bfill', inplace=True)
self.res_df['benchmark'].fillna(method='ffill', inplace=True)
self.res_df.to_csv('./datares.csv')
benchmark与captial_market_value的取得具体如下。
def get_benchmark_index(self):
df = ts.get_k_data(self.benchmark_code, start=self.start_time, end=self.end_time)
benchmark_list = []
for date in self.date_range:
if df[df['date'] == date].empty:
benchmark_list.append(np.nan)
else:
benchmark_list.append(float(df[df['date'] == date]['close']))
return benchmark_list
#得到某天的持仓股票的市值,加上cash,一并返回
def get_market_value(self, date):
market_value = 0
for code in self.position_list:
df = self.data_repository.get_onecode_df(code)
if self.position_list[code] != 0:
close_price = df[df['date'] <= date].tail(1)['close']
market_value += self.position_list[code]*float(close_price)
return round(market_value+self.cash, 2)
最重要的判断买入卖出信号如下。有个小地方需要注意下,给定一个date日期,df = df[df['date'] <= date].tail(3),获取了三天的价格,即今天的价格,昨天的价格,前天的价格,通过前天与昨天的均线价格去判断买入卖出。这是因为每天的均线价格是收盘价的平均(包含当天),策略的思路是每天通过开盘价买入或者卖出,但是当天的均线价格需要等收盘才能得到,也就是当买入或者卖出时候,只能使用昨天与前天的均线价格,否则就使用了未来函数。
def get_sell_signal(self, code, date):
df = self.data_repository.get_onecode_df(code)
sell_signal = 0
sell_open_price = 0
if df[df['date'] == date].empty:
return sell_signal, sell_open_price
df = df[df['date'] <= date].tail(3)
if len(df) == 3 and df.iloc[0]['ma5'] > df.iloc[0]['ma10'] and df.iloc[1]['ma5'] < df.iloc[1]['ma10']:
sell_signal = 1
sell_open_price = df.iloc[1]['open']
return sell_signal, sell_open_price
#以后还要加入判断止盈的方法
def get_buy_signal(self, code, date):
df = self.data_repository.get_onecode_df(code)
buy_signal = 0
buy_open_price = 0
if df[df['date'] == date].empty:
return buy_signal, buy_open_price
df = df[df['date'] <= date].tail(3)
if len(df) == 3 and df.iloc[0]['ma5'] < df.iloc[0]['ma10'] and df.iloc[1]['ma5'] > df.iloc[1]['ma10']:
buy_signal = 1
buy_open_price = df.iloc[1]['open']
return buy_signal, buy_open_pric
剩下的几个就比较简单了,判断持仓数量,计算交易手续费。
def get_sell_amount(self, code):
return self.position_list[code]
def get_buy_amount(self, code, price):
if self.position_list[code] == 0:
amount = math.floor(self.limited_cash/(price*100))*100
return amount
else:
return 0
def cal_cost_function(self, price, amount):
commission = price*amount*0.0003
#最低5元手续费
if commission > 5:
return commission
else:
return 5