ドキュメント1.0 (latest)
ページインジケーター系列 · 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 は各エントリーを、対応するチャートペイン上のラインまたはヒストグラムのオーバーレイに変換します。


仕組み

  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() を作成しないでください。ランタイムのコレクターを使ってください。

なぜ 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_stylerender="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、pointsadd(...) に渡したものそのまま。
line_styleラインでは "solid" / "dotted"、ヒストグラムでは null。
colorcolor= を渡した場合のみ存在。
symbolsymbol= を渡した場合のみ存在。IDE が正しいチャートに配置できるよう、大文字のティッカー。
pane_idpane="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 を渡す。**フォールバックできるクロックシンボルがありません。
インジケーター系列 — ドキュメント (1.0 (latest)) | QuantCraft