σμ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 学习路线›模块 6 · 回测与交易成本›主题 6.2·~1小时 15分钟高级

事件驱动回测器

路线进度模块 6/11 · 主题 2/6
6.2 事件驱动回测器
—实验得分— / 100总 XP—连续天数—

一次一根K线

事件驱动回测器的行为像一个实盘系统:时间逐根K线推进,每一根K线上程序只能看到过去,做出决策、发出订单并更新账户。它比向量化回测慢,但它携带状态(现金、股数),可以模拟订单和成交,同一份策略代码以后还能跑在实时行情上。循环是关键:对K线做 for,账户从一次迭代传到下一次。

def crossover_signal(history, fast, slow):
    if len(history) < slow:
        return 0                                           # not enough history yet
    fast_mean = sum(history[-fast:]) / fast
    slow_mean = sum(history[-slow:]) / slow
    return 1 if fast_mean > slow_mean else 0

prices = [100, 101, 103, 102, 105, 107, 104, 103, 99, 98]
print(crossover_signal(prices[:3], 2, 4))                  # 0: only 3 prices
print(crossover_signal(prices[:6], 2, 4))                  # 1: the 2-bar mean is above the 4-bar mean
print(crossover_signal(prices[:9], 2, 4))                  # 0: the trend has turned

订单会改变账户

订单把现金换成股票,或反过来。股数为整数时,买入 int(cash // price) 股会剩下一点现金,账户价值始终是 cash + shares * price。把订单逻辑放进一个小函数 execute_order,循环就会很短,之后还能在一处加入手续费和滑点。全部卖出就是现金变为 cash + shares * price,股数为零。

def execute_order(cash, shares, price, target):
    if target == 1:
        bought = int(cash // price)                        # whole shares only
        return cash - bought * price, shares + bought
    return cash + shares * price, 0

cash, shares = execute_order(1000.0, 0, 103.0, 1)
print(cash, shares, cash + shares * 103.0)                 # 73.0 9 1000.0
cash, shares = execute_order(cash, shares, 99.0, 0)
print(cash, shares)                                        # 964.0 0

循环,以及它能知道什么

在第 t 根K线上,策略可以使用到上一次收盘为止的 prices[:t],并以 prices[t] 成交。对 prices[:t] 切片就把未来挡在了外面:下一个价格根本不在列表里。由此得到检验事件驱动引擎的好办法:只对前 k 根K线运行,结果必须等于完整运行结果的前 k 个。若不同,说明策略偷看了。

def crossover_signal(history, fast, slow):
    if len(history) < slow:
        return 0
    return 1 if sum(history[-fast:]) / fast > sum(history[-slow:]) / slow else 0

def execute_order(cash, shares, price, target):
    if target == 1:
        bought = int(cash // price)
        return cash - bought * price, shares + bought
    return cash + shares * price, 0

def run_backtest(prices, fast, slow, cash):
    shares = 0
    values = []
    for t, price in enumerate(prices):
        if t > 0:
            target = crossover_signal(prices[:t], fast, slow)       # only what was known at the last close
            cash, shares = execute_order(cash, shares, price, target)
        values.append(cash + shares * price)
    return values

prices = [100, 101, 103, 102, 105, 107, 104, 103, 99, 98]
full = run_backtest(prices, 2, 4, 1000.0)
print([round(v, 2) for v in full])
print(run_backtest(prices[:6], 2, 4, 1000.0) == full[:6])             # True: the future never leaks
# The benchmark: buy once at the start and hold
import random

def buy_and_hold(prices, cash):
    shares = int(cash // prices[0])
    left = cash - shares * prices[0]
    return [left + shares * price for price in prices]

random.seed(3)
prices = [100.0]
for _ in range(99):
    prices.append(round(prices[-1] * (1 + random.gauss(0.0004, 0.012)), 2))
held = buy_and_hold(prices, 10000.0)
print(round(held[0], 2), round(held[-1], 2))
同一份代码可以上线循环看到一根K线就行动:这正是实盘交易程序的形态,只不过下一根K线来自行情推送而不是列表。把信号、订单和账户逻辑分在不同函数里,就可以在不重写策略的情况下,把历史数据源换成实时数据源。
  • 事件驱动回测器带着状态(现金、股数)逐根循环;在第 t 根只能使用 prices[:t]。
  • 订单在现金和整数股之间转换:账户价值是 cash + shares * price;把订单逻辑放进一个函数。
  • 检验前视偏差:只跑前 k 根的结果必须等于完整运行的前 k 个结果;并与买入持有作比较。

下一节:前视偏差,让回测好得不真实的那个隐蔽错误。

实验:评分内容

  • 用 crossover_signal 比较两条均线25 分
  • 用 execute_order 执行订单25 分
  • 用 run_backtest 逐根K线推进25 分
  • 用 buy_and_hold 设定基准25 分
资源管理器 · lab-88-1
▾ lab-88-1
…
bash · 浏览器内
learner@quantcraft:~/lab-88-1$
Python 尚未加载PythonLn 1, Col 1Spaces: 4UTF-8

要重置此主题吗?

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

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

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

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

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