页面策略生命周期 · 8 / 29
策略生命周期
QuantCraft 策略是一个普通的 Python 文件,定义了一小组回调函数。回测引擎在遍历历史数据时,按固定顺序调用这些函数。你不需要自己调用它们——由引擎调用。使用 Test 来驱动它们(运行代码)。
从开始到结束的生命周期:
on_init → on_bar → on_tick (×4 per bar) → account tick(close) → on_finish
└─────────── repeats for every bar ───────────┘上图是回测中的流程。on_tick 在每根 K 线上基于其合成 OHLC 触发四次(没有真实的 tick 历史)。在这四个 tick 之后,引擎会在收盘时对账户做最后一次按市值计价——这不是第五次 on_tick。on_timer 仅在设置了 timerIntervalMs 的前向运行(图表前向、批量前向/QuantCloud)中可用——IDE 的 Run backtest 对话框没有定时器控件。在前向运行中,on_tick 会在已连接券商的每次价格更新时运行;日内边界处经常是先触发 on_tick,再触发 on_bar——不要假定回测的顺序。
本页涵盖每个回调、多标的策略如何接入它们,以及编写你的第一个策略所需的基础知识。
策略文件
一个最小的策略如下所示:
import quantcraft.process.runtime as bt # account, chart_indicators, qc_* handles
from quantcraft import Timeframe
import quantcraft.runtime as rt
def on_init(symbols=None):
pass
def on_bar(bar_index, bar, fundamentals=None, calendar=None, symbol=None):
close_px = float(bar["close"])
daily = rt.get_bar(Timeframe.DAILY, shift=1)
def on_tick(bar_index, tick_in_bar, price, bar, fundamentals=None, calendar=None, symbol=None):
if tick_in_bar == 3:
close_px = price
def on_finish(bars, symbol=None, bars_by_symbol=None):
pass有几条规则适用于所有回调:
- 它们必须是文件中的顶层函数(不能嵌套,也不能放在类里)。
- 回测引擎要求
on_init、on_bar、on_tick和on_finish是可调用的(否则运行会以User script must define callable: …失败)。当活动的.py标签页定义了on_init/on_bar/on_finish时,IDE 的 Test、Compile 和 Debug Test 按钮会出现;缺少on_tick仍会在回测启动时失败。on_timer是可选的,仅在带有定时器间隔的前向运行中使用。 - 只添加你用到的可选参数。
calendar、symbols(在on_init上)以及on_finish的参数是按名称匹配的;fundamentals和symbol则是依次尝试常见形式来匹配的,因此请保持文档规定的顺序。最稳妥的完整签名是def on_bar(bar_index, bar, fundamentals=None, calendar=None, symbol=None)。已声明的calendar始终接收日历句柄或None——绝不会是基本面数据对象。 - 在回调内部,你通过
account下单,并通过chart_indicators(指标序列)记录指标值——两者都从quantcraft.process.runtime读取。
运行时全局变量
引擎在回调运行之前,从 quantcraft.process.runtime 暴露这些内容:
| 名称 | 用途 |
|---|---|
account | 根据 Simulation 标签页(余额 + 佣金/滑点)配置的 PaperAccount 实例。在前向运行中,它是具有相同 API、会下真实订单的实盘券商账户——参见账户模型 |
chart_indicators | 合并到结果中的 ChartIndicatorSeries 收集器——在运行遍历 K 线时收集数值,在 on_finish 中输出序列 |
qc_fundamentals | 加载了基本面数据时可选的 QcFundamentals 句柄 |
qc_fama_french | 加载了全球 Fama-French 因子时可选的 QcFamaFrench;回调中请优先使用 fama_french.qc——参见 Fama-French 因子 |
qc_calendar | BacktestCalendar | None——启用 Include calendar data 时的日历数据;覆盖每个被交易的标的。仅限回测——前向运行中始终为 None。回调也可以通过命名一个 calendar 参数来接收同一个句柄——参见日历数据 |
qc_ohlcv_by_tf | 以时间周期字符串为键的可选副时间周期缓冲区(高级用法;优先使用 get_bar) |
get_bar(tf, shift=1) | 用于主时间周期和附加时间周期的推荐 API——返回 BarSnapshot 或 None;通过 import quantcraft.runtime as rt 导入 |
完整序列的计算请优先使用 on_finish(bars, ...) 的 bars 参数——不要在 IDE 策略中依赖模块级的 qc_ohlcv(它在导入时通常是 None,在以筛选器为目标的运行中是 None,其他情况下涵盖包含预热的完整加载范围)。在回调中请从 bar 对象读取价格。isinstance(bar, dict) 为 False——OhlcvBarView 是一个 Mapping(类字典访问仍然可用)。
请将这些作为模块属性来读取。每个句柄都由按会话的值支撑,所以
bt.account/bt.chart_indicators始终指向本次运行。from quantcraft.process.runtime import account绑定的是快照——在回测的根算法文件中没问题,因为引擎在加载它之后会重新绑定这些名称(前向运行只重新绑定account、qc_fundamentals和qc_fama_french,因此在那里请使用bt.chart_indicators和rt.qc_ohlcv*),但在拆分的辅助模块中则是错误的,该模块会一直缓存在sys.modules中,并保留第一个会话的句柄。参见模块句柄与导入名称。
副时间周期
运行的主时间周期始终可通过 rt.get_bar 使用。在 Run backtest 对话框 Symbol & data 的 Additional timeframes 下拉框中选择额外的 K 线周期(回测),然后在任何回调中读取:
from quantcraft import Timeframe
import quantcraft.runtime as rt
def on_bar(bar_index, bar, fundamentals=None, calendar=None, symbol=None):
daily = rt.get_bar(Timeframe.DAILY, shift=1)
if daily and bar.close[0] > daily.close:
account.open_trade(...)完整的 shift 语义、Timeframe 枚举以及前向运行的注意事项,请参见 OHLCV 与 K 线数据。
成交时机
on_bar中的bar["close"]是当前执行 K 线的收盘价。account.open_trade/close_trade接收一个请求价格,然后应用本次运行的滑点和佣金模型。存储的entry_price/exit_price是成交价;请求价格保存在raw_entry_price/raw_exit_price中。使用默认的成交量占比滑点时,成交价通常与你传入的价格不同。- 要在
on_bar中瞄准K 线收盘价,请传入float(bar["close"])(仍受滑点/佣金影响)。 - 要在 K 线内模拟期间成交,请在
tick_in_bar == 3时从on_tick调用——price就是该标的在这一行的收盘价。
on_init——做好准备
在引擎准备好账户、加载市场数据并构建运行时之后立即运行——但在处理任何 K 线之前。
用它来:
- 初始化策略在 K 线之间携带的变量(计数器、状态机、参数值)。
- 配置指标或预热缓冲区。
- 读取输入参数,让它们驱动运行的其余部分。
def on_init(symbols=None):
global lookback, position_size
lookback = 20
position_size = 100在 on_init 中你无法访问当前 K 线——此时模拟尚未开始。
symbols 参数(仅限筛选器运行):当回测使用筛选器标的来源时,每次再平衡 on_init 都会以新范围的 symbols=<代码列表> 重新触发。这让你的策略无需重启模拟就能对一组新的代码做出反应。
- 运行开始时,
on_init会在短时间内触发两次——一次带symbols=None(标准的初始化调用),一次带symbols=<初始代码>。 - 重新触发不会重置模块级状态;你在第一次调用中设置的任何变量都还在。
- 定义了
def on_init():(无参数)的策略与以前完全一样地工作——引擎按名称匹配参数,所以缺失的参数会被直接忽略。
on_bar——每根 K 线一次
对每根执行 K 线运行一次,在该 K 线的四个 K 线内 tick 之前。大多数策略在这里做决策。
def on_bar(bar_index, bar, fundamentals=None, calendar=None, symbol=None):
if bar.bars_back < 1:
return
if bar.close[0] > bar.close[1]:
account.open_trade(...)参数:
bar_index——当前执行 K 线的从零开始的索引。bar——当前 K 线视图(OhlcvBarView)。行为类似只读映射(bar["open"]、bar["high"]、bar["low"]、bar["close"]、bar["volume"]、bar["t"]),并且还通过偏移暴露历史:bar.close[0]是当前 K 线的收盘价,bar.close[1]是前一根 K 线,依此类推。len(bar.close)和bar.bars_back告诉你有多少历史可用。它不是普通的dict(isinstance(bar, dict)为False)。fundamentals——该 K 线的基本面数据快照,未配置则为None。calendar——启用 Include calendar data 时为BacktestCalendar句柄,否则为None(前向运行中始终为None)。完整 API 请参见日历数据。由于参数按名称匹配,省略该参数不会改变你的回调。symbol——大写的代码字符串。在多标的策略中很有用;如果只加载一个标的,可以省略。
常见模式:
- 在 K 线收盘时决定入场/出场:把
bar.close[0](或float(bar["close"]))传给account.open_trade/account.close_trade。 - 通过读取
bar.close[0]、bar.close[1]、… 等偏移来计算简单指标。 - 跳过没有足够历史的早期 K 线(
if bar.bars_back < N: return)。
提示:
on_bar始终在同一根 K 线的任何 tick 回调之前运行。
on_tick——K 线内及实时价格更新
on_tick 在回测和前向运行中签名相同,但它何时触发以及 price 的含义取决于运行模式。
回测(Test)
在 on_bar 之后,每根 K 线最多运行四次,以模拟 K 线内部的走势。引擎按以下顺序遍历每根 K 线的 OHLC:
tick_in_bar | 含义 |
|---|---|
0 | K 线开盘价 |
1 | K 线最高价 |
2 | K 线最低价 |
3 | K 线收盘价 |
回测中没有真实的 tick 数据。历史运行不会加载券商的 tick 历史。每根 K 线的四次调用使用仅由每根 K 线 OHLC 推导出的合成价格。你可以在模拟中近似 K 线内的成交,但回测的
on_tick行为不会与实时前向 tick 一致。对于 K 线收盘时的决策,on_bar就够了——在模拟中,tick_in_bar == 3的on_tick等同于在 K 线收盘价上行动。
def on_tick(bar_index, tick_in_bar, price, bar, fundamentals=None, calendar=None, symbol=None):
if tick_in_bar == 3:
# Backtest simulation: closing tick fill
account.close_trade(position_id, price)当K 线内的时机在模拟中很重要时使用回测的 on_tick——例如在收盘 tick 成交、模拟 K 线内的止损,或评估是否触及 K 线的最高价或最低价。
前向运行(图表前向、批量前向/QuantCloud)
在前向运行中,只要你的算法已附加,平台每次从你已连接的券商收到价格,on_tick 就会运行。没有固定的四个 tick 的 OHLC 遍历——每次调用都反映一次实时报价。
def on_tick(bar_index, tick_in_bar, price, bar, fundamentals=None, calendar=None, symbol=None):
# Forward run: react to each broker price update
if price <= stop_level:
account.close_trade(position_id, price)price——该标的最新的券商报价。bar——用于历史的当前 K 线视图(与回测中形状相同)。tick_in_bar——可能仍为了签名兼容而存在;在前向运行中,请依据price分支,而不是合成的 OHLC 阶段。
将前向 on_tick 用于实时的 K 线内逻辑——止损、跟踪出场,或对 K 线收盘之间的标记价做出任何反应。
参数(两种模式)
bar_index——与on_bar相同。tick_in_bar——在回测中为0、1、2或3(合成的 OHLC 步骤)。在前向运行中,不要依赖它来处理 K 线阶段逻辑。price——回测中为合成的 OHLC 步骤;前向运行中为实时券商报价。bar——与on_bar相同的 K 线视图。fundamentals、calendar、symbol——含义与on_bar相同。
如果你在回测中只需要 K 线收盘时的决策,可以完全忽略
on_tick,把逻辑放在on_bar中。
on_timer——可选的周期性回调(仅限前向运行)
IDE 回测:Run backtest 对话框不会设置定时器间隔。即使你在策略文件中定义了
on_timer,它在 IDE 回测期间也不会被调用。对于回测,请把周期性或基于时间的逻辑放在on_bar或on_tick中。
前向运行(图表前向测试、批量前向/QuantCloud)可以在其运行对话框中设置 timerIntervalMs。配置之后,on_timer 会在 tick 之间以固定的时间间隔运行,但仅在满足两个条件时:
- 你为本次运行配置了
timer_interval_ms/timerIntervalMs(以毫秒为单位的正整数)。 - 你的文件定义了
on_timer函数。
def on_timer(bar_index, tick_in_bar, price, bar, fundamentals=None, calendar=None, symbol=None):
pass签名与 on_tick 的形状相同。将 on_timer 用于基于时间的行为(例如:每分钟刷新一次变化缓慢的信号,或安排与 K 线数量无关的检查)。如果未设置 timer_interval_ms,即使定义了 on_timer,也永远不会被调用。
回调中的日历数据
当 Symbol & data 标签页上启用了 Include calendar data 时,任何回调都可以通过添加 calendar 参数来接收 BacktestCalendar 句柄:
def on_bar(bar_index, bar, calendar=None, symbol=None):
if calendar is None: # always guard — not every run enables calendar data
return
days = calendar.days_to_earnings(bar["t"], symbol)
if days is not None and days <= 2:
return # avoid trading into an earnings print
divs = calendar.dividends(bar["t"], symbol)
if divs and (divs[-1].amount or 0) > 0.5: # amount can be None
...同一个句柄在 on_tick 和 on_timer 上也可用——但日历数据仅限回测,所以在前向运行中 calendar 始终为 None。所有查找都是时点式(point-in-time)——dividends(bar["t"], symbol) 只返回在该 K 线时已公开的股息,绝不会返回未来的。
主要方法:
| 方法 | 返回 |
|---|---|
dividends(bar_time, symbol=None) | 在该 K 线时已公开的股息(从旧到新) |
splits(bar_time, symbol=None) | 截至该 K 线已宣布的拆股(从旧到新) |
next_earnings(bar_time, symbol=None) | {date, basis, estimated, …} 或 None |
days_to_earnings(bar_time, symbol=None) | int 或 None |
has(symbol=None) | 该标的是否有日历数据 |
列表从旧到新排列——[-1] 是最近的事件。这与基本面数据相反,在基本面数据中索引 0 是最新的期间。
如果你更愿意从 quantcraft.process.runtime 读取,而不是通过回调参数,运行时全局变量 qc_calendar 持有同一个句柄。
有关事件字段模式、公司行动模拟细节和故障排查,请参见日历数据。
on_finish——收尾
只运行一次,在最后一根 K 线之后。用它来计算最终输出、构建图表指标或打印摘要。
def on_finish(bars, symbol=None, bars_by_symbol=None):
closes = [b["close"] for b in bars]
bt.chart_indicators.add(...)参数:
bars——时钟标的的 OHLC 字典按时间顺序的列表,从旧到新——即已执行的窗口,与 Test Results 图表所绘制的是同样的行。每个条目包含t、open、high、low、close、volume,以及可选的fundamentals。这是on_finish的标准完整历史序列。symbol——时钟标的的大写代码。- 在 Screener 运行中没有时钟标的:
bars为[],symbol为"",因此请使用bars_by_symbol。 bars_by_symbol——对于多标的运行,一个把每个代码映射到其按时间顺序的 K 线列表的字典(与执行时钟对齐)。对于单标的运行,可以忽略它。
常见用途:
- 把指标序列交给
chart_indicators.add(...),让它们出现在结果图表上。优先使用你在运行遍历 K 线时按标的累积的值;只对你没有逐根跟踪的指标,才根据bars重新计算——参见指标序列。 - 将最终摘要打印或记录到输出面板。
- 为结果载荷刷新投资组合指标。
**
on_finish中的每个return都是静默的。**运行仍会成功,Test Results 仍会渲染——提前返回只意味着你的序列或指标从未被附加,而且任何地方都不会报错。调试时在每个检查点打印到sys.stderr;消息会显示在输出面板中。
多标的基础
QuantCraft 策略可以针对一个标的或同时针对多个标的运行。生命周期保持不变——只有回调签名及其接收的数据略有变化。
配置多个标的
在 Run backtest 的 Symbol & data 标签页中,选择一个或多个大写代码。所有列出的标的会一起加载和模拟。
时钟标的
运行多个标的时,其中一个驱动时钟——它的 K 线时间戳决定回调何时运行。Run backtest 对话框没有主标的控件。解析顺序:
- 你的策略文件中顶层的
SYMBOL = "AAPL",如果该代码在已加载的列表中。 - 否则是第一个已加载的标的。
使用 Screener 标的来源时根本没有时钟标的——标的集合在运行过程中会变化。rt.get_bar 始终读取时钟标的的 K 线(在 Screener 运行中返回 None),因此要获取另一个代码的历史,请使用该代码自己的 bar.close[i] 偏移。
时钟标的也是 on_finish(bars, symbol=...) 用于其 bars 和 symbol 参数的那个标的。
按标的的回调
对每根执行 K 线,引擎按列表顺序遍历各标的:
on_bar在这个 K 线时间对每个已加载的代码运行一次(在该 K 线的任何 tick 之前)。- 仅限回测:对每个
tick_in_bar(0= 开盘、1= 最高、2= 最低、3= 收盘),on_tick在合成 OHLC 上对每个代码运行一次(没有真实的 tick 数据)。 - 仅限回测:四轮 tick 之后,最终的账户
tick(close_map)以收盘价对权益进行估值——这不是第五次on_tick。 - 前向运行:当报价从你已连接的券商到达时,
on_tick针对每个标的在每次券商价格更新时运行(不限于每根 K 线四次调用)。日内边界处经常是先触发on_tick,再触发on_bar——不要假定回测的顺序。 on_timer(仅在配置了timerIntervalMs的前向运行中——IDE 回测中没有)在 tick 之间遵循相同的按代码模式。
这意味着,如果你加载了三个标的,多标的 on_bar 例如每根 K 线会被调用三次。使用 symbol 参数来了解该次调用是针对哪个代码的:
def on_bar(bar_index, bar, fundamentals=None, calendar=None, symbol=None):
if symbol == "AAPL":
...
elif symbol == "MSFT":
...bar 视图始终属于该回调所调用的标的。on_tick 中的 price 也是如此。
As-of 对齐。对每个执行 K 线时间,每个代码获得其在该时间或之前的最新 K 线。在这个确切时间没有 K 线的代码(停牌、缺失的分钟、不同的交易日历)会再次收到其前一根 K 线。如果你按代码统计 K 线或信号,请通过记住该标的最后看到的 bar["t"] 来跳过重复:
_last_t = {}
def on_bar(bar_index, bar, fundamentals=None, calendar=None, symbol=None):
if _last_t.get(symbol) == bar["t"]:
return # same bar as last step for this ticker
_last_t[symbol] = bar["t"]
...多标的的 on_finish
on_finish 仍然只在最后运行一次。要使用所有标的的历史,请使用 bars_by_symbol:
def on_finish(bars, symbol=None, bars_by_symbol=None):
if bars_by_symbol:
for ticker, ticker_bars in bars_by_symbol.items():
...综合运用
一个典型的单标的策略:
- 在
on_init中设置输入参数或其他状态。 - 在
on_bar中通过bar.close[0]、bar.close[1]、… 读取价格历史(OHLCV)来做交易决策,并调用account.open_trade/account.close_trade。 - 可选地使用
on_tick——在回测中用于合成的 K 线内成交(例如用tick_in_bar == 3表示收盘 tick);在前向运行中用于对 K 线收盘之间的实时券商价格做出反应。 - 可选地在配置了
timerIntervalMs的前向运行中使用on_timer(IDE 的 Run backtest 中不可用)。 - 在
on_finish中使用bars构建图表指标并最终确定结果。
多标的策略使用相同的回调,只是在其中根据 symbol 参数分支,并通过它接收到的 bar 读取各代码的数据——在 on_finish 中则通过 bars_by_symbol。
需要更多细节——参见账户模型、基本面数据、Fama-French 因子、OHLCV 与 K 线数据、指标序列,或右侧边栏中应用内的 Documentation 标签页(工作区布局)。

