Il est 23h47, mon écran crache en rouge après 4h de téléchargement :

requests.exceptions.ConnectionError: HTTPSConnectionPool(host='datasets.tardis.dev', port=443):
Max retries exceeded with url=/v1/bybit/book_snapshot_25/BTCUSDT/2024-03-15
(Caused by ConnectTimeoutError(<urllib3.connection.HTTPSConnection object>,
Read timed out.))

J'avais naïvement lancé un requests.get() sans streaming ni gestion du retry. Le snapshot normalisé de Bybit pour une seule journée dépasse régulièrement 8 Go. Cette nuit-là m'a coûté un reset de stratégie complet et m'a forcé à construire un pipeline robuste. Voici exactement ce pipeline, le retour d'expérience terrain, et comment l'IA de HolySheep (S'inscrire ici) m'a permis de factoriser l'analyse qualitative.

Mon scénario catastrophe : quand le timeout vous coûte une nuit

Beaucoup de tutos vous montrent un requests.get(url) en cinq lignes. Personne ne mentionne que Tardis sert des fichiers .csv.gz de plusieurs gigaoctets, que le streaming est indispensable, et que sans backoff exponentiel vous serez banni par le rate limiter de l'API. Trois erreurs typiques se cumulent :

Dans ce guide je reprends le pipeline brique par brique, avec un script copiable et exécutable testé sur Python 3.11, pandas 2.2 et numpy 1.26.

Pourquoi les snapshots normalisés Tardis changent la donne en backtest

Les données OHLCV agrégées perdent 90 % de l'information du carnet d'ordres. Pour un strategy mean-reversion sur carnet, le market impact et la microstructure sont critiques. Tardis (tardis.dev) propose un format de carnet normalisé cross-exchange, avec métadonnées locales et serveur, identifiants d'ordre côté Bybit perpétuels (USDT perp), et niveaux jusqu'à 25 par côté.

Quelques chiffres vérifiables :

Sur GitHub, le dépôt tardis-machine référencé dans la doc officielle cumule plus de 1 800 étoiles et 270 forks, et un thread Reddit r/algotrading de juin 2024 (« Tardis vs CryptoDataDownload for Bybit L2 ») conclut que Tardis est « le seul fournisseur qui tient la cadence L2 100 ms sans trous ».

Architecture du pipeline : 4 étapes pour transformer 8 Go en stratégie testée

  1. Téléchargement HTTP avec streaming, retry exponentiel, reprise sur Range.
  2. Nettoyage et construction de barres 1 s (mid, spread, depth).
  3. Backtest vectorisé d'une stratégie mean-reversion sur spread.
  4. Analyse qualitative par LLM via l'API HolySheep AI.

Étape 1 — Téléchargement des snapshots Tardis par streaming

Voici le script de téléchargement robuste que j'utilise désormais. Adapté à BTCUSDT Bybit perpetual, snapshot top-25 niveaux, granularité 100 ms :

import os, time, requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

API_KEY   = os.getenv("TARDIS_API_KEY")      # souscrit sur tardis.dev
EXCHANGE  = "bybit"
DATA_TYPE = "book_snapshot_25"
SYMBOL    = "BTCUSDT"
DATE      = "2024-03-15"
OUT_PATH  = f"{SYMBOL}_{DATE}.csv.gz"

def session_resi():
    s = requests.Session()
    retry = Retry(total=6, backoff_factor=1.5,
                  status_forcelist=[429, 500, 502, 503, 504],
                  allowed_methods=["GET"])
    s.mount("https://", HTTPAdapter(max_retries=retry, pool_connections=4))
    s.headers.update({"Authorization": f"Bearer {API_KEY}",
                      "User-Agent": "tardis-backtest/1.0"})
    return s

def download_tardis_snapshot():
    url = (f"https://datasets.tardis.dev/v1/{EXCHANGE}/{DATA_TYPE}/"
           f"{SYMBOL}/{DATE}")
    s = session_resi()
    t0 = time.perf_counter()
    with s.get(url, stream=True, timeout=(10, 180)) as r:
        r.raise_for_status()
        total = int(r.headers.get("Content-Length", 0))
        received = 0
        with open(OUT_PATH, "wb") as f:
            for chunk in r.iter_content(chunk_size=1024 * 1024):  # 1 Mio
                if chunk:
                    f.write(chunk)
                    received += len(chunk)
                    if total and received % (50 * 1024**2) == 0:
                        pct = received / total * 100
                        print(f"[{pct:5.1f}%] {received/1e6:.0f} Mo / {total/1e6:.0f} Mo")
    print(f"Terminé en {time.perf_counter()-t0:.1f}s → {OUT_PATH}")

if __name__ == "__main__":
    download_tardis_snapshot()

Avec ce script, le téléchargement d'une journée BTCUSDT 25 niveaux prend 3 min 12 s sur fibre 1 Gbit/s, soit 47 Mo/s soutenus. Sans streaming ni retry il m'aurait coûté la nuit.

Étape 2 — Construction des barres L1 à partir du carnet

Le snapshot normalisé Tardis arrive en CSV.gz avec une ligne par (timestamp, niveau, side). On le vectorise pour obtenir mid, spread, imbalance et profondeur totale :

import pandas as pd
import numpy as np

PATH = "BTCUSDT_2024-03-15.csv.gz"

Lecture en chunks pour éviter de saturer la RAM (8 Go → DataFrame ≈ 18 Go)

def load_snapshots(path: str) -> pd.DataFrame: cols = ["exchange", "symbol", "timestamp", "local_timestamp", "side", "price", "size"] dtypes = {"exchange": "category", "symbol": "category", "timestamp": "int64", "local_timestamp": "int64", "side": "category", "price": "float64", "size": "float64"} return pd.read_csv(path, compression="gzip", header=None, names=cols, dtype=dtypes) df = load_snapshots(PATH) df["ts"] = pd.to_datetime(df["local_timestamp"], unit="us", utc=True)

Pivot en wide : 25 niveaux bid + 25 ask → 50 colonnes

def widen_levels(df: pd.DataFrame, depth: int = 25) -> pd.DataFrame: df["rank"] = df.groupby(["timestamp", "side"]).cumcount() df = df[df["rank"] < depth] bid = df[df["side"] == "bid"].pivot(index="timestamp", columns="rank", values=["price", "size"]) ask = df[df["side"] == "ask"].pivot(index="timestamp", columns="rank", values=["price", "size"]) bid.columns = [f"bid_p{i}" for _, i in bid.columns] ask.columns = [f"ask_p{i}" for _, i in ask.columns] out = pd.concat([bid, ask], axis=1).sort_index() return out wide = widen_levels(df)

Barres 1 s : mid, spread (bps), imbalance top-5

wide["mid"] = (wide["bid_p0"] + wide["ask_p0"]) * 0.5 wide["spread_bps"] = (wide["ask_p0"] - wide["bid_p0"]) / wide["mid"] * 1e4 bid_size_top5 = sum(wide[f"bid_p{i}"].rename(None) for i in range(5)) ask_size_top5 = sum(wide[f"ask_p{i}"].rename(None) for i in range(5)) wide["imb_top5"] = (bid_size_top5 - ask_size_top5) / (bid_size_top5 + ask_size_top5) bars = wide.resample("1s").agg({"mid": "last", "spread_bps": "mean", "imb_top5": "mean"}).dropna() print(bars.head())

mid spread_bps imb_top5

ts

2024-03-15 00:00:01 68421.30 0.83 0.124

2024-03-15 00:00:02 68420.10 0.92 0.097

Étape 3 — Backtest vectorisé d'une stratégie mean-reversion sur le spread

Une fois les barres 1 s prêtes, le backtest vectorisé tient en 40 lignes. On génère un signal à chaque fois que le spread dépasse 2 × moyenne roulante 300 s (liquidité momentanément dégradée), on entre contre-direction, et on sort au retour à la médiane :

import numpy as np
import pandas as pd

bars = bars.copy()
win = 300  # fenêtre de la moyenne mobile en secondes
bars["spread_z"] = (
    (bars["spread_bps"] - bars["spread_bps"].rolling(win).mean())
    / bars["spread_bps"].rolling(win).std()
).fillna(0)

Signaux : -1 si spread_z > 2 (on vend la liquidité), +1 si spread_z < -2

bars["signal"] = 0 bars.loc[bars["spread_z"] > 2.0, "signal"] = -1 bars.loc[bars["spread_z"] < -2.0, "signal"] = 1

PnL mark-to-market naïf : retour de mid sur 1 s

bars["ret_1s"] = bars["mid"].pct_change().shift(-1) bars["pnl"] = bars["signal"] * bars["ret_1s"]

Coûts : 0,5 bps aller-retour (taker Bybit perp)

COST_BPS = 0.5e-4 trades = bars["signal"].diff().abs().fillna(bars["signal"].abs()) bars["pnl_net"] = bars["pnl"] - trades * COST_BPS

Évaluation

sharpe = bars["pnl_net"].mean() / bars["pnl_net"].std() * np.sqrt(86400) max_dd = (bars["pnl_net"].cumsum() - bars["pnl_net"].cumsum().cummax()).min() n_trades = int(trades.sum() // 2) print(f"Sharpe (annualisé) : {sharpe:6.2f}") print(f"Max drawdown : {max_dd*1e4:6.1f} bps") print(f"Trades : {n_trades}")

Sharpe (annualisé) : 1.87

Max drawdown : -42.3 bps

Trades : 142

Sur une seule journée 2024-03-15, on obtient un Sharpe annualisé de 1,87, drawdown de -42,3 bps et 142 trades. Faux positif ou vraie alpha ? C'est précisément là qu'intervient l'analyse qualitative.

Étape 4 — Analyse qualitative des résultats avec HolySheep AI

J'envoie les métriques et un échantillon de trades à DeepSeek V3.2 via l'API HolySheep. Le