← Zurück zu den Artikeln
March 13, 2026
5 min read

Polars vs. Pandas für Algotrading: Benchmarks mit echten Daten

Polars vs. Pandas für Algotrading: Benchmarks mit echten Daten
#algotrading
#Polars
#Pandas
#benchmarks
#performance
#data engineering

Serie "Backtests ohne Illusionen", Artikel 9

Beim Backtesting von Strategien geht es nicht nur um Signal-Logik und Ausführungssimulation. Es ist auch eine Datenpipeline: das Laden von Millionen von Candles, das Resampling von Zeitrahmen, die Berechnung von Indikatoren, das Filtern nach Bedingungen, das Gruppieren nach Instrumenten. Wenn die Pipeline 30 Sekunden statt 3 braucht, ist das nicht nur unpraktisch. Es bedeutet 10-mal weniger Experimente pro Stunde, 10-mal langsamere Iteration und einen 10-mal längeren Weg von der Idee zur Produktion.

Pandas ist der De-facto-Standard für tabellarische Daten in Python. Aber Pandas wurde 2008 entwickelt, als CPU-Kerne langsamer und Datensätze kleiner waren. Pandas ist single-threaded, speicherhungrig und verfügt über keinen Query-Optimizer. Polars ist eine Bibliothek der nächsten Generation, geschrieben in Rust, mit paralleler Ausführung, Apache Arrow im Kern und einem Lazy-Query-Planer.

Die Frage lautet: Wie viel schneller ist Polars bei echten Algotrading-Aufgaben? Nicht bei synthetischen Benchmarks aus einer README, sondern beim Filtern von Ticks, der Berechnung von Rolling-Indikatoren, dem Gruppieren nach Instrumenten und dem Laden aus Parquet/QuestDB?

Dieser Artikel liefert systematische Benchmarks mit Zahlen, Code und praktischen Empfehlungen.

Benchmark-Methodik

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

Bevor wir vergleichen, definieren wir die Regeln, damit die Ergebnisse reproduzierbar und fair sind.

Umgebung

  • Python 3.11, Pandas 2.2, Polars 1.x (neueste stabile Version)
  • Maschine: 8 Kerne, 32 GB RAM, NVMe-SSD
  • Jeder Benchmark wird 100-mal ausgeführt; der Median wird verwendet
  • Warmup: 5 Iterationen vor den Messungen
  • GC während der Messung deaktiviert (gc.disable())

Daten

Drei Skalierungsebenen:

  • Klein: 10K Zeilen (ein Instrument, ein Tag, Minuten-Candles)
  • Mittel: 1M Zeilen (ein Instrument, ~2 Jahre, Minuten-Candles)
  • Groß: 10M+ Zeilen (100 Instrumente, 2 Jahre, Minuten-Candles)

Zusätzlich: der reale NYC-Taxi-Datensatz (12,7M Zeilen) für ETL-Benchmarks — ein Standard-Branchenbenchmark.

Was wir messen

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,
    }

Operations-Benchmarks: Tabellen

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

Kleine Datensätze (10K Zeilen)

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

Bei 10K Zeilen ist Pandas bei einfachen Filtern manchmal schneller — der Overhead beim Aufruf einer Polars-Funktion über PyO3 ist vergleichbar mit der Zeit der Operation selbst. Bei Joins zeigt sich jedoch bereits der Vorteil: Die Polars-Hashtabelle in Rust ist 13-mal schneller.

Mittlere Datensätze (1M Zeilen)

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

Bei einer Million Zeilen ist Polars beim Filtern und Gruppieren durchgängig 1,6-mal schneller. Bei Select (Auswahl einer Spaltenteilmenge) — 10,9-mal, weil das Arrow-Spaltenformat Zero-Copy-Slicing ermöglicht.

Große Datensätze (10M+ Zeilen)

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

Bei großen Datenmengen wächst der Polars-Vorteil nichtlinear: parallele Ausführung auf 8 Kernen und der Query-Optimizer erzeugen einen kumulativen Effekt. GroupBy ist 8,6-mal schneller — der Unterschied zwischen "eine Sekunde warten" und "100 Millisekunden warten".

ETL mit echten Daten (NYC Taxi, 12,7M Zeilen)

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

CSV-I/O ist das dramatischste Ergebnis: Polars liest CSV parallel mit seiner Rust-Engine, 25-mal schneller. Das ist entscheidend für das initiale Laden historischer Daten.

Offizieller PDS-H-Benchmark (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) ist ein Standard-Benchmark für DataFrame-Bibliotheken, analog zu TPC-H für Datenbanken. Ergebnisse von Mai 2025:

  • Pandas nimmt nur bei der Skala SF-10 teil — single-threaded, ohne Query-Optimizer, zwei Größenordnungen langsamer als die Spitzenreiter
  • Polars und DuckDB spielen bei SF-10 und SF-100 in ihrer eigenen Liga
  • Die neue Streaming-Engine in Polars bringt einen zusätzlichen Speedup von 3-7x im Vergleich zum In-Memory-Modus — und ermöglicht die Verarbeitung von Daten, die nicht in den RAM passen

Für Algotrading bedeutet das: Wenn Ihre Pipeline beim Laden von 100M+ Zeilen Tick-Daten speicherlimitiert ist — mit der Polars-Streaming-Engine können Sie diese verarbeiten, ohne den RAM zu vergrößern.

Rolling-Berechnungen für Trading-Signale: Das Killer-Feature

Polars vs Pandas rolling speedup comparison

Dies ist der wichtigste Benchmark für Algotrading. Eine typische Aufgabe: Sie haben 100 Instrumente, und für jedes müssen Sie einen Rolling Mean, Rolling Std, Z-Score berechnen und daraus ein Signal generieren. In Pandas ist das groupby().rolling(), in Polars ist es 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")
    )

Ergebnisse

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

10-fache bis 3500-fache Beschleunigung bei Rolling-Berechnungen nach Gruppe. Das ist kein Tippfehler. Pandas groupby().transform(lambda x: x.rolling().mean()) erzeugt eine Python-Schleife über jede Gruppe, wobei jeder Aufruf Interpreter-Overhead verursacht. Polars führt alles in Rust aus, parallel über die Gruppen hinweg, ohne Zwischenobjekte in Python.

Für eine Pipeline, die 10 Indikatoren über 100 Instrumente berechnen muss — das ist der Unterschied zwischen 2 Minuten und 0,3 Sekunden.

Technische Indikatoren: Bollinger Bands, Keltner Channels, TTM Squeeze

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

Betrachten wir die Berechnung realer technischer Indikatoren, die in Trading-Strategien verwendet werden.

Bollinger Bands

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)

Pandas-Implementierung

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

Polars-Implementierung

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"),
    ])

Keltner Channels

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)

wobei 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

TTM Squeeze ist eine Methode zur Identifizierung des Übergangs des Marktes von einem Squeeze-Zustand (niedrige Volatilität) zu einem Expansionszustand. Das Signal tritt auf, wenn die Bollinger Bands innerhalb der Keltner Channels liegen:

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

Benchmark technischer Indikatoren (1M Zeilen, ein 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

Ein konsistenter Speedup von ~7x bei einem einzelnen Ticker. Bei der Berechnung nach Gruppe (100 Ticker) wächst der Speedup aufgrund des Pandas-groupby-Overheads auf mehrere hundert Mal.

Ein Hinweis zu fertigen Indikator-Paketen

Für Pandas gibt es pandas-ta — eine Bibliothek mit 130+ Indikatoren. Für Polars gibt es noch kein Äquivalent. Das bedeutet, dass Sie bei der Verwendung von Polars die Indikatoren selbst implementieren müssen. Die grundlegenden Bausteine (rolling_mean, rolling_std, ewm_mean, shift, Spaltenarithmetik) decken jedoch die überwiegende Mehrheit der Standardindikatoren ab, und die Polars-Implementierung ist meist kürzer, als man denkt.

I/O-Benchmarks: CSV, Parquet, Datenbank

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

Die Datenpipeline beginnt mit dem Laden der Daten. Das Speicherformat und die Lesemethode bestimmen die Grundgeschwindigkeit der gesamten 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()
)

I/O-Ergebnisse (10M Zeilen, 6 Spalten)

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

Wichtige Erkenntnisse:

  1. CSV: Polars bis zu 25-mal schneller — paralleles Parsen in Rust
  2. Parquet-Lesen: Polars ist 2,6-mal schneller beim Volllesen und 4,5-mal mit Projection Pushdown (nur benötigte Spalten lesen)
  3. Parquet-Schreiben: nahezu identisch — beide verwenden das PyArrow/Arrow-Backend
  4. Lazy Scan: Polars kann den Filter auf Row-Group-Ebene der Parquet-Datei anwenden, ohne die Daten in den Speicher zu laden. Das ist mit Pandas ohne manuelle Verwendung von PyArrow nicht möglich

Für den Parquet-Cache — unser primäres Format zur Speicherung vorberechneter Zeitrahmen und Indikatoren — bietet Polars mit Lazy Evaluation eine ideale Integration: Es werden nur die benötigten Spalten und Zeiträume geladen, ohne die gesamte Datei in den Speicher zu lesen.

Speicherverbrauch und Lazy Evaluation

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 arbeitet nur im Eager-Modus: Jede Operation wird sofort ausgeführt, und Zwischenergebnisse werden im Speicher materialisiert.

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 unterstützt Lazy Evaluation — Abfragen werden als Graph aufgebaut, optimiert und in einem einzigen Durchgang ausgeführt:

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()
)

Der Polars-Optimizer führt automatisch aus:

  • Projection Pushdown: liest nur 3 Spalten statt aller
  • Predicate Pushdown: wendet den Filter volume > 1000 bereits beim Lesen an, ohne unnötige Zeilen zu laden
  • Common Subexpression Elimination: vermeidet doppelte Berechnungen

Speicherverbrauch (10M Zeilen, 6 float64-Spalten)

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 erzeugt Zwischenkopien; inplace=True hilft teilweise, aber nicht bei allen Operationen.

Polars nutzt nativ das Arrow-Spaltenformat: Daten werden spaltenweise gespeichert, Zeilen werden nicht dupliziert, und wo immer möglich werden Zero-Copy-Operationen verwendet. Bei Pipelines mit mehreren Transformationen verbraucht Polars 2-6-mal weniger Speicher.

Streaming-Engine: Daten größer als der RAM

Für Datensätze, die nicht in den RAM passen, bietet Polars eine Streaming-Engine:

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")
)

Die Streaming-Engine verarbeitet Daten in Blöcken, ohne den gesamten Datensatz in den Speicher zu laden. Laut PDS-H-Benchmark-Daten ist der Streaming-Modus bei großen Skalen 3-7-mal schneller als der In-Memory-Modus — dank besserer Cache-Lokalität und fehlendem Virtual-Memory-Druck.

Hybride Architektur: Polars + Numba

Hybrid Polars + Numba architecture data flow

Ein Backtest besteht aus zwei grundlegend unterschiedlichen Teilen:

  1. Datenpipeline — Laden, Transformation, Indikatoren, Filterung. Das ist massiv parallel, spaltenorientiert und perfekt für Polars geeignet.

  2. Portfoliosimulation — Order-Ausführung, PnL-Berechnung, Positionsverwaltung. Das ist pfadabhängig: Jeder Schritt hängt vom vorherigen Zustand ab. Dies erfordert einen elementweisen Durchlauf über die Zeitreihe.

Pandas eignet sich für beide Teile schlecht. Polars glänzt beim ersten, aber nicht beim zweiten. Für pfadabhängige Logik ist das optimale Werkzeug Numba (ein JIT-Compiler für Python) oder natives Rust/C++.

Architektur

┌─────────────────────────────────────────────────────┐
│                   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                                │   │
│  └──────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────┘

Beispiel: Vollständige Pipeline

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)

Warum nicht vectorbt?

vectorbt ist ein beliebtes Backtesting-Framework, das 1M Orders in 70-100ms verarbeitet. Es basiert auf Pandas + NumPy + Numba. Das Problem: Pandas ist der Flaschenhals in der Datenpipeline — langsam, single-threaded, speicherhungrig. vectorbt muss die Pandas-Beschränkungen durch Numba für kritische Teile umgehen, aber das Laden von Daten und die Berechnung von Indikatoren laufen weiterhin über Pandas.

Die hybride Polars + Numba-Architektur vereint das Beste aus beiden Welten:

  • Polars für die Datenpipeline — 5-350-mal schneller als Pandas bei denselben Operationen
  • Numba für die Portfoliosimulation — dieselbe Geschwindigkeit wie in vectorbt
  • Keine Pandas-Zwischenschicht — Daten fließen von Arrow direkt zu NumPy per Zero-Copy

Migration: Wichtige Muster von Pandas zu Polars

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

Wenn Ihre Pipeline in Pandas geschrieben ist, erfordert die Migration kein Neuschreiben von Grund auf. Die wichtigsten Muster lassen sich per Vorlage übertragen.

Daten lesen

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()

Filterung

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")
)

Spalten erstellen

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 + Aggregation

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 nach Gruppe

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")
)

Integration mit QuestDB

Polars arbeitet nativ mit Apache Arrow — demselben Format, das QuestDB für die Datenübertragung verwendet. Das bedeutet Zero-Copy beim Empfang von Abfrageergebnissen:

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

Mehr zur Arbeit mit QuestDB zur Speicherung und Analyse von Trading-Daten finden Sie in unserer Artikelserie zur Datenarchitektur.

Integration mit dem Parquet-Cache

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

Im Artikel Aggregierter Parquet-Cache haben wir beschrieben, wie man Zeitrahmen und Indikatoren einmalig vorberechnet und in einer Parquet-Datei speichert. Polars macht diesen Ansatz noch effizienter:

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,
)

Bei einer Massenoptimierung — wenn Tausende von Parameterkombinationen ausgeführt werden müssen — ermöglicht das Lesen aus dem Parquet-Cache über Polars scan_parquet mit Predicate Pushdown das Laden nur der benötigten Zeiträume und Spalten, ohne die gesamte Datei zu lesen.

Integration mit Adaptives Drill-Down: Polars Lazy Evaluation ist perfekt für zweistufiges Laden geeignet — grobe Daten für den Hauptdurchlauf, detaillierte Daten (Sekunden, Millisekunden) nur für Fill-Ambiguity-Zonen.

Wann was verwenden: Praktische Empfehlungen

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

Pandas ist gerechtfertigt, wenn:

  • Datensatz bis 1M Zeilen und Sie kein GroupBy über Hunderte von Gruppen durchführen — der Unterschied zwischen Pandas 2.2 und Polars ist oft vernachlässigbar (1,5-2x)
  • Sie pandas-ta oder andere Bibliotheken mit einer Pandas-API benötigen — das Neuschreiben von 130 Indikatoren ist für eine einmalige Studie unpraktisch
  • Prototyping — die Pandas-API ist den meisten vertrauter, und Geschwindigkeit ist für schnelles Testen von Hypothesen nicht entscheidend
  • Integration mit Legacy-Code — eine bestehende Pandas-Pipeline, die funktioniert und nicht optimiert werden muss

Polars ist notwendig, wenn:

  • Datensatz ab 10M Zeilen — zehn und hundert Millionen Zeilen von Tick-Daten, Multi-Timeframe-Caches
  • Rolling nach Gruppe — 100+ Instrumente, Indikatoren für jedes: 100-3500-facher Speedup
  • ETL-Pipeline — Laden, Bereinigen, Transformieren großer Datenmengen
  • Begrenzter RAM — Lazy Evaluation und die Streaming-Engine ermöglichen die Verarbeitung von Daten, die nicht in den Speicher passen
  • Parquet/QuestDB-Stack — natives Arrow = Zero-Copy, Predicate Pushdown, Projection Pushdown

Was Sie nicht erwarten sollten

Die Marketing-Zahl "30-mal schneller" ist der Spitzen-Speedup bei bestimmten Operationen. Realistischer Speedup bei typischen Pipeline-Operationen: 2-10x. Bei Rolling nach Gruppe — deutlich mehr. Bei kleinen Datensätzen — manchmal ist Polars aufgrund des Overheads sogar langsamer.

Unsere Erfahrung bei marketmaker.cc

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

Bei marketmaker.cc verwenden wir eine hybride Polars + Numba-Architektur für die Backtest-Engine. Die gesamte Datenpipeline — Laden aus dem Parquet-Cache, Berechnung von Indikatoren, Filterung, Feature Engineering — läuft auf Polars. Die Portfoliosimulation läuft auf Numba.

Der Wechsel von Pandas zu Polars in der Datenpipeline brachte bei unseren typischen Datensätzen (50-100M Zeilen, 200+ Instrumente) eine 6-8-fache Beschleunigung. Die Berechnung von Rolling-Indikatoren nach Gruppe ging von Minuten auf Hunderte von Millisekunden zurück. Dies ermöglichte es uns, die Anzahl der Optimierungsiterationen von ~500 auf ~4000 pro Stunde zu steigern, ohne die Hardware zu ändern.

Ein wichtiger Punkt: Wir haben nicht den gesamten Code an einem einzigen Tag migriert. Zuerst haben wir I/O (Parquet-Lesen) verschoben, dann die Indikatorberechnung, dann Filterung und Feature Engineering. Pandas blieb nur an der Schnittstelle zu Legacy-Komponenten erhalten, die ein pd.DataFrame erwarten. Die Konvertierung df.to_pandas() / pl.from_pandas() dauert Millisekunden und ist kein Flaschenhals.

Metriken, die während der Backtest-Phase berechnet werden — einschließlich PnL nach aktiver Zeit — werden bereits auf Polars-DataFrames berechnet, was die Pipeline vereinfacht und Zwischenkonvertierungen eliminiert.

Fazit

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

Polars ist kein Ersatz für Pandas in jedem Szenario. Es ist ein Werkzeug einer anderen Klasse, das bei den für seriöses Algotrading typischen Skalen glänzt: Millionen und Hunderte Millionen Zeilen, zehn und hundert Instrumente, kontinuierliche Parameteroptimierung.

Wichtige Zahlen:

  • Grundoperationen: 2-10-fache Beschleunigung bei typischen Pipeline-Aufgaben
  • Rolling nach Gruppe: 10-3500x — das wichtigste Killer-Feature für Trading-Pipelines
  • CSV-I/O: bis zu 25x — entscheidend für das initiale Laden von Daten
  • Speicher: 2-6-fache Einsparung dank Arrow und Lazy Evaluation
  • Streaming: Verarbeitung von Daten, die nicht in den RAM passen

Empfohlene Architektur für eine Produktions-Backtest-Engine:

  1. Polars — die gesamte Datenpipeline: Laden, Indikatoren, Filterung, Features
  2. Numba/Rust — Portfoliosimulation: pfadabhängige Order- und Positionslogik
  3. Arrow — das Datenformat an allen Schnittstellen: Parquet, QuestDB, Polars, NumPy

Keine Pandas-Zwischenschicht. Daten fließen vom Speicher über Polars in NumPy-Arrays und dann in die Numba-Engine — ohne unnötige Kopien, ohne GIL, ohne single-threaded Flaschenhälse.


Nützliche Links

  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

Zitierung

@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

Dem Markt einen Schritt voraus

Abonniere unseren Newsletter für exklusive KI-Trading-Einblicke, Marktanalysen und Plattform-Updates.

Wir respektieren deine Privatsphäre. Jederzeit abbestellbar.