← 返回文章列表
March 13, 2026
5 分鐘閱讀

Polars vs Pandas 演算法交易實戰基準測試

Polars vs Pandas 演算法交易實戰基準測試
#演算法交易
#Polars
#Pandas
#基準測試
#效能
#資料工程

系列文章"回測無幻覺",第9篇

策略回測不僅僅是訊號邏輯和執行模擬。它還是一個數據管道:載入數百萬根K線、重取樣時間框架、計算指標、按條件過濾、按標的分組。當管道執行需要30秒而不是3秒時,這不僅僅是不便。這意味著每小時少做10倍的實驗、10倍更慢的迭代、從想法到生產的路徑延長10倍。

Pandas 是 Python 中處理表格資料的事實標準。但 Pandas 設計於2008年,當時 CPU 核心更慢,資料集更小。Pandas 是單執行緒的,記憶體消耗大,且缺少查詢最佳化器。Polars 是用 Rust 編寫的新一代庫,具有並行執行能力,以 Apache Arrow 為核心,並帶有惰性查詢規劃器。

問題是:Polars 在真實演算法交易任務上到底快多少?不是 README 中的合成基準測試,而是在 tick 過濾、滾動指標計算、按標的分組以及從 Parquet/QuestDB 載入資料的場景中?

本文提供系統的基準測試,包含資料、程式碼和實踐建議。

基準測試方法論

基準測試環境 未來感精密測量實驗室:受控參數的可復現基準測試環境

在比較之前,讓我們定義規則,確保結果可復現且公平。

環境

  • Python 3.11Pandas 2.2Polars 1.x(最新穩定版本)
  • 機器:8核、32 GB RAM、NVMe SSD
  • 每個基準測試執行100次,取中位數
  • 預熱(warmup):測量前5次迭代
  • 測量期間停用 GC(gc.disable()

資料

三個規模級別:

  • 小型:10K 行(單個標的,一天,分鐘K線)
  • 中型:1M 行(單個標的,約2年,分鐘K線)
  • 大型:10M+ 行(100個標的,2年,分鐘K線)

額外:真實 NYC Taxi 資料集(1270萬行)用於 ETL 基準測試 — 行業標準基準。

測量內容

import timeit, gc

def bench(fn, n=100, warmup=5):
    """公平基准测试:预热 + n 次运行的中位数。"""
    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,
    }

按操作分類的基準測試:表格

操作基準測試對比 不同資料規模下 filter、groupby、join 和 select 操作的效能對比

小型資料集(10K 行)

操作 Pandas (ms) Polars (ms) 加速比
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行資料上,Pandas 在簡單過濾時有時更快 — 通過 PyO3 呼叫 Polars 函數的開銷與操作本身的時間相當。但在 join 操作上,優勢已經顯現:Polars 的 Rust 雜湊表比 Pandas 快13倍。

中型資料集(1M 行)

操作 Pandas (ms) Polars (ms) 加速比
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

在百萬行資料上,Polars 在過濾和分組上穩定快1.6倍。在 select(選擇列子集)上快10.9倍,因為 Arrow 列式格式允許零複製切片。

大型資料集(10M+ 行)

操作 Pandas (ms) Polars (ms) 加速比
Filter 185 50 3.7x
GroupBy 860 100 8.6x
Join 1450 120 12.1x
Select 240 40 6.0x

在大數據上,Polars 的優勢呈非線性增長:8核上的並行執行和查詢最佳化器產生累積效果。GroupBy 加速8.6倍 — 這是"等待一秒"和"等待100毫秒"之間的區別。

真實資料 ETL(NYC Taxi,1270萬行)

操作 Pandas (s) Polars (s) 加速比
CSV 載入 28.5 1.14 25.0x
Filter + GroupBy + Agg 3.8 0.42 9.0x
多列轉換 2.1 0.7 3.0x
完整 ETL 管道 34.4 2.26 15.2x

CSV I/O 是最引人注目的結果:Polars 在 Rust 引擎上並行讀取 CSV,快25倍。這對於歷史資料的初始載入至關重要。

官方 PDS-H 基準測試(2025年5月)

PDS-H 基準測試排行榜 DataFrame 庫效能競賽:Polars 和 DuckDB 遙遙領先,Pandas 落後數個數量級

PDS-H(Performance Data Science — Holistic)是 DataFrame 庫的標準基準測試,類似於資料庫的 TPC-H。2025年5月的結果:

  • Pandas 僅參與 SF-10 規模的測試 — 單執行緒,無查詢最佳化器,比領先者慢兩個數量級
  • PolarsDuckDB 在 SF-10 和 SF-100 上遙遙領先
  • Polars 的新流式引擎相比記憶體模式提供額外3-7倍加速 — 可以處理不適合 RAM 的資料

對於演算法交易,這意味著:如果您的管道在載入1億+行 tick 資料時遇到記憶體瓶頸 — Polars 流式引擎可以在不增加 RAM 的情況下處理它們。

交易訊號的滾動計算:殺手級特性

Polars vs Pandas rolling speedup comparison

這是演算法交易最重要的基準測試。典型任務:您有100個標的,需要為每個計算滾動均值、滾動標準差、z-score,並據此生成訊號。在 Pandas 中是 groupby().rolling(),在 Polars 中是 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 + 滾動表示式

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

結果

操作 Pandas (ms) Polars (ms) 加速比
滾動均值,100組 x 10萬行 4200 12 350x
滾動標準差,100組 x 10萬行 5100 15 340x
Z-score(均值 + 標準差 + 算術運算) 12500 35 357x
滾動均值,1000組 x 1萬行 38000 11 3454x

按組滾動計算獲得 10倍到3500倍的加速。這不是筆誤。Pandas 的 groupby().transform(lambda x: x.rolling().mean()) 對每個組建立 Python 迴圈,每次呼叫都有直譯器開銷。Polars 在 Rust 中執行所有操作,跨組並行,沒有中間 Python 物件。

對於需要為100個標的計算10個指標的管道 — 這是2分鐘和0.3秒之間的區別。

技術指標:布林帶、肯特納通道、TTM Squeeze

技術指標視覺化 布林帶和肯特納通道包裹價格序列,TTM Squeeze 區域高亮顯示

讓我們來看交易策略中使用的真實技術指標的計算。

布林帶

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 實現

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 實現

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

肯特納通道

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)

其中 ATR(平均真實範圍):

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 是一種識別市場從擠壓狀態(低波動性)向擴充狀態過渡的方法。當布林帶位於肯特納通道內部時產生訊號:

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

技術指標基準測試(1M 行,單個標的)

指標 Pandas (ms) Polars (ms) 加速比
布林帶 (20, 2) 8.4 1.2 7.0x
肯特納通道 (20, 1.5) 14.2 2.1 6.8x
TTM Squeeze (完整) 28.6 4.1 7.0x
RSI (14) 6.8 1.1 6.2x
MACD (12, 26, 9) 5.2 0.8 6.5x

單個標的上穩定獲得 約7倍 加速。按組計算(100個標的)時,由於 Pandas groupby 開銷,加速倍數增長到數百倍。

注意:現成的指標包

Pandas 有 pandas-ta — 包含130+指標的庫。Polars 目前還沒有等效的包。這意味著使用 Polars 時,您需要自行實現指標。不過,基本構建塊(rolling_meanrolling_stdewm_meanshift、列算術運算)覆蓋了絕大多數標準指標,而且 Polars 實現通常比想像的更簡短。

I/O 基準測試:CSV、Parquet、資料庫

資料 I/O 管道視覺化 來自 CSV、Parquet 和資料庫的資料流:並行 Rust I/O 對比單執行緒 Python

資料管道從載入資料開始。儲存格式和讀取方式決定了整個管道的基準速度。

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 結果(1000萬行,6列)

操作 Pandas (s) Polars (s) 加速比
CSV 讀取 28.5 1.14 25.0x
CSV 寫入 42.0 2.8 15.0x
Parquet 讀取(所有列) 0.82 0.31 2.6x
Parquet 讀取(6列中的3列) 0.54 0.12 4.5x
Parquet 寫入 0.95 0.91 1.04x
Parquet 惰性(過濾 + 選擇) N/A 0.08 謂詞下推

關鍵結論:

  1. CSV:Polars 快達25倍 — Rust 中的並行解析
  2. Parquet 讀取:Polars 全量讀取快2.6倍,投影下推(僅讀取需要的列)快4.5倍
  3. Parquet 寫入:幾乎相同 — 兩者都使用 PyArrow/Arrow 後端
  4. 惰性掃描:Polars 可以在 Parquet 檔案的行組級別應用過濾器,而無需將資料載入到記憶體中。對於 Pandas,不手動使用 PyArrow 就無法實現這一點

對於 Parquet 快取 — 我們儲存預計算時間框架和指標的主要格式 — Polars 的惰性求值提供了理想的整合:僅載入需要的列和時間段,而無需將整個檔案讀入記憶體。

記憶體消耗與惰性求值

記憶體消耗與惰性求值 急切模式 vs 惰性模式:橙色的冗餘資料副本對比青色的最佳化 Arrow 列式佈局

急切模式 vs 惰性模式

Pandas 僅在急切模式下工作:每個操作立即執行,中間結果被例項化到記憶體中。

df = pd.read_csv("big_file.csv")           # 整个文件加载到 RAM
df = df[df["volume"] > 1000]                # 过滤后的副本
df = df[["timestamp", "close", "volume"]]   # 又一个副本
df["returns"] = df["close"].pct_change()    # 再一个副本

Polars 支援惰性求值 — 查詢構建為計算圖,經過最佳化後一次性執行:

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

Polars 最佳化器自動完成:

  • 投影下推:僅讀取3列而非全部
  • 謂詞下推:在讀取時應用 volume > 1000 過濾器,不載入不需要的行
  • 公共子表示式消除:避免重複計算相同內容

記憶體消耗(1000萬行,6個 float64 列)

場景 Pandas (GB) Polars 急切 (GB) Polars 惰性 (GB)
CSV 載入 0.92 0.46 0.46
Filter + Select 3列 1.38* 0.22 0.22
5步轉換管道 2.76* 0.48 0.48
Parquet 載入(6列中的3列) 0.46 0.23 0.23

* Pandas 建立中間副本;inplace=True 部分有幫助,但並非對所有操作有效。

Polars 原生使用 Arrow 列式格式:資料按列儲存,行不重複,儘可能使用零複製操作。對於包含多個轉換的管道,Polars 消耗的記憶體少2-6倍。

流式引擎:處理超出 RAM 的資料

對於不適合 RAM 的資料集,Polars 提供流式引擎:

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

流式引擎分塊處理資料,無需將整個資料集載入到記憶體中。根據 PDS-H 基準測試資料,流式模式在大規模上比記憶體模式快3-7倍 — 得益於更好的快取區域性性和沒有虛擬記憶體壓力。

混合架構:Polars + Numba

Hybrid Polars + Numba architecture data flow

回測由兩個本質不同的部分組成:

  1. 資料管道 — 載入、轉換、指標、過濾。這是大規模並行、面向列的,完美適合 Polars。

  2. 投資組合模擬 — 訂單成交、PnL 計算、倉位管理。這是路徑依賴的:每一步都取決於前一個狀態。這需要對時間序列進行逐元素遍歷。

Pandas 對這兩部分都不擅長。Polars 擅長第一部分,但不擅長第二部分。對於路徑依賴邏輯,最優工具是 Numba(Python 的 JIT 編譯器)或原生 Rust/C++。

架構

┌─────────────────────────────────────────────────────┐
│                     数据管道                          │
│                                                      │
│  Parquet/QuestDB  ──→  Polars LazyFrame             │
│       │                     │                        │
│       │              ┌──────┴──────┐                 │
│       │              │   指标      │                 │
│       │              │   过滤器    │                 │
│       │              │   特征      │                 │
│       │              └──────┬──────┘                 │
│       │                     │                        │
│       │              NumPy 数组                      │
│       │              (从 Arrow 零拷贝)              │
│       ▼                     ▼                        │
│  ┌──────────────────────────────────────────────┐   │
│  │        投资组合模拟 (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                                │   │
│  └──────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────┘

示例:完整管道

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()    # 从 Arrow 零拷贝
signals = df["signal"].to_numpy()  # 从 Arrow 零拷贝

@njit
def simulate_strategy(prices, signals, threshold=1.5, stop_loss=0.02):
    """
    路径依赖模拟:Numba 编译为机器码。
    100万次迭代耗时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)

為什麼不用 vectorbt?

vectorbt 是一個流行的回測框架,可在70-100ms內處理100萬筆訂單。它基於 Pandas + NumPy + Numba 構建。問題在於:Pandas 是資料管道的瓶頸 — 慢、單執行緒、記憶體消耗大。vectorbt 不得不通過 Numba 繞過 Pandas 的限制來處理關鍵部分,但資料載入和指標計算仍然通過 Pandas 進行。

Polars + Numba 混合架構取兩者之長:

  • Polars 用於資料管道 — 在相同操作上比 Pandas 快5-350倍
  • Numba 用於投資組合模擬 — 與 vectorbt 中的速度相同
  • 沒有中間 Pandas 層 — 資料通過零複製直接從 Arrow 流向 NumPy

遷移:從 Pandas 到 Polars 的關鍵模式

從 Pandas 到 Polars 的遷移 連線遺留程式碼與現代程式碼的橋樑:將 Pandas 模式轉換為 Polars 表示式

如果您的管道是用 Pandas 編寫的,遷移不需要從頭重寫。主要模式可以按模板轉換。

讀取資料

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")  # 在 .collect() 之前不读取任何内容

過濾

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

建立列

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 + 聚合

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

按組滾動計算

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

與 QuestDB 整合

Polars 原生支援 Apache Arrow — 與 QuestDB 用於資料傳輸的格式相同。這意味著接收查詢結果時零複製:

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)  # 零拷贝!

df_pd = arrow_table.to_pandas()  # 拷贝 + 类型转换

有關使用 QuestDB 儲存和分析交易資料的更多資訊,請參閱我們的資料架構系列文章。

與 Parquet 快取的整合

Parquet 快取架構 列式 Parquet 快取:通過謂詞下推和投影下推實現選擇性資料載入

在文章聚合 Parquet 快取中,我們描述瞭如何預計算時間框架和指標一次並儲存到 Parquet 檔案中。Polars 使這種方法更加高效:

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

在大規模最佳化期間 — 當需要執行數千種參數組合時 — 通過 Polars scan_parquet 配合謂詞下推從 Parquet 快取讀取,可以僅載入需要的時間段和列,而無需讀取整個檔案。

自適應逐層細化的結合:Polars 惰性求值非常適合兩級載入 — 主遍歷使用粗粒度資料,僅在成交模糊區域使用詳細資料(秒級、毫秒級)。

何時使用什麼:實踐建議

Pandas vs Polars 選擇指南 決策矩陣:小規模原型開發與大規模生產管道的不同路徑

Pandas 適用於以下情況:

  • 資料集不超過1M行且您不需要對數百個組進行 GroupBy — Pandas 2.2 和 Polars 之間的差異通常不大(1.5-2倍)
  • 需要 pandas-ta 或其他具有 Pandas API 的庫 — 為一次性研究重寫130個指標不切實際
  • 原型設計 — Pandas API 對大多數人更熟悉,快速假設驗證時速度不是關鍵
  • 與遺留程式碼整合 — 現有的 Pandas 管道正常執行,不需要最佳化

Polars 適用於以下情況:

  • 資料集超過1000萬行 — 數千萬和數億行的 tick 資料、多時間框架快取
  • 按組滾動計算 — 100+標的,每個都需要計算指標:100-3500倍加速
  • ETL 管道 — 載入、清洗、轉換大量資料
  • 有限的 RAM — 惰性求值和流式引擎允許處理不適合記憶體的資料
  • Parquet/QuestDB 技術棧 — 原生 Arrow = 零複製、謂詞下推、投影下推

不應期望什麼

營銷數字"快30倍"是特定操作上的峰值加速。典型管道操作的實際加速:2-10倍。按組滾動計算上 — 顯著更多。在小資料集上 — 有時 Polars 由於開銷甚至更慢。

我們在 marketmaker.cc 的經驗

生產效能儀表盤 生產指標:管道加速 6-8 倍,每小時最佳化迭代次數增加 8 倍

marketmaker.cc,我們為回測引擎使用 Polars + Numba 混合架構。整個資料管道 — 從 Parquet 快取載入、計算指標、過濾、特徵工程 — 在 Polars 上執行。投資組合模擬在 Numba 上執行。

在資料管道中從 Pandas 切換到 Polars,在我們的典型資料集(5000萬-1億行,200+標的)上獲得了6-8倍的加速。按組滾動指標計算從幾分鐘降到幾百毫秒。這使我們在不更換硬體的情況下,將每小時最佳化迭代次數從約500次增加到約4000次。

關鍵點:我們沒有在一天內遷移所有程式碼。首先遷移了 I/O(讀取 Parquet),然後是指標計算,然後是過濾和特徵工程。Pandas 僅保留在與期望 pd.DataFrame 的遺留元件的介面中。df.to_pandas() / pl.from_pandas() 轉換隻需幾毫秒,不是瓶頸。

在回測階段計算的指標 — 包括按活躍時間計算的 PnL — 已經在 Polars DataFrame 上計算,這簡化了管道並消除了中間轉換。

結論

架構融合 三大技術流匯聚:Polars、Numba 和 Arrow 融合為統一的最佳化管道

Polars 並非在每個場景中都能替代 Pandas。它是一個不同類別的工具,在嚴肅演算法交易中典型的規模上才能充分發揮:數百萬和數億行、數十和數百個標的、持續的參數最佳化。

關鍵數字:

  • 基本操作:典型管道任務加速2-10倍
  • 按組滾動計算:10-3500倍 — 交易管道的主要殺手級特性
  • CSV I/O:高達25倍 — 對初始資料載入至關重要
  • 記憶體:得益於 Arrow 和惰性求值,節省2-6倍
  • 流式處理:處理不適合 RAM 的資料

推薦的生產回測引擎架構:

  1. Polars — 整個資料管道:載入、指標、過濾、特徵
  2. Numba/Rust — 投資組合模擬:路徑依賴的訂單和倉位邏輯
  3. Arrow — 所有連線點的資料格式:Parquet、QuestDB、Polars、NumPy

沒有中間 Pandas 層。資料從儲存通過 Polars 流入 NumPy 陣列,再進入 Numba 引擎 — 沒有不必要的複製,沒有 GIL,沒有單執行緒瓶頸。


有用的連結

  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

引用

@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 = {Polars 与 Pandas 在算法交易任务上的详细对比:过滤、聚合、滚动信号计算、I/O 和内存消耗的基准测试。Polars + Numba 混合架构实现最大回测性能。}
}
免責宣告:本文提供的資訊僅用於教育和參考目的,不構成財務、投資或交易建議。加密貨幣交易涉及重大損失風險。

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

緊跟市場步伐

訂閱我們的時事通訊,獲取獨家 AI 交易見解、市場分析和平台更新。

我們尊重您的隱私。您可以隨時退訂。