文档1.0 (latest)
页面指标序列 · 15 / 29

指标序列

当回测结束后,IDE 中的**测试结果视图会渲染你本次运行的图表。指标序列让你把自己计算的额外时间序列**——移动平均线、RSI、MACD 柱状图、自定义信号——附加到这些图表上,使它们显示在 OHLCV K 线旁边、自己的窗格中,或者在多标的运行中按标的分别显示。

QuantCraft 不提供内置指标。平台中没有 SMA、MACD 或 RSI 的 API。你需要自己实现计算(或使用你自己的库),构建一个 {"time": ..., "value": ...} 行的列表,然后交给运行时收集器。在 IDE 回测中,引擎在 runtime.chart_indicators 上持有一个 ChartIndicatorSeries,并将 chart.to_payload() 作为 chart_indicator_series 附加到结果中。该模块规范化时间并存储序列——它不会替你计算指标值。

序列始终从 on_finish 交付。数值最好在运行遍历 K 线时收集——参见快速开始。

IDE 会把每个条目转换成对应图表窗格上的折线或柱状图叠加层。


工作原理

  1. 你计算一个或多个指标序列(例如由收盘价计算 SMA,由你的公式计算 RSI),形式为 {"time": ..., "value": ...} 行的列表。
  2. 你添加每个序列到运行时收集器,并带上显示选项(哪个窗格、折线还是柱状图、可选的名称/颜色/标的)。
  3. 回测结束时,引擎将该集合合并到结果的 chart_indicator_series 下。
  4. Test Results 中的 IDE 图表按你选择的选项渲染每个序列。

把收集器作为模块属性读取,而不是作为导入的名称:

import quantcraft.process.runtime as rt ci = rt.chart_indicators # assigned by the engine before on_init

chart_indicators 是一个 ChartIndicatorSeries 实例。你对它 .add(...) 的任何内容(带有你构建的点)都会通过引擎的 to_payload() 附加到 Test Results。不要为 IDE 的 Test Results 构造私有的 ChartIndicatorSeries()——请使用运行时收集器。

为什么用 rt.chart_indicators 而不是 from ... import chart_indicators?运行时句柄是按会话的,因此 from ... import 绑定的是一个快照。在根算法文件中这没问题——引擎会在加载你的脚本后重新绑定该名称。在拆分的辅助模块中则不行:辅助模块会缓存在 sys.modules 中,因此该名称会保留首次导入时当前会话的收集器,这对同一进程中的下一次运行(前向/批量)是错误的。参见模块句柄与导入名称。


快速开始

推荐的模式是在策略计算值的地方记录该值——在 on_bar / on_tick 中——并且只用 on_finish 来交付收集到的序列。这样绘制的值恰好就是策略交易时所依据的值,使用引擎自己的执行时钟,无需重新计算或重新对齐。

import sys import quantcraft.process.runtime as rt PERIOD = 45 # symbol -> parallel lists, appended in lockstep so indices can never drift _bar_times: dict[str, list] = {} _sma: dict[str, list[float]] = {} def on_bar(bar_index, bar, fundamentals=None, symbol=None): sym = (symbol or "").upper() # `bar.close[i]` returns None past the start of the loaded series. closes = [bar.close[i] for i in range(PERIOD)] # One append per list, per bar — NaN while warming up, never a skip. value = float("nan") if None in closes else sum(closes) / PERIOD _bar_times.setdefault(sym, []).append(bar["t"]) # UTC bar time, verbatim _sma.setdefault(sym, []).append(value) ... # trade on `value` def on_finish(bars, symbol=None, bars_by_symbol=None): ci = rt.chart_indicators if ci is None: return for sym in sorted(_bar_times): times = _bar_times[sym] vals = _sma.get(sym, []) if len(times) != len(vals): # cheap guard against drift print(f"{sym}: {len(times)} times vs {len(vals)} values", file=sys.stderr) continue # NaN warmup rows and unparseable times are dropped by `add`. points = [{"time": t, "value": v} for t, v in zip(times, vals)] ci.add( points, pane="main", render="line", line_style="solid", id=f"sma_{PERIOD}_{sym}", name=f"SMA({PERIOD}) {sym}", color="#ff9800", symbol=sym, )

结果图表随后会把你的 SMA(45) 显示为 K 线图上的实线叠加层。

请从策略交易所依据的同一个常量(上面的 PERIOD)派生 id 和 name,绝不要写死数字——过时的图例与过时的图线一样无法分辨。

关于导入路径规则(到处都用 quantcraft.*;对现有脚本,较旧的 ide.* 路径仍可解析),参见参考。


保持时间与数值对齐

add 是按位置将 time 和 value 配对的,所以任何不匹配都会悄悄让整条线错位——它能渲染,只是对着错误的 K 线。

  • **每根 K 线都在同一条语句中追加时间和数值。**两个以不同速度填充、之后再 zip 的并行列表是经典的 bug:zip 会截断到较短的列表,因此 N 根 K 线的预热缺口会让前 N 个时间与属于第 N… 根 K 线的数值配对——线会向左偏 N 根 K 线,而且任何地方都不会报错。
  • 在指标预热期间,追加 float("nan"),而不是跳过。add 会替你丢弃非有限值(参见构建序列),因此预热行会从图中消失,而两个列表保持相同长度。
  • 原样传递 K 线的 t。coerce_chart_time 接受 ISO 字符串、epoch 秒/毫秒和 datetime。不带时区的字符串——包括像 "2024-01-02" 这样的 QuantCraft B2 日线日期——按 UTC 读取。然而,朴素的 datetime 对象按本地时间读取,这会使每个点都发生偏移。K 线行带有 UTC 的 t;请保留它。
  • add 已经会规范化每个 time 并丢弃非有限值,因此在你自己的循环中用 coerce_chart_time / math.isnan 预先过滤是可选的。

**on_finish 中的每个 return 都是静默的。**运行仍会成功,Test Results 仍会渲染——只是没有你的序列。当某个序列没有出现时,在每个检查点用 print(..., file=sys.stderr);消息会显示在输出面板中。


替代方案——在最后重新计算

对于你没有逐根 K 线跟踪的指标,在 on_finish 中由 bars(时钟标的的已执行窗口,从旧到新,与 Test Results 图表所绘制的是同样的行)或 bars_by_symbol 推导。请从你提供给计算的同样那些行中获取时间戳,这样被丢弃或过滤的行就不会使映射失去同步:

import quantcraft.process.runtime as rt PERIOD = 20 def on_finish(bars, symbol=None, bars_by_symbol=None): ci = rt.chart_indicators if ci is None or not bars: return closes = [float(b["close"]) for b in bars] points = [ {"time": bars[i]["t"], "value": sum(closes[i - PERIOD + 1 : i + 1]) / PERIOD} for i in range(PERIOD - 1, len(bars)) ] if not points: return ci.add( points, pane="main", render="line", line_style="dotted", id=f"sma_{PERIOD}", name=f"SMA({PERIOD})", symbol=symbol, )

不要由 runtime.qc_ohlcv 构建序列:在以筛选器为目标的运行中它是 None,而在已设置的地方,它涵盖包含预热的完整加载范围,而不是图表所绘制的已执行窗口。


构建序列——points 列表

每个序列都是你在运行自己的指标逻辑之后构建的字典列表。每一行需要:

键类型说明
time数字 / datetime / ISO 字符串规范化为 Unix 秒(int)。请原样传递 K 线的时间戳——回调中的 bar["t"],on_finish 中的 bars[i]["t"]。
value数字必须是有限的——NaN / inf 行会被丢弃,预热行就是这样从图中消失的。

字典上的其他键会被忽略,时间不合法或数值非有限的行会被悄悄丢弃——包括太小而不可能是 epoch 秒的数值时间(小于 100,000,000)。根本不是字典的行是错误:add 会抛出 TypeError: points[i] must be a dict with 'time' and 'value' keys。


序列选项(每次 add 调用)

你在调用 ci.add(...) 时,为每个序列设置一次:

参数是否必填取值
pane是"main"——叠加在价格/K 线图上。"separate"——图表下方独立的窗格(适合 RSI、MACD 等)。
render是"line" 或 "histogram"。
line_style当 render="line" 时"solid" 或 "dotted"。折线必填——省略会抛出 ValueError: When render is "line", line_style must be "solid" or "dotted". 柱状图省略即可(会被忽略)。
id否序列的稳定标识符;不提供则自动生成。有助于在多次运行之间保持相同的颜色/顺序。
name否图表图例中显示的名称。默认为 id。
color否颜色提示(例如 "#22c55e")。
symbol否大写代码(例如 "MSFT")。对于多标的回测,将序列固定到该标的的图表。时钟/主标的则省略。Screener 运行没有时钟标的,所以那里必须始终传 symbol。
pane_id否当 pane="separate" 时,具有相同 pane_id 的序列共享一个子窗格(例如 RSI 加上点状的 30/70 参考线)。每个窗格一个序列则省略。pane="main" 时被忽略。

每个序列显示在哪里

两个设置共同决定序列渲染的位置:

  • pane 控制垂直位置:
    • "main"——位于其图表的 K 线之上。
    • "separate"——在 K 线下方获得自己的窗格。
  • symbol 控制多标的运行中的哪张图表:
    • 省略——附加到主/时钟标的的图表(与单标的运行的默认行为一致)。
    • "AAPL"(等等)——仅附加到该代码的图表。

对于单标的回测,你可以完全忽略 symbol;所有内容都放在唯一的图表上。


折线与柱状图

这些是针对你已经计算好的序列的显示选择。你可能绘制的示例:

折线叠加层(render="line")

用于与收盘价对齐的连续序列:

  • 移动平均线(SMA、EMA)——通常是 pane="main"。
  • 布林带/肯特纳通道——pane="main",多个序列。
  • RSI / 随机指标 / ADX——pane="separate",以免价格刻度被压扁。

选择 line_style:

  • 主要指标用 "solid"。
  • 次要/参考序列(快线旁边的慢速均线、上轨)用 "dotted"。
# rsi_points must come from your own RSI calculation rt.chart_indicators.add(rsi_points, pane="separate", render="line", line_style="solid", id="rsi_14", name="RSI(14)")

柱状图(render="histogram")

用于柱状风格的序列(正值和负值以柱形渲染):

  • MACD 柱状图、类似成交量的评分、信号强度——在你计算出它们之后。
rt.chart_indicators.add( [{"time": t, "value": v} for t, v in zip(hist_times, hist_vals)], pane="separate", render="histogram", id="macd_hist", name="MACD Hist", )

柱状图不使用 line_style——省略即可。


多标的回测

在多标的运行中,每个已加载的代码在 Test Results 中都有自己的图表。使用 symbol="..." 把序列送到特定图表,并对每个代码调用一次 add。

逐根 K 线累积可以顺带解决这个问题。on_bar 本来就对每个代码运行一次,所以像快速开始那样按 symbol 为收集的数值建立键,就能让每个标签页得到一个序列,无需额外工作。on_finish 在整个运行结束时只运行一次,其 bars 参数只是时钟标的的:如果你仅从 bars 构建指标并传入 symbol=symbol,每个序列都会落在时钟标的的图表上,其他标签页就没有指标序列。

**重新计算路径的修复方法:**遍历 bars_by_symbol,根据每个标的的 K 线计算点,并在每次 add 时传入该代码:

def on_finish(bars, symbol=None, bars_by_symbol=None): ci = rt.chart_indicators if ci is None: return sources = bars_by_symbol if bars_by_symbol else {(symbol or "").upper(): bars} for ticker, ticker_bars in sources.items(): sma_points = compute_sma(ticker_bars, window=20) # your function ci.add( sma_points, pane="main", render="line", line_style="solid", id=f"sma_20_{ticker.lower()}", name="SMA(20)", symbol=ticker, )

没有 symbol 的序列放在时钟/主图表上。带有 symbol 的序列只放在该代码的图表上。

提示:在各标的之间保持相同的 name(例如 "SMA(20)")——id 和 pane_id(如果使用)应对每个标的唯一。


在一个子窗格上分组序列(pane_id)

当 pane="separate" 时,每次 add(...) 调用通常会获得自己的子窗格。传入相同的 pane_id 可以把相关序列叠放在一起——例如带有点状超买/超卖线的 RSI:

import quantcraft.process.runtime as rt RSI_PANE = "rsi" def on_finish(bars, symbol=None, bars_by_symbol=None): ci = rt.chart_indicators if ci is None or not bars: return bar_times = [b["t"] for b in bars] rsi_points = compute_rsi(bars) # your function ci.add( rsi_points, pane="separate", render="line", line_style="solid", id="rsi_14", name="RSI(14)", pane_id=RSI_PANE, ) ci.add( [{"time": t, "value": 70.0} for t in bar_times], pane="separate", render="line", line_style="dotted", id="rsi_ob", name="Overbought", pane_id=RSI_PANE, color="#94a3b8", ) ci.add( [{"time": t, "value": 30.0} for t in bar_times], pane="separate", render="line", line_style="dotted", id="rsi_os", name="Oversold", pane_id=RSI_PANE, color="#94a3b8", )

对于多标的运行,请使用每个标的各自的 pane_id(例如 f"rsi_{ticker}")以及每个代码唯一的 id(例如 f"rsi_{ticker.lower()}")。


一次添加多个序列

你可以添加任意多个序列——每个序列调用一次 add(...),各自带上你计算好的点:

ci.add(sma20_points, pane="main", render="line", line_style="solid", id="sma_20", name="SMA(20)") ci.add(sma50_points, pane="main", render="line", line_style="dotted", id="sma_50", name="SMA(50)") ci.add(rsi_points, pane="separate", render="line", line_style="solid", id="rsi_14", name="RSI(14)") ci.add(macd_hist, pane="separate", render="histogram", id="macd", name="MACD Hist")

在回测之外构建序列

to_result_dict(key=DEFAULT_RESULT_KEY) 和 merge_into(result, key=...) 是为自己持有结果字典的笔记本/自定义运行器准备的辅助方法——它们返回 {key: to_payload()},或将其合并到 result 中。DEFAULT_RESULT_KEY 为 "chart_indicator_series"。IDE 回测路径不使用它们(引擎从 runtime.chart_indicators 读取 to_payload())。

其他成员:ChartIndicatorSeries(max_entries=None) 在设置了 max_entries 时只保留最新的 N 个序列(批量运行在共享收集器上设置此项);add(...) 返回存储的 SeriesEntry;entries() 列出到目前为止添加的 SeriesEntry 对象。

from quantcraft.indicator_series import ChartIndicatorSeries # Points come from your own indicator logic, not from this module points = [ {"time": 1_700_000_000, "value": 150.0}, {"time": 1_700_008_640, "value": 151.2}, ] series = ChartIndicatorSeries() series.add( points, pane="main", render="line", line_style="dotted", id="sma_20", name="SMA(20)", ) test_result = {"success": True, "stdout": "..."} test_result = series.merge_into(test_result) # test_result["chart_indicator_series"] -> ready for the chart client

在正常的 IDE 回测中,只使用 rt.chart_indicators 和 .add(...)——不要另建收集器并对本地字典执行 merge_into。


结果载荷的结构(供参考)

chart_indicator_series 中的每个条目最终如下:

字段说明
id、name、pane、render、points正是你传给 add(...) 的内容。
line_style折线为 "solid" / "dotted",柱状图为 null。
color仅当你传了 color= 时才存在。
symbol仅当你传了 symbol= 时才存在——大写代码,以便 IDE 将其放到正确的图表上。
pane_id仅当 pane="separate" 且你传了非空的 pane_id= 时才存在。

每个 points 行都规范化为仅有 {"time": int, "value": float}。


提示与注意事项

  • **QuantCraft 负责绘制;你负责计算。**使用 pandas、numpy、TA-Lib 或你自己的循环——然后把得到的 points 传给 ci.add(...)。
  • **在交易的地方计算,在 on_finish 中输出。**在运行遍历 K 线时按标的记录 (K 线时间, 数值),绘制的正是策略据以行动的内容。只对你没有逐根跟踪的指标,才根据 on_finish 的 bars 重新计算。
  • 以 rt.chart_indicators 的方式读取收集器。from quantcraft.process.runtime import chart_indicators 绑定的是快照——在根算法文件中是安全的,在拆分的辅助模块中则是错误的。
  • **预热期间追加 NaN,不要跳过。**在一个列表中跳过行而在另一个中不跳,会使整条线错位;add 会替你丢弃非有限行。
  • **time 使用 K 线时间戳,原样传递。**回测 K 线的 t 通常是 ISO 字符串(例如 "2024-06-10T13:30:00+00:00",B2 日线则是 "2024-01-02"),且为 UTC。不要假设 int(b["t"]) 可行,也不要把它重建成朴素的 datetime——那样会被当作本地时间读取,使每个点都偏移。
  • **on_finish 的 return 是静默的。**没有序列也没有错误,通常意味着提前 return 了;在每个检查点打印到 sys.stderr。
  • **不要来自 qc_ohlcv。**在以筛选器为目标的运行中它是 None,否则它包含图表从不绘制的预热 K 线。
  • **稳定的 id 有助于样式。**在多次运行中复用同一个 id,图表就能保持颜色和顺序稳定;从策略所用的同一个常量派生 id / name,则能让图例保持真实。
  • **选择正确的窗格。**有界指标(RSI 0–100)应放在 "separate" 中——把它们放在 "main" 上会扭曲价格刻度。
  • **柱状图 = 没有 line_style;折线 = 必须有 line_style。**没有它的折线会抛出 ValueError。
  • **Screener 运行:始终传 symbol。**没有时钟标的可供回退。
阅读约 6 分钟建议修改 ↗
指标序列 — 文档 (1.0 (latest)) | QuantCraft