第 10 章 · 03 测试与贡献


文档摘要

第 10 章 · 03 测试与贡献 本节摘要:PyPortfolioOpt 是个开放项目——欢迎你写自己的优化器(上一节)、回填测试用例、修 bug、补文档,甚至把研究成果发到 JOSS 引用。本节讲三件事:测试规范(pytest 框架、100% 覆盖、tests/resources/stockprices.csv 数据集)、贡献流程(pre-commit 钩子、ruff 格式化、提 PR 的礼节)、JOSS 论文引用方式。读完本节,你能为 PyPortfolioOpt 提交第一个 PR,或在学术作品中正确引用它。 内容来源: 、 、 、 、 、 目录,汉化并套用体系化模板。 学习目标 阅读完本节,你应当能够: 跑通 PyPortfolioOpt 的 pytest 测试套件。

第 10 章 · 03 测试与贡献

本节摘要:PyPortfolioOpt 是个开放项目——欢迎你写自己的优化器(上一节)、回填测试用例、修 bug、补文档,甚至把研究成果发到 JOSS 引用。本节讲三件事:测试规范(pytest 框架、~100% 覆盖、tests/resources/stock_prices.csv 数据集)、贡献流程(pre-commit 钩子、ruff 格式化、提 PR 的礼节)、JOSS 论文引用方式。读完本节,你能为 PyPortfolioOpt 提交第一个 PR,或在学术作品中正确引用它。

内容来源:CONTRIBUTING.mdREADME.md.pre-commit-config.yamlpyproject.tomldocs/Citing.rsttests/ 目录,汉化并套用体系化模板。

学习目标

阅读完本节,你应当能够:

  1. 跑通 PyPortfolioOpt 的 pytest 测试套件
  2. 理解 stock_prices.csv 数据集的设计意图。
  3. ruff + pre-commit 规范格式化代码。
  4. 走完一次 PR 贡献流程
  5. 在论文中正确引用 JOSS 文章

一、测试规范:pytest 与 ~100% 覆盖

PyPortfolioOpt 的测试用 pytest(不用 unittest),覆盖接近 100%。README 明确表述:

Tests are written in pytest ... and I have tried to ensure close to 100% coverage. Run the tests by navigating to the package directory and simply running pytest on the command line.

跑测试:

cd PyPortfolioOpt pytest # 跑全部 pytest tests/test_efficient_frontier.py # 单个文件 pytest -k max_sharpe # 按关键字过滤 pytest --cov=pypfopt # 带覆盖率

测试文件结构

tests/ 目录按模块一一对应:

测试文件 对应模块 行数(参考)
test_efficient_frontier.py EfficientFrontier ~45K
test_efficient_semivariance.py EfficientSemivariance ~19K
test_efficient_cvar.py EfficientCVaR ~13K
test_efficient_cdar.py EfficientCDaR ~13K
test_black_litterman.py BlackLittermanModel ~21K
test_hrp.py HRPOpt ~2.4K
test_cla.py CLA ~4.8K
test_discrete_allocation.py DiscreteAllocation ~12K
test_plotting.py plotting ~12K
test_base_optimizer.py BaseOptimizer ~10K
test_custom_objectives.py 自定义目标 ~11K
test_expected_returns.py expected_returns ~9.9K
test_risk_models.py risk_models ~12K
test_objective_functions.py objective_functions ~4.4K

公共测试工具:utilities_for_tests.py

测试用 get_data()setup_efficient_frontier() 等工具复用配置:

# tests/utilities_for_tests.py def get_data(): return pd.read_csv(resource("stock_prices.csv"), parse_dates=True, index_col="date") def setup_efficient_frontier(data_only=False, *args, **kwargs): df = get_data() mean_return = expected_returns.mean_historical_return(df) sample_cov_matrix = risk_models.sample_cov(df) if data_only: return mean_return, sample_cov_matrix return EfficientFrontier(mean_return, sample_cov_matrix, verbose=True, *args, **kwargs)

测试风格:断言为主

PyPortfolioOpt 的测试极简,基本都是 assert:

def test_es_example(): es = setup_efficient_semivariance() w = es.efficient_return(0.2) assert isinstance(w, dict) assert set(w.keys()) == set(es.tickers) np.testing.assert_almost_equal(es.weights.sum(), 1) assert all([i >= -1e-5 for i in w.values()]) np.testing.assert_allclose( es.portfolio_performance(risk_free_rate=0.02), (0.20, 0.091287, 1.971794), rtol=1e-4, atol=1e-4, )

💡 写测试的门槛极低:CONTRIBUTING.md 明确说「just find the relevant test file (or create a new one), and write a bunch of assert statements」。新手练手友好。

二、stock_prices.csv:精心设计的测试数据集

20 个 ticker 的日线数据集,在 tests/resources/stock_prices.csv。README 解释了选择标准:

These tickers have been informally selected to meet several criteria:

  • reasonably liquid
  • different performances and volatilities
  • different amounts of data to test robustness

ticker 列表:

['GOOG', 'AAPL', 'FB', 'BABA', 'AMZN', 'GE', 'AMD', 'WMT', 'BAC', 'GM', 'T', 'UAA', 'SHLD', 'XOM', 'RRC', 'BBY', 'MA', 'PFE', 'JPM', 'SBUX']

设计巧思:

  • 跨行业:科技(GOOG/AAPL)、消费(WMT/BBY)、金融(BAC/JPM/MA)、能源(XOM/RRC)、电信(T)、医疗(PFE)。
  • 跨波动率:AMD/SHLD 高波动,WMT/MA 低波动。
  • 跨数据长度:有些 ticker 数据缺失多(SHLD 已退市),专门测试鲁棒性。

⚠️ 写测试用这份固定数据:不要依赖 yfinance 实时拉取,网络和时效会让 CI 不稳定。所有 PR 的测试必须基于 tests/resources/

三、代码风格:ruff + pre-commit

历史版本用 Black,新版本(pyproject.toml.pre-commit-config.yaml)迁移到 ruff(更快,集成 lint + format):

# .pre-commit-config.yaml repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: - id: check-toml - id: check-yaml - repo: https://github.com/astral-sh/ruff-pre-commit rev: 'v0.14.5' hooks: - id: ruff args: [ --fix, --exit-non-zero-on-fix, --unsafe-fixes ] - id: ruff-format

pyproject.toml 的关键设置:

[tool.ruff] line-length = 88 target-version = "py311" [tool.ruff.lint] select = ["F", "I"] # F=pyflakes, I=isort [tool.ruff.format] quote-style = "double" indent-style = "space"

安装 pre-commit 钩子

pip install pre-commit pre-commit install # 装到 .git/hooks/ pre-commit run --all-files # 全量跑一次

之后每次 git commit 自动跑 ruff 格式化与 lint。如果格式不达标,会自动修改文件并阻止提交,你 git add 修改后再 commit 即可。

💡 ruff vs black:ruff 用 Rust 写,比 black 快几十倍;且集成 isort、pyflakes 等多个工具到一个命令。PyPortfolioOpt 已迁移到 ruff,新 PR 不要再用 black。

四、贡献流程:从想法到合并

CONTRIBUTING.md 的指南:

1. 先开 issue 讨论

Before you start coding your contribution, it may be wise to raise an issue on GitHub to discuss whether the contribution is appropriate for the project.

特别是涉及 API 变更的贡献,作者明确说「I am unlikely to accept a random PR that significantly complicates the API」——先沟通,避免白干。

2. Fork + branch

git clone https://github.com/<你的 GitHub>/PyPortfolioOpt.git cd PyPortfolioOpt git checkout -b feature/my-new-objective

3. 写代码 + 写测试 + 写文档

  • 代码:pypfopt/ 下相应模块。
  • 测试:tests/test_xxx.py,基于 stock_prices.csv
  • 文档:docs/XXX.rst(sphinx 格式)。
  • inline 注释「don't go overboard」,大段说明放 ReadTheDocs。

4. pre-commit + pytest 全过

pre-commit run --all-files pytest --cov=pypfopt

5. 提 PR

PR 描述里写清:动机、改动要点、测试覆盖情况。作者会做 code review,可能要求调整 API 或测试。

6. 贡献类型

CONTRIBUTING.md 列出欢迎的贡献:

  • 性能优化:numpy 向量化技巧。
  • 新目标函数:新优化目标写成函数。
  • 测试用例:边角情况、更大规模数据集。

五、JOSS 论文引用

PyPortfolioOpt 发表在 Journal of Open Source Software (JOSS)。如果你在学术工作中使用,按 docs/Citing.rst 引用:

文本引用:

Martin, R. A., (2021). PyPortfolioOpt: portfolio optimization in Python. Journal of Open Source Software, 6(61), 3066, https://doi.org/10.21105/joss.03066

BibTeX:

@article{Martin2021, doi = {10.21105/joss.03066}, url = {https://doi.org/10.21105/joss.03066}, year = {2021}, publisher = {The Open Journal}, volume = {6}, number = {61}, pages = {3066}, author = {Robert Andrew Martin}, title = {PyPortfolioOpt: portfolio optimization in Python}, journal = {Journal of Open Source Software} }

💡 为什么引用重要:JOSS 是开源软件的学术认证,引用数直接影响作者学术声誉与项目可持续性。即使不强制,出于对维护者的尊重,正式发表时都应引用。

六、报 Bug 的正确姿势

CONTRIBUTING.md 列出报 bug 应包含:

  • 描述性标题:其他用户能搜到。
  • 环境:OS、Python 版本、发行版。
  • 最小复现示例:尽量精简,不要贴整个项目。
  • 预期 vs 实际:你期望发生什么,实际发生什么。
  • 完整 traceback:报错堆栈,删掉敏感信息。

模板:

**标题**:`EfficientFrontier.max_sharpe()` raises OptimizationError on dummy data **环境**:Windows 11 / Python 3.11 / pypfopt 1.6.0 **复现**: ```python from pypfopt import EfficientFrontier ef = EfficientFrontier(mu, S) ef.max_sharpe() # OptimizationError

预期:返回权重 dict。
实际:抛 OptimizationError,traceback 如下:

... 完整堆栈 ...
## 本节要点回顾 1. **pytest 测试**:接近 100% 覆盖,测试文件与模块一一对应,公共工具 `utilities_for_tests.py`,风格以 `assert` 为主,门槛极低。 2. **stock_prices.csv**:20 个跨行业、跨波动、跨数据长度的 ticker,专为测试鲁棒性设计;PR 测试必须基于这份固定数据,避免网络依赖。 3. **ruff + pre-commit**:新版用 ruff(lint + format 集成,比 black 快几十倍),`pre-commit install` 后每次提交自动跑;`pyproject.toml` 里 line-length=88、target=py311。 4. **贡献流程**:先开 issue 沟通 → Fork + branch → 写代码/测试/文档 → 本地 pre-commit + pytest 全过 → 提 PR → Code Review。 5. **API 简洁至上**:作者明确「不接受让 API 复杂化的随机 PR」,改动 API 前必须先沟通。 6. **JOSS 引用**:学术用途请按 `Martin, R. A., (2021)` 文本或 BibTeX 引用,支持项目可持续。 > 至此,PyPortfolioOpt 中文教程第 6~10 章全部讲完。从风险度量的多样选择(半方差/CVaR/CDaR),到稳健的收益估计(Black-Litterman),到绕开协方差求逆的替代优化器(HRP/CLA),到落地的离散分配,再到可视化和扩展开发——你已经具备用 PyPortfolioOpt 解决真实组合优化问题、并参与上游开发所需的全部知识。

发布者: 作者: 灏天文库 转发
评论区 (0)
U