Documentation1.0 (latest)
PagesModèle de compte · 9 / 29

Modèle de compte

Chaque stratégie QuantCraft s'exécute sur un compte de trading papier intégré. Ce compte suit votre trésorerie, détient vos positions ouvertes, comptabilise le profit et la perte réalisés lorsque vous clôturez des opérations, et calcule les métriques de performance à la fin d'un backtest.

Vous ne le construisez pas vous-même en usage normal : le moteur de backtest le crée à partir de l'onglet Simulation de la fenêtre Run backtest (solde initial, commission / slippage) et l'affecte à quantcraft.process.runtime.account avant l'exécution de on_init(). Un SL/TP par défaut optionnel et un dimensionnement basé sur le risque peuvent être branchés via la configuration / les clés .qcs lorsqu'elles sont présentes ; la fenêtre n'expose actuellement pas de champs SL/TP / risque. Les callbacks reçoivent les données de marché comme décrit dans Cycle de vie de la stratégie et Données OHLCV et de barres ; les métriques apparaissent dans Résultats des tests.


Démarrage rapide

import quantcraft.process.runtime as rt def on_init(): pass def on_bar(bar_index, bar, fundamentals=None, symbol=None): account = rt.account px = float(bar["close"]) snap = account.open_trade("AAPL", "long", 10, px) if snap["open_positions"]: pid = snap["open_positions"][0]["id"] account.close_trade(pid, px + 1.0) def on_finish(bars, symbol=None, bars_by_symbol=None): last_px = float(bars[-1]["close"]) metrics = rt.account.refresh_metrics({"AAPL": last_px})

Voilà le schéma complet : ouvrir une opération, conserver l'identifiant de la position, la clôturer plus tard et (en option) actualiser les métriques à la fin.

Ce que vous lisez sur l'instance : account.cash, account.balance, account.realized_pnl, account.closed_trades et account.open_positions_payload(). Il n'existe pas d'attribut account.open_positions — les jambes ouvertes proviennent du snapshot (snap["open_positions"]) ou de open_positions_payload().

Les exécutions forward passent de vrais ordres. Dans un test forward sur graphique ou une exécution en masse, runtime.account est un compte de courtier réel — une sous-classe de PaperAccount avec la même API qui envoie de vrais ordres à votre compte de courtier connecté (papier ou réel), y compris des ordres bracket SL/TP pour les actions. Traitez open_trade / close_trade comme de vraies opérations. Le trading comporte un risque de perte.


Ce que fait PaperAccount

Le compte simule un compte de trading au comptant simple :

  • Suit le solde initial, la trésorerie, le solde et le capital (equity).
  • Détient des positions longues et courtes dans le même compte.
  • Comptabilise le P/L réalisé lorsque vous clôturez une opération et suit le P/L latent des positions ouvertes.
  • Maintient un historique du capital alimenté à chaque mise à jour de prix.
  • Calcule en option un ensemble complet de métriques de performance (Sharpe, Sortino, drawdown, CAGR, etc.).

Ce qu'il ne modélise pas : la marge, le coût d'emprunt ni l'exécution automatique des SL/TP. Les valeurs SL/TP stockées ne sont que des nombres — c'est à votre code de stratégie de vérifier les prix et d'appeler close_trade lorsqu'un stop ou un objectif est atteint. La commission et le slippage s'appliquent lorsque des modèles sont configurés dans l'onglet Simulation — voir ci-dessous.

Les dividendes et les fractionnements sont modélisés lorsque les données de calendrier sont activées pour l'exécution. À la date d'effet du fractionnement, le nombre d'actions est multiplié par le ratio et le coût de base divisé par celui-ci (la courbe de capital reste continue). À la date ex, qty × amount est crédité en trésorerie pour les positions longues et débité pour les courtes. Le dividende alimente balance et le capital mais pas realized_pnl — lisez total_dividend_income et total_splits_applied dans account.refresh_metrics(). Voir Données de calendrier.

La commission et le slippage se configurent dans l'onglet Simulation de la fenêtre Run backtest. Lorsqu'ils sont activés, les exécutions sont ajustées automatiquement ; vous ne les définissez pas en Python. La fenêtre Run utilise par défaut une commission par action de 0,001 $ par action avec un minimum de 0 $, et un slippage de type part du volume. (Un PaperAccount que vous construisez vous-même n'a par défaut aucune commission et aucun slippage.)


Configuration du compte

Vous configurez le compte dans l'onglet Simulation de Run backtest :

ParamètreRôle
starting_balanceTrésorerie de départ du compte.
Modèles de commission / slippageComment les exécutions sont ajustées — voir ci-dessous.

Un SL/TP par défaut optionnel et un dimensionnement par le risque (sl_pct / sl_dollars, tp_pct / tp_dollars, risk_pct_per_trade / risk_fixed_per_trade) peuvent encore être appliqués depuis des assistants .qcs / de configuration lorsqu'ils sont présents, mais la fenêtre Run backtest n'expose pas actuellement ces champs. Lorsqu'ils sont définis :

  • Les champs en pourcentage acceptent soit des pourcentages entiers comme 2, soit des fractions comme 0.02 — les deux signifient la même chose.
  • Pour les positions longues, le stop est sous l'entrée et l'objectif au-dessus de l'entrée. Les positions courtes inversent cela.
  • Les valeurs par défaut s'appliquent à chaque nouvelle opération, sauf si vous les remplacez à chaque appel.

Pour les notebooks ou tests uniquement (hors flux de backtest de l'IDE), vous pouvez construire un compte manuellement :

account = PaperAccount( 100_000, sl_pct=2, tp_pct=4, risk_pct_per_trade=1, slippage_model=None, # optional; defaults to no slippage (IDE engine wires from Simulation tab) commission_model=None, # optional; defaults to zero commission (IDE engine wires from Simulation tab) equity_history_cap=None, # optional; keep only the newest N equity_history points )

Commission et slippage (onglet Simulation)

L'onglet Simulation de la fenêtre Run backtest configure la façon dont les exécutions sont ajustées pour tenir compte des coûts de trading. Les réglages s'appliquent automatiquement à runtime.account — vous ne les configurez pas en Python. Voir Backtests et Préréglages d'exécution (.qcs) pour enregistrer ces options.

Modèles de commission

ModèleParamètresComportement
Par action (défaut)Coût par action ($), minimum par opération ($)max(qty × cost, min) lorsque le minimum > 0
Par opérationCoût fixe par ordre ($)Même frais quelle que soit la quantité
Zéro—Aucune commission — utile pour tester l'alpha pur

Modèles de slippage

ModèleParamètresComportement
Part du volume (défaut)Limite de volume (défaut 0,025), impact sur le prix (défaut 0,1)L'impact évolue avec (qty / bar_volume)² ; les achats sont exécutés plus haut, les ventes plus bas. À l'ouverture uniquement, volume_limit réduit aussi silencieusement la quantité à volume_limit × bar_volume — vérifiez le snapshot renvoyé pour connaître la taille réellement obtenue. (Si rien ne reste, open_trade lève qty after volume_limit cap is not positive.) Les clôtures ne sont jamais plafonnées. Volume de barre nul / absent → aucun impact.
Spread fixeSpread total par action ($)Demi-spread appliqué à chaque jambe d'exécution
Zéro—Exécution au prix exact demandé

Les enregistrements d'opérations clôturées incluent commission, slippage_cost, raw_entry_price et raw_exit_price. Les entry_price / exit_price stockés sont les prix d'exécution après slippage. account.refresh_metrics() expose total_commission et total_slippage_cost.


Positions

Une position représente une opération ouverte sur un seul symbole. Vous ne construisez pas les positions directement — elles sont créées par open_trade(...) et supprimées par close_trade(...), qui peut aussi clôturer une partie d'une position et laisser le reste en place.

Ouvrir une opération

snap = account.open_trade( symbol, # "AAPL" side, # "long" or "short" qty, # integer or float quantity entry_price, # float, the requested price (slippage/commission applied) sl_price=None, # optional stop price (overrides default sl_pct/sl_dollars) tp_price=None, # optional target price (overrides default tp_pct/tp_dollars) position_id=None, )

L'appel renvoie un snapshot du compte (voir ci-dessous). La position nouvellement ouverte apparaît dans snap["open_positions"] — récupérez son id si vous voulez la clôturer plus tard par identifiant. L'entry_price stocké est l'exécution après slippage ; le prix demandé est conservé dans raw_entry_price.

Une ouverture longue est rejetée avec insufficient cash for long lorsque qty × fill_price + commission (prix d'exécution après slippage) dépasse la trésorerie. Le snapshot renvoyé valorise ce symbole au prix d'exécution.

Dimensionner par le risque

Si vous voulez que le compte dimensionne l'opération d'après votre budget de risque (l'un de risk_pct_per_trade ou risk_fixed_per_trade) et une distance de stop, utilisez :

qty = account.position_size_for_risk(entry_price, stop_price)

Cela exige qu'exactement un mode de risque soit configuré et que la distance de stop ne soit pas nulle. Par défaut, risk_pct_per_trade utilise un pourcentage de la trésorerie ; passez equity_for_risk=... pour dimensionner par rapport au capital.

Clôturer une opération

account.close_trade(position_id, exit_price) # close the whole position account.close_trade(position_id, exit_price, qty=4) # scale out 4 units, keep the rest

La signature complète est close_trade(position_id, exit_price, *, qty=None) — qty est uniquement nommé (keyword-only).

open_trade / close_trade prennent un prix demandé, puis appliquent les modèles de slippage et de commission de l'exécution. L'exit_price stocké est l'exécution ; le prix demandé est conservé dans raw_exit_price. Un schéma courant de simulation de backtest : demander le tick synthétique de clôture de la barre depuis on_tick :

def on_tick(bar_index, tick_in_bar, price, bar, fundamentals=None, calendar=None, symbol=None): if tick_in_bar == 3: account.close_trade(pid, price)

Sur les exécutions forward, utilisez à la place le price en direct de chaque cotation du courtier dans on_tick au lieu de vérifier tick_in_bar.

Clôtures partielles (qty=)

Passer qty ne clôture que ce nombre d'unités et laisse le reste ouvert sur place — même identifiant de position, même entry_price, même entry_time — de sorte que la position restante conserve son coût de base et sa durée de détention :

pid = account.open_positions_payload()[0]["id"] account.close_trade(pid, px, qty=4) # scale out 4 units, keep the rest

L'enregistrement d'une clôture partielle dans closed_trades porte partial: True et remaining_qty.

  • Les coûts sont facturés sur les unités réellement échangées, et la commission d'ouverture du lot est répartie au prorata, de sorte qu'une suite de clôtures partielles facture exactement ce qu'une seule clôture complète facturerait.
  • Omettez qty — ou passez une valeur égale ou supérieure à la taille de la position — pour une clôture complète.
  • Le reste demeure une position ouverte normale : elle continue d'être valorisée à chaque étape et apparaît dans snap["open_positions"] sous le même identifiant, ce qui vous permet de la réduire à nouveau plus tard.

Vous pouvez aussi clôturer en masse par symbole. symbol= est obligatoire dans les exécutions multi-symboles afin de ne pas clôturer par accident les positions de tous les tickers à la fois :

account.close_all_longs(exit_price, symbol="AAPL") account.close_all_shorts(exit_price, symbol="AAPL")

Autres assistants

MéthodeRôle
open_positions_payload(prices=None)Jambes ouvertes sous forme de dicts, chacune avec last_price et unrealized_pnl aux prices (par défaut : dernier tick).
open_position_symbols()Ensemble des symboles ayant au moins une jambe ouverte.
position_unrealized_pnl(pos, prices)P/L ouvert d'une OpenPosition.
apply_split(symbol, ratio, *, when=None) / apply_dividend(symbol, amount_per_share, *, estimated=False, when=None)Opérations sur titres — le moteur les appelle lorsque les données de calendrier sont activées ; voir Données de calendrier.
corporate_actionsHistorique des fractionnements / dividendes appliqués jusqu'ici.
set_bar_volume(symbol, volume)Volume de barre utilisé par le slippage de type part du volume ; le moteur le définit à chaque barre.

Prix de sortie et indicateurs : Passez le prix que vous visez (p. ex. float(bar["close"]) depuis on_bar) ; le moteur applique quand même le slippage / la commission. Il ne réécrit pas votre demande en une SMA ou un autre indicateur. En backtest, tick_in_bar == 3 dans on_tick est un tick de clôture synthétique (simulation OHLC de la barre, pas de vraies données de ticks). Sur les exécutions forward, clôturez en utilisant le price en direct de on_tick à chaque mise à jour du courtier.


Trésorerie, solde, capital et PnL

Ce sont les champs de suivi monétaire essentiels, disponibles sur le compte et dans chaque snapshot :

ChampSignification
starting_balanceLa trésorerie avec laquelle le compte a démarré.
cash / balanceTrésorerie réglée après ouvertures/clôtures. Les ouvertures longues débitent qty × fill_price + commission ; les ventes à découvert créditent qty × fill_price − commission. Les entry_price / exit_price stockés sont les prix d'exécution après slippage.
equityTotal valorisé au marché : cash + Σ(long qty × mark) − Σ(short qty × mark).
unrealized_pnlSomme du P/L des positions ouvertes, valorisées aux cours actuels.
realized_pnlProfit/perte cumulé des opérations clôturées.

Comment les cours de valorisation sont mis à jour :

  • Le moteur appelle tick(prices, time=...) sur le compte à chaque pas de prix simulé, de sorte que equity, unrealized_pnl et l'historique du capital restent automatiquement à jour.
  • Si le cours de valorisation d'un symbole est absent de la table prices, cette position est valorisée à son prix d'entrée — le P/L latent de cette jambe est 0 tant que vous ne fournissez pas une vraie cotation.
  • close_trade reporte les cours précédents et valorise le symbole clôturé à son prix de sortie, de sorte que les positions laissées ouvertes — y compris le reste d'une clôture partielle — restent valorisées. refresh_metrics() sans argument réutilise les cours du snapshot le plus récent (open_trade / close_trade / tick) et ne revient au dernier capital / à la trésorerie que lorsque cette table est vide.

Dans on_bar et on_tick, vous n'avez pas besoin d'appeler account.tick(...) vous-même — le moteur s'en charge. Il avance avant chaque on_tick, mais on_bar s'exécute avant les ticks de ce pas ; dans on_bar, les derniers cours sont donc encore la clôture de la barre précédente. Un tick redondant ajoute un point supplémentaire à equity_history. Préférez passer des last_prices explicites à refresh_metrics({symbol: last_px}) dans on_finish lorsque quelque chose peut encore être ouvert.

Opérations courtes et trésorerie

Les positions longues et courtes sont toutes deux prises en charge dans le même compte :

  • Ouverture courte — le produit augmente la trésorerie (moins la commission lorsqu'elle est configurée).
  • Clôture courte (achat pour couvrir) — réduit la trésorerie.

Il n'y a ni modèle de marge ni coût d'emprunt : les ventes à découvert sont donc une simulation pure de trésorerie et de valorisation (plus commission/slippage lorsque des modèles sont définis).


Snapshots

Chaque appel à open_trade, close_trade et tick renvoie le même dictionnaire snapshot décrivant l'état du compte juste après l'appel. Clés utiles :

CléContenu
starting_balanceTrésorerie initiale.
cash / balanceTrésorerie actuelle.
equityCapital actuel valorisé au marché.
unrealized_pnlP/L des positions ouvertes aux cours actuels.
realized_pnlP/L réalisé cumulé.
open_positionsListe de dicts de positions ouvertes : id, symbol, side, qty, entry_price, sl_price, tp_price, raw_entry_price, commission_at_open, entry_time (peut être null). Il n'y a aucun champ de P/L par jambe dans les lignes de positions ouvertes — lisez unrealized_pnl dans le snapshot pour le P/L latent agrégé.
closed_tradesListe des opérations clôturées : position_id, symbol, side, qty, prix d'entrée/sortie, P/L réalisé, commission (inclut la part de cette clôture dans la commission d'ouverture), slippage_cost (slippage d'entrée + de sortie), raw_exit_price, raw_entry_price optionnel, et entry_time / exit_time ; les clôtures partielles ajoutent partial et remaining_qty. Également disponible via account.closed_trades sur l'instance.
tick_pricesCopie de la table de prix utilisée pour ce snapshot. close_trade reporte les cours précédents et valorise le symbole clôturé à son prix de sortie, de sorte que les jambes ouvertes restent valorisées à travers une clôture.

Il n'existe pas d'API account.get_open_positions(symbol). Filtrez snap["open_positions"] (ou account.open_positions_payload()) par symbol en majuscules, ou lisez account.closed_trades pour l'historique clôturé.

Cela facilite l'inspection du compte dans votre stratégie :

snap = account.open_trade("AAPL", "long", 10, px) print("equity now:", snap["equity"]) print("open positions:", len(snap["open_positions"]))

Métriques de performance

Lorsque l'exécution se termine, vous pouvez demander au compte de calculer un ensemble complet de métriques :

def on_finish(bars, symbol=None, bars_by_symbol=None): last_px = float(bars[-1]["close"]) metrics = account.refresh_metrics({"AAPL": last_px})

L'argument est une table {symbol: last_price} utilisée comme valorisation finale afin que les positions ouvertes soient correctement évaluées. Utilisez la liste bars passée à on_finish (la plus ancienne en premier) pour obtenir la dernière clôture — c'est la série canonique sur tout l'historique.

Vous pouvez aussi passer deux arguments optionnels :

  • risk_free_rate=0 — utilisé par les ratios de type Sharpe/Sortino.
  • calmar_annualized_return=None — remplace le rendement annualisé utilisé par Calmar.

Métriques disponibles

refresh_metrics(...) renvoie un dictionnaire qui comprend :

GroupeChamps
Rendementstotal_pnl, total_return, cagr
Risquemax_drawdown, volatility, risk_adjusted_return
Ratiossharpe_ratio, sortino_ratio, calmar_ratio, profit_factor
Statistiques d'opérationsaverage_trade_expectancy, average_win, average_loss, total_commission, total_slippage_cost
Expositioncurrent_exposure, average_exposure, max_exposure
Opérations sur titrestotal_dividend_income, total_splits_applied

Remarques :

  • Les ratios basés sur les opérations utilisent le rendement par opération, défini comme realized_pnl / starting_balance.
  • total_return et cagr comparent le capital actuel au solde initial.
  • Pour un CAGR / Calmar significatif, le moteur a besoin d'informations de calendrier — il transmet déjà les horodatages lorsqu'il fait avancer le compte, vous n'avez donc généralement rien de particulier à faire.
  • Certains ratios peuvent renvoyer inf lorsque le dénominateur est nul et que la performance est favorable (par exemple, aucune opération perdante).

Les métriques sont aussi présentées dans la vue Test Results de l'éditeur et dans la charge utile structurée du résultat de l'exécution.


Mise en pratique

Utilisation typique dans une stratégie :

  1. Configurez le solde initial et la commission / le slippage dans l'onglet Simulation (SL/TP / risque optionnels via .qcs / le code lorsque c'est pris en charge).
  2. Ouvrez des opérations depuis on_bar (ou depuis on_tick pour des exécutions intrabarre) avec account.open_trade(...) — conservez l'id renvoyé si vous en aurez besoin.
  3. Clôturez des opérations avec account.close_trade(position_id, exit_price) — ou avec qty= pour réduire la position et laisser le reste du lot ouvert.
  4. Lisez snap["equity"], snap["cash"], snap["unrealized_pnl"], etc. chaque fois que vous voulez inspecter le compte.
  5. Dans on_finish, appelez account.refresh_metrics({symbol: last_price}) pour renseigner les métriques de performance du résultat.

Voilà tout le modèle de compte — une petite surface d'API, mais suffisante pour simuler des positions longues, courtes, des flux de trésorerie, le P/L et un jeu complet de métriques pour toute stratégie exprimable avec les callbacks du cycle de vie.

Modèle de compte — Documentation (1.0 (latest)) | QuantCraft