Parité backtest-live : pourquoi votre bot trade différemment du backtest
Vous avez fait tourner une stratégie dans un backtest. Sharpe 2,1, MaxDD -8 %, PnL +67 %. Vous avez lancé le bot. Un mois plus tard, vous comparez : mêmes signaux, même période — mais le PnL en direct est inférieur de 40 %. Le drawdown est une fois et demie plus profond. Deux trades sur dix n'ont pas été exécutés du tout.
Ce n'est pas un bug. C'est la divergence backtest-live — un écart systématique entre les résultats du backtest et le trading réel. Tout le monde y est confronté. La seule question est de savoir si vous en avez conscience et si vous pouvez la maîtriser.
Cet article propose une taxonomie complète des divergences, des modèles architecturaux pour les minimiser, et une checklist pratique pour surveiller la parité en production.
Le syndrome du « ça marchait dans le backtest »

Tout algotrader traverse ce cycle :
- Écriture d'une stratégie dans un notebook Jupyter
- Backtest sur un CSV historique — les résultats sont excellents
- Réécriture de la logique sous forme de bot (souvent dans un autre langage ou framework)
- Lancement — les résultats ne correspondent pas
- Recherche d'un bug, sans en trouver — « le marché a changé »
Le problème n'est pas le marché. Le problème est que le backtest et le bot sont deux produits logiciels différents qui modélisent la même réalité de manière différente. Les divergences sont inévitables, mais elles peuvent être systématisées et minimisées.
Taxonomie des divergences

Toutes les sources de divergence se répartissent en quatre catégories. Pour chacune — une note de sévérité (de 1 à 5) et une contribution typique à la divergence de PnL.
1. Divergences de données (sévérité : 3/5)
Les données que voit le backtest et celles que voit le bot en temps réel ne sont pas identiques.
Horodatage. Les exchanges livrent les bougies avec des règles différentes d'attribution de l'horodatage. Un exchange marque la bougie avec le début de la période, un autre avec la fin. Une API REST peut renvoyer une bougie avec un délai de 1 à 3 secondes après la clôture réelle. Le backtest travaille avec des horodatages « idéaux » issus du fichier historique.
Agrégation OHLCV. Les données historiques sont souvent agrégées par le fournisseur différemment de la manière dont l'exchange le fait en temps réel. La différence se situe au dernier chiffre — mais avec des signaux à seuil (croisement de moyennes mobiles, cassure de niveau), cela détermine si la stratégie entre en position ou non.
Trous et données manquantes. Les données historiques sont généralement propres — les bougies manquantes sont comblées par interpolation. En temps réel, un WebSocket peut se couper, et le bot manque 30 secondes de données.
Contribution typique à la divergence de PnL : 2-5 % du PnL annuel.
2. Divergences d'exécution (sévérité : 5/5)

La classe de divergences la plus dangereuse. Le backtest simule l'exécution parfaitement — la réalité est loin d'être idéale.
Slippage. Le backtest remplit l'ordre au prix de clôture (ou au prix du signal). En réalité, un ordre au marché est exécuté au meilleur bid/ask plus un slippage qui dépend du volume et de la liquidité. Pour une position de 10 000 $ sur un altcoin à liquidité moyenne, le slippage peut être de 0,05 à 0,3 %.
Formule du slippage cumulé sur trades :
où est le slippage du -ème trade, dépendant de la profondeur du carnet d'ordres :
Latence. Entre le moment où un signal est généré et l'exécution de l'ordre, du temps s'écoule : calcul du signal (1-50 ms), transmission de la requête (10-200 ms), matching sur l'exchange (1-10 ms). Dans le backtest, la latence = 0. En direct, le prix peut bouger.
Exécutions partielles. Le backtest suppose que 100 % de l'ordre est rempli instantanément. En réalité, un ordre limite peut être partiellement rempli — ou pas rempli du tout si le prix s'inverse. Pour un ordre au marché sur un marché illiquide, l'ordre « glisse » à travers plusieurs niveaux du carnet d'ordres.
Priorité de file d'attente. Un ordre limite placé au meilleur prix bid ne sera pas exécuté immédiatement — il se met en file d'attente derrière tous les ordres déjà placés à ce niveau. Un backtest qui considère « prix touché = ordre exécuté » surestime systématiquement le taux d'exécution.
Contribution typique à la divergence de PnL : 10-30 % du PnL annuel.
3. Divergences de logique (sévérité : 4/5)
Ce sont des divergences dans le code même de la stratégie entre le backtest et le bot.
Bases de code séparées. L'anti-pattern classique : backtests/strategy_a.py et bot/strategy_a.py — deux fichiers séparés qui « font la même chose ». Après trois mois de modifications, ils divergent inévitablement. Quelqu'un a ajouté un filtre dans le backtest et a oublié de le répliquer dans le bot. Ou l'inverse — un bug a été corrigé dans le bot mais est resté dans le backtest.
Frameworks différents. Backtest sur pandas avec des opérations vectorisées, bot sur asyncio avec une logique événementielle. Même avec une stratégie identique, les cas limites sont traités différemment : arrondi, ordre de vérification des conditions, gestion des NaN.
Gestion d'état. Le backtest est généralement sans état — il itère sur un tableau de données. Le bot est avec état — il stocke les positions, les soldes, l'historique des ordres. Redémarrage du bot, perte d'état, désynchronisation avec l'exchange — tout cela constitue des sources de divergence.
Contribution typique à la divergence de PnL : 5-20 % du PnL annuel.
4. Divergences de coûts (sévérité : 3/5)
Divergences dans la modélisation des coûts de trading.
Funding rates. La plupart des backtests de futures perpétuels ne tiennent pas du tout compte des funding rates. Avec un levier de 10x et un taux moyen de 0,01 % toutes les 8 heures, cela donne par an — plus que le PnL de la plupart des stratégies. Une analyse détaillée figure dans l'article Les funding rates tuent votre levier.
Commissions. Les commissions maker/taker sont généralement modélisées mais souvent avec le mauvais taux. Les paliers VIP, les remises BNB, les rebates — tout cela affecte le résultat final.
Spread. Un backtest basé sur des bougies ne voit pas le spread bid-ask. Sur une bougie d'1 minute, la clôture = 3000, mais en réalité bid = 2999,5 et ask = 3000,5. Chaque trade « coûte » la moitié du spread.
Contribution typique à la divergence de PnL : 5-15 % du PnL annuel.
Effet cumulatif
Les quatre catégories agissent simultanément et, en règle générale, dans une seule direction — contre le trader :
Une divergence totale de 20 à 50 % par rapport au PnL du backtest est normale pour un système non raffiné. Avec du levier, l'effet est multiplié.
Modèles architecturaux pour la parité
Modèle 1 : Shared Core (extraction d'un noyau commun)

L'idée : extraire le noyau de la stratégie — génération de signaux et logique d'exécution — dans un module séparé utilisé à la fois par le backtest et le bot. Seule l'infrastructure environnante diffère : la source de données et le mécanisme de soumission des ordres.
┌─────────────────────────────────────┐
│ strategy_core.py │
│ ┌─────────────┐ ┌───────────────┐ │
│ │ SignalEngine │ │ OrderManager │ │
│ └──────┬──────┘ └──────┬────────┘ │
│ │ │ │
│ generate_signal() create_order()│
└─────────┬───────────────┬───────────┘
│ │
┌─────┴─────┐ ┌─────┴──────┐
│ Backtest │ │ Live │
│ DataFeed │ │ DataFeed │
│ FillModel │ │ Exchange │
└────────────┘ └────────────┘
from dataclasses import dataclass
from typing import Optional
import numpy as np
@dataclass
class Signal:
side: str # 'long' | 'short'
entry_price: float
sl_price: float
tp_price: float
size: float
timestamp: int
@dataclass
class OrderRequest:
side: str
order_type: str # 'market' | 'limit'
price: float
size: float
class StrategyCore:
"""
Strategy core. Identical code for backtest and live.
Depends only on data, not on infrastructure.
"""
def __init__(self, params: dict):
self.fast_period = params.get('fast_ma', 20)
self.slow_period = params.get('slow_ma', 50)
self.sl_pct = params.get('sl_pct', 0.02)
self.tp_pct = params.get('tp_pct', 0.04)
self.position: Optional[Signal] = None
self._closes: list[float] = []
def on_candle(self, timestamp: int, o: float, h: float,
l: float, c: float, v: float) -> Optional[OrderRequest]:
"""
Process a new candle. Returns an OrderRequest or None.
This method is called identically from the backtest and the bot.
"""
self._closes.append(c)
if len(self._closes) < self.slow_period:
return None
fast_ma = np.mean(self._closes[-self.fast_period:])
slow_ma = np.mean(self._closes[-self.slow_period:])
if self.position is not None:
exit_order = self._check_exit(h, l, c)
if exit_order:
self.position = None
return exit_order
if self.position is None:
if fast_ma > slow_ma and self._prev_fast_ma <= self._prev_slow_ma:
self.position = Signal(
side='long', entry_price=c,
sl_price=c * (1 - self.sl_pct),
tp_price=c * (1 + self.tp_pct),
size=1.0, timestamp=timestamp,
)
return OrderRequest('buy', 'market', c, 1.0)
self._prev_fast_ma = fast_ma
self._prev_slow_ma = slow_ma
return None
def _check_exit(self, high: float, low: float,
close: float) -> Optional[OrderRequest]:
pos = self.position
if pos.side == 'long':
if low <= pos.sl_price:
return OrderRequest('sell', 'market', pos.sl_price, pos.size)
if high >= pos.tp_price:
return OrderRequest('sell', 'market', pos.tp_price, pos.size)
return None
Désormais, le backtest et le bot utilisent le même StrategyCore :
from strategy_core import StrategyCore
def run_backtest(candles, params, fill_model):
core = StrategyCore(params)
trades = []
for candle in candles:
order = core.on_candle(
candle['timestamp'], candle['open'], candle['high'],
candle['low'], candle['close'], candle['volume'],
)
if order:
fill_price = fill_model.simulate_fill(order, candle)
trades.append({'price': fill_price, 'side': order.side})
return trades
from strategy_core import StrategyCore
async def run_live(exchange, symbol, params):
core = StrategyCore(params)
async for candle in exchange.stream_candles(symbol, '1m'):
order = core.on_candle(
candle['timestamp'], candle['open'], candle['high'],
candle['low'], candle['close'], candle['volume'],
)
if order:
await exchange.place_order(symbol, order.side,
order.order_type, order.size)
La règle clé : StrategyCore ne sait pas d'où viennent les données ni où les ordres sont envoyés. Il reçoit des OHLCV et renvoie une OrderRequest. Tout le reste relève de la responsabilité de la couche d'infrastructure.
Modèle 2 : Unification événementielle (approche NautilusTrader)

NautilusTrader met en œuvre la parité via un NautilusKernel unifié — un moteur natif en Rust avec un noyau événementiel déterministe et une résolution à la nanoseconde. La même implémentation de stratégie fonctionne aussi bien en backtest qu'en trading en direct.
L'architecture repose sur le modèle ports et adaptateurs (architecture hexagonale) :
┌──────────────────────────────────┐
│ NautilusKernel │
│ ┌───────────┐ ┌─────────────┐ │
│ │ Strategy │ │ RiskEngine │ │
│ │ (Python) │ │ (Rust) │ │
│ └─────┬─────┘ └──────┬──────┘ │
│ │ │ │
│ ┌─────┴───────────────┴──────┐ │
│ │ Message Bus (Rust) │ │
│ └─────┬───────────────┬──────┘ │
└────────┼───────────────┼─────────┘
│ │
┌─────┴─────┐ ┌─────┴──────┐
│ Backtest │ │ Live │
│ Adapter │ │ Adapter │
│ FillModel │ │ Exchange │
│ (L2 book) │ │ Gateway │
└────────────┘ └────────────┘
Avantages :
- Replay déterministe. Les événements sont traités dans un ordre strictement défini — le résultat du backtest est reproductible bit à bit.
- FillModel personnalisé. Simulation du carnet d'ordres L2 pour chaque exécution — le slippage est simulé en fonction de la profondeur réelle du carnet d'ordres.
- Performance. Jusqu'à 5 millions de lignes/sec, traitement de données qui ne tiennent pas en RAM.
- Redis + PostgreSQL. Cache et bus de messages via Redis, persistance via PostgreSQL — infrastructure identique pour le backtest et le direct.
Modèle 3 : Strategy Interface (approche Freqtrade)
Freqtrade utilise une interface unifiée IStrategy : la même classe de stratégie fonctionne à la fois en backtest et en direct. La seule différence est la couche de persistance.
class IStrategy:
"""Unified interface — the implementation does not know if this is a backtest or live."""
def populate_indicators(self, dataframe, metadata):
"""Compute indicators."""
dataframe['fast_ma'] = dataframe['close'].rolling(20).mean()
dataframe['slow_ma'] = dataframe['close'].rolling(50).mean()
return dataframe
def populate_entry_trend(self, dataframe, metadata):
"""Determine entry signals."""
dataframe.loc[
(dataframe['fast_ma'] > dataframe['slow_ma']) &
(dataframe['fast_ma'].shift(1) <= dataframe['slow_ma'].shift(1)),
'enter_long'
] = 1
return dataframe
def populate_exit_trend(self, dataframe, metadata):
"""Determine exit signals."""
dataframe.loc[
(dataframe['fast_ma'] < dataframe['slow_ma']),
'exit_long'
] = 1
return dataframe
Freqtrade offre en plus :
- Hyperopt via Optuna — optimisation des paramètres de la stratégie
--timeframe-detail— descente vers une échelle de temps plus fine pour affiner les fills (similaire au drill-down adaptatif)
Comparaison des modèles
| Shared Core | Événementiel (NautilusTrader) | Strategy Interface (Freqtrade) | |
|---|---|---|---|
| Complexité d'implémentation | Faible | Élevée | Moyenne |
| Niveau de parité | Moyen | Maximal | Élevé |
| Simulation de fills | FillModel séparé | Carnet d'ordres L2 | --timeframe-detail |
| Langage du noyau | Python | Rust + Python | Python |
| Adapté pour | Moteurs personnalisés | Trading institutionnel | Démarrage rapide |
Précision de la simulation de fill

La simulation de fill est la principale source de divergence d'exécution. Trois niveaux de précision :
Niveau 1 : Naïf (fill au prix de clôture)
fill_price = candle['close']
Erreur : ne tient compte ni du slippage, ni du spread, ni des exécutions partielles. Surestime systématiquement le PnL.
Niveau 2 : Modèle de slippage
def simulate_fill(order, candle, slippage_bps=5):
"""Fill with slippage."""
base_price = candle['close']
slip = base_price * slippage_bps / 10000
if order.side == 'buy':
return base_price + slip # Buy at a higher price
else:
return base_price - slip # Sell at a lower price
Erreur : un slippage fixe ne tient pas compte de la liquidité ni de la taille de l'ordre. Meilleur que le naïf, mais reste un modèle grossier.
Niveau 3 : Drill-down adaptatif avec des données 1s/100ms
La meilleure option : utiliser des données réelles à granularité fine pour déterminer précisément l'ordre d'exécution SL/TP. Décrit en détail dans l'article Drill-down adaptatif : backtesting à granularité variable.
class RealisticFillModel:
"""
Combined fill model: slippage + spread + volume impact.
"""
def __init__(self, avg_spread_bps=3, impact_coeff=0.1):
self.avg_spread_bps = avg_spread_bps
self.impact_coeff = impact_coeff
def simulate_fill(self, order, candle, order_size_usd):
base_price = candle['close']
spread_cost = base_price * self.avg_spread_bps / 20000
candle_volume_usd = candle['volume'] * candle['close']
participation_rate = order_size_usd / max(candle_volume_usd, 1)
impact = base_price * self.impact_coeff * np.sqrt(participation_rate)
if order.side == 'buy':
return base_price + spread_cost + impact
else:
return base_price - spread_cost - impact
Formule d'impact de marché (modèle Almgren-Chriss simplifié) :
où est la volatilité, le coefficient d'impact, le volume de l'ordre, et le volume de marché pour la période.
Checklist pratique de parité

Avant de lancer le bot en direct, vérifiez chaque point :
Code :
- La stratégie utilise un shared core (un seul module pour backtest et live)
- Pas de duplication de la logique de signal à deux endroits
- Des tests unitaires vérifient des sorties identiques du noyau pour des entrées identiques
- L'ordre de vérification des conditions est identique (SL avant TP ? TP avant SL ?)
Données :
- Le format d'horodatage est identique (UTC, même fournisseur)
- L'agrégation OHLCV suit les mêmes règles
- La gestion des bougies manquantes est identique
- Pas de look-ahead bias — le backtest ne regarde pas dans le futur
Exécution :
- Le modèle de slippage est calibré sur des données réelles
- Les exécutions partielles sont modélisées (ou au moins estimées de façon pessimiste)
- Les ordres limites disposent d'un modèle de priorité de file d'attente
- La latence est prise en compte (délai de 100 à 500 ms entre signal et fill)
Coûts :
- Les commissions maker/taker sont incluses avec le taux actuel
- Les funding rates sont prises en compte pour les futures perpétuels
- Le spread est modélisé (au moins la moyenne)
Infrastructure :
- Persistance d'état : le bot récupère les positions après un redémarrage
- Logique de reconnexion : le WebSocket se reconnecte sans perte de données
- Logging : tous les ordres et fills sont journalisés pour l'analyse post-mortem
Surveillance de la divergence en production
La parité n'est pas une vérification ponctuelle mais un processus continu. Après le lancement du bot, les divergences doivent être suivies en temps réel.
Mode shadow (paper trading)

Faites tourner le bot en parallèle du backtest sur les mêmes données. Le bot génère des signaux mais n'envoie pas d'ordres — il se contente de journaliser. Simultanément, le backtest traite les mêmes données. Comparez :
class DivergenceMonitor:
"""
Compares backtest and live bot signals in real time.
"""
def __init__(self, tolerance_pct=0.5):
self.tolerance = tolerance_pct / 100
self.divergences = []
def compare_signal(self, backtest_signal, live_signal, timestamp):
"""Compare backtest and live signals."""
if backtest_signal is None and live_signal is None:
return # Both silent — OK
if (backtest_signal is None) != (live_signal is None):
self.divergences.append({
'timestamp': timestamp,
'type': 'signal_mismatch',
'backtest': backtest_signal,
'live': live_signal,
'severity': 'HIGH',
})
return
price_diff = abs(
backtest_signal.entry_price - live_signal.entry_price
) / backtest_signal.entry_price
if price_diff > self.tolerance:
self.divergences.append({
'timestamp': timestamp,
'type': 'price_divergence',
'diff_pct': price_diff * 100,
'severity': 'MEDIUM',
})
def compare_fill(self, backtest_fill, live_fill, timestamp):
"""Compare execution."""
if backtest_fill and live_fill:
slippage = (live_fill['price'] - backtest_fill['price']
) / backtest_fill['price']
self.divergences.append({
'timestamp': timestamp,
'type': 'fill_divergence',
'slippage_bps': slippage * 10000,
'severity': 'LOW' if abs(slippage) < 0.001 else 'MEDIUM',
})
def report(self):
"""Weekly divergence report."""
from collections import Counter
severity_counts = Counter(d['severity'] for d in self.divergences)
return {
'total_divergences': len(self.divergences),
'by_severity': dict(severity_counts),
'avg_slippage_bps': np.mean([
d['slippage_bps'] for d in self.divergences
if d['type'] == 'fill_divergence'
]) if any(d['type'] == 'fill_divergence'
for d in self.divergences) else 0,
}
Métriques du dashboard
| Métrique | Formule | Seuil d'alerte |
|---|---|---|
| Taux de correspondance des signaux | < 95 % | |
| Slippage moyen | (bps) | > 10 bps |
| Taux d'exécution | < 90 % | |
| Divergence de PnL | > 20 % | |
| Latence p99 | 99e centile signal-à-fill | > 500 ms |
Calibration du modèle de slippage

Après avoir accumulé des données pendant 2 à 4 semaines, vous pouvez calibrer le modèle de slippage du backtest sur des données réelles :
def calibrate_slippage(live_fills: list[dict]) -> dict:
"""
Calibrate slippage model using real fills.
live_fills: [{'expected_price': ..., 'actual_price': ..., 'size_usd': ..., 'volume_usd': ...}]
"""
slippages = []
participation_rates = []
for fill in live_fills:
slip = abs(fill['actual_price'] - fill['expected_price']
) / fill['expected_price']
part = fill['size_usd'] / max(fill['volume_usd'], 1)
slippages.append(slip)
participation_rates.append(part)
slippages = np.array(slippages)
participation_rates = np.array(participation_rates)
from scipy.optimize import curve_fit
def model(x, k, base):
return k * np.sqrt(x) + base
popt, _ = curve_fit(model, participation_rates, slippages,
p0=[0.1, 0.0001])
return {
'impact_coeff': popt[0],
'base_slippage': popt[1],
'mean_slippage_bps': np.mean(slippages) * 10000,
'p95_slippage_bps': np.percentile(slippages, 95) * 10000,
}
Liens avec d'autres outils
La parité backtest-live n'est pas une tâche isolée. Elle recoupe d'autres outils de la série « Backtests sans illusions » :
- Drill-down adaptatif — améliore la précision de la simulation de fill, un composant clé de la parité d'exécution.
- Funding rates — si le backtest ne modélise pas le funding, la parité est impossible avec un levier > 3x.
- Cache Parquet — des échelles de temps et indicateurs précalculés garantissent que le backtest voit les mêmes données que le bot. L'émulation de RunningCandleBuffer = mise à jour en temps réel.
- Polars vs Pandas — en passant de pandas (backtest) à Polars (live), il faut s'assurer que les résultats numériques concordent.
- Walk-Forward — le walk-forward sur des données out-of-sample montre comment la stratégie se dégrade — ce qui est plus proche du direct qu'un backtest in-sample.
Recommandations
-
Le shared core est obligatoire. Une base de code unique pour la génération de signaux est l'exigence minimale pour la parité. Deux fichiers avec une logique identique garantissent une divergence en un mois.
-
Calibrez le modèle de fill. Un slippage fixe de 5 bps vaut mieux que rien. Un modèle de slippage calibré sur des données réelles est nettement meilleur.
-
Utilisez le mode shadow pendant les 2 à 4 premières semaines. Ne tradez pas avec de l'argent réel tant que le taux de correspondance des signaux n'atteint pas 95 % ou plus.
-
Modélisez les funding rates. Pour les futures perpétuels, ce n'est pas optionnel — c'est obligatoire. Le funding peut absorber tout le PnL avec un levier > 5x.
-
Journalisez tout. Chaque signal, chaque ordre, chaque fill — avec horodatage. Sans logs, l'analyse post-mortem est impossible.
-
Automatisez la comparaison. Un rapport hebdomadaire du DivergenceMonitor devrait arriver automatiquement. N'attendez pas que le PnL devienne négatif.
-
Backtest pessimiste par défaut. Il vaut mieux sous-estimer les attentes dans le backtest et être agréablement surpris en direct que l'inverse. Le modèle de slippage doit être conservateur.
Conclusion

La parité backtest-live n'est pas une propriété d'un système mais un processus. La parité parfaite n'existe pas : un backtest est par définition un modèle de la réalité, et un modèle simplifie toujours. Mais la différence entre « le modèle diverge de 5 % » et « le modèle diverge de 50 % » est déterminée par l'architecture.
Trois niveaux de maturité :
- Basique. Shared core, slippage fixe, commissions. Divergence : 10-20 %.
- Avancé. Architecture événementielle, drill-down adaptatif, modèle de funding, mode shadow. Divergence : 5-10 %.
- Institutionnel. Simulation du carnet d'ordres L2, modèle d'impact calibré, monitoring de divergence en temps réel. Divergence : 2-5 %.
Votre tâche consiste à déterminer à quel niveau vous vous situez et à comprendre quelle divergence vous jugez acceptable pour votre taille de position et votre levier.
Liens utiles
- NautilusTrader — High-Performance Algorithmic Trading Platform
- Freqtrade — Free, open source crypto trading bot
- Almgren, R., Chriss, N. — Optimal Execution of Portfolio Transactions (2001)
- Lopez de Prado — Advances in Financial Machine Learning, Chapter 12: Backtesting
- Ernest Chan — Quantitative Trading: How to Build Your Own Algorithmic Trading Business
- Hexagonal Architecture (Ports and Adapters) — Alistair Cockburn
- Optuna — Hyperparameter Optimization Framework
Citation
@article{soloviov2026backtestliveparity,
author = {Soloviov, Eugen},
title = {Backtest-live parity: why your bot trades differently from the backtest},
year = {2026},
url = {https://marketmaker.cc/ru/blog/post/backtest-live-parity},
description = {Complete taxonomy of divergences between backtesting and live trading: from slippage and partial fills to codebase desynchronization. Architectural patterns for achieving parity and a production monitoring checklist.}
}
Authors
Trading-systems engineer
Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.