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.accountest un compte de courtier réel — une sous-classe dePaperAccountavec 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. Traitezopen_trade/close_tradecomme 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ètre | Rôle |
|---|---|
starting_balance | Trésorerie de départ du compte. |
| Modèles de commission / slippage | Comment 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 comme0.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èle | Paramètres | Comportement |
|---|---|---|
| Par action (défaut) | Coût par action ($), minimum par opération ($) | max(qty × cost, min) lorsque le minimum > 0 |
| Par opération | Coû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èle | Paramètres | Comportement |
|---|---|---|
| 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 fixe | Spread 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 restLa 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 restL'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éthode | Rô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_actions | Historique 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"])depuison_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 == 3danson_tickest 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 lepriceen direct deon_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 :
| Champ | Signification |
|---|---|
starting_balance | La trésorerie avec laquelle le compte a démarré. |
cash / balance | Tré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. |
equity | Total valorisé au marché : cash + Σ(long qty × mark) − Σ(short qty × mark). |
unrealized_pnl | Somme du P/L des positions ouvertes, valorisées aux cours actuels. |
realized_pnl | Profit/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 queequity,unrealized_pnlet 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_tradereporte 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_bareton_tick, vous n'avez pas besoin d'appeleraccount.tick(...)vous-même — le moteur s'en charge. Il avance avant chaqueon_tick, maison_bars'exécute avant les ticks de ce pas ; danson_bar, les derniers cours sont donc encore la clôture de la barre précédente. Untickredondant ajoute un point supplémentaire àequity_history. Préférez passer deslast_pricesexplicites àrefresh_metrics({symbol: last_px})danson_finishlorsque 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_balance | Trésorerie initiale. |
cash / balance | Trésorerie actuelle. |
equity | Capital actuel valorisé au marché. |
unrealized_pnl | P/L des positions ouvertes aux cours actuels. |
realized_pnl | P/L réalisé cumulé. |
open_positions | Liste 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_trades | Liste 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_prices | Copie 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 :
| Groupe | Champs |
|---|---|
| Rendements | total_pnl, total_return, cagr |
| Risque | max_drawdown, volatility, risk_adjusted_return |
| Ratios | sharpe_ratio, sortino_ratio, calmar_ratio, profit_factor |
| Statistiques d'opérations | average_trade_expectancy, average_win, average_loss, total_commission, total_slippage_cost |
| Exposition | current_exposure, average_exposure, max_exposure |
| Opérations sur titres | total_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_returnetcagrcomparent 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
inflorsque 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 :
- 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). - Ouvrez des opérations depuis
on_bar(ou depuison_tickpour des exécutions intrabarre) avecaccount.open_trade(...)— conservez l'idrenvoyé si vous en aurez besoin. - Clôturez des opérations avec
account.close_trade(position_id, exit_price)— ou avecqty=pour réduire la position et laisser le reste du lot ouvert. - Lisez
snap["equity"],snap["cash"],snap["unrealized_pnl"], etc. chaque fois que vous voulez inspecter le compte. - Dans
on_finish, appelezaccount.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.

