Aller au contenu

Créer un courtier personnalisé

Utilisez un courtier personnalisé pour connecter AQE à un service d’exécution. Votre adaptateur prend en charge le transport, les observations du compte et l’exécution des ordres. La stratégie conserve la maîtrise de ses insights ; le courtier reçoit des demandes d’ordres immuables plutôt que l’arbre complet des insights de la stratégie.

Ce guide suit les interfaces AQE 0.2.0. Alignez l’implémentation sur les traits de la révision AQE utilisée par votre stratégie. Commencez avec un transport simulé ou l’environnement de test du service, et non avec un compte de trading doté de fonds réels.

1. Implémenter la connexion et la gestion des ordres

Section intitulée « 1. Implémenter la connexion et la gestion des ordres »

Créez un type Rust implémentant aq_engine::core::broker::traits::{Broker, OrderManagementProvider}. Il doit être Send + Sync et posséder sa connexion, ses caches et sa file d’événements.

TraitMéthodes requisesResponsabilité
Brokerconnect, disconnect, is_connectedGérer le transport et libérer ses ressources, même après une connexion annulée.
Brokerget_name, get_account_typeIdentifier correctement l’adaptateur et l’environnement du compte.
OrderManagementProvidersubmit_order, cancel_orderSoumettre un BrokerOrderRequest et annuler un ordre par son identifiant chez le courtier.
OrderManagementProviderupdate_take_profit, update_stop_lossModifier une protection avec le prix et la quantité fournis.
OrderManagementProviderclose_position, close_all_positionsDemander une clôture et gérer les protections restantes.
OrderManagementProviderget_orders, get_orderFournir des enregistrements Order AQE avec les identifiants et les statuts effectifs du courtier.
OrderManagementProviderget_positions, get_positionFournir des enregistrements Position ; la méthode de lecture d’une seule position reçoit un symbole.
OrderManagementProviderget_accountFournir un Account avec soldes, devise, pouvoir d’achat et limites.
OrderManagementProviderdrain_trade_events_bounded, has_pending_trade_eventsExposer une file d’événements ordonnée et non bloquante.
OrderManagementProvidersubscribe_to_trade_stream, unsubscribe_from_trade_streamGérer les fonctions de rappel des transactions et leur cycle de vie.

Plusieurs méthodes exigent explicitement des futures Send, notamment connect, les opérations individuelles de modification des ordres, get_orders, get_positions et get_account. Respectez les signatures exactes des traits ; une implémentation async fn convient si sa future est Send. Ne conservez jamais une garde non-Send pendant .await et n’effectuez pas d’opérations de transport bloquantes sur un thread de travail du moteur.

Sans événements poussés par le service, prévoyez comment l’interrogation périodique produit les événements et respecte le contrat d’abonnement obligatoire. N’annoncez pas TradeEventStream si le courtier ne fournit pas d’événements sans interrogation par AQE.

Redéfinissez Broker::get_features() avec une tranche statique de valeurs BrokerFeature. Déclarez uniquement les capacités réellement prises en charge, telles que MarketOrder, LimitOrder, PartialClose ou BrokerManagedOco. Annoncez AmendLimitPrice et AmendStopPrice séparément lorsque le service accepte ces modifications du prix d’entrée sur un ordre non exécuté.

Restez cohérent sur le pouvoir d’achat : BuyingPowerLeveraged indique qu’il inclut déjà le levier. Reportez les plafonds disponibles dans Account.max_total_orders et Asset.max_total_size ; la taille maximale d’un ordre appartient à Asset.max_order_size. N’inventez pas de limites ni d’estimations faisant autorité à partir d’un simple calcul de notionnel.

Pour une opération non prise en charge, renvoyez BrokerError::UnsupportedOperation avant toute modification transmise au service. Ne substituez pas silencieusement un autre type d’ordre et n’annoncez pas de réussite.

2. Traduire les demandes sans perdre leur identité

Section intitulée « 2. Traduire les demandes sans perdre leur identité »

submit_order reçoit un BrokerOrderRequest. Convertissez le symbole, le sens, la quantité, le type et la classe d’ordre, les prix limite et stop, les protections et le stop suiveur vers la représentation acceptée par le service.

Préservez client_order_id pour la corrélation et utilisez-le comme identifiant d’ordre client auprès du service lorsque celui-ci le permet. Il reste stable lors des nouvelles tentatives ou de la reprise d’une même intention d’entrée ; il ne prouve pas que le service a créé un ordre. Renvoyez l’identifiant effectif de l’ordre chez le courtier dans l’Order résultant.

Distinguez ces résultats :

RésultatComportement de l’adaptateur
Opération non prise en charge avant l’envoiRenvoyer UnsupportedOperation ; aucune modification n’a eu lieu.
Refus avéré avant l’appel de modification au courtierUtiliser SubmissionRejected avec le motif.
Délai réseau dépassé après l’envoiPréserver l’incertitude. Ne pas annoncer un rejet ni soumettre à nouveau sans vérification.
Demande acceptéeRenvoyer l’état de l’ordre observé chez le courtier ; l’acceptation seule ne confirme pas l’exécution de l’ordre.
Exécution, rejet, annulation ou clôture ultérieursÉmettre les éléments attestant le cycle de vie avec l’ordre et l’événement correspondants.

replace_order est facultative et renvoie par défaut une erreur d’opération non prise en charge. Redéfinissez-la pour remplacer le prix ou la quantité d’un ordre d’entrée en attente. Elle ne remplace pas update_take_profit ou update_stop_loss.

amend_entry_prices(order_id, limit_price, stop_price) est une méthode facultative distincte pour modifier le prix limite ou le seuil stop d’un ordre d’entrée en attente sans changer sa quantité. Son implémentation par défaut renvoie une erreur d’opération non prise en charge. Implémentez-la avant d’annoncer l’une de ces capacités de modification. Ne transmettez pas une modification du seuil d’entrée à update_stop_loss, qui gère les stops de protection.

Renseigner les enregistrements de compte, d’ordre et de position

Section intitulée « Renseigner les enregistrements de compte, d’ordre et de position »

Les littéraux de structure Rust doivent inclure tous les champs du type actuel, y compris les champs facultatifs. #[serde(default)] facilite la lecture des anciens enregistrements sérialisés ; il ne rend pas un champ facultatif dans un constructeur Rust.

Enregistrement et champsExigence pour l’adaptateur
Account.account_id, currency, accrued_commission, max_total_ordersLimitez les observations au compte et à la devise concernés. Renseignez la commission accumulée à partir d’éléments attestés par le courtier et le plafond des ordres en attente lorsqu’il est communiqué. Les anciennes valeurs par défaut ne prouvent pas l’absence de coûts ou de limites.
Position.order_idNouvel identifiant facultatif correspondant au ticket exact d’exécution ou de position associé à l’entrée. Utilisez Some(...) uniquement si l’adaptateur peut prouver l’association ; utilisez None pour les anciens enregistrements ou ceux qui agrègent plusieurs propriétaires par symbole. N’inventez jamais d’identité à partir du symbole.
Position.entry_commissionPréservez la commission d’entrée de la position indépendamment du résultat de trading latent. Ne comptabilisez pas à nouveau les mêmes frais dans le bilan final.
Order.filled_qty, realized_pnl, commission, swapUtilisez les quantités d’entrée exécutées cumulées pour un instantané d’entrée, mais uniquement la quantité exécutée par la sortie concernée pour un événement de sortie. Préservez les valeurs d’exécution et les montants monétaires communiqués par le courtier ; l’acceptation ne constitue pas une exécution.
Order.position_accounting, OrderLifecycleCloseDetails.position_accountingÉléments comptables facultatifs de type PositionCloseAccounting pour une position dont le rattachement est prouvé. Conservez None s’ils ne sont pas pris en charge ou si le rattachement est ambigu ; ils ne sont pas requis pour la seule implémentation de la soumission d’ordres.
BrokerLifecycleSnapshot.snapshot_complete, captured_atN’indiquez que l’instantané est complet qu’après avoir recensé toute la vue pertinente du cycle de vie, et conservez son instant d’observation. Un instantané vide et incomplet ne peut pas prouver qu’un ordre ou une position a disparu.

Lorsque vous construisez un Order, conservez ses identifiants d’insight et de stratégie ainsi que l’identifiant effectif de l’ordre chez le courtier. Renvoyez les métadonnées Asset actuelles avec les ordres et les positions, y compris les règles de négociation, le calendrier et les frais lorsqu’ils sont connus. Consultez les métadonnées du flux de données personnalisé pour les champs de l’instrument.

Rechercher les positions par l’identité exacte de leur entrée

Section intitulée « Rechercher les positions par l’identité exacte de leur entrée »

get_position(symbol) peut fournir une vue agrégée. La méthode plus récente get_position_by_order_id(order_id, symbol) effectue une recherche exacte asynchrone et renvoie Result<Option<Position>, BrokerError>. Par défaut, elle examine get_positions() et rejette les identités d’ordre ambiguës ou manquantes plutôt que de sélectionner arbitrairement une position sur le même symbole. Redéfinissez-la si le service nécessite une recherche dédiée par ticket.

Pour une simulation exacte en mémoire ou un cache personnalisé, position_snapshot_reader peut renvoyer un PositionSnapshotReader : une fonction de rappel renvoyée par valeur, utilisable en toute sécurité depuis plusieurs threads et recevant l’identifiant de l’ordre et le symbole. N’y effectuez aucun accès au transport. Renvoyez Ok(None) uniquement pour une absence observée ; renvoyez une erreur lorsque les données sont indisponibles ou ne permettent pas d’établir l’identité demandée. Les décisions de la stratégie en direct utilisent le cache d’instantanés horodatés d’AQE, et non une requête réseau supplémentaire depuis une fonction de rappel d’insight.

Maintenez une file de (Order, TradeUpdateEvent) dans l’ordre de la source. drain_trade_events_bounded(max_events) retire au plus ce nombre d’événements et laisse les autres dans la file. Une limite de zéro ne retire rien. Ne videz jamais toute la file pour tronquer ensuite le résultat.

Cette fonction illustre l’opération sans imposer une bibliothèque de verrouillage. Appelez-la sous une garde de courte durée protégeant la file de votre adaptateur :

use aq_engine::core::broker::types::{Order, TradeUpdateEvent};
use std::collections::VecDeque;
pub fn take_trade_events(
queue: &mut VecDeque<(Order, TradeUpdateEvent)>,
max_events: usize,
) -> Vec<(Order, TradeUpdateEvent)> {
let count = max_events.min(queue.len());
queue.drain(..count).collect()
}

has_pending_trade_events vérifie la présence d’événements dans la file sans accès réseau ni accès à une base de données. Appelez les fonctions de rappel en dehors des sections verrouillées et gardez-les courtes. Signalez précisément les exécutions partielles et les mises à jour des branches de protection ; normalisez les doublons et les messages reçus dans le désordre sans inventer d’états finaux.

Les composants d’encapsulation d’événements par défaut, indépendants du courtier, génèrent des identifiants sans horodatage source connu. Redéfinissez subscribe_to_broker_event_stream et la gestion des enveloppes associées lorsque l’adaptateur peut fournir des identifiants, des numéros de séquence et des horodatages fiables pour BrokerOrderEvent.

close_position prend en charge le nettoyage des protections : une clôture complète annule les branches natives restantes ; une clôture partielle conserve ou redimensionne les protections de l’exposition restante. Ok(true) accuse réception de la commande, sans confirmer la clôture. Émettez séparément les éléments attestant l’état final de la transaction.

Des méthodes optionnelles améliorent la supervision et la reprise :

MéthodeUtilité
get_cached_accountFournir le dernier instantané du compte sans accès réseau.
get_cached_positionsFournir les positions en mémoire pour une exécution déterministe, sans remplacer les rafraîchissements horodatés en direct.
get_cached_lifecycle_snapshotFournir une vue complète et de référence du cycle de vie sans E/S ; renvoyer None tant qu’elle n’existe pas.
estimate_order_marginObtenir une estimation faisant autorité pour la demande exacte, sans placer l’ordre.
reconcile_submissionRechercher le compte et la commande client exacts sans soumettre à nouveau.
reconcile_order_lifecycleRésoudre les identifiants d’ordres à partir de l’historique des ordres et de l’identité des positions après un redémarrage.
lifecycle_reconcilerFournir un Arc<dyn OrderLifecycleReconciler> dont l’adaptateur détient la propriété pour permettre la reprise après un redémarrage.
get_position_by_order_id, position_snapshot_readerRetrouver le ticket exact de l’entrée, en séparant le rafraîchissement asynchrone de la recherche sans accès au transport.
pending_cancellation_recovery, retry_cancellationPermettre la reprise d’une annulation incertaine à partir de preuves, par l’intermédiaire du processus durable de traitement des commandes.
execution_fill_history_readerFournir un historique borné des exécutions en lecture seule pour un compte, un ordre d’entrée et un symbole précis.
position_valuation_readerFournir en lecture seule des valorisations dans la devise du compte pour des positions effectives précisément identifiées, et non une estimation globale par symbole.
account_cash_flow_readerFournir en lecture seule les mouvements de financement et les ajustements du compte, avec l’identité de la source, la séquence et la couverture.
configure_live_session, configure_asset_metadataRecevoir le contexte de session et les règles d’actif si nécessaire.

Le mécanisme de résolution de la soumission renvoie Unsupported par défaut. Le mécanisme de résolution du cycle de vie utilise par défaut get_orders() et signale les identifiants absents comme Unknown ; il ne permet pas d’établir qu’une position issue d’un ordre exécuté a été clôturée. N’interprétez jamais un ordre absent, un instantané incomplet ou un délai dépassé comme une preuve d’annulation ou de clôture.

pending_cancellation_recovery(account, order) fournit en lecture seule des éléments attestant qu’une annulation incertaine cible toujours un ordre en attente. Par défaut, cette méthode renvoie None, ce qui ne prouve pas un état final. Seul le processus durable de traitement des commandes peut appeler retry_cancellation après avoir enregistré une tentative de reprise. Cette méthode ne doit ni soumettre, ni clôturer, ni remplacer un ordre ; elle doit obtenir des preuves d’annulation faisant autorité et les transmettre par le réducteur habituel des événements du courtier. Par défaut, elle n’est pas prise en charge. N’ajoutez pas de minuterie de nouvelle tentative indépendante qui contourne ce processus.

Garder les fonctions analytiques facultatives et en lecture seule

Section intitulée « Garder les fonctions analytiques facultatives et en lecture seule »

Les méthodes fournissant les lecteurs d’historique, de valorisation et de flux de trésorerie renvoient None par défaut. Un adaptateur d’exécution peut conserver ces valeurs par défaut lorsqu’il ne peut pas fournir les éléments probants. Les données analytiques non prises en charge doivent rester indisponibles, et non devenir un historique vide fabriqué, un portefeuille sans position ou un flux de trésorerie nul.

  • ExecutionFillHistoryReader::read(account_id, entry_order_id, symbol) renvoie un ExecutionFillHistory pour une position dont le rattachement à un seul ordre d’entrée est prouvé. Préservez les identifiants des exécutions et leurs horodatages source ; position_snapshot peut ajouter un contrôle de quantité fondé sur une observation indépendante. Un historique manquant ou issu d’une compensation de positions ne prouve pas l’absence d’exécutions.
  • PositionValuationReader::read_positions(account_id, symbol, position_ids) renvoie des PositionValuations pour un lot borné de positions précisément identifiées. Préservez la devise du compte, as_of_us, les horodatages des cotations, le résultat de trading, le portage et une base notionnelle explicite ou un motif d’indisponibilité. NotOpen est une observation d’absence, et non une exécution de clôture ou une autorisation de clôturer un insight.
  • AccountCashFlowReader::read(account_id, source_id, after_sequence) renvoie une AccountCashFlowPage avec l’identité de la source, la devise, la couverture temporelle, le curseur de séquence et les enregistrements. Enregistrez les mouvements de financement ou les ajustements réels ; ne déduisez pas de dépôts ou de retraits à partir des variations des capitaux propres, des exécutions ou de l’état des insights.

Les trois lecteurs renvoient un résultat facultatif à l’intérieur d’un Result. Une lecture indisponible et une erreur se distinguent d’une observation complète faisant autorité sans enregistrement correspondant. La lecture de données analytiques ne doit jamais avoir pour effet de bord de placer des ordres ou d’effectuer un rapprochement du cycle de vie.

UnifiedBroker accepte directement votre type d’exécution. Cette fonction associe tout courtier et flux conformes ; les deux adaptateurs doivent déjà implémenter leurs traits :

use aq_engine::core::broker::{
traits::{Broker, DataFeed, DataProvider, OrderManagementProvider},
UnifiedBroker,
};
pub fn live_with<E, D>(execution: E, datafeed: D) -> UnifiedBroker<E, D>
where
E: Broker + OrderManagementProvider,
D: DataFeed + DataProvider,
{
UnifiedBroker::new(execution, datafeed)
}

Utilisez PaperBroker avec UnifiedBroker::new_backtest pour le simulateur historique intégré. Les traits de courtier en direct ne transforment pas un service distant en simulateur. L’ajout au sélecteur de connexions d’AQS nécessite une intégration distincte dans l’application.

  1. Testez les échecs de connexion, l’annulation, la reconnexion et l’arrêt complet.
  2. Testez chaque type d’ordre, règle de précision et limite annoncés.
  3. Vérifiez les exécutions partielles, annulations, protections, clôtures partielles et nettoyage après clôture complète.
  4. Testez les retraits bornés avec zéro, un et plusieurs événements ; vérifiez l’ordre et la conservation des éléments restants.
  5. Simulez un délai dépassé après l’acceptation par le service. Vérifiez que la reprise ne duplique pas l’ordre.
  6. Redémarrez avec des ordres en attente et des positions ouvertes ; distinguez les résultats inconnus et confirmés.
  7. Comparez les observations du compte, des ordres et des positions aux relevés de l’environnement de test du service avant d’envisager une utilisation en direct.
  8. Gardez distinctes deux entrées sur le même symbole. Testez la recherche par ticket exact, les identités inconnues et les instantanés incomplets plutôt que de sélectionner la première correspondance de symbole.
  9. Simulez une annulation incertaine avec un redémarrage et vérifiez que seul le chemin de reprise durable peut la retenter.
  10. Testez les lecteurs facultatifs avec des données non prises en charge, incomplètes, périmées ou associées à un autre compte. Vérifiez que les lectures analytiques ne peuvent ni soumettre d’ordres ni fabriquer d’éléments comptables probants.

Dans votre copie d’AQE, commencez par :

  • src/core/broker/mod.rs : traits d’exécution, valeurs par défaut et composition.
  • src/core/broker/commands.rs : BrokerOrderRequest et SubmissionResolution.
  • src/core/broker/types/mod.rs : types de compte, ordre, position, cycle de vie et erreur.
  • src/core/broker/features.rs : capacités natives.
  • src/core/broker/fill_history.rs, position_valuation.rs et account_cash_flow.rs : contrats analytiques facultatifs en lecture seule.
  • live-sync-protocol/src/position_accounting.rs et position_valuation.rs : éléments comptables probants et validation des valorisations.
  • src/core/broker/mt5_broker.rs et src/core/broker/paper_broker.rs : implémentations en direct et simulée.

Suite : Créer un flux de données personnalisé ou Exécution en direct.