十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从零搭建你的 A 股量化系统(二十):Jupyter 研究工作流:把研究代码和生产代码分开

从零搭建你的 A 股量化系统(二十):Jupyter 研究工作流:把研究代码和生产代码分开 系列名《从零搭建你的 A 股量化系统》 专栏 S2 Python 工具链 第 8 篇 / 共 14 篇标签#量化投资 #Jupyter #Notebook #工作流 #工程实践 #Python #可复现 #版本控制 #A股 #散户上一篇 S2-019 我们刚把净值曲线、水下回撤图、月度收益热力图做出来了——终于能用机构级的图给自己的策略验血。但有个问题这些分析代码你平时到底是写在哪里的十有八九是一个.ipynb文件里上面import下面读数据中间十几格计算和画图最后还藏着几个上次跑过、但忘了重跑的格子。短期看很爽——改个数、ShiftEnter、看图、完事。可等你攒了三个月、攒了 20 个 notebook就会撞上三件噩梦“我本地能跑啊”其实是某个 cell 之前手动跑过、“这段代码我在哪写过”复制粘贴对不上了、git diff全是乱码因为 .ipynb 里混着代码、输出和图片 base64。这篇就是来帮你把研究和生产彻底切开的。核心就一句话Notebook 负责想.py 负责用。下面我把这条原则落到目录、代码、Git 三个层面每一步都给你能直接抄的命令和示例。 你将学到Notebook 为什么既是量化研究的最佳拍档又是工程灾难的温床隐藏状态 / 难导入 / 难版本控制三宗罪一个原则Notebook 负责想.py 负责用标准目录里研究代码和生产代码各住哪直接接04架构里的notebooks/src/tests/从乱糟糟的 notebook提纯成干净模块的四步法含可抄的前后对比代码三个让 Git 和 Notebook 不再打架的工具jupytext / nbstripout / .gitattributes可复现铁律Restart Run All、不在 notebook 里留随机魔法、papermill 参数化进阶。 前置知识已按 S2-013 装好 conda 环境ashare知道代码该跑在隔离环境里已按 S2-014 理解 NumPy 向量化思维、按 S2-016 知道未来函数陷阱notebook 乱序重跑最容易重新引入未来函数已按 S2-019 画出净值/回撤图——本篇直接拿它当提纯示例有一个能跑的 Python 环境本篇额外用到的工具安装命令见文末jupyterlabjupytextnbstripout。先说结论散户量化最常见的工程错误不是算法烂而是所有东西都堆在 notebook 里。红线违反后的典型惨状正确做法任何会被再次调用的逻辑必须搬出 notebook同一个max_drawdown在 多个 notebook 里各抄一遍改一处要找多处还改不全提纯成src/里的模块谁用谁importnotebook 提交前必须剥掉输出git diff几百行全是image/png的 base64真改动淹没在噪音里用nbstripout自动清空 outputs 再提交复用前必须 Restart Run All 一次跑通我本地能跑上线就崩因为依赖了某个忘了重跑的 cell任何要当真的 notebook先 Kernel 重启、从上跑到下零报错一言以蔽之Notebook 是你的草稿纸可以潦草.py是你正式出版的书必须严谨。草稿纸可以丢书要被人反复读。一、为什么要把两者分开1.1 Notebook 的三个问题Jupyter 的单元格执行模型天生就有三个暗坑① 隐藏状态Hidden State。cell 的执行顺序 ≠ 代码的阅读顺序。你上面改了个变量df下面的格还在用改之前的旧df或者你删了一段但某个下游 cell 还引用着被删的对象。最经典的 bug 就是那句——“我本地能跑啊”——其实只是因为某个 cell 之前手动跑过、状态还热着。② 难导入Not Importable。.ipynb不能直接import my_notebook。想复用里面的函数唯一的懒人办法是复制粘贴。而复制粘贴 版本分叉。三个月后你改了原版的逻辑那 5 个副本还停留在旧行为回测结果对不上时你根本想不起哪份是真的。③ 难版本控制Git Hell。.ipynb本质是 JSON里面混着代码、执行输出、甚至图片的 base64。你只改了一行图的颜色git diff却能蹦出几百行二进制噪音两个人同时改同一个 notebook几乎无法 merge。代码评审根本无从看起。1.2 但 Notebook 不能弃说归说Notebook 大部分研究者一天都离不开因为它精准匹配了研究的天性——边试边看想验证一个直觉print一下中间结果看图说话调个参数想立刻看效果改一个数、ShiftEnter、图变了给客户/自己演示一段探索式分析notebook 从上到下读下来就是一份故事。这种即时反馈是纯.py脚本给不了的。所以结论不是别用 notebook而是用 notebook 想但别用 notebook 交付。1.3 一句话原则Notebook 负责想explore试错、验证直觉、做图。.py 负责用reuse一旦逻辑稳定提纯成模块被 notebook / 脚本 / 回测引擎反复调用。记住这个类比就够了notebook 像草稿纸.py像正式出版的书。草稿可以乱、可以丢书必须严谨、能被反复读。二、两个世界的职责边界一张表把事情做在对的地方先得清楚两边各自该装什么维度Notebook研究.py 模块生产目的探索、试错、验证直觉复用、被调用、跑生产生命周期短结论沉淀后可弃长持续演进能否被 import❌ 不能直接 import✅from src.xxx import yyy进 git 的是仅代码输出须剥离全部源码谁来写测试一般不写必须有tests/配套典型内容读数据、调参、画探索图、assert验证因子计算、回测引擎、绩效指标、数据接口是否含绘图✅ 大量matplotlib/plotly❌ 尽量不画保持纯逻辑关键判断“这段代码我明天还会在另一个策略/另一个 notebook 上重跑吗”会 → 它属于.py只是这次临时看一眼 → 它留在 notebook 没问题。三、标准目录里它们各住哪回头看 之前定的目标目录研究代码和生产代码的位置其实已经替你安排好了ashare-quant/ ├── config/ # 配置加入 .gitignore 的账户类除外 ├── data/ # 数据加入 .gitignore ├── src/ # 【生产代码】所有可复用逻辑 │ ├── datafeed/ # 数据层 │ ├── factor/ # 因子层 │ ├── strategy/ # 策略层base.py 同时被回测/实盘调用 │ ├── backtest/ # 回测层 │ ├── execution/ # 执行层 │ ├── monitor/ # 监控层 │ └── utils/ # 工具层 ├── scripts/ # 【入口脚本】run_backtest.py 等直接调 src/ ├── notebooks/ # 【研究草稿】不进生产按主题分文件 │ ├── 01_探索双均线.ipynb │ ├── 02_因子IC初筛.ipynb │ └── 03_净值回撤可视化.ipynb ├── tests/ # 【单元测试】对应 src/ 的每个模块 │ ├── test_risk_check.py │ └── test_quantkit.py └── pyproject.toml三条用法心法notebooks/按一次研究任务建文件如03_净值回撤可视化.ipynb不做成大杂烩结论沉淀进src/后notebook 退化为实验记录——它可以留着当笔记但真相来源是src/里的模块不是 notebook还记得那个关键设计吗strategy/base.py的策略类同时被回测引擎和实盘清单生成器调用输入标准化市场数据、输出目标持仓字典。这正是生产代码该有的样子与运行环境解耦、可被多方复用。你提纯出来的模块目标就是长这样。四、从 Notebook 到 .py 的提纯四步法光讲道理太空。拿 S2-019 的真实情景开刀你在 notebook 里写了净值构造、max_drawdown、performance_metrics。怎么把它提纯4.1 第一步圈出会被再用的代码 → 提取成函数识别标志很简单这段代码你已经在两个 notebook 里复制过或者你打算明天在另一个策略上重跑。一旦命中立刻包成函数。4.2 第二步把同主题函数 → 收进一个模块比如所有绩效计算收进src/quantkit/metrics.py。本篇为了让你一把跑通直接放成单文件src/s2_020_quantkit.py配套脚本已实测可跑。4.3 第三步补 docstring 类型注解 最小测试这是草稿变书的关键一步类型注解def nav_from_returns(returns: pd.Series) - pd.Series让 IDE 和mypy帮你抓错docstring写清给什么、返回什么、边界条件比如允许含 NaN函数先 dropna 再算最小测试tests/test_quantkit.py保证你半年后重构时不会悄悄改坏行为。4.4 第四步notebook 退化成薄壳提纯之后notebook 里不再有业务算法只剩四件事import 模块 → 读数据 → 调函数 → 画图/打印。对照一下提纯前 vs 提纯后的 notebook# ❌ 提纯前算法全躺在 cell 里复制一份就多一个分身retdf[strategy]equity(1ret).cumprod()# 这行在 5 个 notebook 里各抄一遍equity[0]1.0running_maxequity.cummax()ddequity/running_max-1max_dddd.min()# ... 还有 performance_metrics、monthly_returns 一堆散落各处# ✅ 提纯后notebook 只剩薄壳真相在 src/s2_020_quantkit.pyfromsrc.s2_020_quantkitimport(nav_from_returns,max_drawdown,performance_metrics,monthly_returns,)equitynav_from_returns(df[strategy])# 一行调用逻辑在模块里mdd,peak,troughmax_drawdown(equity)metricsperformance_metrics(equity)print(f最大回撤{mdd:.2%}夏普{metrics[sharpe]:.2f})# 画图matplotlib继续留在 notebook —— 这是它该待的地方importmatplotlib.pyplotasplt plt.plot(equity.index,equity.values)plt.title(Equity Curve)plt.show()注意最后那个细节画图留在 notebook算数进了模块。这就是Notebook 负责想/用.py 负责用的具象化。五、三个工具让 Git 和 Notebook 不再打架哪怕你做到了逻辑提纯notebook 文件本身进 git 还是一场灾难。三个工具按成本从低到高排好5.1 jupytext.ipynb↔.py双向同步核心思路让 notebook 在 git 里以纯文本.py形式存在带# %%单元格标记diff 瞬间可读还能在 VS Code / Vim 里直接改 notebook。pipinstalljupytext# 把一个 notebook 配对成 ipynb pypercent 格式最通用jupytext --set-formats ipynb,py:percent notebooks/03_净值回撤可视化.ipynb# 之后你改 .py 或 .ipynb 任一个另一边自动同步jupytext--syncnotebooks/03_净值回撤可视化.ipynbpy:percent格式长这样普通编辑器里也清清楚楚# %% [markdown]# # 净值回撤可视化# %%fromsrc.s2_020_quantkitimportnav_from_returns equitynav_from_returns(df[strategy])进阶在pyproject.toml里配[tool.jupytext]让新建 notebook 默认就 pair 成 py从源头杜绝忘了同步。5.2 nbstripout提交前自动剥掉输出就算不 pair 成 py至少别把输出和图片塞进 git。装一次、注册 git filter之后每次git add自动清空 outputspipinstallnbstripout nbstripout--install# 在当前仓库注册 filter装完再git diff你只会看到代码本身的改动几百行 base64 噪音消失。仓库体积也小一大截。5.3.gitattributes pre-commit 双保险把规则写进仓库让团队和未来的你都遵守而不是靠记性# .gitattributes —— 让所有 ipynb 提交时自动去输出 *.ipynb filternbstripout # 如果用了 jupytext还可以只跟踪 .py 版本、忽略 .ipynb 的二进制 # notebooks/**.ipynb linguist-generatedtrue想更狠一点上pre-commit框架在提交前自动跑 jupytext 同步校验 nbstripout 代码格式检查任何一步不过就拦下提交。这一步是 S2-022《Git 与量化项目》要展开的本篇先埋个钩子。六、可复现性铁律分离解决的是结构问题可复现解决的是信任问题。三条铁律铁律 1每次要当真前Restart Kernel Run All。从上到下零报错跑通才算数。任何我本地能跑都必须经此检验。这不只是习惯——它直接关联未来函数陷阱notebook 乱序重跑最容易让上面的 cell 用到下面才算出的未来值这种 bug 重新出现。Restart Run All是兜住它的底线。铁律 2不在 notebook 里留随机魔法。任何依赖随机数的实验seed 写死并写明。否则别人复现不了你下周也复现不了。配套脚本里就是这么干的rngnp.random.default_rng(42)# 写死种子结果可复现dailypd.Series(rng.normal(0.0004,0.009,756),index...)铁律 3进阶把要定期跑的分析参数化。如果你有一份每周出一次因子 IC 报告的 notebook别手动跑——用papermill传参执行输出一份带结果的新 notebook再自动存进results/。这等于让 notebook 也能进 pipeline正好接下一篇 S2-021 的任务计划程序自动跑。pipinstallpapermill papermill notebooks/02_因子IC初筛.ipynb results/ic_2026-08-04.ipynb\-puniverse 000300.SH-pstart2020-01-01七、一个最小可落地的实战示例把前面所有原则收口成一个你能今天就用的最小闭环。① 提纯后的生产模块src/s2_020_quantkit.py配套脚本已实测可跑提供纯逻辑函数fromsrc.s2_020_quantkitimport(nav_from_returns,# 日收益 - 净值起点强制 1.0max_drawdown,# 返回 (最大回撤, 峰值日期, 谷值日期)performance_metrics,# 返回 total_return/cagr/sharpe/max_dd/calmar 字典monthly_returns,# 月内收益序列用于热力图)它只算数、不画图、不读写文件——给同样输入必得同样输出可复现对应铁律 2。② notebook 退化成薄壳只做调用 画图# 03_净值回撤可视化.ipynbimportmatplotlib.pyplotaspltfromsrc.s2_020_quantkitimportnav_from_returns,max_drawdown equitynav_from_returns(df[strategy])mdd,peak,troughmax_drawdown(equity)print(f最大回撤{mdd:.2%}{peak.date()}-{trough.date()})plt.plot(equity.index,equity.values)plt.title(Equity Curve);plt.show()③ 给生产代码配最小测试tests/test_quantkit.py保证重构不破坏行为importnumpyasnpimportpandasaspdfromsrc.s2_020_quantkitimportnav_from_returns,max_drawdown,performance_metricsdeftest_nav_starts_at_one():rpd.Series([0.01,-0.02,0.03])navnav_from_returns(r)assertabs(nav.iloc[0]-1.0)1e-9# 起点必为 1.0deftest_monotonic_nav_has_no_drawdown():navpd.Series([1.0,1.1,1.2,1.3])# 单调递增mdd,peak,troughmax_drawdown(nav)assertmdd0.0andpeakisNone# 无回撤返回 (0.0, None, None)deftest_metrics_keys():navnav_from_returns(pd.Series(np.random.default_rng(0).normal(0,0.01,250)))mperformance_metrics(nav)assertset(m){total_return,cagr,sharpe,max_drawdown,calmar}跑测试pytest tests/test_quantkit.py -q。能 import、能测试、与绘图解耦、与运行环境解耦——这就是生产代码的标准形态。S2-019 那段散落的绘图逻辑提纯到这里就成了可以被任何回测、任何策略反复调用的资产。配套脚本src/s2_020_quantkit.py末尾带一个_demo()直接python src/s2_020_quantkit.py就能看到净值/回撤/指标的实跑结果合成数据、固定种子非真实行情。八、本篇小结 下篇预告小结Notebook 三宗罪隐藏状态“我本地能跑”、难导入只能复制粘贴、难版本控制git 全是 base64 噪音一句话原则Notebook 负责想.py 负责用草稿纸可以乱书必须严谨提纯四步法圈出复用代码 → 收进模块 → 补 docstring/类型注解/测试 → notebook 退化成薄壳Git 三件套jupytextipynb↔py 同步diff 可读、nbstripout提交前剥输出、.gitattributes写进仓库强制遵守可复现铁律Restart Run All 兜住未来函数、随机 seed 写死、papermill 让 notebook 进 pipeline真相来源是src/里的模块不是 notebook——notebook 只配当实验记录。下一篇预告专栏 S2 第九篇《Windows 任务计划程序让你的策略每天自动跑》S2-021。现在你已经有了可复现的 notebook 干净模块但每天收盘后还得手动点 Restart Run All下篇把它升级成Windows 任务计划程序每天自动跑把研究成果真正接进日常流程——正好把我们这篇讲的可复现 notebook / 模块当原料喂进去。我们下篇见。附本篇可运行脚本S2-020 示例模块把研究代码提纯成生产代码。 这是《Jupyter 研究工作流把研究代码和生产代码分开》一文的配套模块。 它示范了一件核心的事把 S2-019 里散落在 notebook 中的净值 / 回撤 / 绩效计算逻辑 提取成一个**纯逻辑、可导入、可测试**的 .py 模块。 设计原则对应正文讲的三条 1. 只算数不画图 —— 所有 matplotlib / plotly 调用留在 notebook 里 2. 显式类型注解 docstring —— 半年后你或别人能一眼看懂输入输出 3. 不依赖全局随机状态、不读写文件 —— 给同样输入必得同样输出可复现。 运行方式在仓库根目录 量化博客全案/ 下 python src/s2_020_quantkit.py # 跑一个最小自测 打印示例指标 或 from src.s2_020_quantkit import nav_from_returns, max_drawdown, performance_metrics from__future__importannotationsimportnumpyasnpimportpandasaspddefnav_from_returns(returns:pd.Series)-pd.Series:从日收益率序列构造净值曲线起点强制为 1.0。 净值定义NAV_t prod(1 r_i)NAV_0 1。 这是跨策略公平比较的唯一基准详见 S2-019。 Args: returns: 策略日收益率序列索引为日期DatetimeIndex 最佳。 允许含 NaN函数会先 dropna 再计算避免 cumprod 被 NaN 截断。 Returns: 净值序列首日值为 1.0。 Raises: ValueError: 输入为空或清洗后为空。 rreturns.dropna()ifr.empty:raiseValueError(returns 为空或全是 NaN无法构造净值)# 先算 (1r).cumprod()再把首项强制设为 1.0规避首元素定义歧义nav(1.0r).cumprod()nav.iloc[0]1.0returnnavdefmax_drawdown(equity:pd.Series)-tuple[float,pd.Timestamp|None,pd.Timestamp|None]:计算最大回撤及峰/谷日期。 回撤定义DD_t NAV_t / cummax(NAV)_t - 1永远 0。 MDD 即回撤序列的最小值最负的那个。 Args: equity: 净值序列由 nav_from_returns 得到。 Returns: (最大回撤比例, 峰值日期, 谷值日期)。 最大回撤为 0 的浮点数如 -0.215 表示 -21.5%。 若净值单调递增无回撤返回 (0.0, None, None)。 running_maxequity.cummax()ddequity/running_max-1.0mddfloat(dd.min())ifnp.isclose(mdd,0.0):return0.0,None,Nonetrough_datedd.idxmin()peak_dateequity.loc[:trough_date].idxmax()returnmdd,peak_date,trough_datedefperformance_metrics(equity:pd.Series,periods_per_year:int252,risk_free:float0.0,)-dict:汇总一组常用绩效指标。 Args: equity: 净值序列。 periods_per_year: 年化因子。日频 252、周频 52、月频 12。 risk_free: 年化无风险利率用于夏普。默认 0。 Returns: 含 total_return / cagr / ann_vol / sharpe / max_drawdown / calmar 的字典。 nlen(equity)ifn2:raiseValueError(净值序列至少需要 2 个观测点)total_returnfloat(equity.iloc[-1]/equity.iloc[0]-1.0)yearsn/periods_per_year cagrfloat(equity.iloc[-1]**(1.0/years)-1.0)ifyears0else0.0daily_retequity.pct_change().dropna()ann_volfloat(daily_ret.std(ddof1)*np.sqrt(periods_per_year))sharpefloat((daily_ret.mean()*periods_per_year-risk_free)/ann_vol)ifann_vol0else0.0mdd,_,_max_drawdown(equity)calmarfloat(cagr/abs(mdd))ifmdd0else0.0return{total_return:total_return,cagr:cagr,ann_vol:ann_vol,sharpe:sharpe,max_drawdown:mdd,calmar:calmar,}defmonthly_returns(equity:pd.Series)-pd.Series:把净值切成年-月的月内收益率序列用于热力图等。 注意必须用月末/月初 last/first而非 pct_change()详见 S2-019 的坑。 navequity.copy()nav.indexpd.to_datetime(nav.index)# 按年-月分组取组内首/末交易日的净值做月内收益。# 注意必须用 groupby(period).first()/last()而不是 resample(MS)——# 后者会把锚点钉在每月 1 号若 1 号不是交易日就会得到 NaN首月尤其明显。periodnav.index.to_period(M)gnav.groupby(period)month_endg.last()month_startg.first()mretmonth_end/month_start-1.0returnmret.dropna()def_demo()-None:最小自测用固定种子合成一段 A 股风格日收益验证逻辑可跑。rngnp.random.default_rng(42)# 均值略正、带波动的日收益模拟一个还行的策略dailypd.Series(rng.normal(0.0004,0.009,756),indexpd.bdate_range(2021-01-04,periods756),namestrategy,)navnav_from_returns(daily)mdd,peak,troughmax_drawdown(nav)metricsperformance_metrics(nav)print(净值起点:,float(nav.iloc[0]),终点:,round(float(nav.iloc[-1]),4))print(f最大回撤:{mdd:.2%}(峰{peak.date()}- 谷{trough.date()}))print(绩效指标:,{k:round(v,4)fork,vinmetrics.items()})print(前 3 个月收益:,monthly_returns(nav).head(3).round(4).to_dict())if__name____main__:_demo()1. 生产模块已实测可跑src/s2_020_quantkit.py# 在仓库根目录 量化博客全案/ 下确保已激活 ashare 环境且装了 numpy/pandaspython src/s2_020_quantkit.py# 输出净值起点/终点、最大回撤及峰谷日期、绩效指标字典、前 3 个月收益模块提供的函数nav_from_returns/max_drawdown/performance_metrics/monthly_returns全部纯逻辑、可导入、可测试。2. 工具安装本篇用到pipinstalljupyterlab jupytext nbstripout papermill pytest3. 一键配置 Git 友好notebook 去输出nbstripout--install# 注册 git filter提交自动剥输出# 再在仓库根加一个 .gitattributes# *.ipynb filternbstripout4. 把某个 notebook 配对成可 diff 的 .pyjupytext --set-formats ipynb,py:percent notebooks/03_净值回撤可视化.ipynb所有数据均为合成数据仅用于技术教学不构成任何投资建议。免责声明本文所有内容仅用于量化投资技术教学与知识分享文中的代码示例、目录结构与工具配置均不构成任何投资建议或个股推荐。投资有风险入市需谨慎实际交易前请务必用自己的真实数据充分回测并充分了解相关风险。
返回列表