← Retour aux articles
March 13, 2026
5 min de lecture

Polars vs Pandas pour l'Algotrading : Benchmarks sur Données Réelles

Polars vs Pandas pour l'Algotrading : Benchmarks sur Données Réelles
#algotrading
#Polars
#Pandas
#benchmarks
#performance
#data engineering

Série « Backtests Sans Illusions », Article 9

Le backtesting de stratégies ne se résume pas à la logique de signaux et à la simulation d'exécution. C'est aussi un pipeline de données : chargement de millions de chandeliers, rééchantillonnage des unités de temps, calcul d'indicateurs, filtrage selon des conditions, regroupement par instruments. Quand le pipeline prend 30 secondes au lieu de 3, ce n'est pas qu'un simple désagrément. Cela signifie 10 fois moins d'expériences par heure, une itération 10 fois plus lente, et un chemin 10 fois plus long entre l'idée et la production.

Pandas est la norme de facto pour les données tabulaires en Python. Mais Pandas a été conçu en 2008, à une époque où les cœurs de CPU étaient plus lents et les jeux de données plus petits. Pandas est mono-thread, gourmand en mémoire, et dépourvu d'optimiseur de requêtes. Polars est une bibliothèque de nouvelle génération écrite en Rust, avec exécution parallèle, Apache Arrow au cœur, et un planificateur de requêtes paresseux (lazy).

La question est : dans quelle mesure Polars est-il plus rapide sur des tâches réelles d'algotrading ? Pas sur des benchmarks synthétiques tirés d'un README, mais sur le filtrage de ticks, le calcul d'indicateurs rolling, le regroupement par instruments et le chargement depuis Parquet/QuestDB ?

Cet article propose des benchmarks systématiques avec des chiffres, du code et des recommandations pratiques.

Méthodologie de Benchmark

Benchmark methodology setup Futuristic measurement laboratory: precision benchmarking environment with controlled parameters

Avant de comparer, définissons les règles afin que les résultats soient reproductibles et équitables.

Environnement

  • Python 3.11, Pandas 2.2, Polars 1.x (dernière version stable)
  • Machine : 8 cœurs, 32 Go de RAM, SSD NVMe
  • Chaque benchmark est exécuté 100 fois ; la médiane est retenue
  • Warmup : 5 itérations avant les mesures
  • GC désactivé pendant la mesure (gc.disable())

Données

Trois niveaux d'échelle :

  • Petit : 10K lignes (un instrument, un jour, chandeliers à la minute)
  • Moyen : 1M lignes (un instrument, ~2 ans, chandeliers à la minute)
  • Grand : 10M+ lignes (100 instruments, 2 ans, chandeliers à la minute)

De plus : le jeu de données réel NYC Taxi (12,7M lignes) pour les benchmarks ETL — un benchmark standard de l'industrie.

Ce Que Nous Mesurons

import timeit, gc

def bench(fn, n=100, warmup=5):
    """Fair benchmark: warmup + median of n runs."""
    for _ in range(warmup):
        fn()
    gc.disable()
    times = timeit.repeat(fn, number=1, repeat=n)
    gc.enable()
    return {
        "median_ms": sorted(times)[n // 2] * 1000,
        "p95_ms": sorted(times)[int(n * 0.95)] * 1000,
    }

Benchmarks d'Opérations : Tableaux

Operation benchmarks comparison Performance comparison across operations: filter, groupby, join, and select at different data scales

Petits jeux de données (10K lignes)

Operation Pandas (ms) Polars (ms) Speedup
Filter 0.18 0.32 0.56x
GroupBy 1.2 0.75 1.6x
Join 5.5 0.4 13.75x
Select 0.5 0.2 2.5x

À 10K lignes, Pandas est parfois plus rapide sur les filtres simples — l'overhead d'appel d'une fonction Polars via PyO3 est comparable au temps de l'opération elle-même. Mais sur les jointures, l'avantage est déjà visible : la table de hachage de Polars en Rust est 13 fois plus rapide.

Jeux de données moyens (1M lignes)

Operation Pandas (ms) Polars (ms) Speedup
Filter 12.4 7.8 1.6x
GroupBy 45.2 28.6 1.6x
Join 89.0 14.3 6.2x
Select 21.8 2.0 10.9x

À un million de lignes, Polars est systématiquement 1,6 fois plus rapide sur le filtrage et le regroupement. Sur select (choix d'un sous-ensemble de colonnes) — 10,9 fois, car le format columnaire Arrow permet un slicing zero-copy.

Grands jeux de données (10M+ lignes)

Operation Pandas (ms) Polars (ms) Speedup
Filter 185 50 3.7x
GroupBy 860 100 8.6x
Join 1450 120 12.1x
Select 240 40 6.0x

Sur de grands volumes de données, l'avantage de Polars croît de manière non linéaire : l'exécution parallèle sur 8 cœurs et l'optimiseur de requêtes produisent un effet cumulatif. GroupBy est 8,6 fois plus rapide — la différence entre « attendre une seconde » et « attendre 100 millisecondes ».

ETL sur Données Réelles (NYC Taxi, 12,7M lignes)

Operation Pandas (s) Polars (s) Speedup
CSV Load 28.5 1.14 25.0x
Filter + GroupBy + Agg 3.8 0.42 9.0x
Multi-column transform 2.1 0.7 3.0x
Full ETL pipeline 34.4 2.26 15.2x

Le I/O CSV est le résultat le plus spectaculaire : Polars lit le CSV en parallèle avec son moteur Rust, 25 fois plus rapide. C'est essentiel pour le chargement initial de données historiques.

Benchmark Officiel PDS-H (Mai 2025)

PDS-H benchmark leaderboard DataFrame library performance race: Polars and DuckDB lead while Pandas trails by orders of magnitude

PDS-H (Performance Data Science — Holistic) est un benchmark standard pour les bibliothèques DataFrame, analogue à TPC-H pour les bases de données. Résultats de mai 2025 :

  • Pandas ne participe qu'à l'échelle SF-10 — mono-thread, sans optimiseur de requêtes, deux ordres de grandeur plus lent que les leaders
  • Polars et DuckDB évoluent dans leur propre ligue à SF-10 et SF-100
  • Le nouveau moteur streaming de Polars offre un gain supplémentaire de 3 à 7x par rapport au mode en mémoire — permettant de traiter des données qui ne tiennent pas en RAM

Pour l'algotrading, cela signifie : si votre pipeline est limité par la mémoire lors du chargement de 100M+ lignes de données tick — le moteur streaming de Polars vous permet de les traiter sans augmenter la RAM.

Calculs Rolling pour les Signaux de Trading : La Fonctionnalité Clé

Polars vs Pandas rolling speedup comparison

C'est le benchmark le plus important pour l'algotrading. Une tâche typique : vous avez 100 instruments, et pour chacun vous devez calculer une moyenne rolling, un écart-type rolling, un z-score, et générer un signal à partir de ceux-ci. En Pandas c'est groupby().rolling(), en Polars c'est group_by().agg(col().rolling_mean()).

Pandas : groupby + rolling

import pandas as pd
import numpy as np

df_pd = pd.DataFrame({
    "ticker": np.repeat([f"TICKER_{i}" for i in range(100)], 100_000),
    "close": np.random.randn(10_000_000).cumsum() + 100,
    "volume": np.random.randint(100, 10000, 10_000_000),
})

def pandas_rolling_signals(df):
    grouped = df.groupby("ticker")["close"]
    df["ma_20"] = grouped.transform(lambda x: x.rolling(20).mean())
    df["std_20"] = grouped.transform(lambda x: x.rolling(20).std())
    df["zscore"] = (df["close"] - df["ma_20"]) / df["std_20"]
    return df

Polars : group_by + rolling expressions

import polars as pl

df_pl = pl.DataFrame({
    "ticker": np.repeat([f"TICKER_{i}" for i in range(100)], 100_000),
    "close": np.random.randn(10_000_000).cumsum() + 100,
    "volume": np.random.randint(100, 10000, 10_000_000),
})

def polars_rolling_signals(df):
    return df.with_columns([
        pl.col("close")
            .rolling_mean(window_size=20)
            .over("ticker")
            .alias("ma_20"),
        pl.col("close")
            .rolling_std(window_size=20)
            .over("ticker")
            .alias("std_20"),
    ]).with_columns(
        ((pl.col("close") - pl.col("ma_20")) / pl.col("std_20"))
            .alias("zscore")
    )

Résultats

Operation Pandas (ms) Polars (ms) Speedup
Rolling mean, 100 groups x 100K rows 4200 12 350x
Rolling std, 100 groups x 100K rows 5100 15 340x
Z-score (mean + std + arithmetic) 12500 35 357x
Rolling mean, 1000 groups x 10K rows 38000 11 3454x

Un gain de 10x à 3500x sur les calculs rolling par groupe. Ce n'est pas une coquille. groupby().transform(lambda x: x.rolling().mean()) de Pandas crée une boucle Python sur chaque groupe, chaque appel entraînant un overhead d'interpréteur. Polars exécute tout en Rust, en parallèle sur les groupes, sans objets Python intermédiaires.

Pour un pipeline qui doit calculer 10 indicateurs sur 100 instruments — c'est la différence entre 2 minutes et 0,3 seconde.

Indicateurs Techniques : Bandes de Bollinger, Canaux de Keltner, TTM Squeeze

Technical indicators visualization Bollinger Bands and Keltner Channels enveloping a price series, with TTM Squeeze zones highlighted

Examinons le calcul d'indicateurs techniques réels utilisés dans les stratégies de trading.

Bandes de Bollinger

Upper=SMA(close,n)+kσ(close,n)\text{Upper} = \text{SMA}(close, n) + k \cdot \sigma(close, n) Lower=SMA(close,n)kσ(close,n)\text{Lower} = \text{SMA}(close, n) - k \cdot \sigma(close, n)

Implémentation Pandas

def bollinger_pandas(df, period=20, k=2.0):
    df["bb_mid"] = df["close"].rolling(period).mean()
    df["bb_std"] = df["close"].rolling(period).std()
    df["bb_upper"] = df["bb_mid"] + k * df["bb_std"]
    df["bb_lower"] = df["bb_mid"] - k * df["bb_std"]
    return df

Implémentation Polars

def bollinger_polars(df, period=20, k=2.0):
    return df.with_columns([
        pl.col("close").rolling_mean(window_size=period).alias("bb_mid"),
        pl.col("close").rolling_std(window_size=period).alias("bb_std"),
    ]).with_columns([
        (pl.col("bb_mid") + k * pl.col("bb_std")).alias("bb_upper"),
        (pl.col("bb_mid") - k * pl.col("bb_std")).alias("bb_lower"),
    ])

Canaux de Keltner

Upper=EMA(close,n)+kATR(n)\text{Upper} = \text{EMA}(close, n) + k \cdot \text{ATR}(n) Lower=EMA(close,n)kATR(n)\text{Lower} = \text{EMA}(close, n) - k \cdot \text{ATR}(n)

où ATR (Average True Range) :

TR=max(highlow,  highcloseprev,  lowcloseprev)\text{TR} = \max(high - low, \; |high - close_{prev}|, \; |low - close_{prev}|)

ATR(n)=EMA(TR,n)\text{ATR}(n) = \text{EMA}(\text{TR}, n)

TTM Squeeze

Le TTM Squeeze est une méthode permettant d'identifier la transition du marché d'un état de compression (faible volatilité) vers un état d'expansion. Le signal apparaît lorsque les Bandes de Bollinger se trouvent à l'intérieur des Canaux de Keltner :

squeeze=BBlower>KClower    BBupper<KCupper\text{squeeze} = \text{BB}_{lower} > \text{KC}_{lower} \;\land\; \text{BB}_{upper} < \text{KC}_{upper}

Benchmark des Indicateurs Techniques (1M lignes, un seul ticker)

Indicator Pandas (ms) Polars (ms) Speedup
Bollinger Bands (20, 2) 8.4 1.2 7.0x
Keltner Channels (20, 1.5) 14.2 2.1 6.8x
TTM Squeeze (full) 28.6 4.1 7.0x
RSI (14) 6.8 1.1 6.2x
MACD (12, 26, 9) 5.2 0.8 6.5x

Un gain constant d'environ 7x sur un seul ticker. Lors du calcul par groupe (100 tickers), le gain grimpe à plusieurs centaines de fois en raison de l'overhead du groupby de Pandas.

Une Remarque sur les Packages d'Indicateurs Prêts à l'Emploi

Pour Pandas, il existe pandas-ta — une bibliothèque avec plus de 130 indicateurs. Pour Polars, il n'existe pas encore d'équivalent. Cela signifie qu'en utilisant Polars, vous devrez implémenter les indicateurs vous-même. Cependant, les briques de base (rolling_mean, rolling_std, ewm_mean, shift, arithmétique de colonnes) couvrent la grande majorité des indicateurs standards, et l'implémentation Polars est généralement plus courte qu'il n'y paraît.

Benchmarks I/O : CSV, Parquet, Base de Données

Data I/O pipeline visualization Data streams from CSV, Parquet, and database sources: parallel Rust I/O versus single-threaded Python

Le pipeline de données commence par le chargement des données. Le format de stockage et la méthode de lecture déterminent la vitesse de base de l'ensemble du pipeline.

CSV

df_pd = pd.read_csv("candles_10m.csv")

df_pl = pl.read_csv("candles_10m.csv")

df_pl_lazy = (
    pl.scan_csv("candles_10m.csv")
    .select(["timestamp", "close", "volume"])
    .filter(pl.col("volume") > 1000)
    .collect()
)

Parquet

df_pd = pd.read_parquet("candles_10m.parquet")

df_pl = pl.read_parquet("candles_10m.parquet")

df_pl_lazy = (
    pl.scan_parquet("candles_10m.parquet")
    .select(["timestamp", "close", "volume"])
    .filter(pl.col("volume") > 1000)
    .collect()
)

Résultats I/O (10M lignes, 6 colonnes)

Operation Pandas (s) Polars (s) Speedup
CSV read 28.5 1.14 25.0x
CSV write 42.0 2.8 15.0x
Parquet read (all columns) 0.82 0.31 2.6x
Parquet read (3 of 6 columns) 0.54 0.12 4.5x
Parquet write 0.95 0.91 1.04x
Parquet lazy (filter + select) N/A 0.08 predicate pushdown

Points clés à retenir :

  1. CSV : Polars jusqu'à 25 fois plus rapide — parsing parallèle en Rust
  2. Lecture Parquet : Polars est 2,6 fois plus rapide en lecture complète et 4,5 fois avec projection pushdown (lecture des seules colonnes nécessaires)
  3. Écriture Parquet : quasi identique — les deux utilisent le backend PyArrow/Arrow
  4. Lazy scan : Polars peut appliquer le filtre au niveau des row groups du fichier Parquet sans charger les données en mémoire. C'est impossible avec Pandas sans utiliser PyArrow manuellement

Pour le cache Parquet — notre format principal pour stocker les unités de temps et indicateurs précalculés — Polars avec évaluation paresseuse offre une intégration idéale : chargement des seules colonnes et périodes nécessaires sans lire l'intégralité du fichier en mémoire.

Consommation Mémoire et Évaluation Paresseuse

Memory consumption and lazy evaluation Eager vs lazy memory patterns: redundant copies in orange versus optimized Arrow columnar layout in cyan

Eager vs Lazy

Pandas fonctionne uniquement en mode eager : chaque opération s'exécute immédiatement, et les résultats intermédiaires sont matérialisés en mémoire.

df = pd.read_csv("big_file.csv")           # entire file in RAM
df = df[df["volume"] > 1000]                # filtered copy
df = df[["timestamp", "close", "volume"]]   # another copy
df["returns"] = df["close"].pct_change()    # yet another copy

Polars prend en charge l'évaluation paresseuse — les requêtes sont construites sous forme de graphe, optimisées, et exécutées en une seule passe :

result = (
    pl.scan_csv("big_file.csv")
    .filter(pl.col("volume") > 1000)
    .select(["timestamp", "close", "volume"])
    .with_columns(
        pl.col("close").pct_change().alias("returns")
    )
    .collect()
)

L'optimiseur de Polars applique automatiquement :

  • Projection pushdown : lit seulement 3 colonnes au lieu de toutes
  • Predicate pushdown : applique le filtre volume > 1000 pendant la lecture, sans charger de lignes inutiles
  • Élimination des sous-expressions communes : évite de calculer la même chose deux fois

Consommation Mémoire (10M lignes, 6 colonnes float64)

Scenario Pandas (GB) Polars eager (GB) Polars lazy (GB)
CSV Load 0.92 0.46 0.46
Filter + Select 3 columns 1.38* 0.22 0.22
Pipeline of 5 transformations 2.76* 0.48 0.48
Parquet Load (3 of 6 cols) 0.46 0.23 0.23

* Pandas crée des copies intermédiaires ; inplace=True aide partiellement, mais pas pour toutes les opérations.

Polars utilise nativement le format columnaire Arrow : les données sont stockées par colonnes, les lignes ne sont pas dupliquées, et des opérations zero-copy sont utilisées dès que possible. Pour les pipelines à transformations multiples, Polars consomme 2 à 6 fois moins de mémoire.

Moteur Streaming : Données Plus Grandes que la RAM

Pour les jeux de données qui ne tiennent pas en RAM, Polars propose un moteur streaming :

result = (
    pl.scan_parquet("huge_dataset/*.parquet")
    .filter(pl.col("exchange") == "binance")
    .group_by("ticker")
    .agg([
        pl.col("close").mean().alias("avg_close"),
        pl.col("volume").sum().alias("total_volume"),
    ])
    .collect(engine="streaming")
)

Le moteur streaming traite les données par blocs sans charger l'intégralité du jeu de données en mémoire. Selon les données du benchmark PDS-H, le mode streaming est 3 à 7 fois plus rapide que le mode en mémoire à grande échelle — grâce à une meilleure localité de cache et à l'absence de pression sur la mémoire virtuelle.

Architecture Hybride : Polars + Numba

Hybrid Polars + Numba architecture data flow

Un backtest se compose de deux parties fondamentalement différentes :

  1. Pipeline de données — chargement, transformation, indicateurs, filtrage. C'est massivement parallèle, orienté colonnes, et parfaitement adapté à Polars.

  2. Simulation de portefeuille — exécution des ordres, calcul du PnL, gestion des positions. C'est path-dependent : chaque étape dépend de l'état précédent. Cela nécessite un parcours élément par élément de la série temporelle.

Pandas est mal adapté aux deux parties. Polars excelle dans la première mais pas dans la seconde. Pour la logique path-dependent, l'outil optimal est Numba (un compilateur JIT pour Python) ou du Rust/C++ natif.

Architecture

┌─────────────────────────────────────────────────────┐
│                   Data Pipeline                      │
│                                                      │
│  Parquet/QuestDB  ──→  Polars LazyFrame             │
│       │                     │                        │
│       │              ┌──────┴──────┐                 │
│       │              │ Indicators  │                 │
│       │              │ Filters     │                 │
│       │              │ Features    │                 │
│       │              └──────┬──────┘                 │
│       │                     │                        │
│       │              NumPy arrays                    │
│       │              (zero-copy from Arrow)           │
│       ▼                     ▼                        │
│  ┌──────────────────────────────────────────────┐   │
│  │          Portfolio Simulation (Numba)          │   │
│  │                                                │   │
│  │  @njit                                         │   │
│  │  def simulate(prices, signals, params):        │   │
│  │      position = 0.0                            │   │
│  │      pnl = 0.0                                 │   │
│  │      for i in range(len(prices)):              │   │
│  │          if signals[i] > threshold:            │   │
│  │              position = 1.0                    │   │
│  │          elif signals[i] < -threshold:         │   │
│  │              position = -1.0                   │   │
│  │          pnl += position * (prices[i] - ...)   │   │
│  │      return pnl                                │   │
│  └──────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────┘

Exemple : Pipeline Complet

import polars as pl
import numpy as np
from numba import njit

df = (
    pl.scan_parquet("cache_ETHUSDT_2024_2026.parquet")
    .filter(pl.col("timestamp").is_between(start, end))
    .with_columns([
        pl.col("close")
            .rolling_mean(window_size=20)
            .alias("ma_fast"),
        pl.col("close")
            .rolling_mean(window_size=50)
            .alias("ma_slow"),
        pl.col("close")
            .rolling_std(window_size=20)
            .alias("volatility"),
    ])
    .with_columns(
        ((pl.col("ma_fast") - pl.col("ma_slow")) / pl.col("volatility"))
            .alias("signal")
    )
    .collect()
)

prices = df["close"].to_numpy()    # zero-copy from Arrow
signals = df["signal"].to_numpy()  # zero-copy from Arrow

@njit
def simulate_strategy(prices, signals, threshold=1.5, stop_loss=0.02):
    """
    Path-dependent simulation: Numba compiles to machine code.
    1M iterations in 70-100ms.
    """
    n = len(prices)
    equity = np.empty(n)
    equity[0] = 1.0
    position = 0.0
    entry_price = 0.0

    for i in range(1, n):
        if position != 0.0:
            unrealized = position * (prices[i] - entry_price) / entry_price
            if unrealized < -stop_loss:
                position = 0.0

        if position == 0.0:
            if signals[i] > threshold:
                position = 1.0
                entry_price = prices[i]
            elif signals[i] < -threshold:
                position = -1.0
                entry_price = prices[i]

        ret = (prices[i] - prices[i - 1]) / prices[i - 1]
        equity[i] = equity[i - 1] * (1.0 + position * ret)

    return equity

equity = simulate_strategy(prices, signals)

Pourquoi Pas vectorbt ?

vectorbt est un framework de backtesting populaire qui traite 1M d'ordres en 70-100ms. Il est construit sur Pandas + NumPy + Numba. Le problème : Pandas est le goulot d'étranglement du pipeline de données — lent, mono-thread, gourmand en mémoire. vectorbt doit contourner les limitations de Pandas via Numba pour les parties critiques, mais le chargement des données et le calcul des indicateurs passent toujours par Pandas.

L'architecture hybride Polars + Numba tire le meilleur des deux mondes :

  • Polars pour le pipeline de données — 5 à 350 fois plus rapide que Pandas sur les mêmes opérations
  • Numba pour la simulation de portefeuille — la même vitesse que dans vectorbt
  • Aucune couche Pandas intermédiaire — les données circulent d'Arrow directement vers NumPy en zero-copy

Migration : Principaux Modèles de Pandas vers Polars

Migration from Pandas to Polars Bridge between legacy and modern code: translating Pandas patterns to Polars expressions

Si votre pipeline est écrit en Pandas, la migration ne nécessite pas de tout réécrire depuis zéro. Les principaux modèles se transposent selon des gabarits.

Lecture des Données

df = pd.read_parquet("data.parquet")
df = pd.read_csv("data.csv", parse_dates=["timestamp"])

df = pl.read_parquet("data.parquet")
df = pl.read_csv("data.csv", try_parse_dates=True)

df = pl.scan_parquet("data.parquet")  # reads nothing until .collect()

Filtrage

df_filtered = df[df["volume"] > 1000]
df_filtered = df[(df["close"] > 100) & (df["exchange"] == "binance")]

df_filtered = df.filter(pl.col("volume") > 1000)
df_filtered = df.filter(
    (pl.col("close") > 100) & (pl.col("exchange") == "binance")
)

Création de Colonnes

df["returns"] = df["close"].pct_change()
df["log_returns"] = np.log(df["close"] / df["close"].shift(1))

df = df.with_columns([
    pl.col("close").pct_change().alias("returns"),
    (pl.col("close") / pl.col("close").shift(1)).log().alias("log_returns"),
])

GroupBy + Agrégation

result = df.groupby("ticker").agg(
    avg_close=("close", "mean"),
    total_volume=("volume", "sum"),
    trade_count=("close", "count"),
)

result = df.group_by("ticker").agg([
    pl.col("close").mean().alias("avg_close"),
    pl.col("volume").sum().alias("total_volume"),
    pl.col("close").count().alias("trade_count"),
])

Rolling par Groupe

df["ma_20"] = df.groupby("ticker")["close"].transform(
    lambda x: x.rolling(20).mean()
)

df = df.with_columns(
    pl.col("close")
        .rolling_mean(window_size=20)
        .over("ticker")
        .alias("ma_20")
)

Intégration avec QuestDB

Polars fonctionne nativement avec Apache Arrow — le même format que QuestDB utilise pour le transfert de données. Cela signifie zero-copy à la réception des résultats de requête :

import pyarrow as pa
from questdb.ingress import Sender

arrow_table = questdb_connection.query_arrow(
    "SELECT * FROM candles WHERE ticker = 'ETHUSDT'"
)
df = pl.from_arrow(arrow_table)  # zero-copy!

df_pd = arrow_table.to_pandas()  # copy + type conversion

Pour en savoir plus sur l'utilisation de QuestDB pour le stockage et l'analyse de données de trading, consultez notre série d'articles sur l'architecture des données.

Intégration avec le Cache Parquet

Parquet cache architecture Columnar Parquet cache with predicate pushdown and projection pushdown for selective data loading

Dans l'article Cache Parquet Agrégé, nous avons décrit comment précalculer une fois les unités de temps et les indicateurs et les sauvegarder dans un fichier Parquet. Polars rend cette approche encore plus efficace :

cache = (
    pl.scan_parquet("raw_candles_1m.parquet")
    .with_columns([
        pl.col("close")
            .rolling_mean(window_size=60)
            .alias("ma_1h"),
        pl.col("close")
            .rolling_mean(window_size=240)
            .alias("ma_4h"),
        pl.col("close")
            .rolling_mean(window_size=20)
            .alias("bb_mid"),
        pl.col("close")
            .rolling_std(window_size=20)
            .alias("bb_std"),
    ])
    .with_columns([
        (pl.col("bb_mid") + 2.0 * pl.col("bb_std")).alias("bb_upper"),
        (pl.col("bb_mid") - 2.0 * pl.col("bb_std")).alias("bb_lower"),
    ])
    .collect()
)

cache.write_parquet(
    "cache_ETHUSDT_2024_2026.parquet",
    compression="zstd",
    compression_level=3,
)

Lors d'une optimisation massive — lorsqu'il faut exécuter des milliers de combinaisons de paramètres — la lecture depuis le cache Parquet via scan_parquet de Polars avec predicate pushdown permet de ne charger que les périodes et colonnes nécessaires sans lire l'intégralité du fichier.

Intégration avec le Drill-down adaptatif : l'évaluation paresseuse de Polars est parfaitement adaptée au chargement à deux niveaux — données grossières pour la passe principale, données détaillées (secondes, millisecondes) uniquement pour les zones d'ambiguïté de remplissage.

Quand Utiliser Quoi : Recommandations Pratiques

Decision guide for choosing Pandas vs Polars Decision matrix: diverging paths for small-scale prototyping versus large-scale production pipelines

Pandas est justifié si :

  • Jeu de données jusqu'à 1M de lignes et vous ne faites pas de GroupBy sur des centaines de groupes — la différence entre Pandas 2.2 et Polars est souvent négligeable (1,5-2x)
  • Vous avez besoin de pandas-ta ou d'autres bibliothèques avec une API Pandas — réécrire 130 indicateurs n'est pas pratique pour une étude ponctuelle
  • Prototypage — l'API Pandas est plus familière pour la plupart, et la vitesse n'est pas critique pour tester rapidement des hypothèses
  • Intégration avec du code legacy — un pipeline Pandas existant qui fonctionne et n'a pas besoin d'être optimisé

Polars est nécessaire si :

  • Jeu de données à partir de 10M de lignes — des dizaines et des centaines de millions de lignes de données tick, caches multi-timeframe
  • Rolling par groupe — 100+ instruments, indicateurs pour chacun : gain de 100 à 3500x
  • Pipeline ETL — chargement, nettoyage, transformation de gros volumes de données
  • RAM limitée — l'évaluation paresseuse et le moteur streaming permettent de traiter des données qui ne tiennent pas en mémoire
  • Stack Parquet/QuestDB — Arrow natif = zero-copy, predicate pushdown, projection pushdown

À Ne Pas Attendre

Le chiffre marketing « 30 fois plus rapide » est le gain maximal sur des opérations spécifiques. Le gain réaliste sur des opérations typiques de pipeline est de : 2-10x. Sur le rolling par groupe — nettement plus. Sur les petits jeux de données — Polars est parfois même plus lent en raison de l'overhead.

Notre Expérience chez marketmaker.cc

Production performance dashboard Production metrics: 6-8x pipeline speedup and 8x more optimization iterations per hour

Chez marketmaker.cc, nous utilisons une architecture hybride Polars + Numba pour le moteur de backtest. L'ensemble du pipeline de données — chargement depuis le cache Parquet, calcul des indicateurs, filtrage, feature engineering — tourne sur Polars. La simulation de portefeuille tourne sur Numba.

Le passage de Pandas à Polars dans le pipeline de données a apporté un gain de 6-8x sur nos jeux de données typiques (50-100M lignes, 200+ instruments). Le calcul d'indicateurs rolling par groupe est passé de minutes à des centaines de millisecondes. Cela nous a permis d'augmenter le nombre d'itérations d'optimisation d'environ 500 à environ 4000 par heure sans changer de matériel.

Un point clé : nous n'avons pas migré tout le code en une seule journée. Nous avons d'abord déplacé l'I/O (lecture Parquet), puis le calcul des indicateurs, puis le filtrage et le feature engineering. Pandas n'est resté que dans l'interface avec les composants legacy qui attendent un pd.DataFrame. La conversion df.to_pandas() / pl.from_pandas() prend des millisecondes et n'est pas un goulot d'étranglement.

Les métriques calculées pendant l'étape de backtest — y compris le PnL par Temps Actif — sont déjà calculées sur des DataFrames Polars, ce qui simplifie le pipeline et élimine les conversions intermédiaires.

Conclusion

Architecture convergence Three technology streams converging: Polars, Numba, and Arrow uniting into a single optimized pipeline

Polars ne remplace pas Pandas dans tous les scénarios. C'est un outil d'une autre classe qui brille aux échelles typiques de l'algotrading sérieux : millions et centaines de millions de lignes, dizaines et centaines d'instruments, optimisation continue des paramètres.

Chiffres clés :

  • Opérations de base : gain de 2-10x sur les tâches typiques de pipeline
  • Rolling par groupe : 10-3500x — la principale fonctionnalité clé pour les pipelines de trading
  • I/O CSV : jusqu'à 25x — essentiel pour le chargement initial des données
  • Mémoire : économie de 2-6x grâce à Arrow et à l'évaluation paresseuse
  • Streaming : traitement de données qui ne tiennent pas en RAM

Architecture recommandée pour un moteur de backtest en production :

  1. Polars — l'ensemble du pipeline de données : chargement, indicateurs, filtrage, features
  2. Numba/Rust — simulation de portefeuille : logique d'ordres et de positions path-dependent
  3. Arrow — le format de données à toutes les jonctions : Parquet, QuestDB, Polars, NumPy

Aucune couche Pandas intermédiaire. Les données circulent du stockage à travers Polars vers des tableaux NumPy puis vers le moteur Numba — sans copies inutiles, sans GIL, sans goulots d'étranglement mono-thread.


Liens Utiles

  1. Polars — User Guide
  2. Polars vs Pandas — official benchmark
  3. PDS-H Benchmark — DataFrame libraries comparison
  4. Apache Arrow — columnar format specification
  5. Numba — JIT compiler for Python
  6. vectorbt — backtesting framework
  7. pandas-ta — Technical Analysis Indicators
  8. Ritchie Vink — I wrote one of the fastest DataFrame libraries (Polars origin)
  9. Towards Data Science — Polars vs Pandas: real-world benchmarks
  10. Ernest Chan — Quantitative Trading

Citation

@article{soloviov2026polarsvspandas,
  author = {Soloviov, Eugen},
  title = {Polars vs Pandas for Algotrading: Benchmarks on Real Data},
  year = {2026},
  url = {https://marketmaker.cc/ru/blog/post/polars-vs-pandas-algotrading},
  description = {Detailed comparison of Polars and Pandas on algotrading tasks: benchmarks for filtering, aggregation, rolling signal computations, I/O, and memory consumption. Hybrid Polars + Numba architecture for maximum backtest performance.}
}
blog.disclaimer

Authors

Eugen Soloviov
Eugen Soloviov

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.

Newsletter

Gardez une longueur d'avance sur le marché

Abonnez-vous à notre newsletter pour des insights exclusifs sur le trading IA, des analyses de marché et des mises à jour de la plateforme.

Nous respectons votre vie privée. Désabonnement possible à tout moment.