⚠ 交易存在重大亏损风险。本网站的所有内容仅供教育用途,不构成财务建议。
第 8 课,共 13 课 · 免费

事件驱动回测:带 ATR 止损、仓位规模和 R 倍数交易清单的循环引擎

用 Python 构建一个逐根 K 线运行的回测循环:按 ATR 风险确定仓位规模,严格执行止损(包括跳空情形),并以 R 倍数记录每一笔交易。

作者: Moustafa Hijazi内容审核日期: 2026-10-30阅读约 8 分钟自学课程

你将学到什么

  • 为什么向量化回测无法模拟止损、部分成交或基于风险的仓位规模,以及何时需要循环引擎。
  • 事件驱动引擎必须在 K 线之间传递的状态:现金、仓位、入场价、止损位和未平仓交易。
  • 如何根据账户风险和 ATR 确定每个仓位的规模,以及如何把结果记录为 R 倍数。
  • 如何在代码中诚实地处理跳空越过止损位的情况。

讲解

第 7 课的向量化回测一次性根据整个序列计算出每根 K 线的仓位。它速度很快,但无法表达依赖于路径的规则:入场价下方 2 ATR 处的止损取决于你在哪里入场,而仓位规模取决于那一刻的账户价值。事件驱动回测逐根遍历 K 线,只使用截至该 K 线可得的信息,并完全像交易者那样更新自身状态。

状态与事件顺序

引擎维护一小组变量:cash、position(股数,空仓时为 0)、entry、stop、risk_per_share,以及一个列表 trades。在每根 K 线上,检查的顺序很重要,必须一次写定并始终遵守:

  1. 先处理离场。如果有未平仓头寸,且今天的最低价触及或低于止损位,就离场。成交价为止损价,除非开盘价已经低于止损位(跳空),此时按开盘价成交。计划中 1R 的亏损正是在这里可能变成 1.4R 甚至更多。
  2. 使用今天的收盘价和 ATR 更新移动止损。
  3. 最后处理入场,且只能在下一根 K 线的开盘价入场,因为根据今天收盘价计算出的信号无法以今天的收盘价成交。在代码中这意味着:第 t 根 K 线上的信号设置一个标志,成交发生在 open[t+1]。

循环中的仓位规模

基于风险的仓位规模已在课程《风险与交易管理》(第 2 课)和课程《系统化交易与回测》(第 9 课)中讲过。在代码中它只需一行:size = floor(equity * risk_pct / (entry - stop)),其中分母是每股风险(这里为 2 * ATR)。再加一个上限,例如 size * entry <= 0.25 * equity,这样极小的 ATR 就不会产生一个账户无法承受的仓位。

以 R 记录交易

每笔已平仓交易都以字典形式追加到列表中,包含日期、价格、规模、以欧元计的盈亏以及 R 倍数 = 盈亏 / (规模 × 每股风险)。pd.DataFrame(trades) 把列表变成一张交易表,供第 9 课的绩效指标函数以及课程《风险与交易管理》(第 1 课)中的期望值计算使用。

一个最小化引擎

  • def run(df, risk_pct=0.01, atr_mult=2.0, start_cash=20_000):
  • cash, pos, entry, stop, rps = start_cash, 0, None, None, None
  • trades, equity, pending = [], [], False
  • for i in range(1, len(df)):
  • o, h, l, cl, atr = df.open[i], df.high[i], df.low[i], df.close[i], df.atr[i]
  • if pos > 0 and l <= stop: # 离场:止损或跳空
  • fill = min(o, stop)
  • pnl = pos * (fill - entry)
  • trades.append(dict(exit=df.index[i], pnl=pnl, R=pnl / (pos * rps)))
  • cash += pos * fill; pos = 0
  • if pending and pos == 0: # 在本根 K 线开盘价入场
  • entry, rps = o, atr_mult * df.atr[i-1]
  • pos = int(cash * risk_pct // rps)
  • stop = entry - rps; cash -= pos * entry; pending = False
  • if pos == 0 and df.signal[i]:
  • pending = True # 在下一个开盘价交易
  • equity.append(cash + pos * cl)
  • return pd.DataFrame(trades), pd.Series(equity, index=df.index[1:])

两个细节:用于确定仓位规模的 ATR 是 df.atr[i-1],即入场前一晚已知的数值;止损是对照最低价而不是收盘价进行检查的。移动止损只需在离场代码块之后多加一行:stop = max(stop, cl - rps)。成本可以像第 7 课那样从 pnl 中扣除;为清晰起见,这里省略了成本。

实例演算

假设示例:初始资金 €20,000,每笔交易风险 1%(€200),止损设在 2 × ATR。价格和 ATR 都是编造的整数;不计手续费、点差和税费。

  1. 交易 1。星期一出现信号;星期二开盘价 €50.00,前一日 ATR 为 €2.50。每股风险 = 2 × 2.50 = €5.00;止损 = €45.00。规模 = floor(200 / 5) = 40 股(敞口 €2,000,占资金的 10%,在 25% 的上限之内)。之后因信号在 €58.00 离场:盈利 = 40 × 8 = €320 = 320 / 200 = +1.6R。
  2. 交易 2。同样的设置,入场价 €50.00,止损 €45.00,40 股。隔夜出现消息:股票以 €43.00 开盘。引擎按 min(开盘价, 止损价) = €43.00 成交。亏损 = 40 × 7 = €280 = −1.4R,而不是计划中的 −1.0R。向量化测试只会显示 −1.0R,甚至什么都不显示。
  3. 交易 3。入场价 €50.00,使用移动止损;当最低价触及止损时,止损已上移到 €47.50。亏损 = 40 × 2.50 = €100 = −0.5R。
  4. 交易清单。R 倍数为 +1.6、−1.4、−0.5:合计 −0.3R,平均 −0.1R。三笔交易什么也证明不了,但这张表已具备容纳数百笔交易的正确形式。

另一个价格为 €120、ATR 为 €6 的品种,规模为 floor(200 / 12) = 16 股:风险同样是 €200,但由于波动率更高,股数更少。

常见错误

K 线跳空越过止损位时仍按止损价成交像 if l <= stop: fill = stop 这样的代码,默认假设市场在你的止损价上成交过。而在跳空时并没有。多头离场请始终使用 min(open, stop)(空头则用 max(open, stop))。同样的逻辑也适用于使用止损单入场的情况:如果开盘价为 €54,那么 €52 的买入止损单会按开盘价成交。

练习活动

在纸上为一笔交易追踪引擎的运行过程:资金 €15,000,风险 1.5%,入场前一晚的 ATR 为 €1.80,止损乘数 2.5,在下一个开盘价 €36.00 入场。(a) 计算每股风险、止损位和仓位规模(向下取整)。(b) 离场当天开盘价 €37.00,盘中下跌到最低价 €31.00,收于 €32.00。成交价、以欧元计的盈亏和 R 倍数分别是多少?(c) 如果你对照收盘价而不是最低价检查止损,结果会不同吗?

显示答案

(a) 每笔交易风险 = 0.015 × 15,000 = €225。每股风险 = 2.5 × 1.80 = €4.50。止损 = 36.00 − 4.50 = €31.50。规模 = floor(225 / 4.50) = 50 股。

(b) 开盘价 €37.00 高于止损位,因此没有跳空;最低价 €31.00 触及或低于 €31.50,因此成交价为 min(37.00, 31.50) = €31.50。亏损 = 50 × (31.50 − 36.00) = −€225 = −1.0R。

(c) 在本例中,对照收盘价(€32.00)检查同样会触发离场,但成交价是 €32.00 而不是 €31.50,因此亏损为 €200(−0.89R),看起来比实际情况要好。在其他 K 线上,最低价可能触及止损而收盘价又回升到止损位之上;此时基于收盘价的检查会完全错过一次真实的离场。

局限性:从日线数据无法得知盘中最高价和最低价出现的先后顺序;当止损位和目标位都落在同一根 K 线内时,请假设出现较差的结果。

要点回顾

  • 当规则依赖于路径时,请使用循环引擎:止损、移动止损、基于风险的仓位规模以及部分离场。
  • 固定每根 K 线上的事件顺序:先离场,再更新止损,最后在下一个开盘价入场。
  • 仓位规模 = 资金 × 风险百分比 ÷ 每股风险,并设置敞口上限;为每笔交易保存其 R 倍数。
  • 跳空离场按开盘价成交,而不是按止损价;超过 1R 的亏损正是由此产生的。

下一步

第 9 课把你现在得到的资金曲线和交易清单转化为可复用、经过测试的绩效函数:CAGR、回撤、夏普比率、索提诺比率和盈利因子。

自测

回答问题后核对答案。你可以随意重试多次。

1. 资金 €20,000,每笔交易风险 1%,止损设在 2 × ATR,ATR = €2.50,入场价 €50。离场当天股票以 €43 开盘,而止损价为 €45。一个诚实的引擎会记录多少亏损?

2. 在第 i 根 K 线开盘价入场并确定仓位规模时,为什么引擎使用 df.atr[i-1] 而不是 df.atr[i]?

3. 以下哪一套规则真正需要事件驱动循环,而不能用第 7 课的向量化方法?

供您自行复习的自动自测。您的答案在浏览器中核对,我们不会查看或评估。这不是考试,也不是资格证书。

您的进度

“已完成”仅表示您将本课标记为已学完,并不是对您理解程度的测试。 学习进度和笔记仅保存在此设备的此浏览器中,而非账户中。