σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
uantCraft Academy学习路线
Ctrl K
AO
+1.2k 位学习者
登录加入 ↗
学习路线
AO
+1.2k 位学习者
登录
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
Python 学习路线›模块 10 · 高性能 Python 工程›主题 10.3·~50分钟高级

使用 pytest 测试

路线进度模块 10/11 · 主题 3/13
10.3 使用 pytest 测试
—实验得分— / 100总 XP—连续天数—

为什么要测试,测试长什么样

在交易里,bug 不是屏幕上的异常,而是看起来合理的错误数字:盈亏里符号反了、手续费算了两次、信号悄悄用了明天的价格。测试是用已知答案检查代码的小程序,让这类 bug 在你的机器上被发现,而不是在市场上。pytest 会找到名为 test_* 的函数,逐个运行,并报告 assert 失败的那些。测试无需框架就能读懂:准备输入,调用函数,assert 你期望的结果。

# A test is a function with an assert; pytest runs every test_* function it finds
def net_pnl(entry, exit_price, quantity, fee):
    return (exit_price - entry) * quantity - fee

def test_net_pnl_long_winner():
    assert net_pnl(100.0, 102.0, 50, 1.0) == 99.0

def test_net_pnl_short_loser():
    assert net_pnl(100.0, 102.0, -50, 1.0) == -101.0

for test in (test_net_pnl_long_winner, test_net_pnl_short_loser):
    test()                                           # pytest does this for you and reports failures
    print(test.__name__, "passed")

浮点数、错误与大量用例

浮点数是近似的,所以 0.1 + 0.2 == 0.3 为假。比较数字要带容差:pytest.approx(expected) 接受很小的相对误差(默认百万分之一,另有 1e-12 的极小绝对下限),对于接近零、相对容差无用的值,还可以加绝对容差。要检查不良输入被拒绝,pytest.raises(ValueError) 只有在该异常发生时才通过,没有异常(或发生别的异常)就失败。要让一个测试跑很多用例,@pytest.mark.parametrize 提供输入与期望值的表格,每一行单独报告,并使新增用例只需改一行。

# Floats need a tolerance: approx accepts a tiny relative difference, == does not
import pytest

print(0.1 + 0.2 == 0.3)                              # False
print(0.1 + 0.2 == pytest.approx(0.3))               # True
print(1e-10 == pytest.approx(0.0))                   # False  near zero a relative tolerance cannot help
print(1e-10 == pytest.approx(0.0, abs=1e-9))         # True   a larger absolute tolerance does
# raises: the test passes only if the expected exception happens
import pytest

def leverage(gross, equity):
    if equity <= 0:
        raise ValueError("equity must be positive")
    return gross / equity

with pytest.raises(ValueError) as info:
    leverage(1000.0, 0.0)
print("raised:", info.value)                         # raised: equity must be positive

try:
    with pytest.raises(ValueError):
        leverage(1000.0, 500.0)                      # no exception: pytest.raises itself fails
except BaseException as outcome:
    print(type(outcome).__name__)                    # Failed
# parametrize in miniature: one function, a table of cases, each row judged on its own
def fee(notional, rate=0.001, minimum=1.0):
    return max(notional * rate, minimum)

cases = [
    ((100_000.0,), 100.0),
    ((500.0,), 1.0),                                 # the minimum fee applies
    ((2_000.0, 0.002), 4.0),
    ((0.0,), 1.0),
]
for index, (args, expected) in enumerate(cases):
    got = fee(*args)
    print(index, "ok" if abs(got - expected) <= 1e-9 * abs(expected) else f"FAIL got {got}")

Fixture、种子与测试什么

fixture 是测试按名称请求的共享准备:一小段价格序列、固定种子的随机数生成器、一个临时文件。使用随机数据时,要固定种子(或把它打印出来),这样失败才能复现:一百次才失败一次又无法重放的测试,比没有还糟。测试你关心的行为而不是内部细节:手工算出的已知用例、边界情况(空输入、零、单个元素)、必须始终成立的不变量(概率在 0 到 1 之间,以同价买入再卖出恰好亏掉手续费),以及每修一个 bug 就加一个回归测试。当你能写出对任何输入都成立的规则时,在大量随机输入中寻找反例是一种廉价而有力的测试。

# A seed makes a random test replayable; a property is searched for a counterexample
import random

def find_counterexample(prop, inputs):
    for value in inputs:
        if not prop(value):
            return value
    return None

rng = random.Random(42)                              # fixed seed: the same inputs on every run
returns = [round(rng.uniform(-0.05, 0.05), 4) for _ in range(1000)]

def compound_is_positive_growth(r):
    return (1 + r) > 0                               # a daily return above -100% keeps the value positive

print(find_counterexample(compound_is_positive_growth, returns))      # None  the rule holds for all 1000
print(find_counterexample(lambda r: r > -0.049, returns))             # a return that breaks a wrong rule
不会失败的测试什么也证明不了先故意破坏代码,让每个测试都失败一次,这样你才知道它能抓住它要抓的 bug。不要把函数自己的公式抄进测试:如果两者犯同样的错,它们会一致,测试就通过了。要独立或手工算出期望值。并让测试保持快速,不依赖网络和时钟,这样你才会在每次修改前真正去运行它们。
  • 测试准备输入、调用函数并 assert 结果;pytest 运行每个 test_* 函数并报告失败。
  • 用 pytest.approx 比较浮点数(接近零时加绝对容差),用 pytest.raises 期望错误,用 parametrize 处理用例表。
  • 固定随机种子,测试边界情况、不变量和已修复的 bug,寻找反例,并在信任每个测试之前让它先失败一次。

下一节:Python 代码性能分析,在提速之前先测量时间花在哪里。

实验:评分内容

  • 在 is_close 中带容差地比较浮点数25 分
  • 用 run_cases 运行用例表25 分
  • 用 raises 期望出现错误25 分
  • 用 find_counterexample 寻找反例25 分
资源管理器 · lab-119-1
▾ lab-119-1
…
bash · 浏览器内
learner@quantcraft:~/lab-119-1$
Python 尚未加载PythonLn 1, Col 1Spaces: 4UTF-8

要重置此主题吗?

lab-119-1 中的所有内容将恢复为初始文件,终端也会被清空。你当前的解答将丢失,且无法撤销。

将被重置
  • main.py → 初始代码
  • 终端历史与测试输出
  • 未提交的更改
将被保留
  • 实验最高得分
  • 你的 XP 与连续天数
  • 讲解进度与笔记

登录后即可运行你的代码。

所有讲解均可免费阅读。创建账户或登录后,即可运行代码、运行测试并提交此实验。

  • 浏览器内 IDE 与 bash 终端,无需安装
  • 即时评分、XP 以及你在学习路线中的位置
创建账户登录
或
使用 GitHub 继续