← 返回目录
p4.10动手⏱ 约 17 分钟

打包成开源库:第一个成品的交付

16 个测试通过 → 构建 wheel → 装进全新环境 → 冒烟测试跑通

🔎 最后验证 2026-07📚 来源:构建、安装与冒烟测试全流程于 2026-07-31 在 ECS 实测,输出见 code/outputs/stdout.txt🧰 hatchling、uv 0.12.0、pytest 9.1.1、Python 3.12.13
为什么学这个

P4 章走到这里,qlab 已经有了事件、撮合、账户、回测四个模块,七条撮合规则、六条账户行为、双路对账、与 backtrader 对拍。

但它现在还只是你目录里的一堆 .py 文件

这一节把它变成一个真正的库:

【一】跑测试套件           16 passed
【二】构建 wheel           qlab-0.4.0-py3-none-any.whl (10686 字节)
【三】装进全新的干净环境     import qlab 成功,版本 0.4.0
      冒烟测试            回测跑通:期末权益 908,055

第三步是关键:在一个不含源码目录的全新虚拟环境里 import qlab,回测跑通——这才叫"能给别人用"。

💡 打个比方

你在自己厨房做出一道菜,和把它做成能寄给别人的预制菜,是两回事。

后者要考虑:别人家没有你的调料架(依赖)、别人的锅不一样(Python 版本)、别人不知道你放了多少盐(默认参数)、你改了配方要通知他(版本与变更日志)。

"在我机器上能跑"和"是一个库",中间隔着这一节。

一、测试套件:把 P4 各节的验证汇总

16 个测试,分四组:

数量覆盖
test_matching_*8涨跌停、停牌、T+1、整手、资金约束、限价单、印花税仅卖出
test_account_*3T+1 锁定与解锁、均价含费、清仓归零与已实现盈亏
test_engine_*4无未来函数双路对账、成本单调、可复现
test_regression_*1锁住已修复的 bug

全部不依赖行情数据——用合成价格序列,毫秒级跑完,可以挂在每次提交上。

⚠️ 避坑

写这套测试时,测试自己抓了我一次。

test_engine_strategy_cannot_see_today 我第一版写的断言是:

assert seen == list(range(len(df)))       # [0, 1, 2, ..., 59]

跑出来失败了:实际是 [1, 2, ..., 59]

原因:第 0 天没有任何历史,引擎压根不调用策略函数。是我的断言错了,不是引擎错了。

写测试时对被测代码的假设,同样需要被验证。

这次是测试抓住了我;如果我当时"顺手"把断言改成宽松一点的写法(比如只检查递增),这个认知偏差就会一直留在我脑子里。

回归测试:锁住已修复的 bug

def test_regression_records_unaffordable_lot():
    """p4.1 修复:想建仓但买不起一手时,必须记为被拒,而不是静默跳过。

    这个 bug 的危害是把「你没钱建仓」伪装成「策略选择空仓」——
    净值曲线一模一样,结论完全不同。
    """
    df = make_bars(200, start=500.0)              # 高价股
    r = run_backtest(df, always_in, cash=3000)    # 一手 5 万,只有 3000 元
    assert any("一手" in k for k in r.log.summary()["被拒原因分布"])

每修一个 bug,配一个回归测试。否则三个月后的重构会把它悄悄放回来——而且这类 bug 不会报错。

🔧 动手做:亲手改坏引擎,看测试网兜住它(6 分钟)

16 个测试全绿,是不是就是走个仪式?你来验证它到底有没有用——改坏引擎,看测试抓不抓得住。

打开 qlab/engine/matching.py,把 limit_pct: float = 0.10(涨跌停 10%)改成 1.0(等于关掉涨跌停),跑 python -m pytest tests/ -q

你会看到(实测):立刻两条变红——test_matching_limit_up_blocks_buytest_matching_limit_down_blocks_sell FAILED

想明白:16 个测试不是给你看"全绿"的摆设,是在你(或半年后的你、或帮你改代码的 AI)把引擎改坏时,立刻报警的那张网。一道永远绿的测试等于没测(p0.3);正因为你一改坏它就红,这张网才是真的。这就是为什么成品要打包成带测试的库——不是为了好看,是为了以后动它时不心虚。

二、pyproject.toml:把假设写进配置

[project]
name = "qlab"
version = "0.4.0"
requires-python = ">=3.11"
dependencies = ["pandas>=2.2", "numpy>=1.26"]

[project.optional-dependencies]
dev  = ["pytest>=8.0"]
plot = ["matplotlib>=3.8"]      # 画图才需要
data = ["baostock", "akshare"]  # 取数才需要

可选依赖分组是个容易被忽略的细节:核心库只依赖 pandas + numpy,别人不画图就不用装 matplotlib

一个只想跑回测的人,不该被迫装上 akshare 和它的一长串依赖。

三、CHANGELOG:量化库的一条额外规矩

语义化版本大家都知道:主版本改 API、次版本加功能、修订号修 bug。

但量化库有一条额外的规矩:

任何会改变回测结果的修改,即使 API 完全不变,也必须升次版本并写进 CHANGELOG。

因为使用者会拿新版本重跑旧策略。结果变了却没提示,比 API 不兼容更危险——API 不兼容会立刻报错,结果变化不会。

本次 0.4.0 就有这么一条:

### 变更(**会改变回测结果**
- `backtest`:新增「资金不足一手,无法建仓」的拒单记录。此前该情形被静默跳过,
  回测显示为「策略选择空仓」——净值不变,但结论完全不同(p4.1)

CHANGELOG 里还必须有已知限制一节:

  • 单标的回测;多标的组合需自行聚合(P5 处理)
  • enforce_t1 在日频单次调仓下不绑定(结构性原因,p4.1)
  • 分红口径固定为"后复权 + 不单独入账"(p4.5)

把限制写在明处,比假装没有限制更专业。

四、构建与验证:三步全流程

【一】跑测试套件        16 passed
【二】构建 wheel        Successfully built dist/qlab-0.4.0-py3-none-any.whl (10686 字节)
【三】装进全新环境      + qlab==0.4.0 (from file:///.../qlab-0.4.0-py3-none-any.whl)
      冒烟测试         import qlab 成功,版本 0.4.0
                      回测跑通:期末权益 908,055,{'成交笔数': 21, '被拒笔数': 0}
⚠️ 避坑

第三步必须在"不含源码目录"的地方跑。

如果你在项目根目录下 import qlab,Python 会直接找到源码文件夹——即使你的包配置完全是错的,它也能 import 成功

正确做法:

  1. uv venv /tmp/qlab_fresh 建一个全新环境
  2. wheel 文件安装(不是 pip install -e .)
  3. cd /tmp 切到别处,再 import

只有这样,你验证的才是别人拿到 wheel 之后的真实体验

这和 p4.9 对拍是同一个思路:引入外部视角,而不是自己验自己。

五、README:先说它不做什么

大多数 README 只讲功能。本库的 README 有一节叫“它没做什么”:

## 它没做什么
- 不支持多标的组合(P5 章处理)
- 不接实盘下单通道(**这是刻意的**- 不保证任何收益

一个量化库的可信度,一半来自它承认自己不做什么。

六、这就是第一个成品

回到 p0.1 承诺的三个成品:

成品状态
① 自研回测引擎本节交付
② 因子评价体系P2.7 已给出方法,P5 整合进库
③ 模拟交易系统P8 章

qlab 0.4.0 现在具备:

  • 架构级防未来函数(p4.3)
  • 七条 A 股撮合规则 + 单元测试(p4.4)
  • 账户与盈亏分解(p4.5)
  • 双路对账(p4.6)
  • 与 backtrader 对拍(p4.9)
  • 被拒委托记录——向量化回测里不存在的信息(p4.1)
❓ 测验
你的库在项目目录下 `import mylib` 一切正常,但同事装了你的 wheel 之后报 ModuleNotFoundError。最可能的原因?
✏️ 填空
量化库的额外规矩:任何会改变 ___ 结果的修改,即使 API 不变,也必须升次版本并写进 CHANGELOG。

📌 免责:本课为技术教学,qlab 为教学用途的开源实现,不构成投资建议,不保证任何收益。

✅ 小结

P4 章完结,这门课的第一个成品交付。

这一节做的四件事:

  1. 测试套件 16 个——撮合 8 + 账户 3 + 引擎级 4 + 回归 1,全部不依赖行情数据、毫秒级跑完
  2. pyproject 的可选依赖分组——只想跑回测的人,不该被迫装 akshare
  3. CHANGELOG 的额外规矩——任何改变回测结果的修改都必须升版本,因为"结果变了却没提示"比 API 不兼容更危险;外加必须写"已知限制"
  4. 三步验证:测试 → 构建 wheel → 在不含源码的全新环境里从 wheel 安装并冒烟测试

外加一次"测试抓住了我":test_engine_strategy_cannot_see_today 的断言我写错了(以为策略从第 0 天开始被调用)。写测试时对被测代码的假设,同样需要被验证。

P4 章十节回顾:选型 → 参数 → 信号层 → 撮合层 → 账户层 → 双路对账 → 绩效 → 可视化 → 交叉对拍 → 打包。每一节都留下了一段可运行的代码,而不只是一个知识点。

下一章 P5 开始用这个引擎做策略:规格书、多因子、参数优化与参数平原、滚动前推验证、蒙特卡洛置信区间、成本与容量、衰减监控、风险平价、风险预算。

那一章会反复用到 P3 的统计纪律——因为有了可信的引擎之后,下一个瓶颈就是"你怎么知道这个策略不是搜出来的"

下一节 → P5 章:策略开发与验证
🔎 来源与核验· 3 条,点开核对
本节每个关键论断都对应一个可追溯的来源 —— 这是本课程"靠谱、不过时"的底线。
「测试套件 16 个全部通过(撮合 8 / 账户 3 / 引擎级 4 / 回归 1),不依赖行情数据」
📚 本节 code/tests/test_engine.py,2026-07-31 于 ECS 实测,输出见 code/outputs/stdout.txt✓ 已核验 2026-07
「构建产物 qlab-0.4.0-py3-none-any.whl(10686 字节);在全新 venv 中从 wheel 安装后 import 成功、冒烟回测跑通(期末权益 908,055,成交 21 笔)」
📚 同上实测(uv build + uv pip install + 独立环境冒烟测试)✓ 已核验 2026-07
「test_engine_strategy_cannot_see_today 首版断言写错(期望 range(len(df)),实际 range(1, len(df))),因引擎在第 0 天无历史时不调用策略函数」
📚 本节 code/tests/test_engine.py(首版断言写错 range(len(df)) 被测试当场抓住,过程原样保留在 L135 注释与 L137 断言中)✓ 已核验 2026-07
qlab 为教学用途的开源实现,单标的、不接实盘通道;已知限制见 CHANGELOG。
智图软件的赞赏码
都看到这了,打个赏呗!
接下来 · p5.1
策略规格书:一句话策略有 16 种合法读法
继续读下一节 →