11 - 【股票策略】用backtrader回测在A股上复利年化收益率超20%的“狗股策略”?

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值