Using Expressions¶
The mintalib.expressions module provides polars expression factory functions for use with select and with_columns (polars only).
Expressions are named in upper case (e.g. SMA, EMA, MACD). An optional src keyword parameter sets the source expression; by default series-based indicators use pl.col("close"). Multi-column outputs like MACD return a polars struct expression — use .struct.unnest() to flatten into separate columns, or .struct.field(name) to pick one. Expressions can be chained using .pipe().
import polars as pl
from mintalib.expressions import SMA, EMA, ATR, MACD, ROC
from mintalib.samples import sample_prices
prices = sample_prices(backend="polars")
prices
Series Expressions¶
Series-based expressions default to the close column. The src parameter accepts a column name or any polars expression. The output is named after the indicator in lower case — use .alias() to rename:
prices.select(
SMA(20), # default src is the close column, output named "sma"
SMA(20, src="open").alias("sma_open"), # src accepts a column name or an expression
)
| sma | sma_open |
|---|---|
| f64 | f64 |
| null | null |
| null | null |
| null | null |
| null | null |
| null | null |
| … | … |
| 257.638998 | 257.101501 |
| 258.372999 | 257.978001 |
| 259.4495 | 258.851501 |
| 260.489999 | 259.899001 |
| 261.398499 | 260.931001 |
Prices Expressions¶
Prices-based expressions read the columns they need from the full frame — src is not applicable and the lower case OHLCV columns must be in scope:
prices.select(
ATR(14).alias("atr")
)
| atr |
|---|
| f64 |
| null |
| null |
| null |
| null |
| null |
| … |
| 5.964207 |
| 6.084621 |
| 6.190717 |
| 6.042809 |
| 5.881179 |
Multi-Output Expressions¶
Multi-output expressions return a polars struct — unpack all fields with .unnest(), or pick a single one with .struct.field():
prices.select(
MACD()
).unnest()
| macd | macdsignal | macdhist |
|---|---|---|
| f64 | f64 | f64 |
| null | null | null |
| null | null | null |
| null | null | null |
| null | null | null |
| null | null | null |
| … | … | … |
| 2.872127 | 0.764249 | 2.107878 |
| 2.926444 | 1.196688 | 1.729756 |
| 3.494055 | 1.656161 | 1.837893 |
| 3.919685 | 2.108866 | 1.810819 |
| 4.019427 | 2.490978 | 1.528449 |
prices.select(MACD().struct.field("macdsignal"))
| macdsignal |
|---|
| f64 |
| null |
| null |
| null |
| null |
| null |
| … |
| 0.764249 |
| 1.196688 |
| 1.656161 |
| 2.108866 |
| 2.490978 |
Piping¶
Expressions compose with Expr.pipe: the leading expression is passed as src to the next factory.
# Expression can be piped into other expression factory functions.
# The first expression argument is passed as `src` to the next function.
prices.select(
EMA(20).pipe(ROC, 1)
)
| roc |
|---|
| f64 |
| null |
| null |
| null |
| null |
| null |
| … |
| 0.005064 |
| 0.002044 |
| 0.004399 |
| 0.004057 |
| 0.002799 |
prices.select(
MACD(),
sma=SMA(50),
atr=ATR(14),
trend=EMA(50).pipe(ROC, 1)
).unnest()
| macd | macdsignal | macdhist | sma | atr | trend |
|---|---|---|---|---|---|
| f64 | f64 | f64 | f64 | f64 | f64 |
| null | null | null | null | null | null |
| null | null | null | null | null | null |
| null | null | null | null | null | null |
| null | null | null | null | null | null |
| null | null | null | null | null | null |
| … | … | … | … | … | … |
| 2.872127 | 0.764249 | 2.107878 | 260.501999 | 5.964207 | 0.002049 |
| 2.926444 | 1.196688 | 1.729756 | 260.268199 | 6.084621 | 0.000927 |
| 3.494055 | 1.656161 | 1.837893 | 260.2392 | 6.190717 | 0.001944 |
| 3.919685 | 2.108866 | 1.810819 | 260.2342 | 6.042809 | 0.001904 |
| 4.019427 | 2.490978 | 1.528449 | 260.1454 | 5.881179 | 0.00147 |
Lazy Frames and Window Functions¶
Expressions work in lazy queries and with window functions like .over(), computing each group independently — handy for multi-symbol dataframes.
Results contain proper polars null values (the NaN warm-up periods are converted). Under the hood expressions run through map_batches, so they are opaque to the query optimizer, but the Cython kernels release the GIL and can run in parallel on polars worker threads.
symbols = pl.concat([
prices.with_columns(symbol=pl.lit("AAA")),
prices.with_columns(symbol=pl.lit("BBB")),
])
symbols.lazy().with_columns(sma=SMA(20).over("symbol")).collect()
| date | open | high | low | close | volume | symbol | sma |
|---|---|---|---|---|---|---|---|
| date | f64 | f64 | f64 | f64 | i64 | str | f64 |
| 1980-12-12 | 0.098298 | 0.098725 | 0.098298 | 0.098298 | 469033600 | "AAA" | null |
| 1980-12-15 | 0.093597 | 0.093597 | 0.093169 | 0.093169 | 175884800 | "AAA" | null |
| 1980-12-16 | 0.086758 | 0.086758 | 0.086331 | 0.086331 | 105728000 | "AAA" | null |
| 1980-12-17 | 0.088468 | 0.088895 | 0.088468 | 0.088468 | 86441600 | "AAA" | null |
| 1980-12-18 | 0.091032 | 0.09146 | 0.091032 | 0.091032 | 73449600 | "AAA" | null |
| … | … | … | … | … | … | … | … |
| 2026-04-20 | 270.329987 | 274.279999 | 270.290009 | 273.049988 | 36590200 | "BBB" | 257.638998 |
| 2026-04-21 | 271.5 | 272.799988 | 265.399994 | 266.170013 | 50209800 | "BBB" | 258.372999 |
| 2026-04-22 | 267.820007 | 273.73999 | 266.869995 | 273.170013 | 43249200 | "BBB" | 259.4495 |
| 2026-04-23 | 275.049988 | 275.769989 | 271.649994 | 273.429993 | 33399600 | "BBB" | 260.489999 |
| 2026-04-24 | 272.76001 | 273.059998 | 269.649994 | 271.059998 | 38124500 | "BBB" | 261.398499 |