Documentation1.0 (latest)
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

  1. 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": ...}.
  2. Vous ajoutez chaque série au collecteur du runtime avec des options d'affichage (quel volet, ligne ou histogramme, nom/couleur/symbole optionnels).
  3. À la fin du backtest, le moteur fusionne cette collection dans le résultat sous chart_indicator_series.
  4. 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_init

chart_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_indicators et non from ... import chart_indicators ? Les poignées du runtime sont propres à chaque session ; un from ... import lie 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 dans sys.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 : zip tronque à 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 t de la barre tel quel. coerce_chart_time accepte les chaînes ISO, les secondes/millisecondes epoch et datetime. Les chaînes sans fuseau horaire — y compris les dates journalières QuantCraft B2 comme "2024-01-02" — sont lues comme UTC. Un objet datetime naïf, en revanche, est lu comme heure locale, ce qui décale chaque point. Les lignes de barres portent un t UTC ; conservez-le.
  • add normalise déjà chaque time et écarte les valeurs non finies ; pré-filtrer avec coerce_chart_time / math.isnan dans votre propre boucle est donc optionnel.

Chaque return dans on_finish est 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 un print(..., 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éTypeRemarques
timenombre / datetime / chaîne ISONormalisé en secondes Unix (int). Transmettez l'horodatage de la barre — bar["t"] dans un callback, bars[i]["t"] dans on_finish — tel quel.
valuenombreDoit ê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(...) :

ArgumentObligatoireValeurs
paneoui"main" — superposition sur le graphique des prix/bougies. "separate" — son propre volet sous le graphique (idéal pour RSI, MACD, etc.).
renderoui"line" ou "histogram".
line_stylesi 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é).
idnonIdentifiant 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.
namenonNom d'affichage dans la légende du graphique. Vaut id par défaut.
colornonIndication de couleur (p. ex. "#22c55e").
symbolnonTicker 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_idnonAvec 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 :

  • pane contrôle le placement vertical :
    • "main" — se place sur les bougies de son graphique.
    • "separate" — obtient son propre volet sous les bougies.
  • symbol contrô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'id et le pane_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 client

Dans 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 :

ChampDescription
id, name, pane, render, pointsExactement ce que vous avez passé à add(...).
line_style"solid" / "dotted" pour les lignes, null pour les histogrammes.
colorPrésent uniquement si vous avez passé color=.
symbolPrésent uniquement si vous avez passé symbol= — ticker en majuscules afin que l'IDE puisse le placer sur le bon graphique.
pane_idPré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 points obtenus à 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 des bars de on_finish que les indicateurs que vous n'avez pas suivis barre par barre.
  • Lisez le collecteur via rt.chart_indicators. from quantcraft.process.runtime import chart_indicators lie un instantané — sûr dans le fichier d'algo racine, faux dans un module d'aide séparé.
  • Ajoutez NaN pendant 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. Le t d'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 que int(b["t"]) fonctionne, et ne le reconstruisez pas en datetime naïf — il serait lu comme heure locale et décalerait chaque point.
  • Les return de on_finish sont silencieux. Pas de série et pas d'erreur signifie généralement un return anticipé ; faites un print vers sys.stderr à chaque garde.
  • Pas depuis qc_ohlcv. Il vaut None lors d'une exécution ciblant un screener et inclut sinon des barres de préchauffage que le graphique ne dessine jamais.
  • Des id stables aident au style. Réutiliser le même id d'une exécution à l'autre permet au graphique de garder couleurs et ordre stables, et dériver id / name de 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_style obligatoire. Une ligne sans lui lève ValueError.
  • Exécutions Screener : passez toujours symbol. Il n'y a pas de symbole d'horloge de repli.
Séries d'indicateurs — Documentation (1.0 (latest)) | QuantCraft