Parallel Multi-Asset Scaling with .over()¶
A frequent challenge in quantitative backtesting and real-time scanning is computing indicators across an entire universe of tickers (e.g. all 500 S&P 500 stocks or thousands of crypto spot pairs).
The Traditional Bottleneck (Pandas)¶
In Pandas, grouping operations execute sequentially in a single Python thread:
# Slow & serial (runs on 1 CPU core)
df["sma_20"] = df.groupby("ticker")["close"].transform(lambda x: x.rolling(20).mean())
df["rsi_14"] = df.groupby("ticker")["close"].apply(calculate_rsi)
For 500 tickers \(\times\) 2,000 bars (1,000,000 rows), this single-threaded approach takes seconds to execute and scales poorly.
The Polars Advantage: Native Windowing¶
In Polars, any expression can be partitioned across groups using .over("ticker"):
import polars as pl
import polars_anta
# Fast & parallelized across all CPU cores in Rust
universe_with_indicators = universe_df.with_columns(
[
pl.col("close").anta.sma(20).over("ticker").alias("sma_20"),
pl.col("close").anta.rsi(14).over("ticker").alias("rsi_14"),
pl.col("close").anta.macd().over("ticker").alias("macd"),
pl.col("close").anta.obv("volume").over("ticker").alias("obv"),
pl.col("close").anta.zscore(20).over("ticker").alias("zscore_20"),
]
)
Why it is faster:¶
- Multi-Threaded Rayon Thread Pool: Polars automatically allocates separate tickers to separate worker threads.
- Zero Serialization: No Python function callback overhead per group.
- Cache Coherency: Data stays in Arrow chunked arrays, maximizing CPU L1/L2 cache hit rates.