✅ 代码质量:规范、测试、类型注解与依赖管理

用一个止损函数演示单元测试;类型注解如何拦截对齐错误;Pandas 代码的规范清单;requirements 与锁文件。

研究代码的质量标准与业务软件不同:它允许探索期的凌乱,但任何要进入回测或实盘的逻辑必须达到生产质量——因为一个 sign 错误、一个 off-by-one,亏损的是真金白银。本篇用同一个交易函数贯穿四个话题。

一个函数的四重进化

需求:给定持仓与当日最高最低价,判断固定百分比止损是否触发。

1
2
3
# v0:研究 Notebook 里的顺手一写(有 3 个隐患)
def check_stop(p, low, pct=0.05):
    return low < p * (1 - pct)   # p 是什么价?成本价还是昨收?跌停怎么办?

隐患:参数含义无文档;跌停无法卖出的情形未处理;无法防止未来有人把 low 传成收盘价。

1
2
3
4
5
6
7
8
9
# v1:类型注解 + 明确语义
from decimal import Decimal

def is_stop_triggered(cost_price: float, day_low: float,
                      stop_pct: float = 0.05) -> bool:
    """当日最低价跌破成本价×(1-stop_pct) 时触发止损。"""
    if stop_pct <= 0:
        raise ValueError("stop_pct 必须为正")
    return day_low < cost_price * (1 - stop_pct)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# v2:单元测试(pytest)
import pytest

def test_normal_trigger():
    assert is_stop_triggered(10.0, 9.49) is True       # 9.49 < 9.50

def test_boundary_exact():
    assert is_stop_triggered(10.0, 9.50) is False      # 恰好等于不触发

def test_float_precision():
    assert is_stop_triggered(10.0, 9.499999999) is True  # 浮点边界

def test_invalid_pct():
    with pytest.raises(ValueError):
        is_stop_triggered(10.0, 9.0, stop_pct=-0.1)

测试用例从哪里来?从真实事故里来。 「恰好等于不触发」这种边界,是无数实盘纠纷(触发价 vs 成交价)换来的。量化代码的测试金字塔:纯函数(信号计算、止损判断、仓位计算)全量单测;数据管道用「黄金样本」测试(一小份已知答案的历史数据,跑完比对输出);回测引擎用端到端回归测试(改动前后对同一数据集的净值曲线 diff)。

类型注解:Pandas 时代尤其实用

Python 的动态类型在量化里的代价极高:pd.Series 与 pd.DataFrame 传错(一个是列一个是表)、日期是 str 还是 Timestamp、收益率是小数还是百分数——这些错误运行时才爆,而且爆在三层调用之外。类型注解 + mypy(或 pandas-stubs)能在 IDE 里当场标红:

1
2
3
4
5
6
7
from pandas import DataFrame, Timestamp

def compute_weights(scores: DataFrame,
                    asof: Timestamp,
                    top_n: int = 50) -> DataFrame:
    """scores: index=日期, columns=股票代码 的因子分数表"""
    ...

渐进式采用即可:新代码全部注解,老代码改动时顺手补。团队收益巨大——类型就是最小的可执行文档。

规范:让 code review 聚焦逻辑而不是格式

  • 格式交给工具:black(格式化)+ ruff(lint,含 import 排序、未用变量)+ pre-commit 钩子,仓库里零配置争议;
  • 命名约束写进 README:收益率字段统一 ret 后缀、因子表列名统一 factor_xxx、日期列统一 date(Timestamp 类型)——跨策略的字段口径一致,是多人协作里最省钱的决定;
  • 函数纪律:单个函数不超过一屏;回测里禁止出现魔法数字(0.001 必须命名 COMMISSION_RATE);
  • research 包结构模板:data/(加载与清洗)、factors/(一因子一文件一测试)、backtest/(引擎与成本模型)、portfolio/(优化与权重)、utils/——研究与回测共享同一份因子实现,杜绝「研究用 A 实现、回测用 B 实现」的分叉。

依赖管理与虚拟环境

「在我机器上能跑」在量化里的具体形态是:两年后重跑因子,numpy 新版改变了某个函数的默认行为,结果全变。对策:

  1. 一项目一虚拟环境(python -m venv / conda / uv 任选),绝不往系统 Python 里装包;
  2. 锁文件进仓库:pip freeze > requirements.lock 或用 uv lock / conda env export——精确到版本号的依赖是复现的必要条件;
  3. 升级纪律:依赖升级也走分支 + 回测回归测试——numpy/scipy/pandas 的大版本升级等同于「换了一次底层库」,净值曲线 diff 必须过;
  4. 内网机构场景:搭建私有 PyPI 镜像,锁定数据商 SDK 版本。

关联阅读:Git 与协作 让质量门禁落在合并时刻;回测框架设计 定义哪些模块最值得测试;DevOps 与 CI/CD 把 pytest/mypy 自动化到每次提交。