[升级V2.1.5]回测模块重构:参数确认+异步进度+日志持久化!本地Web版多因子轮动系统

前言

复制代码
````

```go
股票量化系统QTYX支持三因子轮动"抄作业"功能。最新运行情况如下所示:从2025年12月18日至2026年5月15日,策略累计收益率+19.83%,超额同期沪深300指数(+6.74%)+13.09%。
⚠️ 风险提示:以上数据为测试策略数据,不构成投资建议,不预示未来表现,投资须谨慎。

```


````

复制代码
``````

`````

````

```
`"抄作业"虽然方便,但只是为了让大家直观上手。由于每个人的交易风格不同,所以因子参数需要个性化调整,ETF池需要个性化配置。
为了满足大家DIY自己的量化系统的初衷,我们将DAY21中终极系统,一步步升级为本地Web版三因子轮动系统。`

```


````


`````


``````

本次升级

V2.1.5 版本的核心在于重构了回测模块,建立了全流程的日志复盘机制。回测不再是一个黑盒,每一次调仓逻辑、参数生效过程及最终绩效都被结构化地记录在logs/目录中,确保了策略评估的可解释性与可追溯性。****(每一次小升级,都是搭建你个人量化系统的一个坚实小步,从"能用"到"好用",我们一起见证系统的进化)

V2.1.5 围绕回测模块进行了三项核心改进:

1. 参数确认机制:点击回测后首先弹出确认对话框,强制用户核对回测周期、动量周期、持仓数量等关键参数,避免因参数误填导致的无效回测,提升了回测结果的有效性。


2. 异步执行与进度可视化 :回测任务在后台线程中异步执行,前端通过轮询机制实时更新进度条(0%→100%),解决了长时间回测导致界面卡顿的问题,用户可随时了解数据加载、策略计算等环节的执行状态。

3. 全流程日志持久化 :回测启动时自动在 logs/ 目录下生成以时间戳命名的日志文件(如 backtest_20260116_143052.txt),完整记录了参数配置、数据加载结果、每次调仓决策、每笔交易执行及最终绩效指标,为策略复盘、问题定位和参数优化提供了可追溯的依据。

现在策略参数在系统内拥有了"统一调度权":

以下是 V2.1.5 回测模块相关代码的功能讲解,按执行流程分段说明:

参数确认对话框

python 复制代码
@app.callback(    [Output("backtest-confirm-modal", "is_open"),     Output("confirm-start-date", "children"),     ...],    [Input("start-backtest-btn", "n_clicks"),     Input("cancel-backtest-btn", "n_clicks")],    [State("backtest-date-range", "start_date"),     State("initial-capital-input", "value"),     ...])def show_backtest_confirm(...):
功能点 实现方式
拦截回测请求 点击"开始回测"时并不直接执行,而是触发确认对话框
参数回显 将用户在界面填写的参数(日期、资金、动量周期等)格式化后显示在对话框中
二次确认 用户点击"确认开始回测"后才真正启动回测任务
取消操作 点击"取消"则关闭对话框,不做任何处理

设计目的:防止用户因误触或参数填错就开始耗时较长的回测,降低无效计算。

异步回测执行 + 进度监控

启动后台线程

python 复制代码
def start_backtest_after_confirm(...):    global backtest_progress_global    # 重置进度状态    backtest_progress_global = {        'running': True,        'current': 0,        'total': 100,        'status_text': '🚀 启动回测引擎...',        'params': {...}  # 保存参数    }    # 启动后台线程(不阻塞Dash主进程)    thread = threading.Thread(target=run_backtest_in_background)    thread.daemon = True    thread.start()    return backtest_progress_global, False, False  # 启用轮询

后台执行函数

python 复制代码
def run_backtest_in_background():    """在后台线程中执行回测"""    # 1. 设置日志记录器    backtest_logger, log_file_path = setup_backtest_logger()    # 2. 分步骤更新进度(5% → 100%)    backtest_progress_global.update({        'current': 5,        'status_text': '🔧 初始化回测引擎...'    })    # 3. 加载ETF数据(10% → 50%)    for i, etf_code in enumerate(etf_pool):        progress = 10 + int((i / total_etfs) * 40)        backtest_progress_global.update({            'current': progress,            'status_text': f'📥 加载数据 {i+1}/{total_etfs}: {etf_code}'        })    # 4. 配置Cerebro引擎并运行回测(55% → 90%)    cerebro = bt.Cerebro()    cerebro.addstrategy(MomentumRotationStrategy, ...)    results = cerebro.run()    # 5. 计算绩效指标并保存结果(90% → 100%)    backtest_progress_global['result'] = {...}    backtest_progress_global['running'] = False

前端进度轮询

python 复制代码
@app.callback(    [Output("backtest-progress-bar", "style"),     Output("backtest-progress-text", "children"),     ...],    [Input("backtest-progress-interval", "n_intervals")],    [State("backtest-progress-store", "data")])def monitor_backtest_progress(...):    progress = backtest_progress_global    if progress.get('running'):        percent = (current / total) * 100        return {...}  # 返回进度条样式和文字    elif progress.get('result'):        return {...}  # 返回回测结果(收益率、夏普比率、净值曲线)

设计要点

  • 回测在独立线程中运行,Dash界面不卡顿

  • 通过全局变量 backtest_progress_global 在主线程和后台线程间共享状态

  • 前端每 500ms 轮询一次,实时更新进度条和状态文字

日志持久化

python 复制代码
def setup_backtest_logger():    """设置回测日志记录器"""    log_dir = "logs"    if not os.path.exists(log_dir):        os.makedirs(log_dir)    # 以时间戳命名日志文件    log_filename = os.path.join(        log_dir,         f"backtest_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt"    )    logger = logging.getLogger('backtest')    # 同时输出到文件和控制台    file_handler = logging.FileHandler(log_filename, encoding='utf-8')    console_handler = logging.StreamHandler()    return logger, log_filename

日志记录内容

阶段 记录内容
启动 回测参数(周期、资金、动量周期、持仓数量等)
数据加载 每只ETF的数据行数、成功/失败状态
策略执行 每次调仓的日期、持仓变化、动量排名
交易执行 每笔买卖的价格、数量、盈亏
结果汇总 总收益率、夏普比率、最大回撤、胜率、交易次数

策略内部日志

python 复制代码
class MomentumRotationStrategy(bt.Strategy):    def log(self, txt, dt=None):        if self.p.printlog:            dt = dt or self.datas[0].datetime.date(0)            print(f'{dt.isoformat()} | {txt}')  # 会被logger捕获

设计要点

  • 临时替换 builtins.print,将 backtrader 的策略日志重定向到自定义logger

  • 日志文件包含完整的时间戳日志级别,便于复盘分析

  • 每次回测生成独立文件,避免覆盖历史记录

代码架构总结

bash 复制代码
用户点击"开始回测"    ↓【参数确认对话框】 ← 拦截、回显、二次确认    ↓启动后台线程 (threading.Thread)    ↓【run_backtest_in_background】    ├── 创建日志文件 (logs/backtest_时间戳.txt)    ├── 更新进度: 5% → 初始化    ├── 更新进度: 10%-50% → 加载ETF数据    ├── 更新进度: 55%-90% → 运行backtrader回测    └── 更新进度: 100% → 保存结果到全局变量    ↓【前端轮询】 (500ms间隔)    ├── 运行中 → 更新进度条    └── 完成 → 显示净值曲线 + 绩效卡片 + 完成图标

页面功能展示

目前已经完成的功能介绍如下所示:

我的轮动池:实时展示ETF动量排名列表,按动量分数从高到低排序。点击"详情"按钮可对该ETF进行手动买入/卖出操作。点击"刷新数据"重新计算所有ETF的动量分数并更新排名。点击"自动下单"自动对比当前持仓与前5名ETF,执行调仓。

我的账户:展示总资产、持仓市值、可用资金、当前盈亏四大核心数据。通过饼图展示持仓市值分布,柱状图展示持仓收益率排行,表格展示详细持仓明细。策略回测:设置回测日期、初始资金、动量周期、调仓周期、持有数量等参数,点击开始回测后运行Backtrader引擎,展示总收益率、夏普比率、最大回撤、胜率、交易次数、盈亏比等指标,并生成净值曲线图。参数配置:配置策略参数(动量周期、调仓周期、持有数量)、交易参数(手续费率、滑点、最小交易单位)、风险参数(止损止盈、回撤警戒、仓位限制等)。实盘明细:点击刷新按钮获取QMT当日委托记录,展示委托时间、证券代码、证券名称、买卖方向、委托状态、委托量、成交数量、已撤数量、委托价格、成交均价、冻结资金、订单编号、废单原因等信息。数据管理:点击"立即更新"按钮,遍历所有ETF从网络获取历史数据并保存到本地"行情数据库"目录,同时更新表格显示每只ETF的数据状态。系统监控:展示服务器、数据库、策略运行、交易接口四大组件的运行状态;展示CPU和内存使用率曲线图;展示系统运行日志列表。策略文档:展示动量轮动策略的说明、使用指南和注意事项。

复制代码
`````

````

```go
AI学习代码

```

从星球下载代码后,您可以直接将代码输入到AI大模型中。配合我们提供的分点式提示词(已按由浅入深的顺序梳理成多个小问题),您可以循序渐进地理解代码结构、掌握核心逻辑,并高效完成二次开发。

````

![](https://i-blog.csdnimg.cn/img_convert/6b73af385114bd507edb6984574077ba.png)第一部分:系统整体认知

`````


```markdown
请根据以上代码,用简短的一句话回答以下每个问题:1. 这个系统叫什么名字?2. 系统是用什么框架搭建Web界面的?3. 回测功能用的是哪个库?4. 实盘交易连接的是哪个券商接口?5. 历史行情数据是从哪个网站获取的?6. 实时行情数据是从哪里获取的?7. 数据默认存在哪个目录下?8. 持仓记录存在哪个文件里?9. 系统默认运行在哪个端口?10. 页面左侧有几个导航菜单?11. 默认监控多少只ETF?12. 动量计算周期默认是多少天?13. 调仓周期默认是多少天?14. 默认每次持有几只ETF?15. 自动调仓时默认每只买多少股?16. 账户数据每隔几秒自动刷新?17. 系统状态显示在哪里?18. 退出程序时QMT进程会被自动关闭吗?

```


```go
第二部分:数据相关

```


```
`请根据以上代码,用简短的一句话回答以下每个问题:1. get_etf_data() 函数获取数据时,先查哪里再查哪里?2. 本地CSV文件找不到时,系统会怎么办?3. AKShare获取数据用的是哪个函数?4. 沪市ETF代码前面加什么前缀?深市呢?5. 数据保存到CSV时,文件名格式是什么?6. 数据管理页面的状态数据是真实的还是模拟的?7. 点击"立即更新"按钮后,数据会保存到哪里?8. 更新数据时,如果某只ETF获取失败,会影响其他ETF吗?9. 本地数据目录叫什么名字?10. 回测时优先使用本地数据还是网络数据?11. 动量计算需要最少多少天的数据?12. 数据表的K线数列显示的是什么内容?13. 数据状态有哪几种可能的值?14. 如何判断本地数据是否"最新"?15. CSV文件保存时用什么编码格式?`

```


```go
第三部分:交易相关

```


```markdown
请根据以上代码,用简短的一句话回答以下每个问题:1. QMT客户端是运行在主进程还是子进程?2. 主进程和子进程之间通过什么通信?3. 下单函数 place_order 的 is_buy=True 表示买入还是卖出?4. QMT下单时,order_type=23 表示什么?24表示什么?5. 卖出操作会先检查什么?6. 买入操作会先检查什么?7. 下单手续费默认是万分之几?8. 自动调仓时,先执行卖出还是先执行买入?9. 调仓成功后会保存到哪里?10. 手动下单后,用户通过什么组件看到反馈?11. 订单提交失败的提示是什么颜色的?12. 订单提交成功的提示是什么颜色的?13. 实盘明细页面从哪里获取委托记录?14. 委托状态"已报"、"部分成交"、"全部成交"分别是什么意思?15. 什么情况下订单可以被撤单?16. 废单原因存放在哪个字段?

```


```
`第四部分:策略相关`

```


```markdown
请根据以上代码,用简短的一句话回答以下每个问题:1. 动量计算用的是收盘价还是开盘价?2. 动量公式:当前价格减去多少天前的价格?3. 动量分数的单位是什么(百分比还是绝对值)?4. 动量分数为正是涨还是跌?5. backtrader策略类叫什么名字?6. 策略的调仓逻辑写在哪个方法里?7. 每次调仓时,卖出什么条件的ETF?8. 每次调仓时,买入什么条件的ETF?9. 买入时的仓位比例默认是多少?10. 买入数量为什么要除以100再乘以100?11. 回测时的手续费是万几?12. 回测报告的夏普比率从哪里获取?13. 回测报告的最大回撤从哪里获取?14. 回测报告的交易次数从哪里获取?

```


```
`第五部分:前端/UI相关`

```


```markdown
请根据以上代码,用简短的一句话回答以下每个问题:1. 页面切换是通过什么机制实现的?2. 哪个组件负责定时刷新账户数据?3. 定时刷新的间隔是多少毫秒?4. 表格数据是通过哪个回调函数生成的?5. 搜索框输入后,表格会实时过滤吗?6. ETF代码和名称的映射存在哪个字典里?7. 轮动池表格里,排名前5名的颜色是什么?8. 表格中动量分数的正负用什么颜色区分?9. 下单模态框是如何被触发展开的?10. 模态框里的"当前价格"是怎么获取的?11. 填写数量和价格后,委托金额会自动计算吗?12. 饼图展示的是什么数据?13. 柱状图展示的是什么数据?14. 系统监控页面的日志是真实的还是模拟的?

```


```
`第六部分:自动调仓相关``请根据以上代码,用简短的一句话回答以下每个问题:1. 自动调仓按钮的ID是什么?2. 自动调仓需要先点击哪个按钮准备数据?3. 上一次的持仓记录存在哪个文件里?4. CSV文件里按什么格式存储持仓?(几列?什么分隔符?)5. 如何判断哪些ETF需要卖出?6. 如何判断哪些ETF需要买入?7. 调仓过程中如何显示进度条?8. 进度条更新是通过什么组件触发的?9. 调仓完成后状态文本变成什么?10. 如果前后持仓完全一样,系统会怎么做?11. 调仓结果会保存在哪里(除了CSV)?12. 每次调仓最多操作几只ETF(买入+卖出)?`第七部分:异常与边界情况

```


```markdown
请根据以上代码,用简短的一句话回答以下每个问题:1. QMT连接失败时,系统会崩溃吗?2. 获取某只ETF数据失败时,其他ETF还会继续更新吗?3. 本地没有top5_holdings.csv文件时,自动调仓会怎么处理?4. 实盘明细页面,没有当日委托时表格显示什么?5. 我的账户页面,没有持仓时图表显示什么?6. 下单时资金不足,用户会看到什么提示?7. 下单时可卖数量不足,用户会看到什么提示?8. 网络断开时点击刷新数据,会发生什么?9. 回测时如果没有足够的历史数据,会返回什么?10. 动量计算时数据不足会返回什么值?11. 多个用户同时点击自动下单会冲突吗?12. 子进程异常退出后,主进程会怎样?

```


```
第八部分:配置与常量`请根据以上代码,用简短的一句话回答以下每个问题:1. QMT的安装路径在代码的哪里配置的?2. QMT的session_id是多少?3. 沪市ETF代码以哪几个数字开头?4. 深市ETF代码以哪几个数字开头?5. 默认的ETF池子里有多少只ETF?6. 动量周期在代码里默认值是多少?7. 持有数量的默认值是多少?8. 回测初始资金的默认值是多少?9. 下单默认数量的默认值是多少?10. 系统版本号是多少?`

```
复制代码
`````

````

```
`部署与运行`首先在 CMD 里安装依赖。复制粘贴这一整行,回车安装

```

`pip install dash dash-bootstrap-components plotly pandas numpy akshare backtrader``然后,正常启动 QMT 客户端,独立交易模式。``接下来,用PyCharm运行这个项目,右键python文件点运行即可。运行成功后,控制台会显示:``Dash is running on http://127.0.0.1:8090/`打开浏览器,输入:http://localhost:8090 即可使用。

````


`````

总结

当系统迭代至可满足实战需求时,我们会推出线上培训课程,从框架设计到代码实现,逐层拆解、完整讲解。

复制代码
```
`关于Web轮动系统迭代改进的过程可以查看链接:21天搭建ETF量化系统`

```
复制代码
```````

``````
关于QTYX的使用攻略可以查看链接:[QTYX使用攻略](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzUxMjU4NDAwNA==&action=getalbum&album_id=2524935077060001794#wechat_redirect)

`````

````

```
`如何用Python从0一步步搭建出一套ETF量化交易系统,我们推出了DAY1-DAY21链接如下:21天搭建ETF量化交易系统`掌握了方法之后,可以换成期货系统、比特币系统、美股系统,然后在实战中不断去完善自己的系统了。

```


````


`````


``````


```````

说明

此系列为连载专栏,是《玩转股票量化交易》知识星球的配套学习资料,欢迎大家私信交流,共同进步成长!

星球介绍点击:知识星球《玩转股票量化交易》精华内容概览

相关推荐
小程故事多_801 小时前
AI重构DevOps,智能增强而非替代,人始终是最终决策者
人工智能·重构·devops
咋吃都不胖lyh1 小时前
限流重试、指数退避、随机抖动
前端
之歆1 小时前
DAY_11JavaScript BOM与DOM深度解析:底层原理与工程实践(上)
开发语言·前端·javascript·ecmascript
冴羽yayujs2 小时前
GitHub 前端热榜项目 - 日榜(2026-05-17)
前端·github
老马95272 小时前
opencode8-桌面应用实战 3
前端·人工智能·后端
逆yan_2 小时前
🧭 基于 pnpm Workspace 和 Turborepo 的 Monorepo 最佳实践
前端·javascript·架构
广州华水科技2 小时前
单北斗形变监测一体机在大坝安全监测中的应用与技术优势
前端
沙漠2 小时前
Vue总结系列一
前端
渐儿2 小时前
React Native 实操开发文档
前端