SeitenIndikatorreihen · 15 / 29
Indikatorserien
Wenn ein Backtest endet, rendert die Ansicht Testergebnisse der IDE Charts Ihres Laufs. Mit Indikatorserien hängen Sie zusätzliche Zeitreihen, die Sie selbst berechnet haben — gleitende Durchschnitte, RSI, MACD-Histogramme, eigene Signale — an diese Charts an, sodass sie neben den OHLCV-Kerzen erscheinen, in eigenen Panes oder bei Multi-Symbol-Läufen pro Symbol.
QuantCraft bietet keine eingebauten Indikatoren. Es gibt in der Plattform keine SMA-, MACD- oder RSI-API. Sie implementieren die Mathematik (oder nutzen Ihre eigene Bibliothek), bauen eine Liste von {"time": ..., "value": ...}-Zeilen und übergeben sie an den Runtime-Collector. Im IDE-Backtest besitzt die Engine eine ChartIndicatorSeries auf runtime.chart_indicators und hängt chart.to_payload() als chart_indicator_series an das Ergebnis. Das Modul normalisiert Zeiten und speichert Serien — es berechnet keine Indikatorwerte für Sie.
Serien werden immer aus on_finish übergeben. Die Werte sammeln Sie am besten, während der Lauf die Balken durchgeht — siehe Schnellstart.
Die IDE macht aus jedem Eintrag ein Linien- oder Histogramm-Overlay im entsprechenden Chart-Pane.
So funktioniert es
- Sie berechnen eine oder mehrere Indikatorserien (z. B. SMA aus Schlusskursen, RSI nach Ihrer Formel) als Liste von
{"time": ..., "value": ...}-Zeilen. - Sie fügen jede Serie mit Anzeigeoptionen (welches Pane, Linie oder Histogramm, optional Name/Farbe/Symbol) dem Runtime-Collector hinzu.
- Am Ende des Backtests führt die Engine diese Sammlung unter
chart_indicator_seriesins Ergebnis zusammen. - Der IDE-Chart in Test Results rendert jede Serie mit den gewählten Optionen.
Lesen Sie den Collector als Modulattribut, nicht als importierten Namen:
import quantcraft.process.runtime as rt
ci = rt.chart_indicators # assigned by the engine before on_initchart_indicators ist eine ChartIndicatorSeries-Instanz. Alles, was Sie mit .add(...) hinzufügen (mit Punkten, die Sie gebaut haben), wird über das to_payload() der Engine an Test Results angehängt. Konstruieren Sie für IDE-Test-Results keine eigene ChartIndicatorSeries() — nutzen Sie den Runtime-Collector.
Warum
rt.chart_indicatorsund nichtfrom ... import chart_indicators? Die Runtime-Handles gelten pro Sitzung, daher bindet einfrom ... importeinen Schnappschuss. In der Algo-Stammdatei ist das in Ordnung — die Engine bindet den Namen nach dem Laden Ihres Skripts neu. In einem aufgeteilten Hilfsmodul nicht: Hilfsmodule bleiben insys.modulesgecacht, sodass der Name den Collector der Sitzung behält, die beim ersten Import aktuell war, was für den nächsten Lauf im selben Prozess (Forward / Bulk) falsch ist. Siehe Modul-Handles vs. importierte Namen.
Schnellstart
Das empfohlene Muster: den Wert dort aufzeichnen, wo die Strategie ihn berechnet — in on_bar / on_tick — und on_finish nur zur Übergabe der gesammelten Serien nutzen. Die geplotteten Werte sind dann genau die Werte, auf denen die Strategie gehandelt hat, auf der eigenen Ausführungsuhr der Engine, ohne Neuberechnung und ohne Neuausrichtung.
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,
)Der Ergebnis-Chart zeigt dann Ihren SMA(45) als durchgezogenes Overlay auf dem Kerzenchart.
Leiten Sie id und name aus derselben Konstante ab, auf der die Strategie handelt (oben PERIOD), nie aus einer fest eingetragenen Zahl — eine veraltete Legende ist von einem veralteten Plot nicht zu unterscheiden.
Zu den Importpfad-Regeln (quantcraft.* überall; ältere ide.*-Pfade lösen für bestehende Skripte weiterhin auf) siehe Referenz.
Zeiten und Werte ausgerichtet halten
add paart time und value positionsweise, daher verschiebt eine Abweichung die ganze Linie stillschweigend — sie wird gerendert, nur gegen die falschen Kerzen.
- Hängen Sie Zeit und Wert in derselben Anweisung an, bei jedem Balken. Zwei parallele Listen, die mit unterschiedlicher Rate gefüllt und später mit
zipverbunden werden, sind der klassische Fehler:zipkürzt auf die kürzere Liste, sodass eine Warmup-Lücke von N Balken die ersten N Zeiten mit Werten paart, die zu den Balken N… gehören — die Linie landet N Balken weiter links, ohne dass irgendwo ein Fehler auftritt. - Hängen Sie während des Indikator-Warmups
float("nan")an, statt zu überspringen.addverwirft nicht endliche Werte für Sie (siehe Serien aufbauen), sodass Warmup-Zeilen aus dem Plot verschwinden, während beide Listen gleich lang bleiben. - Reichen Sie das
tdes Balkens unverändert durch.coerce_chart_timeakzeptiert ISO-Strings, Epoch-Sekunden/-Millisekunden unddatetime. Strings ohne Zeitzone — auch QuantCraft-B2-Tagesdaten wie"2024-01-02"— werden als UTC gelesen. Ein naivesdatetime-Objekt wird dagegen als lokale Zeit gelesen, was jeden Punkt verschiebt. Balkenzeilen tragen ein UTC-t; behalten Sie es bei. addnormalisiert bereits jedestimeund verwirft nicht endliche Werte, daher ist ein Vorfiltern mitcoerce_chart_time/math.isnanin Ihrer eigenen Schleife optional.
Jedes
returninon_finishist stumm. Der Lauf gelingt trotzdem und Test Results wird weiterhin gerendert — nur ohne Ihre Serien. Wenn eine Serie nicht erscheint, nutzen Sie an jeder Absicherungprint(..., file=sys.stderr); die Meldung erscheint im Ausgabebereich.
Alternative — am Ende neu berechnen
Für einen Indikator, den Sie nicht pro Balken mitgeführt haben, leiten Sie ihn in on_finish aus bars ab — dem ausgeführten Fenster des Taktsymbols, ältester zuerst, denselben Zeilen, die der Test-Results-Chart zeichnet — oder aus bars_by_symbol. Nehmen Sie die Zeitstempel aus denselben Zeilen, die Sie in die Berechnung gegeben haben, damit eine verworfene oder gefilterte Zeile die Zuordnung nicht desynchronisieren kann:
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,
)Bauen Sie Serien nicht aus runtime.qc_ohlcv: Es ist bei einem Lauf mit Screener-Ziel None, und wo es gesetzt ist, umfasst es den gesamten geladenen Bereich einschließlich Warmup, nicht das ausgeführte Fenster, das der Chart zeichnet.
Serien aufbauen — die points-Liste
Jede Serie ist eine Liste von Dicts, die Sie nach Ausführung Ihrer eigenen Indikatorlogik gebaut haben. Jede Zeile braucht:
| Schlüssel | Typ | Hinweise |
|---|---|---|
time | Zahl / datetime / ISO-String | Wird auf Unix-Sekunden (int) normalisiert. Reichen Sie den Zeitstempel des Balkens — bar["t"] in einem Callback, bars[i]["t"] in on_finish — unverändert durch. |
value | Zahl | Muss endlich sein — NaN-/inf-Zeilen werden verworfen, so verschwinden Warmup-Zeilen aus dem Plot. |
Andere Schlüssel im Dict werden ignoriert, und Zeilen mit ungültiger Zeit oder nicht endlichem Wert werden stillschweigend verworfen — auch numerische Zeiten, die zu klein für Epoch-Sekunden sind (unter 100.000.000). Eine Zeile, die gar kein Dict ist, ist ein Fehler: add löst TypeError: points[i] must be a dict with 'time' and 'value' keys aus.
Serienoptionen (pro add-Aufruf)
Sie setzen diese einmal pro Serie beim Aufruf von ci.add(...):
| Argument | Pflicht | Werte |
|---|---|---|
pane | ja | "main" — Overlay auf dem Kurs-/Kerzenchart. "separate" — eigenes Pane unter dem Chart (gut für RSI, MACD usw.). |
render | ja | "line" oder "histogram". |
line_style | bei render="line" | "solid" oder "dotted". Für Linien Pflicht — Weglassen löst ValueError: When render is "line", line_style must be "solid" or "dotted". aus. Bei Histogrammen weglassen (wird ignoriert). |
id | nein | Stabile Kennung der Serie; wird automatisch erzeugt, wenn Sie keine angeben. Nützlich, um Farbe/Reihenfolge über Läufe hinweg beizubehalten. |
name | nein | Anzeigename in der Chart-Legende. Standard ist id. |
color | nein | Farbhinweis (z. B. "#22c55e"). |
symbol | nein | Ticker in Großbuchstaben (z. B. "MSFT"). Bei Multi-Symbol-Backtests heftet es die Serie an den Chart dieses Symbols. Für das Takt-/Primärsymbol weglassen. Ein Screener-Lauf hat kein Taktsymbol, geben Sie dort also immer symbol an. |
pane_id | nein | Bei pane="separate" teilen sich Serien mit gleichem pane_id ein Sub-Pane (z. B. RSI plus gepunktete 30/70-Referenzlinien). Weglassen für eine Serie pro Pane. Wird bei pane="main" ignoriert. |
Wo jede Serie erscheint
Zwei Einstellungen gemeinsam bestimmen, wo die Serie gerendert wird:
panesteuert die vertikale Platzierung:"main"— liegt auf den Kerzen ihres Charts."separate"— erhält ein eigenes Pane unter den Kerzen.
symbolsteuert, welcher Chart bei einem Multi-Symbol-Lauf:- weggelassen — hängt am Chart des Primär-/Taktsymbols (entspricht dem Standard bei Einzelsymbol-Läufen).
"AAPL"(usw.) — hängt nur am Chart dieses Tickers.
Bei Einzelsymbol-Backtests können Sie symbol ganz ignorieren; alles landet im einen Chart.
Linien vs. Histogramme
Das sind Anzeige-Entscheidungen für Serien, die Sie bereits berechnet haben. Beispiele, was Sie plotten könnten:
Linien-Overlays (render="line")
Verwenden Sie sie für kontinuierliche Serien, die sich an Schlusskursen ausrichten:
- Gleitende Durchschnitte (SMA, EMA) — meist
pane="main". - Bollinger-/Keltner-Bänder —
pane="main", mehrere Serien. - RSI / Stochastik / ADX —
pane="separate", damit die Kursskala nicht gestaucht wird.
Wählen Sie line_style:
"solid"für Hauptindikatoren."dotted"für sekundäre / Referenzserien (ein langsamer MA neben einem schnellen, ein oberes Band).
# 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)")Histogramme (render="histogram")
Verwenden Sie sie für Serien im Balken-/Säulen-Stil (positive und negative Werte als Balken dargestellt):
- MACD-Histogramm, volumenähnliche Scores, Signalstärke — nachdem Sie sie berechnet haben.
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 wird bei Histogrammen nicht verwendet — lassen Sie es weg.
Multi-Symbol-Backtests
Bei Multi-Symbol-Läufen hat jeder geladene Ticker in Test Results einen eigenen Chart. Mit symbol="..." senden Sie eine Serie an einen bestimmten Chart, und Sie rufen add einmal pro Ticker auf.
Pro Balken zu sammeln erledigt das kostenlos. on_bar läuft ohnehin einmal pro Ticker, daher liefert das Indizieren der gesammelten Werte nach symbol — wie im Schnellstart — eine Serie pro Tab ohne zusätzlichen Aufwand. on_finish läuft nur einmal am Ende des gesamten Laufs, und sein Argument bars betrifft nur das Taktsymbol: Bauen Sie Indikatoren allein aus bars und übergeben symbol=symbol, landet jede Serie im Chart des Taktsymbols, und die anderen Tabs haben keine Indikatorserien.
Lösung für den Neuberechnungsweg: Schleifen Sie über bars_by_symbol, berechnen Sie die Punkte aus den Balken jedes Symbols und übergeben Sie bei jedem add den jeweiligen Ticker:
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,
)Serien ohne symbol gehen auf den Takt-/Primärchart. Serien mit symbol gehen nur auf den Chart dieses Tickers.
Tipp: Behalten Sie über Symbole hinweg denselben
name(z. B."SMA(20)") —idundpane_id(falls verwendet) sollten pro Symbol eindeutig sein.
Serien in einem Sub-Pane gruppieren (pane_id)
Bei pane="separate" erhält normalerweise jeder add(...)-Aufruf ein eigenes Sub-Pane. Übergeben Sie dieselbe pane_id, um zusammengehörige Serien zu stapeln — zum Beispiel RSI mit gepunkteten Überkauft-/Überverkauft-Linien:
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",
)Nutzen Sie bei Multi-Symbol-Läufen eine pane_id pro Symbol (z. B. f"rsi_{ticker}") und eine eindeutige id pro Ticker (z. B. f"rsi_{ticker.lower()}").
Mehrere Serien auf einmal
Sie können beliebig viele Serien hinzufügen — rufen Sie add(...) einmal pro Serie auf, jeweils mit Punkten, die Sie berechnet haben:
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")Serien außerhalb eines Backtests aufbauen
to_result_dict(key=DEFAULT_RESULT_KEY) und merge_into(result, key=...) sind Hilfsfunktionen für Notebooks / eigene Runner, die das Ergebnis-Dict besitzen — sie geben {key: to_payload()} zurück oder führen das in result zusammen. DEFAULT_RESULT_KEY ist "chart_indicator_series". Sie werden vom IDE-Backtest-Pfad nicht verwendet (die Engine liest to_payload() aus runtime.chart_indicators).
Weitere Mitglieder: ChartIndicatorSeries(max_entries=None) behält nur die neuesten N Serien, wenn max_entries gesetzt ist (Bulk-Läufe setzen dies auf dem gemeinsamen Collector); add(...) gibt den gespeicherten SeriesEntry zurück; entries() listet die bisher hinzugefügten SeriesEntry-Objekte auf.
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 clientVerwenden Sie in einem normalen IDE-Backtest nur rt.chart_indicators und .add(...) — bauen Sie keinen separaten Collector und führen Sie kein merge_into auf ein lokales Dict aus.
Form der Ergebnis-Nutzlast (zur Referenz)
Jeder Eintrag in chart_indicator_series endet als:
| Feld | Beschreibung |
|---|---|
id, name, pane, render, points | Genau das, was Sie an add(...) übergeben haben. |
line_style | "solid" / "dotted" bei Linien, null bei Histogrammen. |
color | Nur vorhanden, wenn Sie color= übergeben haben. |
symbol | Nur vorhanden, wenn Sie symbol= übergeben haben — Ticker in Großbuchstaben, damit die IDE ihn im richtigen Chart platzieren kann. |
pane_id | Nur vorhanden, wenn pane="separate" und Sie eine nicht leere pane_id= übergeben haben. |
Jede points-Zeile wird auf nur {"time": int, "value": float} normalisiert.
Tipps und Stolperfallen
- QuantCraft plottet; Sie berechnen. Nutzen Sie pandas, numpy, TA-Lib oder eigene Schleifen — und übergeben Sie die resultierenden
pointsanci.add(...). - Dort berechnen, wo Sie handeln, in
on_finishausgeben. Das Aufzeichnen von(Balkenzeit, Wert)pro Symbol, während der Lauf die Balken durchgeht, plottet genau das, worauf die Strategie reagiert hat. Berechnen Sie aus denbarsvonon_finishnur Indikatoren neu, die Sie nicht pro Balken mitgeführt haben. - Lesen Sie den Collector als
rt.chart_indicators.from quantcraft.process.runtime import chart_indicatorsbindet einen Schnappschuss — sicher in der Algo-Stammdatei, falsch in einem aufgeteilten Hilfsmodul. - Hängen Sie
NaNim Warmup an, überspringen Sie nicht. Zeilen in einer Liste zu überspringen und in der anderen nicht, verschiebt die ganze Linie;addverwirft die nicht endlichen Zeilen für Sie. - Verwenden Sie Balkenzeitstempel für
timeunverändert. Dasteines Backtest-Balkens ist oft ein ISO-String (z. B."2024-06-10T13:30:00+00:00"oder"2024-01-02"bei B2-Tagesbalken) und in UTC. Nehmen Sie nicht an, dassint(b["t"])funktioniert, und bauen Sie es nicht als naivesdatetimenach — das würde als lokale Zeit gelesen und jeden Punkt verschieben. on_finish-Returns sind stumm. Keine Serie und kein Fehler bedeutet meist ein frühesreturn; geben Sie an jeder Absicherung aufsys.stderraus.- Nicht aus
qc_ohlcv. Es ist bei einem Lauf mit Screener-ZielNoneund enthält sonst Warmup-Balken, die der Chart nie zeichnet. - Stabile
ids helfen beim Styling. Wenn Sie dieselbeidüber Läufe hinweg wiederverwenden, kann der Chart Farben und Reihenfolge stabil halten, undid/nameaus derselben Konstante der Strategie abzuleiten hält die Legende ehrlich. - Wählen Sie das richtige Pane. Begrenzte Indikatoren (RSI 0–100) gehören in
"separate"— auf"main"verzerren sie die Kursskala. - Histogramm = kein
line_style; Linie =line_stylePflicht. Eine Linie ohne ihn löstValueErroraus. - Screener-Läufe: immer
symbolübergeben. Es gibt kein Taktsymbol als Rückfall.

