ページインジケーター系列 · 15 / 29
インジケーター系列
バックテストが終了すると、IDE の**テスト結果**ビューに実行のチャートが表示されます。インジケーター系列を使うと、移動平均、RSI、MACD ヒストグラム、独自のシグナルなど、自分で計算した追加の時系列をそれらのチャートに添付でき、OHLCV のローソク足の隣、専用のペイン、あるいは複数シンボルの実行ではシンボルごとに表示されます。
QuantCraft には組み込みのインジケーターはありません。プラットフォームに SMA、MACD、RSI の API はありません。計算ロジックを自分で実装し(または独自のライブラリを使い)、{"time": ..., "value": ...} の行のリストを作成して、ランタイムのコレクターに渡します。IDE のバックテストでは、エンジンが runtime.chart_indicators 上の ChartIndicatorSeries を保持し、chart.to_payload() を結果に chart_indicator_series として添付します。このモジュールは時刻を正規化して系列を保存しますが、インジケーターの値を代わりに計算してはくれません。
系列は常に on_finish から渡されます。値は、実行がバーを進める間に収集するのが最適です — クイックスタートを参照してください。
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() を作成しないでください。ランタイムのコレクターを使ってください。
なぜ
from ... import chart_indicatorsではなくrt.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) がローソク足チャート上の実線のオーバーレイとして表示されます。
id と name は、ストラテジーが取引に使うのと同じ定数(上の PERIOD)から導出し、ハードコードした数値は使わないでください。古い凡例は、古いプロットと見分けがつきません。
インポートパスの規則(どこでも quantcraft.*。既存のスクリプトでは古い ide.* パスも引き続き解決されます)については、リファレンスを参照してください。
時刻と値の整合を保つ
add は time と value を位置で対応付けるため、ずれると線全体が黙ってずれます。描画はされますが、間違ったローソク足に対して描かれます。
- **時刻と値は、毎バー同じ文では追加してください。**異なる速度で埋めた 2 つの並列リストを後で
zipするのが典型的なバグです。zipは短い方に切り詰めるため、N バーのウォームアップの隙間があると、最初の N 個の時刻が N… 番目以降のバーに属する値と対になり、線は N バー左にずれて、どこにもエラーが出ません。 - インジケーターのウォームアップ中は、スキップせずに
float("nan")を追加してください。addは有限でない値を代わりに破棄する(系列の構築を参照)ため、ウォームアップ行はプロットから消え、2 つのリストは同じ長さのままになります。 - バーの
tはそのまま渡してください。coerce_chart_timeは ISO 文字列、エポック秒/ミリ秒、datetimeを受け付けます。タイムゾーンのない文字列("2024-01-02"のような QuantCraft B2 の日足の日付を含む)は UTC として読まれます。ただし、ナイーブなdatetimeオブジェクトはローカル時刻として読まれ、すべての点がずれます。バー行は UTC のtを持っているので、そのまま使ってください。 addはすでに各timeを正規化し、有限でない値を破棄するため、自分のループでcoerce_chart_time/math.isnanによる事前フィルタリングを行うのは任意です。
**
on_finish内のすべてのreturnは無言です。**実行は成功し、Test Results も描画されますが、系列だけがありません。系列が表示されないときは、各ガードでprint(..., file=sys.stderr)を使ってください。メッセージは出力パネルに表示されます。
代替手段 — 最後に再計算する
バーごとに追跡しなかったインジケーターは、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 リスト
すべての系列は、独自のインジケーターロジックを実行した後に作成した dict のリストです。各行には次が必要です。
| キー | 型 | 備考 |
|---|---|---|
time | 数値 / datetime / ISO 文字列 | Unix 秒(int)に正規化されます。バーのタイムスタンプ(コールバックでは bar["t"]、on_finish では bars[i]["t"])をそのまま渡してください。 |
value | 数値 | 有限である必要があります。NaN / inf の行は破棄され、ウォームアップ行がプロットから消えるのはこのためです。 |
dict のその他のキーは無視され、時刻が不正な行や値が有限でない行は黙って破棄されます。エポック秒としては小さすぎる数値の時刻(100,000,000 未満)も含みます。dict ですらない行はエラーで、add は TypeError: points[i] must be a dict with 'time' and 'value' keys を送出します。
系列オプション(add 呼び出しごと)
系列ごとに、ci.add(...) を呼ぶときに一度だけ設定します。
| 引数 | 必須 | 値 |
|---|---|---|
pane | はい | "main" — 価格/ローソク足チャート上のオーバーレイ。"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 を持つ系列は1 つのサブペインを共有します(例:RSI と点線の 30/70 基準線)。ペインごとに 1 系列にするなら省略します。pane="main" では無視されます。 |
各系列が表示される場所
2 つの設定が組み合わさって、系列がどこに描画されるかが決まります。
paneは縦方向の配置を制御します。"main"— そのチャートのローソク足の上に重なります。"separate"— ローソク足の下に専用のペインが作られます。
symbolは、複数シンボルの実行でどのチャートかを制御します。- 省略 — プライマリ/クロックシンボルのチャートに添付されます(単一シンボルの実行のデフォルトと同じ)。
"AAPL"(など) — そのティッカーのチャートのみに添付されます。
単一シンボルのバックテストでは symbol をまったく無視でき、すべてが唯一のチャートに表示されます。
ラインとヒストグラム
これらは、すでに計算した系列の表示の選択です。プロットできるものの例:
ラインのオーバーレイ(render="line")
終値に沿う連続的な系列に使います。
- 移動平均(SMA、EMA) — 通常は
pane="main"。 - ボリンジャー/ケルトナーバンド —
pane="main"、複数系列。 - RSI / ストキャスティクス / ADX — 価格軸がつぶれないよう
pane="separate"。
line_style を選びます。
- 主要なインジケーターには
"solid"。 - 副次的/基準の系列(速い MA の隣の遅い MA、上側のバンドなど)には
"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 を 1 回呼びます。
バーごとに蓄積すれば、これは自然に解決します。 on_bar はすでにティッカーごとに 1 回実行されるため、クイックスタートのように収集した値を symbol でキー付けすれば、追加の作業なしにタブごとに 1 つの系列が得られます。on_finish は実行全体の最後に1 回だけ実行され、その bars 引数はクロックシンボルのみです。bars だけからインジケーターを作って symbol=symbol を渡すと、すべての系列がクロックシンボルのチャートに載り、他のタブにはインジケーター系列がまったく表示されません。
再計算パスの対処: bars_by_symbol をループし、各シンボルのバーから点を計算して、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(使用する場合)はシンボルごとに一意にしてください。
1 つのサブペインに系列をまとめる(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(...) を 1 回呼び、それぞれに計算した点を渡します。
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=...) は、結果の dict を自前で持つノートブック/独自ランナー向けのヘルパーです。{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(...) だけを使ってください。別のコレクターを作ってローカルの dict に 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で出力する。**実行がバーを進める間にシンボルごとに(バーの時刻, 値)を記録すれば、ストラテジーが実際に反応した値がそのままプロットされます。バーごとに追跡しなかったインジケーターに限り、on_finishのbarsから再計算してください。 - コレクターは
rt.chart_indicatorsとして読み取る。from quantcraft.process.runtime import chart_indicatorsはスナップショットを束縛します。ルートのアルゴファイルでは安全ですが、分割されたヘルパーモジュールでは誤りです。 - **ウォームアップ中は
NaNを追加し、スキップしない。**一方のリストだけ行をスキップすると線全体がずれます。addが有限でない行を代わりに破棄します。 - **
timeにはバーのタイムスタンプをそのまま使う。**バックテストのバーの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であり、それ以外ではチャートが描画しないウォームアップバーを含みます。 - **安定した
idはスタイリングに役立ちます。**実行をまたいで同じidを再利用すると、チャートが色と順序を安定して保て、ストラテジーが使うのと同じ定数からid/nameを導出すると凡例が正確に保たれます。 - **適切なペインを選ぶ。**範囲が決まっているインジケーター(RSI 0–100)は
"separate"に置いてください。"main"に置くと価格軸が歪みます。 - **ヒストグラム =
line_styleなし、ライン =line_style必須。**なしのラインはValueErrorを送出します。 - **Screener の実行:必ず
symbolを渡す。**フォールバックできるクロックシンボルがありません。

