Comment unifier Alpaca, IBKR, Binance et Saxo derrière un ledger interne unique. Routage d'ordres, réconciliation quotidienne, monitoring temps réel, multi-devises, et scaling de 1 à N modes × M brokers — avec du code, des schémas, et des cas réels.
Vous avez un compte Alpaca pour le paper trading US, un IBKR pour le live, un Trading212 pour les ETF européens, un Binance pour le crypto, et un Saxo pour le forex. Chaque broker a sa propre API, ses propres formats, ses propres limites. Comment connaître votre P&L global à l'instant T ?
La question semble triviale. Elle ne l'est pas. Chaque broker rapporte les positions dans un format différent. IBKR utilise des contrats et affiche les P&L en devise locale. Alpaca parle en shares avec un unrealized_pnl en USD. Binance fonctionne en base/quote pairs avec des précisions décimales variables. Et Trading212 ajoute sa propre couche d'abstraction avec des "pies" et des fractions de shares.
Quand vous exécutez 6+ stratégies (Dynamic, Balanced, TKL, Orbit, Alpha, Fortress — plus crypto, métaux et forex en expansion) à travers 4+ brokers, vous perdez la source unique de vérité. Le P&L de votre mode Dynamic est réparti entre Alpaca (paper) et IBKR (live). Votre exposition globale est la somme de N dashboards que personne ne regarde simultanément. Et quand un broker tombe, vous ne savez même pas quelles positions sont affectées.
DailyTickers a résolu ce problème en construisant un ledger interne — un journal comptable unique qui enregistre chaque transaction, indépendamment du broker qui l'a exécutée. Le broker devient un simple canal d'exécution. La vérité est dans le ledger. Et pour s'assurer que la vérité est correcte, un broker-simulator tourne en parallèle, miroir fidèle des trades, pour valider avant de basculer la source de vérité.
Source : IBKR — Multi-Currency Trading
L'architecture repose sur un principe simple : séparer l'exécution de la comptabilité. Les brokers exécutent les ordres. Le ledger enregistre les faits. Le monitoring surveille les écarts. Et le portfolio API expose une vue unifiée à tous les consommateurs.
portfolio/v1/ — 6 modes × endpoints JSON standardisés. Status, positions,
performance, trades.
Chaque broker a son propre langage. L'adapter traduit vers un format commun avant que les données n'entrent dans le système. C'est le même principe que les pilotes d'imprimante : votre application envoie un document, le pilote traduit pour le matériel spécifique.
// Interface commune — chaque broker implémente ces méthodes class BrokerAdapter { // Normalise une position broker → format interne normalizePosition(rawPosition) { return { ticker: this.mapTicker(rawPosition), // AAPL, BTC-USD, ASML.AS → symbole unifié qty: this.parseQuantity(rawPosition), // gère les fractions (T212) avgCost: this.parsePrice(rawPosition), // en devise de trading avgCostBase: this.toBaseCurrency(rawPosition), // converti en USD unrealizedPnl: this.parsePnl(rawPosition), broker: this.name, // 'alpaca' | 'ibkr' | 'binance' | 'saxo' assetClass: this.detectAssetClass(rawPosition), currency: this.parseCurrency(rawPosition), lastUpdate: new Date().toISOString() }; } // Normalise un fill event → transaction ledger normalizeOrder(rawOrder) { return { id: this.generateTxId(), brokerOrderId: rawOrder.id || rawOrder.orderId, side: this.parseSide(rawOrder), // 'BUY' | 'SELL' ticker: this.mapTicker(rawOrder), qty: this.parseQuantity(rawOrder), price: this.parseFillPrice(rawOrder), fees: this.parseFees(rawOrder), broker: this.name, timestamp: this.parseTimestamp(rawOrder), settlementDate: this.computeSettlement(rawOrder), fxRate: this.getCurrentFxRate(rawOrder), }; } }
| Raison | Détail | Exemple |
|---|---|---|
| Couverture d'actifs | Aucun broker ne couvre toutes les classes d'actifs | IBKR pour actions + options, Binance pour crypto, Saxo pour forex |
| Régulation | Certains brokers ne sont pas disponibles dans certaines juridictions | Alpaca = US-only paper, Trading212 = EU résident |
| Redondance | Si un broker tombe (maintenance, incident), le routage bascule | IBKR down le 15/03 → ordres routés vers Alpaca temporairement |
| Optimisation des frais | Les structures de commission varient fortement | Alpaca = $0 US equities, IBKR = $0.005/share, Binance = 0.1% spot |
| Paper vs Live | Tester en paper avant de passer live sur un broker différent | Alpaca paper → IBKR live (status: deploying → live) |
Source : Alpaca — Trading API Documentation
Un ledger interne est un journal comptable append-only qui enregistre chaque événement financier indépendamment de tout broker. Le broker dit "vous avez 100 AAPL" — c'est son opinion. Le ledger dit "vous avez acheté 50 AAPL le 3 mars et 50 le 15 mars" — ce sont des faits.
| Entité | Nature | Stockage | Exemple |
|---|---|---|---|
| Transaction | Fait atomique — immuable une fois écrit | Append-only (.jsonl) | BUY 100 AAPL @ $150.00, IBKR, fee $1.00, mode=dynamic, 2026-03-15T14:30:00Z |
| Position | Vue agrégée — dérivée des transactions | Calculé (snapshot quotidien) | AAPL: qty=100, avgCost=$150.00, unrealizedPnl=+$320 |
| Balance | Cash + valeur positions par broker et par mode | Calculé | IBKR: cash=$45,200, positions=$82,400, total=$127,600 |
| Transfer | Mouvement entre brokers ou modes — pas un trade | Append-only | MOVE $10,000 from Alpaca/dynamic to IBKR/dynamic (paper → live cutover) |
Le ledger repose sur quatre invariants qui doivent toujours être vrais. Si un invariant est violé, c'est un bug — pas une approximation acceptable.
// Invariant 1 — Reconstructibilité // La somme de toutes les transactions = les positions courantes assert( sumTransactions(ledger.transactions, 'AAPL').qty === ledger.positions['AAPL'].qty ); // Invariant 2 — Réconciliation cash assert( cash_start + deposits - withdrawals + realized_pnl - total_fees === cash_current ); // Invariant 3 — Cross-check mode/broker // La somme des positions par mode = la somme des positions par broker assert( sumByMode(positions).totalValue === sumByBroker(positions).totalValue ); // Invariant 4 — Unicité et traçabilité // Chaque transaction a un ID unique, un broker_order_id, et un mode for (const tx of ledger.transactions) { assert(tx.id); // UUID interne assert(tx.brokerOrderId); // ID côté broker assert(tx.mode); // 'dynamic' | 'balanced' | 'tkl' | ... }
On ne modifie jamais une entrée du ledger. Jamais. Si une transaction est erronée, on écrit une correction : une entrée de reversal (qui annule l'originale) suivie d'une entrée correcte. C'est exactement comme la comptabilité en partie double : on ne gomme pas, on passe une écriture inverse.
// Transaction originale (erreur : prix 150 au lieu de 155) { "id": "tx-001", "side": "BUY", "ticker": "AAPL", "qty": 100, "price": 150.00 } // Correction : reversal + nouvelle entrée { "id": "tx-002", "type": "REVERSAL", "refTxId": "tx-001", "side": "SELL", "qty": 100, "price": 150.00 } { "id": "tx-003", "type": "CORRECTION", "refTxId": "tx-001", "side": "BUY", "qty": 100, "price": 155.00 }
Le stockage chez DailyTickers utilise des fichiers JSON (pit-state.json pour
les positions courantes, modes-config-history.json pour l'audit trail des
configurations). Pour des volumes plus importants, SQLite ou PostgreSQL avec des tables
append-only et des vues matérialisées pour les positions.
C'est un journal comptable. Chaque ligne est un fait historique immuable. Les positions et les balances sont des vues dérivées que l'on peut toujours recalculer à partir du journal. Si vous perdez les snapshots de positions, vous pouvez les reconstruire. Si vous perdez le journal, vous avez tout perdu.
Source : Martin Fowler — Event Sourcing
Le parcours d'un ordre suit une chaîne précise : le scanner génère un signal, le mode l'assigne, le routeur sélectionne le broker, le broker exécute, le fill event remonte, et le ledger enregistre. Chaque maillon est indépendant et peut échouer sans corrompre les autres.
| Critère | Règle | Exemples |
|---|---|---|
| Classe d'actif | Crypto → Binance, US equities → Alpaca/IBKR, EU equities → Saxo, Forex → Saxo | BTC-USD → Binance, ASML → Saxo, AAPL → IBKR |
| Mode | Paper modes → Alpaca paper, live modes → IBKR | Alpha (deploying) → Alpaca paper, Dynamic (live) → IBKR |
| Fallback | Si broker primaire down → secondaire, avec retry exponentiel | IBKR timeout → tentative Alpaca live (si disponible) |
| Coûts | À volume équivalent, router vers le broker le moins cher | 1000 shares AAPL : Alpaca $0 vs IBKR $5 → Alpaca si disponible |
Trois scénarios critiques que le routeur doit gérer :
| Scénario | Risque | Solution |
|---|---|---|
| Le même signal se déclenche deux fois | Double exécution → position 2× trop grande | Order ID idempotent : mode-ticker-date-signal_id. Le broker rejette le doublon. |
| Un fill arrive après un cancel | Position non souhaitée dans le portefeuille | Le fill gagne toujours (il a déjà eu lieu sur l'exchange). Le ledger l'enregistre, le monitoring alerte. |
| Deux modes veulent le même ticker | Confusion sur l'attribution de la position | Chaque mode a sa propre position. AAPL/dynamic et AAPL/balanced sont deux entrées distinctes dans le ledger. |
Même si le code crashe après l'envoi, le fill existe sur le broker. Le ledger DOIT enregistrer tout ce qui est envoyé, pas seulement ce qui est confirmé. Au redémarrage, le premier réflexe est de réconcilier les ordres en vol avec le broker pour rattraper les fills manqués.
Source : IBKR — TWS API Documentation
Le ledger dit que vous avez 100 AAPL. IBKR dit 95. Qui a raison ? La réponse n'est jamais évidente, et les deux peuvent avoir tort. La réconciliation est le processus qui détecte ces écarts, les classe, et génère des actions correctives.
| Type | Symptôme | Cause fréquente | Action |
|---|---|---|---|
| Missing transaction | Broker montre une position que le ledger ne connaît pas | Fill reçu pendant un crash, WebSocket déconnecté, latence réseau | Interroger l'API fills du broker, recréer la transaction dans le ledger |
| Phantom position | Le ledger montre une position que le broker ne connaît pas | Ordre annulé mais enregistré comme filled, corporate action (merger) | Vérifier l'historique des ordres, passer une écriture de reversal |
| Price divergence | Quantité identique mais prix moyen différent | Split/reverse-split non traité, conversion FX à un moment différent, fill partiel à prix différent | Ajuster le prix moyen avec une écriture de correction, appliquer le split |
async function reconcile(broker, ledger) { const brokerPositions = await broker.getPositions(); const ledgerPositions = ledger.getPositionsByBroker(broker.name); const discrepancies = []; // 1. Positions chez le broker mais pas dans le ledger → Missing for (const bp of brokerPositions) { const lp = ledgerPositions.find(p => p.ticker === bp.ticker); if (!lp) { discrepancies.push({ type: 'MISSING', ticker: bp.ticker, brokerQty: bp.qty, ledgerQty: 0 }); continue; } // Quantité différente → Price divergence ou partial missing if (Math.abs(bp.qty - lp.qty) > 0.001) { discrepancies.push({ type: 'QTY_MISMATCH', ticker: bp.ticker, brokerQty: bp.qty, ledgerQty: lp.qty }); } // Prix moyen diverge > 1% if (Math.abs(bp.avgCost - lp.avgCost) / lp.avgCost > 0.01) { discrepancies.push({ type: 'PRICE_DRIFT', ticker: bp.ticker, brokerPrice: bp.avgCost, ledgerPrice: lp.avgCost }); } } // 2. Positions dans le ledger mais pas chez le broker → Phantom for (const lp of ledgerPositions) { if (!brokerPositions.find(p => p.ticker === lp.ticker)) { discrepancies.push({ type: 'PHANTOM', ticker: lp.ticker, brokerQty: 0, ledgerQty: lp.qty }); } } // 3. Alerter si nécessaire if (discrepancies.length > 0) { await alertDiscord(`⚠️ ${discrepancies.length} écarts détectés sur ${broker.name}`); for (const d of discrepancies) { if (valueOfDiscrepancy(d) > 500) { await alertDiscord(`🚨 CRITIQUE: ${d.ticker} ${d.type} — broker:${d.brokerQty} vs ledger:${d.ledgerQty}`); } } } return discrepancies; }
Plutôt que de faire confiance aveuglément au ledger, DailyTickers fait tourner un
broker-simulator en parallèle. Ce simulateur reçoit les mêmes signaux,
exécute les mêmes trades de façon indépendante, et maintient son propre état. Chaque
nuit, le reconcile compare les deux :
Pas toutes les semaines. Pas quand quelque chose semble bizarre. Tous les jours. Les erreurs de 24h se corrigent en 5 minutes. Les erreurs de 30 jours se corrigent en 3 heures — si vous arrivez à retrouver ce qui s'est passé. L'expérience montre que les divergences non détectées pendant plus de 5 jours deviennent exponentiellement plus coûteuses à résoudre.
Source : Martin Fowler — Parallel Change
Le monitoring n'est pas un nice-to-have — c'est le système nerveux du multi-broker. Sans lui, vous découvrez les problèmes quand il est trop tard : un drawdown qui a dépassé le seuil il y a 3 jours, un broker déconnecté depuis ce matin, un fill manqué qui a créé une exposition non couverte.
| Métrique | Source | Fréquence | Seuil d'alerte |
|---|---|---|---|
| P&L par mode | Ledger (transactions + MtM) | Chaque fill + EOD | Perte journalière > 2% du capital mode |
| Drawdown par mode | Ledger (peak-to-trough) | Temps réel | > 5% = warning, > 8% = HARD STOP → pausing |
| Exposure par broker | Ledger + broker API | Horaire | > 80% du capital chez un seul broker |
| Fill rate | Order router logs | Par batch d'ordres | Rejection rate > 10% = investigation |
| Réconciliation | Reconciliation engine | Quotidienne | Écart > $500 ou > 1% d'une position |
| Cash utilization | Ledger (cash / total capital) | EOD | < 10% = sous-investi, > 95% = pas de marge d'erreur |
Quand le drawdown d'un mode dépasse le seuil, le monitoring déclenche une transition de statut via la mode status state machine — 8 états qui protègent contre les décisions précipitées :
pausing est l'état critique : pas de nouvelles entrées, pas de rotation,
mais les stops/TP/horizon/trailing continuent sur les positions ouvertes. C'est une sortie
organique, pas un panic sell. Le mode bascule vers paused quand toutes les
positions sont fermées naturellement.
liquidated est l'urgence absolue : toutes les positions sont fermées au marché à la prochaine séance, sans regarder SL/TP/horizon. Compliance, panic, black swan.
Le monitoring alimente le portfolio/v1/ — une API JSON publique qui expose
l'état de chaque mode dans un format standardisé. Chaque mode a ses propres endpoints :
positions, performance, trades récents, configuration, et statut. Le dashboard
(gen-status-page.js) consomme cette API pour afficher la vue unifiée.
// portfolio/v1/status.json — vue agrégée de tous les modes { "generated": "2026-06-21T22:00:00Z", "modes": { "dynamic": { "status": "live", "positions": 8, "dd": "-2.1%", "weekReturn": "+1.4%" }, "balanced": { "status": "live", "positions": 12, "dd": "-3.2%", "weekReturn": "+0.8%" }, "tkl": { "status": "stopped", "positions": 0, "dd": "0.0%", "weekReturn": "0.0%" }, "orbit": { "status": "deploying", "positions": 4, "dd": "-0.8%", "weekReturn": "+0.5%" }, "alpha": { "status": "stopped", "positions": 0, "dd": "0.0%", "weekReturn": "0.0%" }, "fortress": { "status": "pausing", "positions": 3, "dd": "-0.5%", "weekReturn": "+0.1%" } } }
Source : Alpaca — Account Activities API
Vous achetez ASML sur Euronext en euros. Votre base currency est le dollar. Le broker vous montre la position en euros. Votre ledger doit la convertir en dollars — mais à quel taux ? Celui du moment de l'achat ? Celui de la clôture ? Celui du broker, qui peut différer de l'ECB de 15 bps ?
| Couche | Définition | Exemple |
|---|---|---|
| Trade currency | La devise de l'instrument — déterminée par l'exchange | EUR pour ASML (Euronext), USD pour AAPL (NYSE), BTC pour BTC-USD |
| Settlement currency | La devise dans laquelle le broker règle — peut différer de la trade currency | IBKR peut régler ASML en EUR ou en USD (conversion automatique) |
| Base currency | Votre devise de reporting — celle dans laquelle vous mesurez la performance | USD pour DailyTickers (tous les P&L sont exprimés en USD) |
| FX rate source | Le taux de change utilisé pour la conversion | Broker rate (IBKR), ECB mid-rate, Yahoo Finance spot |
La règle fondamentale : stocker les deux montants. Le montant en devise de trading ET l'équivalent en base currency, plus le taux FX utilisé. Cela permet de calculer a posteriori les gains/pertes de change réalisés.
// Transaction multi-devise dans le ledger { "id": "tx-4201", "side": "BUY", "ticker": "ASML", "qty": 10, "price": 920.50, // en EUR (trade currency) "tradeCurrency": "EUR", "fxRate": 1.0845, // EUR/USD au moment du fill "priceBase": 998.28, // 920.50 × 1.0845 = USD "totalBase": 9982.80, // 10 × 998.28 "fees": 4.00, // EUR "feesBase": 4.34, // USD "broker": "ibkr", "mode": "balanced", "timestamp": "2026-06-15T09:15:00Z" } // Plus tard, à la vente — le FX a bougé { "id": "tx-4315", "side": "SELL", "ticker": "ASML", "qty": 10, "price": 940.00, // +2.1% en EUR "fxRate": 1.0720, // EUR/USD a baissé de 1.15% "priceBase": 1007.68, // 940.00 × 1.0720 // P&L en EUR : +195 (gain) // P&L en USD : +94 (le gain EUR est mangé par le FX adverse) // FX P&L seul : -101 (perte de change) }
| Marché | Settlement | Exemple | Impact buying power |
|---|---|---|---|
| US Equities | T+1 | Achat lundi → cash débité mardi | Le cash est bloqué mais la position existe immédiatement |
| EU Equities | T+2 | Achat lundi → cash débité mercredi | 2 jours de float — attention au double-spending |
| Crypto (spot) | T+0 | Achat instantané → cash débité immédiatement | Pas de float, pas de risque de settlement |
| Forex (spot) | T+2 | Trade lundi → settlement mercredi | Le P&L est comptabilisé immédiatement mais le cash bouge en T+2 |
| Futures | Daily MtM | Margin ajustée chaque jour à la clôture | Cash impacté quotidiennement, pas au settlement final |
Le ledger doit tracker les deux dates : la trade date (quand vous décidez) et la settlement date (quand le cash bouge). La trade date détermine le P&L. La settlement date détermine le buying power. Confondre les deux mène à des ordres rejetés pour insuffisance de fonds.
Sur un portefeuille global, les gains et pertes de change peuvent représenter 5 à 15% du P&L total — dans un sens ou dans l'autre. Un trade gagnant de +2% en euros peut devenir un trade perdant en dollars si l'EUR/USD chute de 3%. Ne jamais ignorer le FX dans le reporting.
Source : ECB — Euro Reference Exchange Rates
DailyTickers n'est pas né avec 6 modes et 4 brokers. Le système a évolué en 5 phases, chacune forçant une refonte architecturale. Voici la chronologie — et surtout, ce qui a cassé à chaque étape.
| Phase | Configuration | Ce qui a cassé | Ce qui a été construit |
|---|---|---|---|
| Phase 1 Début 2026 |
1 mode (dynamic), 1 broker (Alpaca paper) | Rien — mais le P&L était dans un seul fichier JSON sans structure | pit-state.json — premier ledger, format plat |
| Phase 2 Mars 2026 |
3 modes (dynamic, balanced, secured), Alpaca | Les 3 modes écrivaient dans le même pit-state.json → conflits, positions mélangées | modes-config.json — séparation par mode, config per-mode |
| Phase 3 Avril 2026 |
3 modes + IBKR pour le live | Les formats Alpaca et IBKR sont incompatibles. Le routeur ne savait pas vers quel broker envoyer. | Adapter pattern + Unified Order Router + Reconciliation engine |
| Phase 4 Mai-Juin 2026 |
6 modes (+ TKL, Orbit/Alpha, Fortress) + crypto/forex en test | Problème de status management (quand pauser un mode ? comment relancer ?), FX non géré | Mode Status State Machine (8 états), multi-currency support, Portfolio API v1 |
| Phase 5 Planifié |
+ metals/mines, forex dédié, geographic routing | (À venir) Volume de transactions, latence de réconciliation, regulatory per-broker | Daily snapshots, archive cold storage, routing géographique |
Tout le système est piloté par un fichier unique : modes-config.json.
Il contient, pour chaque mode :
// Extrait de modes-config.json — configuration du mode Dynamic { "dynamic": { "label": "Dynamic", "status": "live", "statusSince": "2026-03-01", "statusReason": "Initial launch", "statusNextReviewAt": "2026-07-01", "broker": "ibkr", "brokerFallback": "alpaca", "portfolioSize": 50000, "maxPositions": 10, "maxDD": 0.08, // 8% → triggers pausing "atrMultiplier": 2.5, "trailingActivation": 1.5, // R-multiples "regimeFilters": { "riskOff": { "maxNewEntries": 0 }, "earlyRiskOff": { "maxNewEntries": 2, "sizingMultiplier": 0.5 } }, "mcapTiers": { "reject": "< 2B", "halfSize": "2B-10B", "reducedSize": "10B-50B" } } }
Chaque modification de configuration est loguée dans
modes-config-history.json — un fichier append-only qui sert d'audit trail.
On peut reconstruire l'état exact de n'importe quel mode à n'importe quelle date.
Quand le nombre de transactions dépasse 10 000, les requêtes sur le ledger brut deviennent lentes. Trois optimisations :
| Optimisation | Mécanisme | Gain |
|---|---|---|
| Daily snapshots | Chaque soir, matérialiser les positions courantes dans un fichier séparé. Pour le P&L du jour, lire le snapshot d'hier + les transactions d'aujourd'hui. | O(transactions_du_jour) au lieu de O(toutes_transactions) |
| Archive cold storage | Transactions > 1 an → compressées et archivées. Le ledger actif ne contient que l'année en cours. | Taille du fichier réduite de 90%, parsing 10× plus rapide |
| Index par mode + ticker | Un index secondaire qui mappe (mode, ticker) → liste de transaction IDs pour des lookups rapides sans scanner le ledger complet. | Lookup d'une position spécifique en O(1) au lieu de O(n) |
Source : SQLite — When to Use SQLite
Gérer plusieurs brokers semble complexe. En réalité, le système est plus transparent qu'un mono-broker — et c'est un avantage. Chaque décision est traçable, chaque flux est vérifiable, chaque erreur est détectable. Voici la checklist pour construire le vôtre.
pausing ne prend plus
de nouvelles positions mais laisse les existantes se fermer naturellement.modes-config-history.json = audit
trail complet. On peut reconstruire l'état de n'importe quel mode à n'importe quelle date.
Les war rooms utilisent cet historique pour analyser les décisions passées.Le coût de cette transparence est un investissement initial en architecture — les adapters, le ledger, la réconciliation. Mais une fois en place, le système est plus robuste, plus auditable, et plus extensible qu'un mono-broker ne le sera jamais.
| Sujet | Article |
|---|---|
| Valider les données avant publication | Harnesses IA en Finance |
| Le pipeline scanner de bout en bout | Inside the Scanner Pipeline |
| Tester honnêtement les stratégies | Backtesting Honnête |
| Automatiser la publication | Claude Code Trading Bot |