上一篇,我们使用Dash实现了一个轮动策略Web版交互界面。本篇将在其基础上,把功能强大的 Backtrader 回测框架深度集成到系统中,让你能够利用真实历史数据对轮动策略进行验证和优化。



搭建思路与目标
这个系列旨在从零开始,用 Python 一步步搭建出一套完整的ETF量化交易系统。选择ETF作为标的,是因为对普通交易者而言,它相比选股难度更小,且没有退市风险。通过跟随整个实现路径,你可以系统性地学习量化系统构建的方法论。
掌握了核心方法后,你完全可以将其迁移至期货、比特币或美股等不同市场,并在实战中持续迭代和完善自己的系统。
系统架构与核心集成
整体架构:前端采用Dash构建响应式Web界面,通过Bootstrap进行现代化UI设计;后端核心是Backtrader量化回测引擎,通过回调函数与前端交互;数据层使用AKShare从新浪财经获取实时ETF历史数据,形成清晰的三层架构。
技术栈:Dash(前端框架)+ Backtrader(量化回测)+ AKShare(数据获取)+ Pandas(数据处理)+ Plotly(数据可视化),形成了一套完整的量化策略研发工具链。
数据流:用户在Web界面设置参数 → 点击回测按钮触发Dash回调 → Backtrader引擎加载ETF历史数据 → 执行动量轮动策略 → 生成交易记录和绩效指标 → 结果返回前端可视化展示。
Backtrader策略核心实现 (MomentumRotationStrategy)
策略的核心在于动量轮动逻辑:定期计算所有ETF在一定周期内的动量(收益率),卖出不在前列的持仓,并买入动量最强的几只ETF。
class MomentumRotationStrategy(bt.Strategy):
"""
动量轮动策略的核心实现
继承自Backtrader的bt.Strategy基类
"""
params = (
('momentum_period', 5), # 动量计算周期
('rebalance_period', 5), # 调仓周期
('hold_num', 2), # 持有标的数量
('min_momentum', 0), # 最小动量阈值
('position_size', 0.4), # 单个标的仓位比例
('printlog', True), # 日志开关
)
参数说明:
momentum_period: 计算动量(收益率)的回顾周期
rebalance_period: 调仓间隔天数
hold_num: 每次持有表现最好的ETF数量
min_momentum: 动量过滤阈值,避免买入弱势标的
position_size: 每个ETF的最大仓位比例
__init__() - 初始化方法
def __init__(self):
# 为每个ETF计算动量指标
self.momentum = {
data: bt.indicators.Momentum(data.close, period=self.p.momentum_period)
for data in self.datas
}
# 存储ETF排序结果
self.rankings = list(self.datas)
# 调仓计数器
self.rebalance_day = 0
# 订单字典,管理未完成订单
self.order_dict = {}
next() - 逐日执行方法
每个交易日调用一次,当计数器达到预设的调仓周期时,触发调仓逻辑。
def next(self):
self.rebalance_day += 1
# 到达调仓日时执行调仓
if self.rebalance_day >= self.p.rebalance_period:
self.rebalance_portfolio()
self.rebalance_day = 0
rebalance_portfolio() - 核心调仓逻辑
这是策略的“大脑”,包含了排序、卖出、买入和仓位控制的全过程。
def rebalance_portfolio(self):
"""执行调仓:卖出不在前列的,买入排名前列的"""
# 1. 按动量值排序(从高到低)
self.rankings.sort(key=lambda d: self.momentum[d][0], reverse=True)
# 2. 卖出不在前hold_num名的持仓
for data in self.datas:
pos = self.getposition(data).size
if pos > 0 and data not in self.rankings[:self.p.hold_num]:
self.close(data=data) # 平仓
# 3. 买入排名前hold_num且动量达标的标的
for i in range(min(self.p.hold_num, len(self.rankings))):
data = self.rankings[i]
pos = self.getposition(data).size
momentum_value = self.momentum[data][0]
if pos == 0 and momentum_value > self.p.min_momentum and data not in self.order_dict:
# 计算买入数量
target_value = self.broker.getvalue() * self.p.position_size
size = int(target_value / data.close[0] / 100) * 100 # 按手数取整
if size > 0:
self.order_dict[data] = self.buy(data=data, size=size)
notify_order() - 订单状态回调
负责处理订单状态变化,记录交易日志并清理已完成订单。
def notify_order(self, order):
"""处理订单状态变化"""
if order.status in [order.Completed]:
if order.isbuy():
self.log(f'买入执行 {order.data._name}, 价格:{order.executed.price:.2f}')
elif order.issell():
self.log(f'卖出执行 {order.data._name}, 价格:{order.executed.price:.2f}')
self.order_dict.pop(order.data, None) # 移除已完成的订单
数据获取与回测引擎配置
获取ETF历史数据并转换为Backtrader需要的格式。
def get_etf_data(etf_code, start_date='2023-01-01', end_date='2023-12-31'):
"""
从新浪财经获取ETF历史数据
返回格式:DataFrame with columns: ['open', 'high', 'low', 'close', 'volume']
"""
# 市场代码转换:沪市51开头,深市15开头
symbol = f'sh{etf_code}' if etf_code.startswith('51') else f'sz{etf_code}'
# 使用AKShare获取数据
df = ak.fund_etf_hist_sina(symbol=symbol)
# 数据清洗和格式化
df['date'] = pd.to_datetime(df['date'])
df.set_index('date', inplace=True)
df = df[(df.index >= start_date) & (df.index <= end_date)]
return df
数据格式转换:
# Backtrader需要PandasData格式
data = bt.feeds.PandasData(
dataname=df, # 原始DataFrame
name=etf_code, # 数据名称(用于识别)
datetime=None, # 使用索引作为日期
open=0, high=1, # 列索引映射
low=2, close=3,
volume=4, # 成交量列
openinterest=-1 # 无持仓量数据
)
回测引擎的初始化与执行函数 run_backtest 封装了创建引擎、设置参数、添加数据与策略、运行回测并返回结果的完整流程。
Dash回调函数集成:打通前后端
关键一步是将Backtrader回测逻辑嵌入到Dash的回调函数中。当用户在前端点击“开始回测”按钮时,会触发一个复杂的回调函数 run_real_backtest。这个函数负责:
- 从前端组件获取用户设置的所有参数(日期、资金、策略参数)。
- 调用数据获取函数,为ETF池中的每个标的获取历史数据。
- 动态创建并配置Backtrader的Cerebro引擎。
- 将数据和策略添加到引擎中,并附加各类绩效分析器。
- 执行回测,并从结果中提取总收益率、夏普比率、最大回撤、交易详情等指标。
- 处理回测中的时间序列收益,生成可视化的净值曲线图。
- 将所有结果格式化,返回给前端的各个显示组件进行更新。
这一设计实现了从数据获取、策略执行到绩效分析与可视化的完整闭环。系统执行时,会在控制台输出详细的调仓日志,例如:
2024-04-23 | 📅 调仓日期: 2024-04-23
2024-04-23 | 💰 当前总资产: 1000000.00
2024-04-23 | 📊 ETF动量排名 (周期=20天):
2024-04-23 | 1. 159562 动量: +0.09
2024-04-23 | 2. 159652 动量: +0.04
2024-04-23 | 📥 买入清单 (2只):
2024-04-23 | ✔ 159562: 312000股 (动量: +0.09)
2024-04-23 | ✔ 159652: 447900股 (动量: +0.04)
为什么选择Dash?
对于量化研究者和数据科学家而言,Dash极大地降低了构建交互式Web应用的门槛。它允许你完全使用Python来定义前端布局和交互逻辑,无需接触HTML、CSS或JavaScript。这意味着你可以将全部精力集中在核心的策略和数据逻辑上,快速将想法转化为可展示、可交互的产品。
一个最简单的Dash应用结构清晰:
- 创建应用对象:
app = dash.Dash(__name__)
- 定义布局:使用
html.Div, dcc.Input, dcc.Graph 等组件构建页面。
- 定义回调:使用
@app.callback 装饰器,指定输入(哪些组件的变化会触发函数)和输出(函数结果更新哪些组件),编写交互逻辑函数。
- 运行服务器:
app.run_server(debug=True)
在这个最简例程的基础上,你可以轻松地添加图表、多页面标签(Tabs),并连接真实的数据库或API,从而构建出像本文介绍的这样功能复杂的量化交易管理平台。
框架功能模块详解
本系统采用侧边栏导航结合主内容区的经典布局,通过Dash的回调机制实现各功能页面的平滑切换。主要功能模块如下:
1. 我的轮动池
提供ETF池管理功能,支持添加、筛选、排序以构建个性化轮动池。核心是动量排名的可视化,实时计算并展示各ETF的动量分数及排名变化,策略状态一目了然。
动量轮动策略核心逻辑:
- 每20天定期调仓
- 选择动量排名前2的ETF持有
- 动量分数基于20日涨幅计算
- 实现“强者恒强”的轮动效应

2. 我的账户
提供资产全景视角,展示总资产、持仓市值、可用资金及累计收益。通过交互式资产配置饼图直观展示持仓分布,并配有动态收益曲线追踪账户净值变化。

3. 策略回测
构建参数化回测系统,支持灵活设置回测周期、初始资金、调仓频率等。以绩效指标矩阵和净值对比曲线图全面评估策略历史表现。

4. 参数配置
集中管理所有策略参数,分为策略参数、交易参数和风险参数三类,形成从策略逻辑、交易执行到风险管控的完整配置闭环。

5. 实盘明细
提供交易全流程监控,支持多维度筛选交易记录,完整展示每笔交易的详情并自动计算盈亏。

6. 数据管理
实现一体化数据管理,支持自动更新计划配置,监控多数据源状态,并提供手动更新控制。

7. 系统监控
保障系统稳定运行,实时展示服务器、数据库、策略引擎等核心组件状态,监控资源使用率,并追踪关键操作日志。

8. 策略文档
构建知识沉淀体系,包含策略原理说明、详细操作指南、最佳实践分享和技术支持通道。

部署与运行
# 安装核心依赖
pip install dash plotly pandas numpy akshare backtrader
# 开发环境运行
python app.py
# 生产环境部署建议使用Gunicorn
gunicorn -w 4 -b 0.0.0.0:8050 app:server
应用启动后,在浏览器中访问 http://localhost:8050 即可使用。
总结与展望
Dash框架让数据科学家能够专注于业务逻辑,高效构建专业级Web应用。本系统所实现的Web版轮动策略框架,采用了界面与功能架构先行的策略。当前版本已完成所有交互界面和模块逻辑,展示数据为模拟与真实数据结构相结合。
关键在于,后续所有数据接口、回调函数和计算模块均采用标准化设计。这意味着,使用者只需将现有的模拟数据源替换为真实的行情API(如券商接口),并将策略逻辑的核心计算函数替换为最终的实盘算法,即可在不改动任何界面代码的情况下,快速将本演示系统升级为一套完整的、可用于生产环境的交易平台。这种设计充分体现了 开源实战 项目中模块化与可扩展性的重要思想,为个人量化交易者提供了一个极佳的开发起点。通过 云栈社区 这样的平台,开发者可以交流类似项目的经验,共同推动量化工具生态的发展。