文档1.0 (latest)
页面策略生命周期 · 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_calendarBacktestCalendar | 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含义
0K 线开盘价
1K 线最高价
2K 线最低价
3K 线收盘价

回测中没有真实的 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 之间以固定的时间间隔运行,但仅在满足两个条件时:

  1. 你为本次运行配置了 timer_interval_ms / timerIntervalMs(以毫秒为单位的正整数)。
  2. 你的文件定义了 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 对话框没有主标的控件。解析顺序:

  1. 你的策略文件中顶层的 SYMBOL = "AAPL",如果该代码在已加载的列表中。
  2. 否则是第一个已加载的标的。

使用 Screener 标的来源时根本没有时钟标的——标的集合在运行过程中会变化。rt.get_bar 始终读取时钟标的的 K 线(在 Screener 运行中返回 None),因此要获取另一个代码的历史,请使用该代码自己的 bar.close[i] 偏移。

时钟标的也是 on_finish(bars, symbol=...) 用于其 bars 和 symbol 参数的那个标的。

按标的的回调

对每根执行 K 线,引擎按列表顺序遍历各标的:

  1. on_bar 在这个 K 线时间对每个已加载的代码运行一次(在该 K 线的任何 tick 之前)。
  2. 仅限回测:对每个 tick_in_bar(0 = 开盘、1 = 最高、2 = 最低、3 = 收盘),on_tick 在合成 OHLC 上对每个代码运行一次(没有真实的 tick 数据)。
  3. 仅限回测:四轮 tick 之后,最终的账户 tick(close_map) 以收盘价对权益进行估值——这不是第五次 on_tick。
  4. 前向运行:当报价从你已连接的券商到达时,on_tick 针对每个标的在每次券商价格更新时运行(不限于每根 K 线四次调用)。日内边界处经常是先触发 on_tick,再触发 on_bar——不要假定回测的顺序。
  5. 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(): ...

综合运用

一个典型的单标的策略:

  1. 在 on_init 中设置输入参数或其他状态。
  2. 在 on_bar 中通过 bar.close[0]、bar.close[1]、… 读取价格历史(OHLCV)来做交易决策,并调用 account.open_trade / account.close_trade。
  3. 可选地使用 on_tick——在回测中用于合成的 K 线内成交(例如用 tick_in_bar == 3 表示收盘 tick);在前向运行中用于对 K 线收盘之间的实时券商价格做出反应。
  4. 可选地在配置了 timerIntervalMs 的前向运行中使用 on_timer(IDE 的 Run backtest 中不可用)。
  5. 在 on_finish 中使用 bars 构建图表指标并最终确定结果。

多标的策略使用相同的回调,只是在其中根据 symbol 参数分支,并通过它接收到的 bar 读取各代码的数据——在 on_finish 中则通过 bars_by_symbol。

需要更多细节——参见账户模型、基本面数据、Fama-French 因子、OHLCV 与 K 线数据、指标序列,或右侧边栏中应用内的 Documentation 标签页(工作区布局)。

阅读约 6 分钟建议修改 ↗
策略生命周期 — 文档 (1.0 (latest)) | QuantCraft