σμ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.6·~1小时高级

Numba

路线进度模块 10/11 · 主题 6/13
10.6 Numba
—实验得分— / 100总 XP—连续天数—

编译你写的循环

有些计算天生就是循环:每一步依赖上一步,比如指数加权平均、回撤,或路径依赖期权的收益。NumPy 无法把它们向量化,而对一百万个元素的 Python 循环很慢,因为每一步都要经过解释器。Numba 是一个即时编译器:在函数上方加 @njit,首次调用时 Numba 会针对实际传入的类型把 Python 循环翻译成机器码。之后的调用以接近 C 的速度运行,通常比纯 Python 中的同一个循环快五十到一百倍,而代码无需改动。

# An exponentially weighted average is a loop: each value depends on the one before
import numpy as np

try:
    from numba import njit                           # one decorator compiles the function when it is installed
except ImportError:
    def njit(func):                                  # without Numba the same code simply runs as Python
        return func

@njit
def ewma(values, alpha):
    out = np.empty(len(values))
    out[0] = values[0]
    for i in range(1, len(values)):
        out[i] = alpha * values[i] + (1.0 - alpha) * out[i - 1]
    return out

print(ewma(np.array([1.0, 2.0, 3.0]), 0.5))          # [1.   1.5  2.25]

Numba 喜欢的风格

在 @njit 选择的 nopython 模式下,Numba 只编译 Python 的一个子集,对其余部分直接拒绝,而不是悄悄退回到慢代码。它理解数字、NumPy 数组、元组、简单循环、if 以及大部分 NumPy 函数;不理解 pandas、任意 Python 对象或混合类型的字典。所以要用朴素的风格来写热点函数:接收数组和标量,用 np.empty 或 np.zeros 分配输出,用 for i in range(n) 循环,并保持标量累加器(total、peak)。每次用相同类型的参数调用,因为新的类型组合会触发重新编译。

# A drawdown is a running peak and a running worst case: two scalars and one loop
import numpy as np

try:
    from numba import njit
except ImportError:
    def njit(func):
        return func

@njit
def max_drawdown(equity):
    peak = 0.0
    worst = 0.0
    for i in range(len(equity)):
        value = equity[i]
        if value > peak:
            peak = value
        drawdown = (peak - value) / peak
        if drawdown > worst:
            worst = drawdown
    return worst

curve = np.array([100.0, 120.0, 90.0, 130.0, 117.0])
print(max_drawdown(curve))                           # 0.25   from 120 down to 90
print(max_drawdown(np.array([1.0, 2.0, 3.0])))       # 0.0    a curve that only rises

编译耗时、缓存,以及 Numba 帮不上忙的地方

第一次调用要付出编译代价,往往一秒以上,所以给 Numba 函数计时应在它用小数据调用过一次之后,并用 @njit(cache=True) 在多次运行之间保留编译结果。加速需要足够的工作量:一个对三个数调用一百万次的函数,时间都花在调用开销上,而不是循环里。Numba 对数组上的循环有帮助;对本来就是一次 NumPy 调用的代码、等待数据库、或糟糕的算法,它都无能为力。@njit(parallel=True) 配合 prange 可以把相互独立的迭代分散到多个核心,fastmath=True 用一点浮点严格性换取速度,只有在确认结果仍在容差内之后才应启用。

# Counting crossings: Numba compiles any loop like this, so keep the logic in scalars
import numpy as np

try:
    from numba import njit
except ImportError:
    def njit(func):
        return func

@njit(cache=False)
def count_crossovers(fast, slow):
    count = 0
    previous = 0.0
    for i in range(len(fast)):
        gap = fast[i] - slow[i]
        if i > 0 and previous * gap < 0:
            count += 1
        previous = gap
    return count

fast = np.array([1.0, 3.0, 2.0, 1.0, 4.0])
slow = np.full(5, 2.0)
print(count_crossovers(fast, slow))                  # 2     below, above, touch, below, above: two strict sign changes
# Options change how a function is compiled; here they are ignored if Numba is not installed
import numpy as np

try:
    from numba import njit, prange
except ImportError:
    prange = range

    def njit(*args, **kwargs):
        if args and callable(args[0]):
            return args[0]
        return lambda func: func

@njit(cache=True)                                    # keep the compiled code on disk between runs
def total_notional(prices, quantities):
    total = 0.0
    for i in prange(len(prices)):                    # prange runs in parallel only with parallel=True
        total += prices[i] * quantities[i]
    return total

prices = np.array([184.25, 410.5, 99.0])
quantities = np.array([100.0, 50.0, 200.0])
print(total_notional(prices, quantities))            # 58750.0
print(total_notional(prices, quantities) == float(prices @ quantities))     # True  same answer as the vectorised dot product
不仅要看速度,还要核对答案编译后的循环可能以新的方式出错:整数溢出(而不是 Python 的无限整数)、本以为是 float64 却悄悄用了 float32 数组、越界下标读到垃圾数据而不是抛出错误。要用随机输入把编译结果与朴素的 Python 版本对比(用 Numba 保留的 fn.py_func),就像这些实验的测试那样,并在开发期间开启边界检查。
  • @njit 在首次调用时把 Python 函数编译成机器码;依赖前面步骤的循环无需向量化也能变快。
  • 为 nopython 模式编写:数组与标量、预先分配的输出、for i in range(n)、标量累加器;不用 pandas 或混合类型容器。
  • 预热调用之后再计时,使用 cache=True,对本就是一次 NumPy 调用的代码不要期待收益,并用朴素的 Python 版本核对结果。

下一节:Cython,通过添加类型并构建扩展模块来编译 Python 循环的另一种方式。

实验:评分内容

  • 在 rolling_mean 中维护滚动总和25 分
  • 在 max_drawdown 中跟踪峰值25 分
  • 在 count_crossovers 中统计交叉次数25 分
  • 在 ewma 中填充输出数组25 分
资源管理器 · lab-122-1
▾ lab-122-1
…
bash · 浏览器内
learner@quantcraft:~/lab-122-1$
Python 尚未加载PythonLn 1, Col 1Spaces: 4UTF-8

要重置此主题吗?

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

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

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

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

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