多周期策略:三条路径¶
AKQuant 目前有三条独立的多周期路径,覆盖不同场景,互不替代:
subscribe_bars(推荐):引擎原生,Rust 直接从基础 bar 聚合窗口,回测/实盘同一套代码。BarGenerator:策略内的 Python 运行时工具,逐 bar 手工喂入聚合器。feed.resample/feed.replay:离线数据编排,在喂给run_backtest之前就把多个频率的 feed 准备好。
怎么选¶
| 路径 | 适用 | 回测 | 实盘 | 高周期进历史与指标 | 日线时间戳口径 |
|---|---|---|---|---|---|
subscribe_bars(推荐) |
策略内声明式订阅多周期 | 支持 | 支持 | 进 get_history(freq=) / self.I(freq=) |
当日最后一根基础 bar |
BarGenerator |
策略内手工聚合,不想动引擎配置 | 支持 | 支持 | 不进历史/指标,只有聚合后回调收到的那一根 | 次日零点(pandas resample 口径) |
feed.resample / feed.replay |
离线数据编排,喂给 run_backtest 之前就要多频率 feed |
仅回测 | 不支持 | 需要伪标的拼接,用户自己打时间戳 | 用户自定 |
subscribe_bars 用法¶
在策略 __init__ 里声明订阅(只能在 __init__ 里调用,引擎启动后订阅表即冻结):
from akquant import Bar, Strategy
class FiveMinuteTrend(Strategy):
"""按 5 分钟窗口打印近 3 根收盘价, 用来展示 subscribe_bars 的实盘闭合时机."""
def __init__(self) -> None:
super().__init__()
self.subscribe_bars(
"5min", session_windows=[("09:30", "11:30"), ("13:00", "15:00")]
)
self.count = 0
def on_start(self) -> None:
self.set_history_depth(50)
print(f"基础周期 self.freq = {self.freq!r}")
def on_window_bar(self, bar: Bar) -> None:
self.count += 1
closes = self.get_history(3, bar.symbol, "close") # 回调内自动定档到 5min
print(
f"[5min] {self.format_time(bar.timestamp)} "
f"C={bar.close:.2f} 近3根={closes.round(2).tolist()}"
)
def on_stop(self) -> None:
print(f"共收到 {self.count} 根 5 分钟窗口 bar")
完整可运行示例见 examples/71_native_multi_timeframe_live.py(实盘/replay)与 examples/14_multi_frequency.py(回测)。
关键 API:
subscribe_bars(freq, callback=None, symbols=None, *, session_windows=None):声明订阅。freq支持整数分钟"Nmin"、整数小时"Nh"、日线"1d"。symbols=None表示全部标的。callback:省略时闭合的窗口 bar 会回调策略的on_window_bar(bar);传入具体函数(如示例 14 的on_daily)则改由该函数接收,on_window_bar不再触发该条订阅。on_window_bar(bar):默认窗口回调,bar.freq是该 bar 所属的周期标签(如"5min"、"1d")。current_window(symbol, freq):查看某标的当前正在形成、尚未闭合的窗口快照(只能在行情回调内调用),不会触发回调。get_history(count, symbol, field, freq=)/get_history_multi(...)/self.I(..., freq=):freq传已订阅的窗口周期标签,取的是该周期的历史序列;在窗口回调内可省略freq(自动按当前回调定档)。
闭合规则:即时 vs 延迟¶
窗口何时闭合取决于基础数据周期(self.freq)是否已知:
- 基础周期已知(回测传
run_backtest(freq=...),或实盘/replay 行情网关声明metadata["freq"],见示例 71):窗口在归属它的最后一根基础 bar 的on_bar之后同一步闭合并派发(零延迟)。 - 基础周期未知(例如回测直接传一个纯 bar 的
DataFrame,没有走freq=或list[Tick]输入):引擎无法判断"下一根基础 bar 是否已经跨入新窗口",因此窗口要等下一根落入新窗口的基础 bar 到达时才闭合(晚一根),并在配置阶段打印一次性WARNING。示例 14 就是这种情况:日线窗口在次日第一根 09:31 分钟 bar 到达后才闭合。session_windows只负责正确切分交易时段边界,不能让窗口提前到即时闭合——即时闭合只看基础周期是否已知。
限制¶
freq仅支持整数分钟("Nmin")、整数小时("Nh")与日线("1d"),不支持秒级、周线、月线;这些场景请改用feed.resample。- 窗口周期必须严格大于基础数据周期,否则
subscribe_bars在引擎配置阶段报ValueError。 - 同一周期、标的范围有重叠的多条订阅,
session_windows必须完全一致,否则报ValueError(同周期+同session_windows,一条全标的通用回调 + 一条特定标的专属回调是允许的,两个回调都会触发)。 extra字段不参与窗口聚合。- 窗口 bar 不会被撮合引擎用于成交判断——它只是信息,策略仍需在
on_bar(或其他基础行情回调)里正常下单。 - CTP 网关合成 bar 默认打的是区间起点戳;只有同时开启
emit_ticks与emit_bars才会得到区间结束戳。若不同时开启,多周期聚合会因为时间戳口径不一致而错位一根,因此接入 CTP 做多周期时请同时打开emit_ticks与emit_bars。 - 会话结束(回测结束、实盘/replay 会话终止)时,引擎会 flush 尚未闭合的尾部窗口并触发一次回调;此时下的单在回测里不会再有机会成交。若之后
save_checkpoint()再从该快照run_from_checkpoint()续跑,尾部区间会重新形成一根同标签的窗口 bar(历史序列里会出现两根标签相同的 bar,这是已知且刻意保留的行为,不是 bug)。
从伪标的迁移¶
引擎原生多周期上线前,examples/14_multi_frequency.py 靠「伪标的」拼日线:把同一份分钟数据手工 resample 成日线,起一个 000001.SZ_1D 的假代码喂给引擎,策略里再按 bar.symbol 分流处理两条序列,还需要手工把时间戳往后打 15 小时来避免与分钟数据的时间戳冲突。现在改为:
self.subscribe_bars(
"1d",
callback=self.on_daily,
session_windows=[("09:30", "11:30"), ("13:00", "15:00")],
)
self.daily_sma = self.I(
aq.SMA(self.ma_window), name="daily_sma", source="close", freq="1d"
)
不再需要伪标的、不再需要手工 resample/打戳,日线窗口由 Rust 引擎直接从基础 bar 聚合,通过 on_daily 回调派发。完整前后对比见示例 14 的 git 历史与模块 docstring。
离线数据编排:feed.resample / feed.replay¶
feed.resample/feed.replay 是离线方案:在回测数据层面预先构造好多个
频率的 feed,再喂给 run_backtest(data=...),本质是回测专属的多 feed 编排,
不支持实盘。适合"喂给引擎之前就要把多频率数据准备好"的场景。
API¶
resampled = feed.resample(
freq="15min",
agg={"open": "first", "high": "max", "low": "min", "close": "last", "volume": "sum"},
label="right",
closed="right",
)
replayed = feed.replay(
freq="1h",
align="session",
day_mode="trading",
emit_partial=False,
session_windows=[("09:30", "11:30"), ("13:00", "15:00")],
)
当前落地(最小可用)¶
BasePandasFeedAdapter已支持:resample(freq, agg=None, label="right", closed="right", emit_partial=True)replay(freq, align="session", emit_partial=False, agg=None, label="right", closed="right")- 两者均返回可直接传入
run_backtest(data=...)的适配器对象。 - 当前
replay(align="session")已支持按交易日分区聚合,emit_partial=False时按日丢弃尾部未闭合窗口。 - 可通过
session_windows进一步按日内交易时段分区(例如午休前后分段),减少跨时段混合聚合。 align当前支持三种语义:session:按交易日分区,可叠加session_windows。day:按日分区,不接收session_windows,并支持day_mode。global:按全局时间轴聚合,不按交易日切段。day_mode当前支持:trading:按请求时区下的本地交易日切分。calendar:按 UTC 自然日切分。
语义锁定¶
resample¶
- 输入:原始
Bar/Tick流。 - 输出:按目标频率聚合后的 Bar 流。
- 默认聚合:
open=firsthigh=maxlow=minclose=lastvolume=sum- 边界:默认
label=right, closed=right。
replay¶
- 输入:高频数据。
- 输出:按低频时钟重放的事件流。
align=session:按交易时段边界对齐。emit_partial=False:未闭合窗口不发出。
事件对齐策略¶
- 时区统一:内部统一 UTC,展示层可转本地时区。
- 会话优先:跨日、午休等边界以 market session 为准。
- 缺口策略:
- 价格列沿用“无成交不补值”。
- volume 默认置 0。
一致性校验¶
- 与 pandas
resample在同参数下做结果比对。 - 对齐误差允许仅限浮点精度范围。
- 回测与实时流使用同一聚合实现。
运行时聚合(BarGenerator)¶
BarGenerator 是运行时方案:聚合逻辑内嵌在策略里,逐 bar 流式喂入,不
依赖任何离线数据编排。回测与实盘用的是同一句 update_bar 调用——策略
代码本身不需要区分当前是在回测还是实盘运行。两者在相同参数下遵循同一套
时钟对齐语义(label="right", closed="right",与 pandas
resample(label="right", closed="right") 一致),因此同参数产出结果一致。
与 subscribe_bars 的关键差异:BarGenerator 聚合出的 bar 不会进入
get_history/增量指标体系,只有传给回调函数的那一根可用;日线的时间戳口径
也不同(BarGenerator 用 pandas 的次日零点,subscribe_bars 用当日最后一根
基础 bar)。
最小用法:
from akquant import BarGenerator, Strategy
class MyStrategy(Strategy):
def __init__(self):
super().__init__()
# 把 1 分钟 bar 聚合为 5 分钟 bar,窗口闭合时回调 on_5m
self.bg = BarGenerator(self.on_5m, 5, "minute")
def on_bar(self, bar):
# 回测/实盘同一句调用
self.bg.update_bar(bar)
def on_5m(self, bar):
# 收到聚合后的 5 分钟 bar,写自己的信号逻辑
...
def on_stop(self):
# 收尾时强制闭合尾部未满窗口,否则最后一根不完整窗口会被丢弃
self.bg.flush()
window/interval组合出目标周期(如BarGenerator(cb, 15, "minute")、BarGenerator(cb, 1, "hour")、BarGenerator(cb, 1, "day"))。session_windows用于按交易时段分段聚合,避免跨午休等段拼接出脏 bar,例如session_windows=[("09:30", "11:30"), ("13:00", "15:00")];不传则按纯时钟 对齐(跨会话边界不做特殊处理)。timezone指定时钟/session 对齐所在的市场时区(如"Asia/Shanghai")。current(symbol)可查看某标的当前正在形成、尚未闭合的窗口快照,不会触发 回调,适合盘中展示。- 多标的相互独立聚合,互不影响。
完整可运行示例见 examples/65_bar_generator.py。