Documentation1.0 (latest)
PagesCycle de vie d'une stratégie · 8 / 29

Cycle de vie de la stratégie

Une stratégie QuantCraft est un fichier Python ordinaire qui définit un petit ensemble de fonctions de callback. Le moteur de backtest appelle ces fonctions dans un ordre fixe pendant qu'il parcourt les données historiques. Vous ne les appelez pas vous-même — c'est le moteur qui le fait. Utilisez Test pour les piloter (exécution du code).

Le cycle de vie, du début à la fin :

on_init → on_bar → on_tick (×4 per bar) → account tick(close) → on_finish └─────────── repeats for every bar ───────────┘

En backtest, on_tick se déclenche quatre fois par barre sur un OHLC synthétique issu de chaque barre (sans véritable historique de ticks). Après ces quatre ticks, le moteur effectue une dernière valorisation du compte à la clôture — pas un cinquième on_tick. on_timer n'est disponible que sur les exécutions forward (forward sur graphique, forward en masse / QuantCloud) lorsque timerIntervalMs est défini — la fenêtre Run backtest de l'IDE n'a pas de commande de minuteur. Sur les exécutions forward, on_tick s'exécute à chaque mise à jour de prix du courtier connecté ; aux frontières intrajournalières, on_tick puis on_bar se déclenchent souvent — ne supposez pas l'ordre du backtest.

Cette page présente chaque callback, la façon dont les stratégies multi-symboles s'y raccordent, et les bases nécessaires pour écrire votre première stratégie.


Le fichier de stratégie

Une stratégie minimale ressemble à ceci :

import quantcraft.process.runtime as bt # account, chart_indicators, qc_* handles from quantcraft import Timeframe import quantcraft.runtime as rt def on_init(symbols=None): pass def on_bar(bar_index, bar, fundamentals=None, calendar=None, symbol=None): close_px = float(bar["close"]) daily = rt.get_bar(Timeframe.DAILY, shift=1) def on_tick(bar_index, tick_in_bar, price, bar, fundamentals=None, calendar=None, symbol=None): if tick_in_bar == 3: close_px = price def on_finish(bars, symbol=None, bars_by_symbol=None): pass

Quelques règles s'appliquent à tous les callbacks :

  • Ils doivent être des fonctions de niveau supérieur dans votre fichier (pas imbriquées ni dans une classe).
  • Le moteur de backtest exige des on_init, on_bar, on_tick et on_finish appelables (sinon l'exécution échoue avec User script must define callable: …). Les boutons Test, Compile et Debug Test de l'IDE apparaissent lorsque l'onglet .py actif définit on_init / on_bar / on_finish ; un on_tick manquant fait tout de même échouer le démarrage du backtest. on_timer est optionnel et n'est utilisé que sur les exécutions forward avec un intervalle de minuteur.
  • N'ajoutez que les arguments optionnels que vous utilisez. calendar, symbols (sur on_init) et les arguments de on_finish sont associés par nom ; fundamentals et symbol sont associés en essayant tour à tour les formes courantes, donc gardez-les dans l'ordre documenté. La signature complète la plus sûre est def on_bar(bar_index, bar, fundamentals=None, calendar=None, symbol=None). Un calendar déclaré reçoit toujours le handle du calendrier ou None — jamais l'objet de données fondamentales.
  • Dans les callbacks, vous passez des ordres via account et enregistrez les valeurs d'indicateurs via chart_indicators (séries d'indicateurs) — tous deux lus depuis quantcraft.process.runtime.

Globales du runtime

Le moteur expose ces éléments depuis quantcraft.process.runtime avant l'exécution des callbacks :

NomRôle
accountInstance de PaperAccount configurée depuis l'onglet Simulation (solde + commission / slippage). Sur les exécutions forward, c'est un compte de courtier réel avec la même API qui passe de vrais ordres — voir Modèle de compte
chart_indicatorsCollecteur ChartIndicatorSeries fusionné dans le résultat — collectez les valeurs au fil du parcours des barres, émettez les séries dans on_finish
qc_fundamentalsHandle QcFundamentals optionnel lorsque les données fondamentales sont chargées
qc_fama_frenchQcFamaFrench optionnel lorsque les facteurs Fama-French globaux sont chargés ; préférez fama_french.qc dans les callbacks — voir Facteurs Fama-French
qc_calendarBacktestCalendar | None — données de calendrier lorsque Include calendar data est activé ; couvre tous les symboles tradés. Backtests uniquement — toujours None sur les exécutions forward. Les callbacks peuvent recevoir le même handle en nommant un argument calendar — voir Données de calendrier
qc_ohlcv_by_tfTampons optionnels d'intervalles secondaires indexés par chaîne d'intervalle (avancé ; préférez get_bar)
get_bar(tf, shift=1)API recommandée pour l'intervalle principal et les intervalles supplémentaires — renvoie un BarSnapshot ou None ; à importer via import quantcraft.runtime as rt

Préférez l'argument bars de on_finish(bars, ...) pour les calculs en série complète — ne vous appuyez pas sur le qc_ohlcv de niveau module dans les stratégies de l'IDE (il est souvent None à l'import, vaut None lors d'une exécution ciblant un screener, et couvre sinon toute la plage chargée préchauffage compris). Lisez les prix depuis l'objet bar dans les callbacks. isinstance(bar, dict) est False — OhlcvBarView est un Mapping (l'accès de type dict fonctionne toujours).

Lisez-les comme des attributs de module. Chaque handle repose sur une valeur propre à la session, donc bt.account / bt.chart_indicators se rapportent toujours à cette exécution. from quantcraft.process.runtime import account lie un instantané — acceptable dans le fichier d'algo racine en backtest, car le moteur relie ces noms après l'avoir chargé (les exécutions forward ne relient que account, qc_fundamentals et qc_fama_french ; utilisez donc bt.chart_indicators et rt.qc_ohlcv* dans ce cas), mais incorrect dans un module d'aide séparé, qui reste en cache dans sys.modules et conserverait le handle de la première session. Voir Poignées de module et noms importés.

Intervalles secondaires

L'intervalle principal de l'exécution est toujours disponible via rt.get_bar. Sélectionnez des tailles de barre supplémentaires dans la liste Additional timeframes de la fenêtre Run backtest, onglet Symbol & data (Backtests), puis lisez-les depuis n'importe quel callback :

from quantcraft import Timeframe import quantcraft.runtime as rt def on_bar(bar_index, bar, fundamentals=None, calendar=None, symbol=None): daily = rt.get_bar(Timeframe.DAILY, shift=1) if daily and bar.close[0] > daily.close: account.open_trade(...)

La sémantique complète de shift, l'énumération Timeframe et les réserves propres aux exécutions forward figurent dans Données OHLCV et de barres.

Moment d'exécution des ordres

  • bar["close"] dans on_bar est la clôture de la barre d'exécution courante.
  • account.open_trade / close_trade prennent un prix demandé, puis appliquent les modèles de slippage et de commission de l'exécution. Les entry_price / exit_price stockés sont les prix d'exécution ; les prix demandés sont conservés dans raw_entry_price / raw_exit_price. Avec le slippage par part du volume par défaut, les exécutions diffèrent généralement du prix que vous transmettez.
  • Pour viser la clôture de la barre depuis on_bar, transmettez float(bar["close"]) (toujours soumis au slippage/à la commission).
  • Pour exécuter pendant la simulation intrabarre, appelez depuis on_tick lorsque tick_in_bar == 3 — price est alors la clôture de ce symbole pour la ligne.

on_init — tout préparer

S'exécute juste après que le moteur a préparé le compte, chargé les données de marché et construit le runtime — mais avant le traitement de toute barre.

Utilisez-la pour :

  • Initialiser les variables que votre stratégie conserve d'une barre à l'autre (compteurs, machines à états, valeurs de paramètres).
  • Configurer des indicateurs ou des tampons de préchauffage.
  • Lire les paramètres d'entrée afin qu'ils pilotent le reste de l'exécution.
def on_init(symbols=None): global lookback, position_size lookback = 20 position_size = 100

Vous n'avez pas accès à une barre courante dans on_init — la simulation n'a pas encore commencé.

Paramètre symbols (exécutions avec screener uniquement) : Lorsque le backtest utilise une source de symboles Screener, on_init est de nouveau déclenchée à chaque rééquilibrage avec symbols=<liste de tickers> pour le nouvel univers. Cela permet à votre stratégie de réagir à un nouvel ensemble de tickers sans redémarrer la simulation.

  • Au démarrage de l'exécution, on_init se déclenche deux fois de suite — une fois avec symbols=None (l'appel d'initialisation standard) et une fois avec symbols=<tickers initiaux>.
  • Les nouveaux déclenchements ne réinitialisent pas l'état de niveau module ; toutes les variables définies lors du premier appel sont toujours là.
  • Les stratégies qui définissent def on_init(): (sans paramètre) fonctionnent exactement comme avant — le moteur associe les arguments par nom, donc le paramètre manquant est simplement ignoré.

on_bar — une fois par barre

S'exécute une fois par barre d'exécution, avant les quatre ticks intrabarre de cette barre. C'est ici que la plupart des stratégies prennent leurs décisions.

def on_bar(bar_index, bar, fundamentals=None, calendar=None, symbol=None): if bar.bars_back < 1: return if bar.close[0] > bar.close[1]: account.open_trade(...)

Paramètres :

  • bar_index — indice, base zéro, de la barre d'exécution courante.
  • bar — la vue de la barre courante (OhlcvBarView). Se comporte comme un mapping en lecture seule (bar["open"], bar["high"], bar["low"], bar["close"], bar["volume"], bar["t"]) et expose aussi l'historique par décalages : bar.close[0] est la clôture de la barre courante, bar.close[1] celle de la barre précédente, et ainsi de suite. len(bar.close) et bar.bars_back indiquent la quantité d'historique disponible. Ce n'est pas un dict ordinaire (isinstance(bar, dict) est False).
  • fundamentals — instantané des données fondamentales pour cette barre, ou None s'il n'est pas configuré.
  • calendar — handle BacktestCalendar lorsque Include calendar data est activé, sinon None (toujours None sur les exécutions forward). Voir Données de calendrier pour l'API complète. Les arguments étant associés par nom, omettre ce paramètre laisse votre callback inchangé.
  • symbol — chaîne du ticker en majuscules. Utile dans les stratégies multi-symboles ; vous pouvez l'omettre si vous ne chargez qu'un seul symbole.

Schémas courants :

  • Décider des entrées/sorties à la clôture de la barre : transmettez bar.close[0] (ou float(bar["close"])) à account.open_trade / account.close_trade.
  • Calculer des indicateurs simples en lisant des décalages comme bar.close[0], bar.close[1], …
  • Ignorer les premières barres qui n'ont pas assez d'historique (if bar.bars_back < N: return).

Astuce : on_bar s'exécute toujours avant les callbacks de tick de la même barre.


on_tick — mises à jour de prix intrabarre et en direct

on_tick a la même signature en backtest et en forward, mais le moment où elle se déclenche et la signification de price dépendent du mode d'exécution.

Backtest (Test)

S'exécute jusqu'à quatre fois par barre, après on_bar, pour simuler le mouvement à l'intérieur de la barre. Le moteur parcourt l'OHLC de chaque barre dans cet ordre :

tick_in_barSignification
0Ouverture de la barre
1Plus haut de la barre
2Plus bas de la barre
3Clôture de la barre

Pas de vraies données de ticks en backtest. Les exécutions historiques ne chargent pas l'historique de ticks du courtier. Les quatre appels par barre utilisent des prix synthétiques dérivés du seul OHLC de chaque barre. Vous pouvez approximer des exécutions intrabarre en simulation, mais le comportement de on_tick en backtest ne correspondra pas aux ticks forward réels. Pour des décisions à la clôture de la barre, on_bar suffit — on_tick avec tick_in_bar == 3 équivaut à agir sur la clôture de la barre en simulation.

def on_tick(bar_index, tick_in_bar, price, bar, fundamentals=None, calendar=None, symbol=None): if tick_in_bar == 3: # Backtest simulation: closing tick fill account.close_trade(position_id, price)

Utilisez on_tick en backtest lorsque le moment dans la barre compte en simulation — par exemple pour exécuter au tick de clôture, simuler un stop intrabarre ou évaluer le toucher du plus haut ou du plus bas de la barre.

Exécutions forward (forward sur graphique, forward en masse / QuantCloud)

Sur les exécutions forward, on_tick s'exécute chaque fois que la plateforme reçoit un prix de votre courtier connecté tant que votre algo est attaché. Il n'y a pas de parcours OHLC fixe en quatre ticks — chaque appel reflète une cotation en direct.

def on_tick(bar_index, tick_in_bar, price, bar, fundamentals=None, calendar=None, symbol=None): # Forward run: react to each broker price update if price <= stop_level: account.close_trade(position_id, price)
  • price — la dernière cotation du courtier pour ce symbole.
  • bar — la vue de la barre courante pour l'historique (même forme qu'en backtest).
  • tick_in_bar — peut rester présent pour la compatibilité de signature ; sur les exécutions forward, raisonnez sur price, pas sur des phases OHLC synthétiques.

Utilisez on_tick en forward pour la logique intrabarre en direct — stops, sorties suiveuses ou toute réaction aux valorisations entre les clôtures de barres.

Paramètres (les deux modes)

  • bar_index — identique à on_bar.
  • tick_in_bar — en backtest, 0, 1, 2 ou 3 (étape OHLC synthétique). Sur les exécutions forward, ne vous y fiez pas pour une logique de phase de barre.
  • price — étape OHLC synthétique en backtest ; cotation du courtier en direct sur les exécutions forward.
  • bar — la même vue de barre que dans on_bar.
  • fundamentals, calendar, symbol — même sens que dans on_bar.

Si vous n'avez besoin que de décisions à la clôture de la barre en backtest, vous pouvez ignorer entièrement on_tick et placer votre logique dans on_bar.


on_timer — callback périodique optionnel (exécutions forward uniquement)

Backtest de l'IDE : la fenêtre Run backtest ne définit pas d'intervalle de minuteur. Même si vous définissez on_timer dans votre fichier de stratégie, elle n'est pas appelée pendant les backtests de l'IDE. Placez la logique périodique ou temporelle dans on_bar ou on_tick pour les backtests.

Les exécutions forward (test forward sur graphique, forward en masse / QuantCloud) peuvent définir timerIntervalMs dans leur fenêtre d'exécution. Lorsqu'il est configuré, on_timer s'exécute à intervalle de temps fixe entre les ticks, uniquement si deux conditions sont réunies :

  1. Vous avez configuré timer_interval_ms / timerIntervalMs pour l'exécution (un entier positif en millisecondes).
  2. Votre fichier définit une fonction on_timer.
def on_timer(bar_index, tick_in_bar, price, bar, fundamentals=None, calendar=None, symbol=None): pass

La signature a la même forme que on_tick. Utilisez on_timer pour des comportements temporels (par exemple : rafraîchir un signal lent chaque minute, ou planifier une vérification indépendante du nombre de barres). Si timer_interval_ms n'est pas défini, on_timer n'est jamais appelée même si elle est définie.


Données de calendrier dans les callbacks

Lorsque Include calendar data est activé dans l'onglet Symbol & data, n'importe quel callback peut recevoir un handle BacktestCalendar en ajoutant un paramètre calendar :

def on_bar(bar_index, bar, calendar=None, symbol=None): if calendar is None: # always guard — not every run enables calendar data return days = calendar.days_to_earnings(bar["t"], symbol) if days is not None and days <= 2: return # avoid trading into an earnings print divs = calendar.dividends(bar["t"], symbol) if divs and (divs[-1].amount or 0) > 0.5: # amount can be None ...

Le même handle est disponible dans on_tick et on_timer — mais les données de calendrier sont réservées au backtest ; sur les exécutions forward, calendar vaut donc toujours None. Toutes les recherches sont point-in-time — dividends(bar["t"], symbol) ne renvoie que les dividendes qui étaient publics à cette barre, jamais les futurs.

Méthodes principales :

MéthodeRenvoie
dividends(bar_time, symbol=None)Dividendes publics à cette barre (du plus ancien au plus récent)
splits(bar_time, symbol=None)Fractionnements annoncés à cette barre (du plus ancien au plus récent)
next_earnings(bar_time, symbol=None){date, basis, estimated, …} ou None
days_to_earnings(bar_time, symbol=None)int ou None
has(symbol=None)Indique si ce symbole a des données de calendrier

Les listes vont du plus ancien au plus récent — [-1] est l'événement le plus récent. C'est l'inverse des données fondamentales, où l'indice 0 correspond à la période la plus récente.

La globale du runtime qc_calendar contient le même handle si vous préférez le lire depuis quantcraft.process.runtime plutôt que depuis l'argument du callback.

Voir Données de calendrier pour les schémas de champs d'événements, les détails de simulation des opérations sur titres et le dépannage.


on_finish — conclure

S'exécute une seule fois, après la dernière barre. Utilisez-la pour calculer les sorties finales, construire des indicateurs de graphique ou afficher un résumé.

def on_finish(bars, symbol=None, bars_by_symbol=None): closes = [b["close"] for b in bars] bt.chart_indicators.add(...)

Paramètres :

  • bars — liste chronologique de dictionnaires OHLC pour le symbole d'horloge, du plus ancien au plus récent — la fenêtre exécutée, les mêmes lignes que dessine le graphique de Test Results. Chaque entrée contient t, open, high, low, close, volume et, éventuellement, fundamentals. C'est la série canonique sur tout l'historique pour on_finish.
  • symbol — ticker en majuscules du symbole d'horloge.
  • Dans une exécution Screener, il n'y a pas de symbole d'horloge : bars vaut [] et symbol vaut "" ; utilisez donc bars_by_symbol.
  • bars_by_symbol — pour les exécutions multi-symboles, un dict associant chaque ticker à sa liste chronologique de barres (alignée sur l'horloge d'exécution). Pour les exécutions à un seul symbole, vous pouvez l'ignorer.

Usages courants :

  • Transmettre les séries d'indicateurs à chart_indicators.add(...) pour qu'elles apparaissent sur les graphiques de résultats. Préférez les valeurs que vous avez accumulées par symbole pendant le parcours des barres ; ne recalculez à partir de bars que les indicateurs que vous n'avez pas suivis barre par barre — voir Séries d'indicateurs.
  • Afficher ou journaliser un résumé final dans le Panneau de sortie.
  • Actualiser les métriques du portefeuille pour la charge utile du résultat.

Chaque return dans on_finish est silencieux. L'exécution réussit quand même et Test Results s'affiche toujours — un retour anticipé signifie simplement que vos séries ou métriques n'ont jamais été jointes, sans aucune erreur. Affichez vers sys.stderr à chaque garde pendant le débogage ; le message apparaît dans le Panneau de sortie.


Bases du multi-symboles

Les stratégies QuantCraft peuvent s'exécuter sur un symbole ou sur plusieurs à la fois. Le cycle de vie reste le même — seules les signatures des callbacks et les données qu'ils reçoivent changent légèrement.

Configurer plusieurs symboles

Dans l'onglet Symbol & data de Run backtest, sélectionnez un ou plusieurs tickers en majuscules. Tous les symboles listés sont chargés et simulés ensemble.

Le symbole d'horloge

Lorsque vous exécutez plusieurs symboles, l'un d'eux pilote l'horloge — ses horodatages de barres décident du moment où les callbacks s'exécutent. La fenêtre Run backtest n'a aucune commande de symbole principal. Ordre de résolution :

  1. Un SYMBOL = "AAPL" de niveau supérieur dans votre fichier de stratégie, si ce ticker figure dans la liste chargée.
  2. Sinon, le premier symbole chargé.

Avec une source de symboles Screener, il n'y a aucun symbole d'horloge — l'ensemble de symboles change au fil de l'exécution. rt.get_bar lit toujours les barres du symbole d'horloge (et renvoie None sur une exécution Screener) ; pour l'historique d'un autre ticker, utilisez donc les décalages bar.close[i] de ce ticker.

Le symbole d'horloge est aussi celui que on_finish(bars, symbol=...) utilise pour ses arguments bars et symbol.

Callbacks par symbole

Pour chaque barre d'exécution, le moteur parcourt les symboles dans l'ordre de la liste :

  1. on_bar s'exécute une fois par ticker chargé à l'heure de cette barre (avant tout tick de cette barre).
  2. Backtest uniquement : pour chaque tick_in_bar (0 = ouverture, 1 = plus haut, 2 = plus bas, 3 = clôture), on_tick s'exécute une fois par ticker sur un OHLC synthétique (pas de vraies données de ticks).
  3. Backtest uniquement : après les quatre tours de ticks, un tick(close_map) final du compte valorise le capital à la clôture — pas un cinquième on_tick.
  4. Exécutions forward : on_tick s'exécute à chaque mise à jour de prix du courtier pour chaque symbole à mesure que les cotations arrivent de votre courtier connecté (sans se limiter à quatre appels par barre). Aux frontières intrajournalières, on_tick puis on_bar se déclenchent souvent — ne supposez pas l'ordre du backtest.
  5. on_timer (uniquement sur les exécutions forward avec timerIntervalMs configuré — pas en backtest de l'IDE) suit le même schéma par ticker entre les ticks.

Cela signifie qu'un on_bar multi-symboles est invoqué, par exemple, trois fois par barre si vous avez trois symboles chargés. Utilisez le paramètre symbol pour savoir pour quel ticker l'appel est fait :

def on_bar(bar_index, bar, fundamentals=None, calendar=None, symbol=None): if symbol == "AAPL": ... elif symbol == "MSFT": ...

La vue bar appartient toujours au symbole pour lequel le callback a été invoqué. Il en va de même pour price dans on_tick.

Alignement « as-of ». Pour chaque heure de barre d'exécution, chaque ticker reçoit sa dernière barre à cette heure ou avant. Un ticker sans barre à cette heure précise (arrêt de cotation, minute manquante, calendrier de négociation différent) reçoit de nouveau sa barre précédente. Si vous comptez des barres ou des signaux par ticker, ignorez les répétitions en mémorisant le dernier bar["t"] vu pour ce symbole :

_last_t = {} def on_bar(bar_index, bar, fundamentals=None, calendar=None, symbol=None): if _last_t.get(symbol) == bar["t"]: return # same bar as last step for this ticker _last_t[symbol] = bar["t"] ...

on_finish en multi-symboles

on_finish ne s'exécute toujours qu'une fois, à la fin. Pour travailler avec l'historique de tous les symboles, utilisez bars_by_symbol :

def on_finish(bars, symbol=None, bars_by_symbol=None): if bars_by_symbol: for ticker, ticker_bars in bars_by_symbol.items(): ...

Mise en pratique

Une stratégie typique à un seul symbole :

  1. Définit des paramètres d'entrée ou un autre état dans on_init.
  2. Lit l'historique des prix (OHLCV) via bar.close[0], bar.close[1], … dans on_bar pour prendre ses décisions de trading, et appelle account.open_trade / account.close_trade.
  3. Utilise éventuellement on_tick — en backtest, pour des exécutions intrabarre synthétiques (p. ex. tick_in_bar == 3 pour le tick de clôture) ; sur les exécutions forward, pour réagir aux prix du courtier en direct entre les clôtures de barres.
  4. Utilise éventuellement on_timer sur les exécutions forward lorsque timerIntervalMs est configuré (indisponible dans Run backtest de l'IDE).
  5. Construit les indicateurs de graphique et finalise les résultats dans on_finish à l'aide de bars.

Une stratégie multi-symboles utilise les mêmes callbacks, se contente de se brancher sur l'argument symbol à l'intérieur et lit les données par ticker via la bar qu'elle reçoit — et via bars_by_symbol dans on_finish.

Besoin de plus de détails — voir Modèle de compte, Données fondamentales, Facteurs Fama-French, Données OHLCV et de barres, Séries d'indicateurs, ou l'onglet Documentation intégré dans la barre latérale droite (disposition de l'espace de travail).

Cycle de vie d'une stratégie — Documentation (1.0 (latest)) | QuantCraft