第 4 章:事件驱动回测原理 (Event-Driven Architecture)¶
在第 1 章中,我们运行了一个简单的策略;在第 3 章中,我们准备好了数据。现在,让我们揭开 akquant 引擎盖下的秘密,从软件工程的角度深入理解事件驱动 (Event-Driven) 架构的设计原理。
本章实践入口¶
快速运行与验收¶
验收要点:
- 脚本能够输出向量化、Python 事件驱动、AKQuant 事件驱动三组结果。
- 可以对比三种范式的执行速度与回测指标差异。
- 能解释为何事件驱动更贴近真实交易状态机。
4.1 回测系统的两种范式¶
量化回测系统主要分为两大类:向量化回测 (Vectorized Backtesting) 和 事件驱动回测 (Event-Driven Backtesting)。
为了直观理解这两种模式的区别,我们编写了一个对比脚本 examples/textbook/ch04_comparison.py,分别使用 Pandas (向量化)、Backtrader (Python 事件驱动) 和 AKQuant (Rust 事件驱动) 实现了同一个双均线策略。
4.1.0 完整主示例(建议先通读)¶
"""
第 4 章:回测框架对比 (Pandas vs Backtrader vs AKQuant).
本脚本通过实现同一个简单的“双均线策略” (Golden Cross),展示三种不同风格的回测实现方式:
1. **Pandas (向量化)**:利用 DataFrame 进行全量矩阵运算,速度极快但细节模拟能力弱。
2. **Backtrader (事件驱动 - Python)**:经典的纯 Python 事件驱动框架,功能丰富但循环
速度较慢。
3. **AKQuant (事件驱动 - Rust)**:结合了事件驱动的精确性和 Rust 的高性能。
策略逻辑:
- 当 5 日均线 > 20 日均线 (金叉) -> 全仓买入
- 当 5 日均线 < 20 日均线 (死叉) -> 清仓卖出
"""
import time
import numpy as np
import pandas as pd
# 模拟数据生成 (为了方便演示,不依赖外部文件)
def generate_mock_data(length: int = 1000) -> pd.DataFrame:
"""生成模拟数据."""
np.random.seed(42)
dates = pd.date_range(start="2020-01-01", periods=length, freq="D")
prices = 100 + np.cumsum(np.random.randn(length))
df = pd.DataFrame(
{
"date": dates,
"open": prices,
"high": prices + 1,
"low": prices - 1,
"close": prices,
"volume": 100000,
}
)
df["symbol"] = "MOCK"
return df
# ==============================================================================
# 1. Pandas 向量化回测
# ==============================================================================
def run_pandas_backtest(df: pd.DataFrame) -> None:
"""运行 Pandas 向量化回测."""
print("\n[Pandas] 开始向量化回测...")
start_time = time.time()
initial_cash = 100000.0
target_weight = 0.95
# 1. 计算指标 (全量计算)
df["ma5"] = df["close"].rolling(5).mean()
df["ma20"] = df["close"].rolling(20).mean()
# 2. 生成信号 (1: 持仓, 0: 空仓)
# shift(1) 是为了避免未来函数:今天的信号只能基于昨天的收盘价计算,用于今天的交易
df["signal"] = np.where(df["ma5"] > df["ma20"], 1.0, 0.0)
df["position"] = df["signal"].shift(1).fillna(0.0)
# 3. 逐笔订单复利(使用 open 成交,最后未平仓按 close 估值)
pos_diff = df["position"].diff().fillna(df["position"])
entry_prices = df.loc[pos_diff > 0, "open"].to_numpy(dtype=float)
exit_prices = df.loc[pos_diff < 0, "open"].to_numpy(dtype=float)
if entry_prices.size > exit_prices.size:
exit_prices = np.append(exit_prices, float(df["close"].iloc[-1]))
final_equity = initial_cash
if entry_prices.size > 0 and exit_prices.size > 0:
trades_count = min(entry_prices.size, exit_prices.size)
entry_used = entry_prices[:trades_count]
exit_used = exit_prices[:trades_count]
factors = (1.0 - target_weight) + target_weight * (exit_used / entry_used)
final_equity = initial_cash * float(np.prod(factors))
cumulative_return = final_equity / initial_cash - 1.0
print(f"[Pandas] 耗时: {time.time() - start_time:.4f}s")
print(f"[Pandas] 累计收益: {cumulative_return:.2%}")
# ==============================================================================
# 2. Backtrader 事件驱动回测
# ==============================================================================
def run_backtrader_backtest(df: pd.DataFrame) -> None:
"""运行 Backtrader 回测."""
try:
import backtrader as bt # type: ignore
except ImportError:
print("\n[Backtrader] 未安装 backtrader,跳过演示 (pip install backtrader)")
return
print("\n[Backtrader] 开始事件驱动回测...")
class SmaCross(bt.Strategy):
params = (
("pfast", 5),
("pslow", 20),
)
def __init__(self) -> None:
self.sma1 = bt.ind.SMA(period=self.params.pfast) # type: ignore
self.sma2 = bt.ind.SMA(period=self.params.pslow) # type: ignore
self.crossover = bt.ind.CrossOver(self.sma1, self.sma2)
def next(self) -> None:
if not self.position:
if self.crossover > 0:
self.buy()
elif self.crossover < 0:
self.close()
cerebro = bt.Cerebro()
# 转换数据格式
data = bt.feeds.PandasData(
dataname=df.set_index("date"),
# Backtrader 默认不包含 symbol,这里仅演示单标的
)
cerebro.adddata(data)
cerebro.addstrategy(SmaCross)
cerebro.broker.setcash(100000.0)
cerebro.addsizer(bt.sizers.PercentSizer, percents=95)
start_time = time.time()
cerebro.run()
end_val = cerebro.broker.getvalue()
cumulative_return = end_val / 100000.0 - 1.0
print(f"[Backtrader] 耗时: {time.time() - start_time:.4f}s")
print(f"[Backtrader] 最终资金: {end_val:.2f}")
print(f"[Backtrader] 累计收益: {cumulative_return:.2%}")
# ==============================================================================
# 3. AKQuant 事件驱动回测
# ==============================================================================
def run_akquant_backtest(df: pd.DataFrame) -> None:
"""运行 AKQuant 回测."""
import akquant as aq
from akquant import Bar, Strategy
print("\n[AKQuant] 开始事件驱动回测 (Rust Engine)...")
class AKStrategy(Strategy):
def __init__(self) -> None:
super().__init__()
self.ma_short = 5
self.ma_long = 20
self.warmup_period = 20
def on_bar(self, bar: Bar) -> None:
symbol = bar.symbol
closes = self.get_history(
count=self.ma_long + 1, symbol=symbol, field="close"
)
if len(closes) < self.ma_long + 1:
return
# 为了避免未来函数,我们使用 [:-1] 切片,仅使用截止到昨天的数据
# 或者,如果我们在收盘后交易(日线级别通常假设次日开盘成交),可以使用当前值
# 这里为了演示方便,直接使用当前值计算信号,但在真实交易中要注意信号产生的
# 时机
# 计算均线
ma5 = closes[-self.ma_short :].mean()
ma20 = closes[-self.ma_long :].mean()
pos = self.get_position(symbol)
if ma5 > ma20 and pos == 0:
self.order_target_percent(0.95, symbol)
elif ma5 < ma20 and pos > 0:
self.close_position(symbol)
start_time = time.time()
result = aq.run_backtest(
strategy=AKStrategy, data=df, initial_cash=100000.0, commission_rate=0.0
)
metrics = result.metrics_df
end_value = 0.0
if "end_market_value" in metrics.index:
val = metrics.loc["end_market_value", "value"]
end_value = float(str(val))
else:
# 尝试从 result.equity_curve 获取
equity = result.equity_curve
if not equity.empty:
val = equity.iloc[-1]
end_value = float(str(val))
print(f"[AKQuant] 耗时: {time.time() - start_time:.4f}s")
print(f"[AKQuant] 最终资金: {end_value:.2f}")
print(f"[AKQuant] 累计收益: {end_value / 100000.0 - 1.0:.2%}")
if __name__ == "__main__":
# 1. 准备一份共用的数据
df = generate_mock_data(length=3000) # 约 12 年的数据
# 2. 运行对比
run_pandas_backtest(df.copy())
run_backtrader_backtest(df.copy())
run_akquant_backtest(df.copy())
4.1.1 向量化回测 (Pandas)¶
向量化回测利用 Pandas/NumPy 的矩阵运算能力,一次性计算出所有时间点的信号。这在数学上等同于对整个时间序列矩阵进行线性代数变换。
# Pandas 向量化回测核心逻辑 (节选自 examples/textbook/ch04_comparison.py)
def run_pandas_backtest(df):
# 1. 计算指标 (一次性计算整列)
df['ma5'] = df['close'].rolling(5).mean()
df['ma20'] = df['close'].rolling(20).mean()
# 2. 生成信号 (使用 np.where 全量生成)
# 核心:必须使用 shift(1) 将信号后移一天,避免前视偏差 (Look-ahead Bias)
df['signal'] = np.where(df['ma5'] > df['ma20'], 1, 0)
df['position'] = df['signal'].shift(1)
# 3. 计算收益
df['strategy_return'] = df['position'] * df['close'].pct_change()
- 优点:代码极简,计算效率极高(通常是毫秒级),适合早期的 Idea 验证。
- 缺点:
- 前视偏差风险:极易引入未来函数(如忘记
shift(1))。 - 路径依赖缺失:难以模拟撮合机制、滑点、资金占用等与“交易路径”强相关的状态。
- 前视偏差风险:极易引入未来函数(如忘记
4.1.2 事件驱动回测 (Backtrader & AKQuant)¶
事件驱动回测模拟了真实世界的时间流逝。它本质上是一个无限循环 (Event Loop),不断地从队列中取出事件并处理。
状态机 (State Machine) 模型:
其中 \(State\) 包含账户资金、持仓、挂单等,\(Event\) 包含行情更新、订单成交等。
Backtrader (Python 经典框架):
# Backtrader 策略逻辑 (节选)
class SmaCross(bt.Strategy):
def next(self):
# 每个时间步都会调用一次 next()
if not self.position:
if self.crossover > 0:
self.buy() # 发出买单,将在下一根 Bar 成交
AKQuant (Rust 高性能框架):
# AKQuant 策略逻辑 (节选)
class AKQuantSmaStrategy(Strategy):
def on_bar(self, bar: Bar):
# 也是逐个 Bar 处理
# 区别在于 AKQuant 底层循环由 Rust 实现,速度远快于 Python
ma5 = ...
if ma5 > ma20 and pos == 0:
self.order_target_percent(...)
- 优点:
- 零未来函数:在处理当前 Bar 时,物理上无法访问下一个 Bar 的数据。
- 高度仿真:支持限价单、止损单、复杂资金管理等微观结构模拟。
- 性能对比:
- Backtrader 由于使用 Python 循环,在数据量大时存在 GIL 锁限制,速度较慢。
- AKQuant 利用 Rust 的无 GC 特性和零成本抽象,在保持事件驱动精确性的同时,性能接近向量化回测。
你可以运行以下命令亲自体验三者的差异:
4.2 AKQuant 架构深度解析 (Architecture Deep Dive)¶
akquant 采用了一种独特的混合架构 (Hybrid Architecture),旨在结合 Python 的易用性和 Rust 的高性能。
4.2.1 核心设计理念:Rust Core + Python API¶
整个系统分为两个清晰的层级:
- Rust Core (核心层):负责所有计算密集型任务。
- Engine: 维护全局状态、资金、持仓、订单簿。
- DataFeed: 高效的数据流管理。
- Matching Engine: 订单撮合逻辑。
- Risk Manager: 实时风控检查。
- Python API (应用层):负责用户交互和策略逻辑。
- Strategy: 用户编写策略的基类。
- Indicator: 技术指标计算。
- Plotting: 结果可视化。
这两层通过 PyO3 进行零开销绑定。当你在 Python 中调用 self.buy() 时,实际上直接触发了 Rust 层的函数调用,没有任何中间序列化成本。
4.2.2 系统组件图¶
graph TD
subgraph "Python Layer (Strategy & Data)"
UserStrategy["用户策略 Strategy"]
PyData["Pandas DataFrame"]
end
subgraph "Rust Layer (High Performance Core)"
Engine["Engine (引擎核心)"]
DataFeed["DataFeed (数据源)"]
Portfolio["Portfolio (账户状态)"]
OrderBook["OrderBook (订单簿)"]
RiskManager["RiskManager (风控)"]
end
PyData -->|转换 & 加载| DataFeed
DataFeed -->|Bar/Tick Event| Engine
Engine -->|Context Update| UserStrategy
UserStrategy -->|Order Request| Engine
Engine -->|Order Check| RiskManager
RiskManager -->|Approved| OrderBook
OrderBook -->|Fill Event| Portfolio
Portfolio -->|Update| UserStrategy
4.2.3 关键组件详解¶
1. Engine (引擎)¶
引擎是系统的调度中心 (Dispatcher)。它维护着全局时钟 (Global Clock) 和事件优先队列 (Priority Queue)。在 akquant 中,Engine 是一个 Rust 结构体,它完全接管了 Python 的控制流。
- 职责:推进时间、分发事件、触发回调。
- 特性:单线程极速循环,避免了 Python GIL 的锁竞争。
2. DataFeed (数据源)¶
负责按时间顺序向引擎“滴灌”行情数据。
- 实现:在 Rust 中,
DataFeed内部维护了一个时间排序的 B-Tree 或 Vec,确保数据严格按时间戳推送。 - 多标的同步:当回测多个标的时,DataFeed 会自动对齐时间,确保
on_bar接收到的数据在时间轴上是同步的。
3. StrategyContext (策略上下文)¶
这是 Python 策略与 Rust 引擎通信的桥梁。
- 数据共享:
StrategyContext在 Rust 中持有对 Portfolio 和 Orders 的引用,并通过 PyO3 暴露给 Python。 - 零拷贝访问:当你访问
self.ctx.positions时,你实际上是直接读取 Rust 的内存,没有数据复制。
4.3 配置系统详解 (The Configuration System)¶
akquant 拥有一个结构化的配置系统,旨在清晰地定义回测的“何时 (When)”、“何地 (Where)”、“何物 (What)”以及“如何 (How)”。
4.3.1 四大配置支柱¶
-
BacktestConfig (回测配置):定义宏观场景。
- When:
start_time,end_time - What:
instruments(交易标的列表) - How:
strategy_config(策略配置)
- When:
-
InstrumentConfig (标的配置):定义资产属性。
- What:
symbol,multiplier(合约乘数),margin_ratio(保证金率) - Cost:
commission_rate,slippage(标的特有滑点)
- What:
-
StrategyConfig (策略配置):定义账户与执行。
- Capital:
initial_cash - Execution:
slippage(全局滑点),volume_limit_pct(成交量限制) - Risk:
risk(风控配置)
- Capital:
-
RiskConfig (风控配置):定义安全边界。
- Constraints:
max_position_pct,max_drawdown
- Constraints:
4.3.2 配置示例¶
from akquant.config import BacktestConfig, StrategyConfig, RiskConfig, InstrumentConfig
# 1. 定义风控
risk = RiskConfig(max_position_pct=0.1, stop_loss_threshold=0.8)
# 2. 定义策略账户
strategy_conf = StrategyConfig(
initial_cash=1_000_000,
slippage=0.0002, # 万2滑点
risk=risk
)
# 3. 定义特殊标的 (如期货)
rb_conf = InstrumentConfig(symbol="RB2305", multiplier=10, margin_ratio=0.1)
# 4. 组装回测配置
config = BacktestConfig(
start_time="2023-01-01",
end_time="2023-12-31",
strategy_config=strategy_conf,
instruments=["AAPL"],
instruments_config={"RB2305": rb_conf}
)
4.4 高级特性:状态快照与热启动 (State Snapshot & Warm Start)¶
作为事件驱动架构的一大优势,AKQuant 能够随时暂停并保存整个引擎的状态(Memory Dump),并在之后完全恢复。这种能力被称为热启动 (Warm Start)。
4.4.1 原理¶
由于 AKQuant 的核心状态(持仓、订单、资金)都由 Rust 的 Engine 结构体集中管理,我们可以利用 Python 的 pickle 协议将这个结构体序列化到磁盘。
当恢复时,我们反序列化 Engine,并重新连接数据源(DataFeed),使其无缝继续运行。
4.4.2 应用场景¶
- 超长周期回测:可以将 10 年的回测拆分为 10 个 1 年的片段,并行运行或顺序运行。
- 滚动训练 (Rolling Walk-Forward):训练模型 -> 运行回测 -> 保存状态 -> 使用新数据微调模型 -> 恢复回测。
- 模拟实盘:每天收盘后保存状态,第二天开盘前加载状态并接入实时行情。
详细使用方法请参考 高级指南:热启动。
4.5 撮合引擎揭秘 (Matching Engine Internals)¶
回测引擎的核心在于撮合 (Matching):如何根据历史行情判断你的订单是否成交,以及以什么价格成交。
4.5.1 基于 Bar 的撮合逻辑¶
在没有 Tick 数据的情况下,我们通常使用 OHLCV 数据进行近似撮合。假设当前 Bar 的数据为 \((O, H, L, C, V)\)。
-
市价单 (Market Order):
- 买入:以 \(Open\) 价(或 \(Close\) 价,取决于策略是在开盘还是收盘下单)成交。
- 滑点:通常在成交价基础上增加 \(N\) 个跳 (Tick Size)。
-
限价单 (Limit Order):假设买入限价为 \(P_{limit}\)。
- 完全成交:如果 \(Low < P_{limit}\),说明盘中价格跌破了限价,订单必然成交。
- 无法成交:如果 \(Low > P_{limit}\),说明盘中最低价都高于限价,订单无法成交。
- 部分成交:如果 \(Low = P_{limit}\),情况比较复杂。通常保守起见,假设只有部分成交或不成交。
-
止损单 (Stop Order):假设卖出止损价为 \(P_{stop}\)。
- 触发:如果 \(Low < P_{stop}\),止损被触发,转为市价单卖出。
- 成交价:通常取 \(P_{stop}\) 或 \(Low\) 中的较差者(模拟跳空低开的情况)。
4.5.2 涨跌停处理¶
在 A 股市场,涨跌停板会锁死流动性。
- 涨停 (Limit Up):\(High = LimitUp\)。此时买入市价单无法成交,买入限价单也无法成交(除非排队在前)。
- 跌停 (Limit Down):\(Low = LimitDown\)。此时卖出订单无法成交。
akquant 引擎在撮合时会严格检查涨跌停状态,避免在涨停板上买入或跌停板上卖出。
4.6 滑点与冲击成本模型 (Slippage & Impact Models)¶
真实交易中,你的买入行为会推高价格,卖出行为会压低价格。这种冲击成本 (Market Impact) 是大资金回测必须考虑的。
4.6.1 线性滑点模型¶
最简单的模型,假设滑点与交易量无关。
其中 \(\text{Slippage}\) 可以是固定值(如 0.01 元)或百分比(如 0.1%)。
4.6.2 平方根法则 (Square Root Law)¶
这是学术界和业界公认的冲击成本模型,由 Barra 提出。
其中:
- \(\sigma\):资产的波动率。
- \(Q\):你的交易量。
- \(V\):市场的总成交量。
这表明:冲击成本与交易量的平方根成正比。如果你想把交易量翻倍,冲击成本只会增加 \(\sqrt{2} \approx 1.414\) 倍,而不是 2 倍。这为大资金拆单提供了理论依据。
4.7 事件循环伪代码 (Event Loop Pseudo-code)¶
为了更清晰地理解 akquant 的运行机制,我们可以用伪代码描述其主循环:
def run_backtest():
engine = Engine()
strategy = UserStrategy()
data_feed = DataFeed(start_date, end_date)
while not data_feed.is_finished():
event = data_feed.next()
engine.current_time = event.datetime
engine.match_orders(event)
strategy.on_bar(event)
engine.emit_stream_event(event_type="bar", payload={"symbol": event.symbol})
engine.generate_report()
这个循环确保了时间流逝的单向性,杜绝了未来函数。
4.7.1 run_backtest(..., on_event=...) 统一事件流¶
除策略回调外,run_backtest 还支持 on_event 参数,用于接收统一事件流(如 bar、order、trade、risk、progress、equity),常用于监控台、告警与审计落盘。
import akquant as aq
events = []
result = aq.run_backtest(
data=data_feed,
strategy=MyStrategy,
symbols="AAPL",
on_event=events.append,
stream_progress_interval=1,
stream_equity_interval=1,
)
这个入口与策略类风格、函数式风格兼容,可以把策略逻辑与可观测性解耦。
4.8 风控引擎 (Risk Engine)¶
在真实的交易系统中,风控 (Risk Management) 是最后一道防线。akquant 引擎内置了一个强大的预交易风控模块 (RiskManager),它独立于策略逻辑之外,直接在引擎层面拦截不合规的订单。
4.8.1 为什么要预交易风控?¶
- 胖手指 (Fat Finger):手抖多敲了一个零,导致下单数量巨大。
- 逻辑 Bug:策略代码写错了,导致无限循环下单或满仓梭哈。
- 合规要求:某些基金有严格的行业集中度或杠杆限制。
4.8.2 内置风控规则¶
akquant 的风控能力通常通过 RiskConfig 与策略级参数映射启用,常见约束包括:
- 单笔限制:
max_order_size/max_order_value。 - 持仓限制:
max_position_size/max_position_pct。 - 标的限制:
restricted_list/sector_concentration。 - 账户级限制:
max_account_drawdown/max_daily_loss/stop_loss_threshold。
4.8.3 配置示例¶
推荐在回测配置阶段声明风控参数:
import akquant as aq
risk = aq.RiskConfig(
max_order_size=5_000,
max_order_value=200_000,
max_position_pct=0.2,
restricted_list=["ST0001"],
max_daily_loss=0.05,
)
config = aq.BacktestConfig(
strategy_config=aq.StrategyConfig(
initial_cash=1_000_000,
risk=risk,
)
)
result = aq.run_backtest(
strategy=MyStrategy,
data=data_feed,
symbols="AAPL",
config=config,
)
当策略发出的订单违反上述规则时,Engine 会拒绝该订单,并返回 Rejected 状态和具体的错误信息(如 Risk: Position value ratio 15.00% exceeds limit 10.00%)。
本章小结:
4.9 订单生命周期与状态机 (Order Lifecycle)¶
在事件驱动系统中,理解订单的状态流转至关重要。一个订单从产生到最终成交,会经历严格的状态机变换。
stateDiagram-v2
[*] --> New: 策略创建订单
New --> Submitted: 发送到交易所
Submitted --> Accepted: 交易所确认收到
Accepted --> PartiallyFilled: 部分成交
Accepted --> Filled: 全部成交
PartiallyFilled --> Filled: 剩余部分成交
New --> Rejected: 风控拒单
Submitted --> Cancelled: 策略撤单
Accepted --> Cancelled: 策略撤单
Filled --> [*]
Cancelled --> [*]
Rejected --> [*]
4.9.1 关键状态解析¶
-
New (新建):
- 策略调用
self.buy()后,订单对象被创建,但在风控检查通过前,状态为New。 - 此时订单尚未进入撮合队列。
- 策略调用
-
Submitted (已提交):
- 订单通过了客户端风控(如资金检查),已发送到交易所(或模拟撮合引擎)。
- 在实盘中,这代表网络请求已发出。
-
Accepted (已受理):
- 交易所确认收到订单。在
akquant的回测模式中,通常Submitted后立即转为Accepted(除非模拟了网络延迟)。
- 交易所确认收到订单。在
-
Filled (全部成交):
- 订单的所有数量都已成交。此时会触发
on_trade回调,并且持仓和资金会相应更新。
- 订单的所有数量都已成交。此时会触发
-
Rejected (已拒绝):
- 订单因某些原因被拒绝。常见原因:
- 资金不足 (Insufficient Margin): 可用资金不足以支付保证金或全额。
- 非法数量 (Invalid Quantity): 例如 A 股买入必须是 100 的整数倍。
- 废单: 价格超过涨跌停板。
- 订单因某些原因被拒绝。常见原因:
4.10 撮合引擎机制 (Matching Engine Mechanics)¶
akquant 的模拟撮合引擎 (SimulatedExecutionClient) 旨在尽可能逼真地模拟真实交易所的撮合逻辑。
4.10.1 撮合逻辑 (Matching Logic)¶
对于每一根新的 Bar (或 Tick),引擎会遍历所有活跃订单进行撮合:
-
市价单 (Market Order):
- 成交价: 取决于
fill_policy三轴(见下文)。 - 成交量: 尽可能全部成交,除非受限于当根 Bar 的成交量(Volume Limit)。
- 成交价: 取决于
-
限价单 (Limit Order):
- 买入单: 当
Low Price <= Limit Price时成交。- 价格优化: 如果
Open Price < Limit Price,则以Open Price成交(模拟开盘撮合)。
- 价格优化: 如果
- 卖出单: 当
High Price >= Limit Price时成交。 - 成交价: 也就是常说的“价格优先”。
- 买入单: 当
-
止损单 (Stop Order):
- 当市场价格突破触发价 (
Trigger Price) 时,止损单会转化为市价单或限价单。 akquant支持穿透检查 (Gap Detection):例如,昨日收盘 100,今日跳空低开 90,如果你有 95 的止损卖单,引擎会正确地在 90 成交(而不是 95),真实模拟跳空风险。
- 当市场价格突破触发价 (
4.10.2 三轴成交语义 (Three-Axis Fill Policy)¶
为了平衡回测的严谨性和灵活性,akquant 使用三轴成交策略:
| 维度 | 取值 | 描述 |
|---|---|---|
price_basis |
open / close / ohlc4 / hl2 |
使用哪种价格基准 |
bar_offset |
0 / 1 |
使用当前 Bar 还是下一根 Bar |
temporal |
same_cycle / next_event |
timer 订单在当前周期或下一事件撮合 |
4.10.3 成交时序策略 (Temporal Policy)¶
AKQuant 通过 fill_policy.temporal 控制 on_timer 下单的撮合时点:
| 时序策略 | 描述 | 典型场景 |
|---|---|---|
same_cycle (默认) |
在当前 timer 事件周期内撮合 | 定时调仓后立即成交的仿真 |
next_event |
延后到下一条行情事件撮合 | 更保守的“信号与成交分离”建模 |
推荐统一使用:
result = akquant.run_backtest(
data=data,
strategy=MyStrategy,
fill_policy={"price_basis": "close", "bar_offset": 0, "temporal": "next_event"},
)
4.10.4 滑点与冲击成本 (Slippage & Impact)¶
回测中最容易高估收益的因素是忽略了交易成本。akquant 支持配置滑点模型:
- 买入: 价格向上滑动(买得更贵)。
- 卖出: 价格向下滑动(卖得更便宜)。
此外,你还可以设置 Volume Limit(例如 10%),限制策略在单根 Bar 上的成交量不超过市场总成交量的 10%,以模拟流动性限制。
4.10.5 配置分层与覆盖优先级¶
akquant 在成交相关参数上采用统一的四层覆盖模型(从高到低):
- 订单级:
buy/sell/submit_order传入fill_policy/slippage/commission。 - 策略映射级:
strategy_fill_policy/strategy_slippage/strategy_commission(按strategy_id/slot)。 - 运行级:
run_backtest(...)全局参数(例如fill_policy/slippage)。 - 市场默认:market model 的内建默认规则(费率、制度等)。
实务建议:
* 单策略场景可先用运行级,局部例外再用订单级覆盖。
* 多策略槽位场景优先使用 strategy_*,避免不同策略互相覆盖全局参数。
4.11 资金与风控管理 (Portfolio & Risk)¶
4.11.1 资金校验 (Pre-trade Check)¶
在订单提交前,Rust 层的 RiskManager 会进行严格的资金校验:
- 计算成本:
- 股票:
Price * Quantity - 期货:
Price * Quantity * Multiplier * MarginRatio
- 股票:
- 计算费用: 预估佣金、印花税等。
- 比较:
Total Cost > Free Cash?- 如果资金不足,订单会被自动拒绝 (Rejected),或者根据配置自动缩减数量 (Auto-resize) 以适应剩余资金。
4.11.2 T+1 制度模拟¶
对于 A 股市场,akquant 内置了 T+1 规则支持:
- 可用持仓 (Available Position): 当日买入的股票,在当日的
available_positions中为 0,只有到下一个交易日才会释放。 - 卖出检查: 卖出时检查
available_positions而非总持仓。
这意味着如果你在 T 日买入,尝试在 T 日卖出,订单会被拒绝,错误信息提示 "Insufficient available position"。
当前范围说明:
* t_plus_one 是运行级/市场级开关,不是按 strategy_id 的分层参数。
* 即使启用了多策略槽位,不同策略仍共享同一市场制度与可用持仓结算口径。
4.12 多标的与时间流 (Time Flow & Multi-Asset)¶
在回测多个标的(例如全市场选股)时,时间的同步至关重要。
akquant 的 DataFeed 实现了一个全局优先队列 (Global Priority Queue)。无论你加载了多少个 CSV 文件或 DataFrame,引擎都会将它们的数据打散并重新排序。
工作流程:
- 加载 AAPL 的数据,放入队列。
- 加载 MSFT 的数据,放入队列。
DataFeed.sort():对所有事件按时间戳进行全局排序。- Event Loop:引擎调用
feed.next(),总是返回时间戳最小的那个事件。
这意味着,即使 AAPL 和 MSFT 在同一分钟都有数据,引擎也会按顺序处理它们(虽然逻辑上是同一时刻),确保了多标的策略在任何时刻看到的都是“当时”的全局状态。
4.13 盈亏计算原理 (PnL Mathematics)¶
理解 akquant 的盈亏计算逻辑,对于分析策略表现至关重要。
4.13.1 浮动盈亏 (Unrealized PnL)¶
浮动盈亏反映了当前持仓的未结收益。
- Entry Price (入场均价):采用加权平均法计算。
4.13.2 平仓盈亏 (Realized PnL)¶
当平仓发生时,浮动盈亏转化为平仓盈亏。akquant 采用 FIFO (先进先出) 原则进行结算。
示例:
- 买入 100 股 @ 10 元。
- 买入 100 股 @ 12 元。
- 卖出 100 股 @ 15 元。
结算: 卖出的 100 股会优先匹配第一笔 10 元的买单。
剩余持仓:100 股 @ 12 元。
4.13.3 总权益 (Total Equity)¶
其中市值计算包含保证金占用(对于期货/期权)。
4.14 常见问题排查 (Troubleshooting)¶
如果你的订单没有成交,请检查以下清单:
- 价格未触发:
- 限价买单价格必须
>=Bar Low。 - 限价卖单价格必须
<=Bar High。
- 限价买单价格必须
- 资金/持仓不足:
- 检查日志中是否有
Order Rejected: Insufficient cash或Insufficient available position。 - A 股回测请注意 T+1 限制。
- 检查日志中是否有
- 成交量限制:
- 如果你设置了
volume_limit,而当根 Bar 成交量很小,订单可能只成交一部分或完全不成交。
- 如果你设置了
- 最小交易单位 (Lot Size):
- A 股买入数量必须是 100 的整数倍。
- 时间窗口:
- 确保数据覆盖了订单产生的时间段。
4.15 性能优化与内存管理¶
akquant 之所以快,除了 Rust 本身的高性能外,还做了大量内存优化:
- 避免 DataFrame 碎片化:历史数据在 Rust 中以连续内存块(Vector)存储,而不是 Python 的分散对象。
- 按需计算:指标(Indicator)通常是增量计算的(Streaming),而不是每次重算整个序列。
- 对象池 (Object Pooling):虽然目前主要通过栈分配优化,但设计上避免了频繁的大对象创建和销毁。
本章小结¶
- 回测范式上,事件驱动在真实交易仿真能力上显著优于纯向量化。
akquant的 Python/Rust 分层降低了策略开发门槛,同时保留高性能执行能力。- 理解订单状态与撮合规则,是保证回测可信度的核心前提。
课后练习¶
- 在主示例中修改滑点或手续费参数,比较收益与回撤变化。
- 记录三种回测范式的运行时长,并解释差异来源。
- 人工构造一笔“部分成交”场景,验证订单状态流转是否符合预期。