PagesSéries d'indicateurs · 15 / 29
Séries d'indicateurs
Lorsqu'un backtest se termine, la vue Résultats des tests de l'IDE affiche des graphiques de votre exécution. Les séries d'indicateurs vous permettent de rattacher à ces graphiques des séries temporelles supplémentaires que vous avez calculées vous-même — moyennes mobiles, RSI, histogrammes MACD, signaux personnalisés — afin qu'elles apparaissent à côté des bougies OHLCV, dans leurs propres volets, ou par symbole pour les exécutions multi-symboles.
QuantCraft ne fournit pas d'indicateurs intégrés. Il n'existe aucune API SMA, MACD ou RSI dans la plateforme. Vous implémentez le calcul (ou utilisez votre propre bibliothèque), construisez une liste de lignes {"time": ..., "value": ...}, puis la transmettez au collecteur du runtime. Dans le backtest de l'IDE, le moteur possède une ChartIndicatorSeries sur runtime.chart_indicators et joint chart.to_payload() au résultat sous chart_indicator_series. Le module normalise les temps et stocke les séries — il ne calcule pas les valeurs des indicateurs à votre place.
Les séries sont toujours transmises depuis on_finish. Le mieux est de collecter les valeurs au fil du parcours des barres — voir Démarrage rapide.
L'IDE transforme chaque entrée en une superposition ligne ou histogramme sur le volet de graphique correspondant.
Fonctionnement
- Vous calculez une ou plusieurs séries d'indicateurs (p. ex. une SMA à partir des clôtures, un RSI à partir de votre formule) sous forme de liste de lignes
{"time": ..., "value": ...}. - Vous ajoutez chaque série au collecteur du runtime avec des options d'affichage (quel volet, ligne ou histogramme, nom/couleur/symbole optionnels).
- À la fin du backtest, le moteur fusionne cette collection dans le résultat sous
chart_indicator_series. - Le graphique de l'IDE dans Test Results affiche chaque série avec les options choisies.
Lisez le collecteur comme un attribut de module, et non comme un nom importé :
import quantcraft.process.runtime as rt
ci = rt.chart_indicators # assigned by the engine before on_initchart_indicators est une instance de ChartIndicatorSeries. Tout ce que vous y ajoutez avec .add(...) (avec des points que vous avez construits) est joint à Test Results via le to_payload() du moteur. Ne construisez pas de ChartIndicatorSeries() privée pour les Test Results de l'IDE — utilisez le collecteur du runtime.
Pourquoi
rt.chart_indicatorset nonfrom ... import chart_indicators? Les poignées du runtime sont propres à chaque session ; unfrom ... importlie donc un instantané. Dans le fichier d'algo racine, ce n'est pas un problème — le moteur relie le nom après avoir chargé votre script. Dans un module d'aide séparé, si : les modules d'aide restent en cache danssys.modules, de sorte que le nom conserve le collecteur de la session en cours lors du premier import, ce qui est faux pour l'exécution suivante dans le même processus (forward / en masse). Voir Poignées de module et noms importés.
Démarrage rapide
Le schéma recommandé consiste à enregistrer la valeur là où la stratégie la calcule — dans on_bar / on_tick — et à n'utiliser on_finish que pour transmettre les séries collectées. Les valeurs tracées sont alors exactement celles sur lesquelles la stratégie a tradé, sur l'horloge d'exécution du moteur, sans recalcul ni réalignement.
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,
)Le graphique de résultat affiche alors votre SMA(45) comme une superposition pleine sur le graphique en bougies.
Dérivez id et name de la même constante que celle sur laquelle la stratégie trade (PERIOD ci-dessus), jamais d'un nombre codé en dur — une légende périmée est indiscernable d'un tracé périmé.
Pour les règles de chemins d'import (quantcraft.* partout ; les anciens chemins ide.* fonctionnent toujours pour les scripts existants), voir Référence.
Garder temps et valeurs alignés
add associe time et value par position ; un décalage déplace donc toute la ligne en silence — elle s'affiche, mais face aux mauvaises bougies.
- Ajoutez le temps et la valeur dans la même instruction, à chaque barre. Deux listes parallèles remplies à des rythmes différents puis
zippées constituent le bug classique :ziptronque à la plus courte, de sorte qu'un trou de préchauffage de N barres associe les N premiers temps à des valeurs appartenant aux barres N… — la ligne se retrouve décalée de N barres vers la gauche, sans la moindre erreur. - Pendant le préchauffage de l'indicateur, ajoutez
float("nan")au lieu de sauter.addécarte les valeurs non finies pour vous (voir Construire des séries), de sorte que les lignes de préchauffage disparaissent du tracé tandis que les deux listes gardent la même longueur. - Transmettez le
tde la barre tel quel.coerce_chart_timeaccepte les chaînes ISO, les secondes/millisecondes epoch etdatetime. Les chaînes sans fuseau horaire — y compris les dates journalières QuantCraft B2 comme"2024-01-02"— sont lues comme UTC. Un objetdatetimenaïf, en revanche, est lu comme heure locale, ce qui décale chaque point. Les lignes de barres portent untUTC ; conservez-le. addnormalise déjà chaquetimeet écarte les valeurs non finies ; pré-filtrer aveccoerce_chart_time/math.isnandans votre propre boucle est donc optionnel.
Chaque
returndanson_finishest silencieux. L'exécution réussit quand même et Test Results s'affiche toujours — simplement sans vos séries. Lorsqu'une série n'apparaît pas, faites unprint(..., file=sys.stderr)à chaque garde ; le message s'affiche dans le Panneau de sortie.
Alternative — recalculer à la fin
Pour un indicateur que vous n'avez pas suivi barre par barre, calculez-le dans on_finish à partir de bars — la fenêtre exécutée du symbole d'horloge, la plus ancienne en premier, les mêmes lignes que dessine le graphique de Test Results — ou à partir de bars_by_symbol. Prenez les horodatages dans les mêmes lignes que celles fournies au calcul, afin qu'une ligne abandonnée ou filtrée ne puisse pas désynchroniser la correspondance :
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,
)Ne construisez pas de séries à partir de runtime.qc_ohlcv : il vaut None lors d'une exécution ciblant un screener et, lorsqu'il est défini, il couvre la plage complète chargée, préchauffage compris, et non la fenêtre exécutée que dessine le graphique.
Construire des séries — la liste points
Chaque série est une liste de dicts que vous avez construite après avoir exécuté votre propre logique d'indicateur. Chaque ligne nécessite :
| Clé | Type | Remarques |
|---|---|---|
time | nombre / datetime / chaîne ISO | Normalisé en secondes Unix (int). Transmettez l'horodatage de la barre — bar["t"] dans un callback, bars[i]["t"] dans on_finish — tel quel. |
value | nombre | Doit être finie — les lignes NaN / inf sont écartées, c'est ainsi que les lignes de préchauffage disparaissent du tracé. |
Les autres clés du dict sont ignorées, et les lignes dont le temps est invalide ou la valeur non finie sont écartées en silence — y compris les temps numériques trop petits pour être des secondes epoch (inférieurs à 100 000 000). Une ligne qui n'est pas un dict constitue une erreur : add lève TypeError: points[i] must be a dict with 'time' and 'value' keys.
Options de série (par appel à add)
Vous les définissez une fois par série, lors de l'appel à ci.add(...) :
| Argument | Obligatoire | Valeurs |
|---|---|---|
pane | oui | "main" — superposition sur le graphique des prix/bougies. "separate" — son propre volet sous le graphique (idéal pour RSI, MACD, etc.). |
render | oui | "line" ou "histogram". |
line_style | si render="line" | "solid" ou "dotted". Obligatoire pour les lignes — l'omettre lève ValueError: When render is "line", line_style must be "solid" or "dotted". À omettre pour les histogrammes (ignoré). |
id | non | Identifiant stable de la série ; généré automatiquement si vous n'en fournissez pas. Utile pour conserver la même couleur/le même ordre d'une exécution à l'autre. |
name | non | Nom d'affichage dans la légende du graphique. Vaut id par défaut. |
color | non | Indication de couleur (p. ex. "#22c55e"). |
symbol | non | Ticker en majuscules (p. ex. "MSFT"). Pour les backtests multi-symboles, rattache la série au graphique de ce symbole. À omettre pour le symbole d'horloge / principal. Une exécution Screener n'a pas de symbole d'horloge ; passez donc toujours symbol dans ce cas. |
pane_id | non | Avec pane="separate", les séries ayant le même pane_id partagent un seul sous-volet (p. ex. un RSI plus des lignes de référence 30/70 en pointillés). À omettre pour une série par volet. Ignoré lorsque pane="main". |
Où chaque série apparaît
Deux réglages décident ensemble de l'endroit où la série s'affiche :
panecontrôle le placement vertical :"main"— se place sur les bougies de son graphique."separate"— obtient son propre volet sous les bougies.
symbolcontrôle quel graphique dans une exécution multi-symboles :- omis — se rattache au graphique du symbole principal / d'horloge (comme par défaut pour les exécutions à un seul symbole).
"AAPL"(etc.) — se rattache uniquement au graphique de ce ticker.
Pour les backtests à un seul symbole, vous pouvez ignorer symbol ; tout va sur l'unique graphique.
Lignes et histogrammes
Ce sont des choix d'affichage pour des séries que vous avez déjà calculées. Exemples de ce que vous pourriez tracer :
Superpositions en ligne (render="line")
À utiliser pour des séries continues alignées sur les clôtures :
- Moyennes mobiles (SMA, EMA) — généralement
pane="main". - Bandes de Bollinger / Keltner —
pane="main", plusieurs séries. - RSI / Stochastique / ADX —
pane="separate"pour que l'échelle des prix ne soit pas écrasée.
Choisissez line_style :
"solid"pour les indicateurs principaux."dotted"pour les séries secondaires / de référence (une MA lente à côté d'une rapide, une bande supérieure).
# 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)")Histogrammes (render="histogram")
À utiliser pour des séries de type barres/colonnes (valeurs positives et négatives rendues sous forme de barres) :
- Histogramme MACD, scores de type volume, force du signal — une fois calculés.
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 n'est pas utilisé pour les histogrammes — omettez-le.
Backtests multi-symboles
Dans les exécutions multi-symboles, chaque ticker chargé a son propre graphique dans Test Results. Utilisez symbol="..." pour envoyer une série vers un graphique précis, et appelez add une fois par ticker.
Accumuler à chaque barre règle cela gratuitement. on_bar s'exécute déjà une fois par ticker ; indexer les valeurs collectées par symbol — comme dans Démarrage rapide — donne une série par onglet sans travail supplémentaire. on_finish ne s'exécute qu'une seule fois à la fin de toute l'exécution, et son argument bars ne concerne que le symbole d'horloge : si vous construisez des indicateurs à partir de bars seul et passez symbol=symbol, chaque série atterrit sur le graphique du symbole d'horloge et les autres onglets n'ont aucune série d'indicateurs.
Correctif pour la voie « recalcul » : bouclez sur bars_by_symbol, calculez les points à partir des barres de chaque symbole et passez ce ticker à chaque 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,
)Les séries sans symbol vont sur le graphique d'horloge / principal. Les séries avec symbol vont uniquement sur le graphique de ce ticker.
Astuce : gardez le même
name(p. ex."SMA(20)") pour tous les symboles — l'idet lepane_id(lorsqu'il est utilisé) doivent être uniques par symbole.
Regrouper des séries sur un même sous-volet (pane_id)
Avec pane="separate", chaque appel à add(...) obtient normalement son propre sous-volet. Passez le même pane_id pour empiler des séries liées — par exemple un RSI avec des lignes de surachat/survente en pointillés :
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",
)Pour les exécutions multi-symboles, utilisez un pane_id par symbole (p. ex. f"rsi_{ticker}") et un id unique par ticker (p. ex. f"rsi_{ticker.lower()}").
Plusieurs séries à la fois
Vous pouvez ajouter autant de séries que vous voulez — appelez add(...) une fois par série, chacune avec des points que vous avez calculés :
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")Construire des séries hors d'un backtest
to_result_dict(key=DEFAULT_RESULT_KEY) et merge_into(result, key=...) sont des assistants pour les notebooks / exécuteurs personnalisés qui possèdent le dict de résultat — ils renvoient {key: to_payload()}, ou fusionnent cela dans result. DEFAULT_RESULT_KEY vaut "chart_indicator_series". Ils ne sont pas utilisés par le chemin de backtest de l'IDE (le moteur lit to_payload() depuis runtime.chart_indicators).
Autres membres : ChartIndicatorSeries(max_entries=None) ne conserve que les N séries les plus récentes lorsque max_entries est défini (les exécutions en masse le définissent sur le collecteur partagé) ; add(...) renvoie le SeriesEntry stocké ; entries() liste les objets SeriesEntry ajoutés jusqu'ici.
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 clientDans un backtest normal de l'IDE, utilisez uniquement rt.chart_indicators et .add(...) — ne construisez pas un collecteur séparé avec merge_into sur un dict local.
Forme de la charge utile du résultat (pour référence)
Chaque entrée de chart_indicator_series se présente ainsi :
| Champ | Description |
|---|---|
id, name, pane, render, points | Exactement ce que vous avez passé à add(...). |
line_style | "solid" / "dotted" pour les lignes, null pour les histogrammes. |
color | Présent uniquement si vous avez passé color=. |
symbol | Présent uniquement si vous avez passé symbol= — ticker en majuscules afin que l'IDE puisse le placer sur le bon graphique. |
pane_id | Présent uniquement lorsque pane="separate" et que vous avez passé un pane_id= non vide. |
Chaque ligne de points est normalisée en {"time": int, "value": float}.
Conseils et pièges
- QuantCraft trace ; vous calculez. Utilisez pandas, numpy, TA-Lib ou vos propres boucles — puis passez les
pointsobtenus àci.add(...). - Calculez là où vous tradez, émettez dans
on_finish. Enregistrer(heure de barre, valeur)par symbole au fil du parcours des barres trace exactement ce sur quoi la stratégie a agi. Ne recalculez à partir desbarsdeon_finishque les indicateurs que vous n'avez pas suivis barre par barre. - Lisez le collecteur via
rt.chart_indicators.from quantcraft.process.runtime import chart_indicatorslie un instantané — sûr dans le fichier d'algo racine, faux dans un module d'aide séparé. - Ajoutez
NaNpendant le préchauffage, ne sautez pas. Sauter des lignes dans une liste et pas dans l'autre décale toute la ligne ;addécarte les lignes non finies pour vous. - Utilisez les horodatages des barres pour
time, tels quels. Letd'une barre de backtest est souvent une chaîne ISO (p. ex."2024-06-10T13:30:00+00:00", ou"2024-01-02"pour les barres journalières B2) et est en UTC. Ne supposez pas queint(b["t"])fonctionne, et ne le reconstruisez pas endatetimenaïf — il serait lu comme heure locale et décalerait chaque point. - Les
returndeon_finishsont silencieux. Pas de série et pas d'erreur signifie généralement unreturnanticipé ; faites un print verssys.stderrà chaque garde. - Pas depuis
qc_ohlcv. Il vautNonelors d'une exécution ciblant un screener et inclut sinon des barres de préchauffage que le graphique ne dessine jamais. - Des
idstables aident au style. Réutiliser le mêmeidd'une exécution à l'autre permet au graphique de garder couleurs et ordre stables, et dériverid/namede la même constante que celle de la stratégie garde la légende honnête. - Choisissez le bon volet. Les indicateurs bornés (RSI 0–100) appartiennent à
"separate"— les mettre sur"main"déformera l'échelle des prix. - Histogramme = pas de
line_style; ligne =line_styleobligatoire. Une ligne sans lui lèveValueError. - Exécutions Screener : passez toujours
symbol. Il n'y a pas de symbole d'horloge de repli.

