使用Backtrader针对自有数据集制作交易回测模版
本文介绍了基于Backtrader的回测框架使用指南。主要内容包括:1)使用Tushare获取股票交易数据并保存为CSV文件;2)自定义TushareCSVData数据加载类,扩展处理股票交易数据中的额外字段;3)实现最简单的"买入并持有"交易策略模板;4)展示Backtrader框架的基本使用方法。该模板为初学者提供了快速上手的回测工具,可在此基础上开发更复杂的交易策略。文章
写在前面
本模板致力于让读者可以了解回测框架,并制定自己的交易策略,总体上会分为两部分,一部分是基础策略模板,后续会持续更新。另外也会实现深度学习模型可以使用的交易回测模板。
回测框架
本文使用的回测框架为Backtrader,以下会实现一个最简单的策略,也就是买入并持有(buy and hold)。通过直接导入下面模板直接运行,即可得到回测结果,这个模板目的是为了帮助初学者认识backtrader这个框架,想要了解更多,请参考官方文档,或中文版本文档。
数据获取
我们的数据使用tushare的数据,数据获取过程如下,我们将其保存为getData.py:
import tushare as ts
import pandas as pd
# 初始化 Tushare
ts.set_token('YOUR_TOKEN')
pro = ts.pro_api()
# 定义起止日期
start_date = '19900101'
end_date = '20251106'
# 定义要获取的股票代码
ts_code = '000776.SZ'
# 获取数据
df = pro.daily(ts_code=ts_code, start_date=start_date, end_date=end_date)
# 排序并保存
df = df.sort_values("trade_date")
df.to_csv('res.csv', index=False)
我们给出下载后,前若干行的数据
| ts_code | trade_date | open | high | low | close | pre_close | change | pct_chg | vol | amount |
|---|---|---|---|---|---|---|---|---|---|---|
| 000776.SZ | 19970611 | 10.0 | 10.0 | 8.2 | 8.92 | 3.6 | 5.32 | 147.78 | 146684.0 | 130758.3485 |
| 000776.SZ | 19970612 | 8.58 | 9.3 | 8.52 | 8.98 | 8.92 | 0.06 | 0.67 | 73309.0 | 65916.5156 |
| 000776.SZ | 19970613 | 8.48 | 9.88 | 8.3 | 9.88 | 8.98 | 0.9 | 10.02 | 65911.0 | 60784.1554 |
| 000776.SZ | 19970616 | 10.18 | 10.37 | 9.8 | 10.01 | 9.88 | 0.13 | 1.32 | 100191.0 | 101676.6104 |
| 000776.SZ | 19970617 | 9.8 | 9.98 | 9.3 | 9.3 | 10.01 | -0.71 | -7.09 | 39510.0 | 37564.0698 |
| 000776.SZ | 19970618 | 9.15 | 9.45 | 8.88 | 8.99 | 9.3 | -0.31 | -3.33 | 21961.0 | 19946.2714 |
| 000776.SZ | 19970619 | 9.01 | 9.07 | 8.7 | 8.79 | 8.99 | -0.2 | -2.22 | 14389.0 | 12743.8352 |
数据类加载
获取完数据后,我们需要自定义个自己的数据加载类,此处可以参考我们的模板,需要注意的是,默认的backtrader的数据加载方式,不能自定义我们数据的格式,此处我进行了封装,至于要我们数据的每一列进行对应,先对应框架中原本就需要的列,然后解析以下我们额外的列/特征。
import datetime
import backtrader as bt
class TushareCSVData(bt.feeds.GenericCSVData):
# 我们额外的列需要定义
lines = (
'pre_close', # 昨日收盘价
'chg', # 涨跌额
'pct_chg', # 涨跌幅(%)
'amount', # 成交额
)
# 列对应csv中的是哪一列,在此处进行指定
params = (
('nullvalue', float('nan')), # 若csv中为空值,指定其为nan
('dtformat', '%Y%m%d'), # 日期格式化
('datetime', 1), # trade_date
('time', -1), # 此处照写即可
('open', 2), # 开盘价
('high', 3), # 最高价
('low', 4), # 最低价
('close', 5), # 收盘价
('volume', 9), # 成交量
('openinterest', -1), # 这个参数描述我们有多少比交易没有结束,此处照写即可
# 扩展字段
('pre_close', 6), # 昨日收盘价
('chg', 7), # 涨跌额
('pct_chg', 8), # 涨跌幅(%)
('amount', 10), # 成交额
)
# 将这些列进行解析对应
def _loadline(self, linetokens):
# 解析日期
dtstr = linetokens[self.p.datetime]
try:
dt = datetime.datetime.strptime(dtstr, self.p.dtformat)
except Exception as e:
# 若解析失败,跳过这一行
return False
self.lines.datetime[0] = bt.date2num(dt)
# 先调用父类来解析 open/high/low/close/volume 等
super()._loadline(linetokens)
# 注意:GenericCSVData._loadline 会解析所有在 params 中声明的列
# 但因为我们自定义了扩展字段,仍需在本方法中手动赋值
# 解析扩展字段
# pre_close
try:
self.lines.pre_close[0] = float(linetokens[self.p.pre_close])
except:
self.lines.pre_close[0] = self.p.nullvalue
# chg(change)
try:
self.lines.chg[0] = float(linetokens[self.p.chg])
except:
self.lines.chg[0] = self.p.nullvalue
# pct_chg
try:
self.lines.pct_chg[0] = float(linetokens[self.p.pct_chg])
except:
self.lines.pct_chg[0] = self.p.nullvalue
# amount
try:
self.lines.amount[0] = float(linetokens[self.p.amount])
except:
self.lines.amount[0] = self.p.nullvalue
return True
如果你想要自定义自己的数据,可以在这个模板下进行简单修改,添加自己想要的字段。此处应该也有更好的处理方案,同学们可以自行挖掘。
第一个策略
完成了数据加载类的设计,我们就进入了我们的第一个策略设计,首先导入相关的包。
from __future__ import (absolute_import, division, print_function,
unicode_literals)
import datetime
import os
import sys
# 导入backtrader
import backtrader as bt
# 导入我们的数据处理类
from MyCSVData import TushareCSVData
最简单的策略,买入并持有(buy and hold)模板如下:
# 第一个策略类
class MyStrategy(bt.Strategy):
# 初始化
def __init__(self):
pass
# next() 函数定义了每个时间步要做的事,例如我们的数据是日线数据,我们就需要在每天做点什么,例如买入或者卖出股票。
def next(self):
# 开盘第一天买入
if len(self.data) == 1:
self.buy()
self.buy()
self.buy()
self.buy()
买入函数,我们使用self.buy()调用,通常为买一手。
运行策略的模板,即如何使用backtrader进行回测,最简单的案例
if __name__ == '__main__':
#cerebro是这个框架的核心,我们直接实例化
cerebro = bt.Cerebro()
# 我们需要读取数据,所以我们定义一下目录,然后导入数据
modpath = os.path.dirname(os.path.abspath(sys.argv[0]))
datapath = os.path.join(modpath, './res.csv')
# 数据加载
data = TushareCSVData(
dataname=datapath, # 文件路径
format='%Y%m%d', # 日期格式
fromdate=datetime.datetime(1997, 6, 11), # 起始日期,这里是可以对原始读到的数据进行切割的
todate=datetime.datetime(2025, 11, 1), # 终止日期,此处同理也可以切割
headers=True,
reverse=False)
cerebro.adddata(data) # 向 cerebro 导入数据
cerebro.broker.setcash(100000.0) # 设置初始现金
cerebro.addstrategy(MyStrategy) # 加载策略
cerebro.broker.setcommission(0.0003) # 设置手续费
cerebro.signal_accumulate(False) # False 禁用
# 这里在打印初始资金和最总资金
print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())
cerebro.run()
print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())
# 画出交易回测图
cerebro.plot()
这里的运行结果如下图所示。

控制台结果为
C:\Users\hell\anaconda3\python.exe E:\postgraduate\dream_code_v2\backtrader\backtrader\my_backtrader\test1.py
Starting Portfolio Value: 100000.00
Final Portfolio Value: 100055.95
可以看到,我们持有”广发证券“约28年,怒赚55.95元。
现在我们已经对backtrader的使用,有了一点认知。接下来,我们开始写一些复杂一点的策略。
我们已经知道怎么买了,那么下一个要考虑的问题就是怎卖,一般我们在策略的def next()函数中,调用self.sell()就可以卖出了。
基础策略
考虑一个策略,就是要定义什么时候买,什么时候卖的问题。判断依据不能只依靠数据中给定的open、high、low、close等简单的指标,我们需要考虑其他的交易信号,来指导我们的交易。其中,双均线策略就是一个经典的策略。
双均线策略
双均线策略,即给定两个不同的周期,分别计算一条能代表股价波动趋势的趋势线,其中小周期的线会被称为”快线“,长周期的线会被称为”慢线“。当快线上穿慢线时(金叉),代表股票将进入上涨阶段,考虑买入;反之,当股票慢线下穿快线时(死叉),代表股票将进入下跌阶段,考虑卖出。我们来实现一下。
首先介绍以下快线慢线的计算方法,不管我们的快线还是慢线,都指的是我们的移动平均线(MA, Moving Average)。
移动平均线(MA)用于平滑价格波动,计算公式如下:
M A n = C 1 + C 2 + C 3 + ⋯ + C n n MA_n = \frac{C_1 + C_2 + C_3 + \cdots + C_n}{n} MAn=nC1+C2+C3+⋯+Cn
其中:
- ( M A n MA_n MAn ): n n n 日移动平均值
- ( C i C_i Ci ):第 i i i 天的收盘价(Close)
- ( n n n ):计算周期(例如 5、10、30 等)
示例(MA5)
假设最近 5 日的收盘价为:
| 日期 | 收盘价 |
|---|---|
| 第1天 | 10.0 |
| 第2天 | 10.2 |
| 第3天 | 10.4 |
| 第4天 | 10.3 |
| 第5天 | 10.1 |
则:
M A 5 = 10.0 + 10.2 + 10.4 + 10.3 + 10.1 5 = 10.2 MA_5 = \frac{10.0 + 10.2 + 10.4 + 10.3 + 10.1}{5} = 10.2 MA5=510.0+10.2+10.4+10.3+10.1=10.2
了解了MA的计算方法,我们就可以计算了。首先我们的快线慢线周期分别为10日和20日,当然你也可以设置为5日和10日,自己进行尝试。然后当计算结果计算出来后,我们判断其是金叉还是死叉,然后根据信号进行交易即可。
我们使用模板,为其添加东西。首先,我们要初始化快慢线周期,然后我们在策略初始化的时候传入数据,计算出我们的快慢线结果。为了能够让我们在next函数中拿到我们的信号,我们必须要把我们的快线和慢线的计算结果放到我们的self中。然后在策略执行的时候进行每一步判断,直到时间步结束。
class MAStrategy(bt.Strategy):
params = dict(
fast=10, # 快速均线
slow=20 # 慢速均线
)
def __init__(self):
self.ma_fast = bt.indicators.SMA(self.data.close, period=self.p.fast)
self.ma_slow = bt.indicators.SMA(self.data.close, period=self.p.slow)
# 判断是否发生金叉/死叉的内置指标,感兴趣可以查看源码,肥肠简单!
self.crossover = bt.indicators.CrossOver(self.ma_fast, self.ma_slow)
def next(self):
# 如果没有持仓
print(self.data.close[0])
if not self.position:
if self.crossover > 0: # 金叉 → 买入
self.buy()
# 如果已经持仓
else:
if self.crossover < 0: # 死叉 → 卖出
self.sell()
我们使用模板来运行策略;
from __future__ import (absolute_import, division, print_function,
unicode_literals)
import datetime
import os
import sys
import backtrader as bt
from my_backtrader.MyCSVData import TushareCSVData
class MAStrategy(bt.Strategy):
params = dict(
fast=10, # 快速均线
slow=20 # 慢速均线
)
def __init__(self):
self.ma_fast = bt.indicators.SMA(self.data.close, period=self.p.fast)
self.ma_slow = bt.indicators.SMA(self.data.close, period=self.p.slow)
# 判断是否发生金叉/死叉的内置指标
self.crossover = bt.indicators.CrossOver(self.ma_fast, self.ma_slow)
def next(self):
# 如果没有持仓
print(self.data.close[0])
if not self.position:
if self.crossover > 0: # 金叉 → 买入
self.buy()
# 如果已经持仓
else:
if self.crossover < 0: # 死叉 → 卖出
self.sell()
if __name__ == '__main__':
cerebro = bt.Cerebro()
modpath = os.path.dirname(os.path.abspath(sys.argv[0]))
datapath = os.path.join(modpath, './res.csv')
# --- 数据加载 ---
data = TushareCSVData(
dataname=datapath,
format='%Y%m%d',
fromdate=datetime.datetime(1997, 6, 11),
todate=datetime.datetime(2025, 11, 1),
headers=True)
cerebro.adddata(data)
cerebro.broker.setcash(100000.0)
cerebro.addstrategy(J_Min_Strategy)
cerebro.broker.setcommission(0.0003)
cerebro.signal_accumulate(False) # 或 False 禁用
print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())
cerebro.run()
print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())
cerebro.plot()
我们运行代码,得到的结果如下:

控制台输出为:
C:\Users\hell\anaconda3\python.exe E:\postgraduate\dream_code_v2\backtrader\backtrader\my_backtrader\test.py
Starting Portfolio Value: 100000.00
Final Portfolio Value: 100060.56
可以看到,我们通过密密麻麻的交易,怒赚 60.56 60.56 60.56 元,比buy and hold 还要多整整 60.56 − 55.95 = 4.61 60.56-55.95=4.61 60.56−55.95=4.61(元)。
KDJ策略
这个策略来源于网络。顾名思义,交易信号来自于KDJ指标,当KDJ指标中J处于极小值的时候考虑买入。相应的,当J处于极大值的时候考虑卖出(事实上,出入场都可以灵活变换,本案例就是用了不同的离场方案)。为了得到我们的KDJ指标,首先要知道是怎么计算的。
KDJ 是在随机指标(Stochastic Indicator, KD)基础上发展而来的技术指标,常用于判断股票的超买超卖状态。
🧩 KDJ 计算步骤
- 计算 RSV(未成熟随机值)
R S V = C L O S E − L L V ( L O W , N ) H H V ( H I G H , N ) − L L V ( L O W , N ) × 100 RSV = \frac{CLOSE - LLV(LOW, N)}{HHV(HIGH, N) - LLV(LOW, N)} \times 100 RSV=HHV(HIGH,N)−LLV(LOW,N)CLOSE−LLV(LOW,N)×100
其中:
- C L O S E CLOSE CLOSE :当日收盘价
- L O W LOW LOW :当日最低价
- ( H I G H HIGH HIGH :当日最高价
- L L V ( L O W , N ) LLV(LOW, N) LLV(LOW,N) :最近 N 日内最低价的最小值
- H H V ( H I G H , N ) HHV(HIGH, N) HHV(HIGH,N) :最近 N 日内最高价的最大值
- 通常 N = 9 N = 9 N=9
- 计算 K 值(平滑 RSV)
通达信算法使用 SMA 平滑移动平均:
K = S M A ( R S V , 3 , 1 ) K = SMA(RSV, 3, 1) K=SMA(RSV,3,1)
即:
- 3:平滑周期
- 1:权重参数(表示新值的权重较高)
- 计算 D 值
同样使用 SMA:
D = S M A ( K , 3 , 1 ) D = SMA(K, 3, 1) D=SMA(K,3,1)
- 计算 J 值
J = 3 × K − 2 × D J = 3 \times K - 2 \times D J=3×K−2×D
我们接下来,来实现这个策略,KDJ在计算的时候需要考虑初始化问题,在同花顺和通达信中,会将KDJ的初始值设置为RSV,这样就可以进行滚动计算。我们首先实现以下SMA和KDJ这俩个函数。
class SMA(bt.Indicator):
lines = ('sma',)
params = (('period', 3), ('m', 1))
def __init__(self):
self.addminperiod(1)
def next(self):
n = self.p.period
m = self.p.m
x = self.data[0]
if len(self) == 1:
# 第一天 SMA = X
self.lines.sma[0] = x
else:
prev = self.lines.sma[-1]
self.lines.sma[0] = (m * x + (n - m) * prev) / n
SMA相对比与MA,做了平滑处理。
class KDJ(bt.Indicator):
lines = ('k', 'd', 'j')
params = (('period', 9),)
def __init__(self):
self.addminperiod(1)
# 自己维护 9 日高低
self.high_buf = []
self.low_buf = []
def next(self):
period = self.p.period
# push 当前 high/low
self.high_buf.append(self.data.high[0])
self.low_buf.append(self.data.low[0])
# 保证窗口维持 period 大小
if len(self.high_buf) > period:
self.high_buf.pop(0)
self.low_buf.pop(0)
# 通达信:不足 period 时,用现有窗口计算
hh = max(self.high_buf)
ll = min(self.low_buf)
# 计算 RSV
if hh == ll:
rsv = 50.0 # 防止除零,通达信中该情况 RSV=50
else:
rsv = (self.data.close[0] - ll) / (hh - ll) * 100
# KDJ
if len(self) == 1:
# 第一天
k = d = j = rsv
else:
# SMA 平滑
prev_k = self.lines.k[-1]
prev_d = self.lines.d[-1]
# 通达信 SMA(K,3,1)
k = (1 * rsv + 2 * prev_k) / 3
d = (1 * k + 2 * prev_d) / 3
j = 3 * k - 2 * d
# 输出结果
self.lines.k[0] = k
self.lines.d[0] = d
self.lines.j[0] = j
然后我们实现一下这个策略。与之前不同的是,我们设置了仓位,且为全仓。之前的双均线策略使用的是买一手,且我们设定盈利7%时离场。
class J_Min_Strategy(bt.Strategy):
params = dict(
buy_j=-5, # 买入条件:J < 0
sell_gain=0.07, # 卖出条件:涨幅达到7%
invert_ratio=0.99, # 仓位(全仓)
)
def __init__(self):
self.kdj = TDX_KDJ(self.data) # 你自己定义的通达信版KDJ指标
self.buy_price = None # 记录买入价
def log(self, txt, dt=None): # 设定了一个日志函数
dt = dt or self.data.datetime.date(0)
print(f"{dt} | {txt}")
def next(self):
close = float(self.data.close[0])
j = float(self.kdj.j[0])
if not self.position:
if j < self.p.buy_j:
self.buy_price = close
cash = self.broker.get_cash()
# 计算买入手数(按可用现金和 invest_ratio)使用仓位
size = int((cash / close) * float(self.p.invert_ratio))
if size <= 0:
self.log(f"买入信号但资金不足或 invest_ratio 太小:cash={cash:.2f}, price={close:.4f}, size={size}")
return
self.log(f"下买单:J={j:.2f}, 价格={close:.4f}, 计划数量={size}")
self.buy(size=size) # 下单并记录未决订单
else:
if self.buy_price is None:
self.buy_price = self.position.price if hasattr(self.position, 'price') else close
gain = (close - self.buy_price) / self.buy_price
if gain >= self.p.sell_gain:
size = self.position.size
self.log(f"下卖单:当前涨幅={gain * 100:.2f}%, 价格={close:.4f}, 卖出数量={size}")
self.sell(size=size)
我们运行以下这个策略来看看结果。

控制台输出
C:\Users\hell\anaconda3\python.exe E:\postgraduate\dream_code_v2\backtrader\backtrader\my_backtrader\test.py
Starting Portfolio Value: 100000.00
1997-06-27 | 下买单:J=-5.16, 价格=8.1500, 计划数量=12147
1997-11-03 | 下卖单:当前涨幅=7.98%, 价格=8.8000, 卖出数量=12147
1999-02-01 | 下买单:J=-5.37, 价格=17.1200, 计划数量=6221
1999-07-02 | 下卖单:当前涨幅=10.92%, 价格=18.9900, 卖出数量=6221
1999-09-17 | 下买单:J=-11.99, 价格=16.5100, 计划数量=7230
2010-02-12 | 下卖单:当前涨幅=203.15%, 价格=50.0500, 卖出数量=7230
2010-03-09 | 下买单:J=-9.47, 价格=51.8500, 计划数量=7033
2010-03-11 | 下卖单:当前涨幅=7.21%, 价格=55.5900, 卖出数量=7033
2010-07-01 | 下买单:J=-11.47, 价格=29.9900, 计划数量=12844
2010-07-23 | 下卖单:当前涨幅=7.47%, 价格=32.2300, 卖出数量=12844
2010-08-11 | 下买单:J=-6.18, 价格=31.3200, 计划数量=13091
2010-10-08 | 下卖单:当前涨幅=8.37%, 价格=33.9400, 卖出数量=13091
2010-11-15 | 下买单:J=-9.07, 价格=50.9900, 计划数量=8897
2010-12-13 | 下卖单:当前涨幅=7.26%, 价格=54.6900, 卖出数量=8897
2011-01-14 | 下买单:J=-8.98, 价格=47.0300, 计划数量=10315
Final Portfolio Value: 239430.64
看起来结果还是不错的,但是我们发现我们最后一笔单子锁定在了2011年,我们长达14年没有进行操作。读者可以思考一下离场是否有其他的方式呢?对于本策略中的离场方式,是否还有优化的空间呢?
我们现在知道了,想要写策略,就得给我们的策略函数传递一些数据信息,或者直接给定交易信号,然后在next函数中,判断数据信息进行交易,或者直接根据信号进行交易。策略制定的过程就是选出/算出好的数据信息的过程。由此,如果我们想要通过深度学习来指定策略,那就是通过深度学习的预测输出制定交易策略。
深度学习策略
未完待续…
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐


所有评论(0)