Comment un système indépendant qui exécute les mêmes trades en parallèle permet de valider la fiabilité de votre tracking de positions — et de migrer la source de vérité sans risque, sans big-bang, et sans downtime.
Vous avez construit un système de trading algorithmique. Chaque nuit, il scanne 6 000+
tickers, sélectionne les meilleurs setups, calcule les positions, applique les stops et
les take-profits. Le pipeline génère un fichier pit-state.json qui représente
l'état de chaque portefeuille à chaque instant. Mais comment savez-vous
que ce fichier est correct ?
La réponse courte : vous ne le savez pas. Un bug de prix stale, un fill manqué, un arrondi de position, un stop qui se déclenche au mauvais moment — chacun de ces problèmes peut silencieusement corrompre votre état de portefeuille sans qu'aucun test unitaire ne le détecte. Parce que le problème n'est pas dans le code : il est dans l'interaction entre le code et les données réelles du marché.
La solution de DailyTickers : le Broker Simulator
(simulator.dailytickers.com).
Un service Go indépendant qui reçoit exactement les mêmes trades que le système principal,
les exécute de façon autonome, puis compare ses résultats avec pit-state.json
chaque nuit. Si les deux systèmes sont d'accord pendant 20 jours consécutifs, le
simulateur devient la source de vérité. S'ils divergent : alerte immédiate, revert
automatique, investigation.
dynamic — parité parfaite dès le jour 1pit-state.json systématiqueLe simulateur ne connaît rien des internals de DailyTickers. Il ne lit
pas pit-state.json. Il ne connaît pas les scores, les régimes, ni les
configurations des modes. Il reçoit des intents (« acheter 50 AAPL demain
à l'ouverture ») et les exécute selon ses propres règles. C'est cette
indépendance qui fait la valeur de la validation : si deux systèmes
qui ne partagent aucun code arrivent au même résultat, la probabilité d'un bug
commun est quasi nulle.
Ce guide technique détaille l'architecture complète du parallel-run : les 6 étapes du cycle autonome, le mécanisme de réconciliation, le cutover automatique avec auto-revert, et comment intégrer le simulateur dans votre propre pipeline de trading.
Source : Martin Fowler — Parallel Change (le pattern architectural derrière le parallel-run)
Le parallel-run s'intègre dans le pipeline scanner nocturne sans le ralentir ni le bloquer. Chaque étape est non-bloquante : si le simulateur est indisponible, le scan continue normalement et un avertissement Discord est émis.
Le broker simulator est un service Go hébergé sur
simulator.dailytickers.com,
développé dans le repo dailystocks-platform. Il expose une API REST :
| Endpoint | Méthode | Description |
|---|---|---|
/api/accounts/{id}/mirror-order |
POST | Soumettre un ordre miroir (achat/vente avec prix cible et stops) |
/api/accounts/{id}/mirror-run |
POST | Lancer le replay intraday (SL/TP/horizon/trailing exits) |
/api/accounts/{id}/backfill |
POST | Injecter l'historique gelé pour le bootstrap initial |
/api/accounts |
GET | Lister les comptes (auto-scoped par token de service) |
Chaque mode de trading a un compte dédié dans le simulateur, nommé par convention
mirror:<mode> (ex: mirror:dynamic,
mirror:balanced). Le token de service scope automatiquement l'accès
au workspace pilot — pas besoin de header X-Org-Id.
Le simulateur ne lit pas le code du scanner, ne connaît pas les scores, ne consulte pas les configurations de modes. Il reçoit des intents atomiques :
// Intent publié par publish-to-simulator.js { "ticker": "AAPL", "side": "buy", "qty": 50, "entry": "next-open", "vwapGate": true, "stopLoss": 185.50, "takeProfit": 210.00, "horizonDays": 10, "trailingStop": { "activateR": 2.0, "lockPct": 0.5 } }
Et les exécute selon ses propres règles de pricing et de timing. Si le résultat
final (positions, P&L, NAV) correspond à celui de pit-state.json,
c'est que les deux systèmes sont corrects — ou qu'ils ont exactement le même bug,
ce qui est statistiquement improbable sur 20 jours.
Si le token de service est absent (BROKERSIM_SERVICE_TOKEN non défini
dans .env), le parallel-run est un no-op propre :
le scan logge un WARNING et continue normalement. Le simulateur ne peut
jamais bloquer la publication des résultats du scanner.
Source : GitHub — Deployment patterns (le parallel-run est une variante du canary deployment appliquée aux données)
Chaque nuit, le pipeline scanner exécute les 6 étapes du parallel-run dans l'ordre. Chaque étape est idempotente (relançable sans effet de bord) et résiliente (une erreur à l'étape N n'empêche pas l'étape N+1).
Backfill de l'historique gelé pour les nouveaux modes. Exécuté une seule fois par mode, puis ignoré pour toujours.
export-to-simulator.js --sync
Exécution des intents d'entrée au next-open + VWAP gate. Replay intraday pour les exits (SL/TP/horizon/trailing).
run-mirror.js
Comparaison sim vs pit-state.json par mode. Prix ±0.5%, NAV ±2%. Append dans reconciliation-log.json.
reconcile-simulator.js
Après 20 jours consécutifs sans divergence → flip source-of-truth.json vers "sim". Auto-revert sur toute divergence.
cutover-decision.js
gen-api.js et gen-status-page.js lisent depuis le sim pour les modes cutover. Hard fallback sur pit-state.json si erreur.
sim-source.js
Après le sweep, poste les intents d'entrée du lendemain au simulateur pour préparer le prochain cycle.
publish-to-simulator.js
Quand un mode est ajouté au pilote (par exemple, le mode bull rejoint
le parallel-run), le bootstrap injecte tout l'historique gelé de
pit-state.json dans le compte mirror:bull du simulateur.
C'est un backfill complet : toutes les positions ouvertes, tous les trades clôturés,
l'equity courante.
// Commande node tools/export-to-simulator.js --sync // Ce qui se passe : // 1. Lire les modes pilotes depuis simulator-config.json // 2. Pour chaque mode sans fills dans mirror:<mode> → backfill // 3. Pour chaque mode déjà backfillé → skip (idempotent) // Résultat attendu : ✅ dynamic: already backfilled (skip) ✅ balanced: already backfilled (skip) 🔄 bull: backfilling 12 positions, 47 closed trades... ✅ bull: backfill complete — equity parity 0.0%
Le flag « déjà backfillé » est persisté côté simulateur. Même si vous relancez
--sync 100 fois, le backfill ne se produit qu'une fois par mode.
C'est le cœur du parallel-run. Le mirror-run prend les intents publiés la veille et les exécute selon les règles du simulateur :
Le simulateur utilise les données Yahoo Finance 1-minute pour le replay intraday, ce qui lui permet de détecter les touches de SL/TP avec une précision de 1 minute.
La réconciliation compare, mode par mode, l'état du simulateur avec
pit-state.json. Trois métriques sont vérifiées :
| Métrique | Tolérance | Pourquoi cette marge |
|---|---|---|
| Prix moyen d'entrée | ±0.5% | Différences de timing d'exécution (ouverture vs VWAP) |
| NAV (Net Asset Value) | ±2% | MtM intraday, dividendes, corporate actions |
| Positions | Exacte | Même ticker, même quantité, même direction |
Le résultat est appendé dans data/reconciliation-log.json, un fichier
append-only qui constitue l'audit trail complet de chaque comparaison.
L'étape 4 (cutover-decision) est l'algorithme de promotion :
si le mode a 20 entrées consécutives dans le log de réconciliation avec
divergence = 0, le flag dans source-of-truth.json passe de
"articles" à "sim". Le revert est automatique :
dès qu'une divergence apparaît, le flag revient à "articles".
L'étape 5 (read-switch) fait que gen-api.js et
gen-status-page.js lisent les positions depuis le simulateur pour
les modes qui ont été promus. Le hard fallback est intégré : si l'appel au sim
échoue (réseau, token expiré, service down), le système revient silencieusement
à pit-state.json.
L'étape 6 (publish) est la boucle : après que le sweep a déterminé les trades du lendemain, les intents sont postés au simulateur pour que le prochain mirror-run puisse les exécuter.
Source : Martin Fowler — Canary Release
La réconciliation est le moment de vérité du parallel-run. Chaque nuit, après le
mirror-run, le script reconcile-simulator.js compare position par
position, mode par mode, les résultats du simulateur avec ceux de
pit-state.json.
Pour chaque mode pilote (turbo, dynamic, balanced, bull, secured), la réconciliation vérifie :
// Entrée dans reconciliation-log.json { "date": "2026-06-19", "mode": "dynamic", "status": "MATCH", "simEquity": 127450.32, "pitEquity": 127389.18, "equityDiffPct": 0.048, "positionsMatch": true, "openPositions": 8, "divergences": [], "consecutiveMatches": 14 } // Entrée avec divergence { "date": "2026-06-20", "mode": "balanced", "status": "DIVERGENCE", "simEquity": 98200.44, "pitEquity": 95100.77, "equityDiffPct": 3.26, "positionsMatch": false, "divergences": [ { "type": "missing_position", "ticker": "CRDO", "inSim": true, "inPit": false } ], "consecutiveMatches": 0 }
| Type | Signification | Cause probable | Action |
|---|---|---|---|
missing_position |
Position dans un système mais pas l'autre | Fill manqué, intent non publié, ou bug de clôture | Investiguer le log d'intents, vérifier le mirror-order |
price_drift |
Prix moyen diverge de >0.5% | Timing d'exécution différent, données Yahoo stale | Vérifier les fills bruts des deux côtés |
nav_mismatch |
Equity du mode diverge de >2% | Split/dividende non appliqué, cash accounting | Recalculer le MtM manuellement, vérifier corporate actions |
qty_mismatch |
Même ticker mais quantités différentes | Partial fill, sizing différent, arrondi | Comparer les ordres originaux et les fills |
staleness_artifact |
Grande divergence NAV sur positions backfillées | Yahoo 1m limité à ~30 jours — pas de replay précis | Attendu au début, se résout naturellement (forward-only) |
Lors du premier backfill, les positions anciennes (ouvertes depuis plus de 30 jours) ne peuvent pas être rejouées en intraday car Yahoo Finance ne fournit les données 1-minute que sur les ~30 derniers jours. Cela crée une divergence NAV initiale qui est un artefact, pas un bug. Les trades forward (à partir du moment où le parallel-run est actif) ne sont pas affectés. La divergence se résorbe naturellement à mesure que les vieilles positions se ferment.
Chaque divergence déclenche une alerte Discord dans le canal de monitoring, avec :
Les matchs ne déclenchent pas d'alerte (bruit inutile) — seul le franchissement du seuil de 20 jours génère une notification positive (AUTO-CUTOVER).
Le cutover est le moment où le simulateur passe de « miroir de validation » à « source de vérité ». C'est la transition la plus critique du système, et elle est entièrement automatique — par design.
// Pseudo-code de cutover-decision.js for (const mode of pilotModes) { const log = readReconciliationLog(mode); const recent = log.slice(-20); // 20 dernières entrées const currentSource = readSourceOfTruth(mode); const allMatch = recent.length === 20 && recent.every(e => e.status === 'MATCH'); const latestDiverged = log.at(-1)?.status === 'DIVERGENCE'; if (allMatch && currentSource === 'articles') { // PROMOTION : 20 jours consécutifs de convergence flipSourceOfTruth(mode, 'sim'); discordAlert(`AUTO-CUTOVER: ${mode} → sim`); } if (latestDiverged && currentSource === 'sim') { // REVERT : divergence détectée sur un mode promu flipSourceOfTruth(mode, 'articles'); discordAlert(`AUTO-REVERTED: ${mode} → articles`); } }
Le seuil de 20 jours de trading n'est pas arbitraire :
simulator-config.json
(clé cutoverDays), modifiable sans toucher au code.// data/source-of-truth.json { "dynamic": "sim", // ← cutover effectué après 20j "balanced": "articles", // ← encore en validation (jour 14) "turbo": "articles", // ← en validation (jour 7) "bull": "articles", // ← récemment onboardé (jour 3) "secured": "articles" // ← en validation (jour 11) }
Ce fichier est la seule source de décision pour le read-switch. Un simple
JSON.parse suffit pour savoir d'où lire les données de chaque mode.
C'est la règle la plus importante du système : l'API publique ne peut
jamais être vide, en erreur, ou stale à cause du simulateur. Le
fallback est implémenté dans sim-source.js :
async function getPositions(mode) { const source = readSourceOfTruth(mode); if (source === 'sim') { try { const simData = await fetchFromSimulator(mode); if (isStale(simData)) throw new Error('stale sim data'); return simData; } catch (err) { // HARD FALLBACK — toujours pit-state.json console.warn(`Sim error for ${mode}: ${err.message} — falling back`); return readPitState(mode); // JAMAIS null, JAMAIS vide } } return readPitState(mode); // source = 'articles' }
Ce design garantit trois propriétés :
| Propriété | Garantie | Comment |
|---|---|---|
| Disponibilité | L'API publique retourne toujours des données | Le fallback lit un fichier local (pas de réseau) |
| Fraîcheur | Les données ne sont jamais plus stale que pit-state.json | Le check isStale() rejette les données sim trop vieilles |
| Réversibilité | Un cutover peut être annulé en <1 seconde | Un seul champ dans un fichier JSON |
Ne jamais basculer tous les modes en même temps. Le cutover par mode permet
de limiter le blast radius : si le mode dynamic
a un problème après cutover, les 4 autres modes ne sont pas affectés.
C'est le même principe que le canary deployment en infrastructure.
Source : Google SRE Book — Reliable Product Launches at Scale
Le simulateur est conçu pour être intégré dans n'importe quel pipeline de trading qui produit des intents structurés. Voici comment connecter votre système.
# Dans articles/.env (gitignored) # IMPORTANT : valeur NON-QUOTÉE (loadEnv ne strip pas les quotes) BROKERSIM_SERVICE_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... # Alternative : export dans ~/.profile export BROKERSIM_SERVICE_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Le token scope automatiquement l'accès au workspace pilot. Il ne
permet pas d'accéder aux données d'autres workspaces. Ne le commitez
jamais dans le repo — .env est dans
.gitignore.
Le fichier tools/lib/simulator-client.js encapsule toute la communication
avec le simulateur :
const { SimulatorClient } = require('./lib/simulator-client'); // Initialisation (charge le token depuis .env automatiquement) const sim = new SimulatorClient(); // Vérifier la connexion const accounts = await sim.listAccounts(); console.log(accounts); // → [{ id: 'abc123', label: 'mirror:dynamic' }, ...] // Publier un intent await sim.mirrorOrder('abc123', { ticker: 'AAPL', side: 'buy', qty: 50, entry: 'next-open', vwapGate: true, stopLoss: 185.50, takeProfit: 210.00, horizonDays: 10 }); // Lancer le mirror-run await sim.mirrorRun('abc123'); // Lire l'état pour la réconciliation const simState = await sim.getAccountState('abc123'); console.log(simState.equity, simState.positions);
Pour ajouter un mode au parallel-run, deux étapes suffisent :
simulator-config.json (clé pilotModes)pit-state.json contient les données du modeAu prochain run de export-to-simulator.js --sync, le mode sera
automatiquement backfillé et intégré au cycle de réconciliation.
{
"baseUrl": "https://simulator.dailytickers.com",
"pilotModes": ["turbo", "dynamic", "balanced", "bull", "secured"],
"tolerances": {
"pricePct": 0.5, // ±0.5% sur les prix moyens
"navPct": 2.0, // ±2% sur l'equity du mode
"qtyExact": true // quantités doivent matcher exactement
},
"cutoverDays": 20, // jours consécutifs de convergence
"workspace": "pilot" // workspace du simulateur
}
| Classe d'actif | Statut | Limitation |
|---|---|---|
| Equities US | ✅ Actif | Aucune — couverture complète via Yahoo 1m |
| Crypto | 🔜 Planifié | Binance fournit les données 1m, intégration en cours |
| Forex | ❌ Exclu | Pas de données intraday FX fiables et gratuites |
| Metals / Futures | ❌ Exclu | Pas de pricing intraday sur les futures metals |
Source : The Twelve-Factor App — Config
Le parallel-run n'est pas un exercice académique. Il apporte des bénéfices concrets et mesurables à chaque étape de la chaîne de valeur.
Sans le simulateur, la seule façon de savoir si pit-state.json est correct
est de le vérifier manuellement — un exercice fastidieux, sujet aux erreurs, et qui
ne scale pas avec 5 modes et des dizaines de positions. Avec le parallel-run, la
confiance est quantifiée : le compteur de jours consécutifs est
une métrique objective de fiabilité.
Tout bug de tracking dans pit-state.json — un fill manqué, un stop
déclenché au mauvais prix, un arrondi de quantité — apparaît comme une divergence
dans la réconciliation du lendemain. Pas besoin d'attendre un audit mensuel ou
une anomalie visible dans les résultats.
Le cutover progressif par mode transforme une migration terrifiante (« on bascule tout d'un coup ») en une série de micro-migrations réversibles. Chaque mode est promu individuellement, et un revert prend moins d'une seconde.
| Dimension | Sans Simulator | Avec Simulator |
|---|---|---|
| Confiance dans les données | Subjective — « ça a l'air correct » | Quantifiée — 20 jours de convergence indépendante |
| Détection de bugs | Semaines/mois — découvert à la prochaine rétrospective | 24 heures — divergence détectée au prochain reconcile |
| Risque de migration | Big-bang — tout ou rien, revert complexe | Progressif — par mode, réversible en <1 seconde |
| Audit trail | Inexistant — git blame sur pit-state.json | Complet — reconciliation-log.json append-only |
| Impact d'une panne | N/A (pas de second système) | Zéro — hard fallback transparent |
| Coût opérationnel | Vérifications manuelles chronophages | Entièrement automatique, <2 min par nuit |
Exemples concrets de problèmes que le parallel-run détecterait en 24h :
missing_positionqty_mismatchprice_drift massifSource : Increment — Testing in Production
Le parallel-run du broker simulator incarne un principe fondamental en ingénierie des systèmes critiques : ne faites pas confiance — vérifiez. Mais vérifiez de façon structurée, automatique, et réversible.
Le pattern est transposable à tout système qui produit des données critiques : systèmes de facturation, pipelines de données financières, moteurs de recommandation, systèmes de scoring. À chaque fois que la question « est-ce que ces données sont correctes ? » est importante, un miroir indépendant est la réponse la plus robuste.
pit-state.json est toujours là.Le résultat validé sur le mode dynamic — 0.0% de divergence dès le
premier backfill le 18 juin 2026 — est un point de données encourageant. Mais
c'est le processus qui compte, pas le résultat ponctuel : la réconciliation
quotidienne, l'auto-revert, le cutover progressif. Ce sont ces mécanismes qui
transforment un « ça marche aujourd'hui » en « ça marche, et on le sait ».
L'architecture multi-broker et le ledger append-only qui alimente le simulateur
Contraindre les modèles à produire des résultats fiables — le même esprit de validation appliqué à l'IA
Le bot Discord qui orchestre le pipeline scanner — dont le parallel-run fait partie