1. 什么是攻守兼备的“狗股策略”?
狗股理论是美国基金经理迈克尔·希金斯于1991年提出的一种投资策略。具体的做法是,投资者每年年底从道琼斯工业平均指数成分股找出10只股息率最高的股票,年初买入,一年后再找出10只股息率最高的成分股,卖出手中不在名单的股票,买入新进入的成分股股票,每年年初年底都重复这个投资动作,便可以获取超过大盘的回报。根据统计,1975年至1999年运用狗股理论,投资平均复利回报18%,远高于市场的平均水平3%。
投资高股息,可以称得上是攻守兼备的策略:股价低迷时,只要能获得高于银行定期存款的股息,就相当于为资金建立了保护伞,此为守;当股价上扬时,补单能继续享受股息收入,还能让资产像坐轿子一样升值,高位抛出的话,可赚取资本利得,此为攻。从量化的角度,这个策略本质上是一个单因子策略——以股息率为因子,买入股息率高的策略。
2. 策略回测
策略原理:每年8月的第一个交易日结束,先清仓过去持有的股票,然后筛选出N只股息率最高的股票,每次开仓的时候使用90%的资金,并且平分到N只股票上,然后第二个交易日开盘买入,持有一年时间,到第二年8月。不断重复同样的操作,期望获取超过市场的收益率。
策略有效性分析:高的股息率代表着公司经营比较好,客观上能够获得股息收益这个是实实在在的。投资30个股票,在某种程度上,分散了个股的特有风险。之所以8月份调仓是因为A股的好多股息发放是在每年6、7月份,发放过股息后,才能确定这个股票股息率的高低。
交易费用:万分之二的手续费。
3. 回测结果
最大回撤发生在2008年股灾。该策略整体表现尚可,从2006年到2019年底,复利年化收益率达到20%。
4. 回测代码
from utils.backtesting import *
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
#%% 定义策略
class TestDividendRateStrategy(bt.Strategy):
params = (
('window', 200),
)
def log(self, txt, dt=None):
""" Logging function for this strategy """
dt = dt or self.datas[0].datetime.date(0)
print('{}, {}'.format(dt.isoformat(), txt))
def __init__(self):
self.bar_num = 0
self.stock_dividend_info = pd.read_csv("./data/股票历史股息率数据.csv", index_col=0)
self.buy_list = []
self.value_list = []
self.trade_list = []
self.order_list = []
def prenext(self):
self.next()
def next(self):
# 假设100万资金,每次成分股调整,每个股票使用1万元
self.bar_num += 1
self.log(self.bar_num)
# 需要调仓的时候
pre_current_date = self.datas[0].datetime.date(-1).strftime("%Y-%m-%d")
current_date = self.datas[0].datetime.date(0).strftime("%Y-%m-%d")
total_value = self.broker.get_value()
self.value_list.append([current_date, total_value])
# 如果是8月第一个交易日
if current_date[5:7] == '08' and pre_current_date[5:7] != '08':
# 获取当前股息率前10的股票
dividend_info = self.stock_dividend_info[self.stock_dividend_info['tradeDate'] == current_date]
dividend_info = dividend_info.sort_values("divRate", ascending=False)
dividend_info = dividend_info.drop_duplicates("secID")
dividend_stock_list = [i.split('.')[0] for i in list(dividend_info['secID'])]
if len(dividend_stock_list) > 10:
stock_list = dividend_stock_list[:10]
else:
stock_list = dividend_stock_list
self.log(stock_list)
# 平掉原来的仓位
for stock in self.buy_list:
data = self.getdatabyname(stock)
if self.getposition(data).size > 0:
self.close(data)
# 取消原来的仓位
for order in self.order_list:
self.cancel(order)
self.buy_list = stock_list
value = 0.90 * self.broker.getvalue() / len(stock_list)
# 开新的仓位,按照90%比例
for stock in stock_list:
data = self.getdatabyname(stock)
# 没有把手数设为100的倍数
lots = value / data.close[0]
order = self.buy(data, size=lots)
self.log(f"symbol:{data._name}, price:{data.close[0]}")
self.order_list.append(order)
def notify_order(self, order):
if order.status in [order.Submitted, order.Accepted]:
# order被提交和接受
return
if order.status == order.Rejected:
self.log(f"order is rejected : order_ref:{order.ref} order_info:{order.info}")
if order.status == order.Margin:
self.log(f"order need more margin : order_ref:{order.ref} order_info:{order.info}")
if order.status == order.Cancelled:
self.log(f"order is concelled : order_ref:{order.ref} order_info:{order.info}")
if order.status == order.Partial:
self.log(f"order is partial : order_ref:{order.ref} order_info:{order.info}")
# Check if an order has been completed
# Attention: broker could reject order if not enougth cash
if order.status == order.Completed:
if order.isbuy():
self.log("buy result : buy_price : {} , buy_cost : {} , commission : {}".format(
order.executed.price, order.executed.value, order.executed.comm))
else: # Sell
self.log("sell result : sell_price : {} , sell_cost : {} , commission : {}".format(
order.executed.price, order.executed.value, order.executed.comm))
def notify_trade(self, trade):
# 一个trade结束的时候输出信息
if trade.isclosed:
self.log('closed symbol is : {} , total_profit : {} , net_profit : {}'.format(
trade.getdataname(), trade.pnl, trade.pnlcomm))
self.trade_list.append([self.datas[0].datetime.date(0), trade.getdataname(), trade.pnl, trade.pnlcomm])
if trade.isopen:
self.log('open symbol is : {} , price : {} '.format(
trade.getdataname(), trade.price))
def stop(self):
value_df = pd.DataFrame(self.value_list)
value_df.columns = ['datetime', 'value']
value_df.to_csv("股息率value结果.csv")
trade_df = pd.DataFrame(self.trade_list)
# trade_df.columns =['datetime','name','pnl','net_pnl']
trade_df.to_csv("股息率-trade结果.csv")
#%% 策略回测
# 初始化cerebro
cerebro = bt.Cerebro()
# 加载数据
data_root = "./data/stock/day/"
file_list = sorted(os.listdir(data_root))
params = dict(
fromdate=datetime(2006, 1, 4),
todate=datetime(2019, 12, 31),
timeframe=bt.TimeFrame.Days,
dtformat="%Y-%m-%d",
compression=1,
datetime=0,
open=1,
high=2,
low=3,
close=4,
volume=5,
openinterest=-1)
for file in file_list:
feed = bt.feeds.GenericCSVData(dataname=data_root+file, **params)
cerebro.adddata(feed, name=file.split('.')[0])
print("加载数据完毕!")
# 添加手续费,按照万分之二收取
cerebro.broker.setcommission(commission=0.0002, stocklike=True)
# 设置初始资金为100万
cerebro.broker.setcash(1000000.0)
# 添加策略
cerebro.addstrategy(TestDividendRateStrategy)
cerebro.addanalyzer(bt.analyzers.TotalValue, _name='_TotalValue')
# 运行回测
results = cerebro.run()
#%% end