页面指标序列 · 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 会把每个条目转换成对应图表窗格上的折线或柱状图叠加层。
工作原理
- 你计算一个或多个指标序列(例如由收盘价计算 SMA,由你的公式计算 RSI),形式为
{"time": ..., "value": ...}行的列表。 - 你添加每个序列到运行时收集器,并带上显示选项(哪个窗格、折线还是柱状图、可选的名称/颜色/标的)。
- 回测结束时,引擎将该集合合并到结果的
chart_indicator_series下。 - Test Results 中的 IDE 图表按你选择的选项渲染每个序列。
把收集器作为模块属性读取,而不是作为导入的名称:
import quantcraft.process.runtime as rt
ci = rt.chart_indicators # assigned by the engine before on_initchart_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。**没有时钟标的可供回退。

