[TOC]

前言

由于backtrader的官方文档,只是阐述了框架的基本原理和相关类的方法。但是并没对,backtrader应用做深入的研究。本文档,皆从项目的角度,阐述backtrader在项目中应用。

MACD结合MA做趋势择时策略

为什么选择以趋势择时策略开始?根据有效市场理论,国内的证券投资市场,目前还不到弱有效市场,技术分析可以获取超额利润。既然技术分析可以获取超额利润,基本面分析更加可以获取超额利润。所以先从简单的入手,后面会陆续写到如何用基本面因子进行多因子选股。

Tushare数据准备

首先需要为回测准备数据。这里交易数据的接口是Tushare,为什么在最初的时候选择Tushare,主要因为当时比较靠谱的交易数据接口只有Tushare。什么现在还在用Tushare,两个原因:一是切换框架成本高,二是Tushare的数据质量我觉确实不错。(ps:广告打完,开始正文)

MySQL与Tushare构建本地交易数据库

你可以选择直接在线通过Tushare接口来获取数据,但是这么做不科学。因为,一是Tushare的接口是有频次限制,如果频繁远程读取,还是不方遍。二是性能和效率,虽然Tushare的接口响应时间还是比较快的,但是也抗不住所有人循环访问。所以,需要构建本地数据库。

1. 构建本地交易数据库脚本

/*
 * 沪深所有股票
 * 表中的字段对应tushare的stock_basic接口返回字段,可以查看此接口对应的字段解释
  */
CREATE TABLE `stocks`  (
  `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
  `ts_code` varchar(16) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `symbol` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL,
  `name` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL,
  `area` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL,
  `industry` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL,
  `fullname` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL,
  `market` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL,
  `list_status` varchar(2) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL,
  `list_date` timestamp(0) NULL DEFAULT NULL,
  `delist_date` timestamp(0) NULL DEFAULT NULL,
  `is_hs` varchar(2) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `idx_stocks_ts_code`(`ts_code`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3836 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

/*
 * 沪深所有股票日交易数据
 * 表中的字段解释对应tushare的daily接口返回字段,可以查看此接口对应的字段解释
*/
CREATE TABLE `trade_data`  (
  `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
  `ts_code` varchar(16) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `trade_date` timestamp(0) NULL DEFAULT NULL,
  `open` float NOT NULL DEFAULT 0,
  `high` float NOT NULL DEFAULT 0,
  `low` float NOT NULL,
  `close` float NOT NULL,
  `pre_close` float NOT NULL,
  `change` float NOT NULL,
  `pct_chg` float NOT NULL,
  `vol` float NOT NULL,
  `amount` float NOT NULL DEFAULT 0,
  `cdate` timestamp(0) NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0),
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `idx_trade_code_date`(`ts_code`, `trade_date`) USING BTREE,
  INDEX `idx_date_pct_chg`(`trade_date`, `pct_chg`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 7187583 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '2002年至今日交易数据' ROW_FORMAT = Dynamic;

2. 数据下载
下载沪深所有股票基础信息

    data = pro.stock_basic(fields='ts_code,symbol,name,fullname,area,industry,market,list_status,list_date,delist_date,is_hs')
    pd.io.sql.to_sql(data, 'stocks', con=engine,index=False, if_exists='append',chunksize=500)

下载所有股票的日交易数据,此数据量非常庞大,大概有4000x18x250条交易数据,为什么是这个量?Tushare的交易数据是从2002年开始的,有18年每年大概250个交易日,所以我们取得是日交易数据。

# 批量更新日交易数据
def update_trade(self,beg_id,end_id,start_date,end_date):
    session = DBSession()
    data = session.query(dbStock).filter(dbStock.id < end_id+1, dbStock.id > beg_id).all()
    # session.commit()
    for dt in data:
        # print(dt.__dict__)
        daily = pro.daily(ts_code= dt.ts_code, start_date=start_date, end_date=end_date)
        print('stock:%s,count:%s' % (dt.id,daily.shape[0]))
        pd.io.sql.to_sql(daily, 'trade_data', con=engine, index=False, if_exists='append', chunksize=500)
    print('finshed')

# 调用方法
id_ticket = 0 
# id从1-50的股票,获取从20020418到2020601的交易数据
update_trade(id_ticket, id_ticket + 50, '20020418', '2020601')

3. 数据同步
以上实现的是一次性下载数据,我们还要每天设置一个定时任务,同步每天的交易数据,当然节假日要排除在外。

# 每天更新股票交易数据
def update_trade():
    if is_holiday():
        return False
    day = time.strftime("%Y%m%d", time.localtime())
    daily = pro.daily(trade_date=day)
    pd.io.sql.to_sql(daily, 'trade_data', con=engine, index=False, if_exists='append', chunksize=500)
    print('%s更新了%s条记录' % (day,len(daily)))

# 定时任务设定每晚8:30更新
schedule.every().day.at("20:30").do(on_timer, "update_trade")

以上内容解决了本地回测数据的问题,下面就要开始策略开发了。

策略实现

选择的指标是10日均线配合MACD。当DIF、DEA都大于0,Bar为红柱,同时收盘价在10日均线上时买入。当DIF、DEA都小于0,Bar为绿柱,且收盘价在10日均线以下时候卖出。


import backtrader as bt
from strategy.base import BaseStrategy

# 策略逻辑:macd 为主,10日均线为辅
class Calf(BaseStrategy):
    params = (
        ('maperiod', 10),
    )

    def __init__(self):
        # 继承BaseStrategy的构造函数
        super(Calf, self).__init__()


        # 10日均线
        sma = bt.indicators.SimpleMovingAverage(
            self.data, period=self.params.maperiod)

        above_sma = self.data.close > sma
        below_sma = self.data.close <= sma

        MACD = bt.indicators.MACD(self.data)
        macd = MACD.macd
        signal = MACD.signal
        histo = bt.indicators.MACDHisto(self.data)

        # 收盘价大于histo,买入
        macd_buy = bt.And(macd > 0, signal > 0, histo > 0)
        # 收盘价小于等于histo,卖出
        macd_sell = bt.And(macd <= 0, signal <= 0, histo <= 0)

        self.buy_signal = bt.And(above_sma, macd_buy)
        self.sell_signal = bt.And(below_sma, macd_sell)


    # 制定交易策略的函数,策略模块最核心的部分
    def next(self):
        if self.order:  # 检查是否有指令等待执行,
            return
            # 检查是否持仓
        if not self.position:  # 没有持仓
            if self.buy_signal:
                # 执行买入
                self.order = self.buy()
        else:
            if self.sell_signal:
                # 执行卖出
                self.order = self.sell()
        pass

    # 测略结束时,多用于参数调优
    def stop(self):
        if self.params.show_profit:
            self.log('(均线周期 %2d)期末资金 %.2f' %
                 (self.params.maperiod, self.broker.getvalue()))

上面的策略看着如此简单,下面主要是回测了。回测逻辑:

  • 按年份分组
    随机抽取100支股票,形成一个投资组合。组合不变分别回测2002-2006,2006-2010,2010-2015,2015-2020时间段范围内的数据,看看收益情况。

  • 按股票分组
    时间段不变,随机生成4个投资组合,每个组合里100支股票,再次分别回测上面4个时间段段数据。

# 回测检验
def test_calf(stocks, beg_date, end_date):
    rs = pd.DataFrame(columns=['stock', 'profit', 'sharp', 'draw'])
    for stock in stocks:
        # 获取数据
        df = helper.get_stock(stock.ts_code, beg_date, end_date)
        df.index = pd.to_datetime(df.trade_date)

        if not df.empty:
            try:
                cerebro = bt.Cerebro()
                # 导入策略参数寻优
                # cerebro.optstrategy(MA20, maperiod=range(10, 20))
                # 添加策略
                cerebro.addstrategy(Calf)
                data = bt.feeds.PandasData(dataname=df)
                cerebro.adddata(data)
                # 设置账户资金
                cash = 100000.0
                cerebro.broker.setcash(100000.0)
                # 设置手续费
                cerebro.broker.setcommission(commission=0.002)
                # 设定需要设定每次交易买入的股数
                cerebro.addsizer(bt.sizers.FixedSize, stake=100)
                # 添加分析器
                cerebro.addanalyzer(btanalyzers.SharpeRatio, _name='sharp')
                cerebro.addanalyzer(btanalyzers.DrawDown, _name='draw')
                thestrats = cerebro.run()
                thestrat = thestrats[0]
                sharp = thestrat.analyzers.sharp.get_analysis()
                draw = thestrat.analyzers.draw.get_analysis()
                profit = cerebro.broker.getvalue() / cash - 1
                record = pd.DataFrame([[stock.ts_code, profit, '%.2f' % sharp['sharperatio'], '%.2f' % draw['drawdown']]], columns=['stock', 'profit', 'sharp', 'draw'])
                print(record)
                rs.append(record)
            except Exception as e:
                print(str(e))
                continue
        else:
            continue
    rs.to_csv('stocks.csv', index=False, header=True)

代码中,传入3个参数,stocks是股票投资组合。beg_date是回测开始日期,end_date是回测截止日期。
组合里每只股票都有相同的初始化资金,每次下单买卖也是相同的股数。回测中主要统计的数据是,收益率和最大回撤率。

注意:
这里创建了多个Cerebro实例,也可以只创建一个Cerebro实例,但是创建一个Cerebro实例时候,就需要在策略的next方法中处理一下投资组合和仓位管理。

调用回测方法

# 同一个组合不同时间段
test_calf(stock1, '20020509','20060509')
test_calf(stock1, '20060509','20100509')
test_calf(stock1, '20100509','20150509')
test_calf(stock1, '20150509','20200509')

策略优化

通过上面的回测,我们会得到相同组合的平均收益率。 表格里只是列出当前策略的盈利,如果不理想可以通过调整策略查看结果的变化。
然而收益率很可能跟选择的投资组合有关。所以,在策略确定以后,随机变化100支股票的投资组合,再看看收益如何。

可优化的空间:

  • MA使用5日-15日间取最佳值
  • MACD低位金叉时买入,能量柱3日连缩等等

调整后从头回测。

问题

为什么有些年份,收益率高,有些年份收益率为负数?
是不是趋势择时的同时加上多因子选股效果会更好?
陆续通过回测来验证以上问题。

多因子回测

asfaf