How expressions work¶
Mechanics of the expression system in mplchart.expressions (polars backend). This is the polars counterpart of the indicators page: with a pandas DataFrame charts are driven by mplchart.indicators, with a polars DataFrame by polars expressions — the charting layer is the same.
This page focuses on how expressions compose and plot; for a visual catalog of charts see the Gallery page.
from mplchart.chart import Chart
from mplchart.samples import sample_prices
from mplchart.primitives import Candlesticks, LinePlot, Stripes, Pane
from mplchart.expressions import SMA, EMA, RSI, MACD, OPEN, HIGH, LOW, CLOSE
prices = sample_prices(backend="polars")
Factories return native polars expressions¶
mplchart.expressions contains expression factories: calling SMA(20) returns a plain pl.Expr, aliased as sma by default. Use .alias(...) when parameters need to be reflected in the name. Being native expressions, they work anywhere polars accepts an expression — select, with_columns, filters — not just in charts. The module also exports OPEN, HIGH, LOW, CLOSE, VOLUME as shorthands for the corresponding pl.col(...).
prices.select(CLOSE, SMA(20)).tail(3)
| close | sma |
|---|---|
| f64 | f64 |
| 309.380005 | 323.841 |
| 311.0 | 323.7215 |
| 313.5 | 323.5855 |
Multi-output indicators return a single struct expression, one field per output. When plotting, the chart unnests the struct and draws each field.
prices.select(MACD().struct.unnest()).tail(3)
| macd | macdsignal | macdhist |
|---|---|---|
| f64 | f64 | f64 |
| 3.113124 | 6.63537 | -3.522246 |
| 2.091683 | 5.726633 | -3.63495 |
| 1.467001 | 4.874706 | -3.407705 |
Plotting and panes¶
Pass expressions to plot() alongside primitives, exactly like indicators. Everything draws on the current pane — use pane() (or the Pane primitive) to switch panes; overlays like SMA share the price scale, while oscillators like RSI typically get their own pane. Legend labels come from the expression alias.
Chart(prices, title="Plotting and panes", max_bars=250).plot(
Candlesticks(),
SMA(50).alias("sma-50"),
SMA(200).alias("sma-200"),
).pane("below").plot(
RSI(14).alias("rsi-14"),
).show()
Binding to primitives¶
By default expressions render with an automatic line plot. To control the rendering, pass the expression to a primitive like LinePlot (or AreaPlot, BarPlot, Stripes, Markers). The operator form RSI(14) @ LinePlot(...) is an equivalent alternative to the constructor form.
Chart(prices, title="Binding to primitives", max_bars=250).plot(
Candlesticks(),
LinePlot(SMA(50), color="red", width=2, label="sma-50"),
).pane("below", yticks=(30, 50, 70)).plot(
LinePlot(RSI(14), overbought=70, oversold=30, label="rsi-14"),
).show()
Composing expressions¶
Every factory takes its source via src (default CLOSE), so composition is functional nesting rather than the | chaining used by pandas indicators: pass any expression as the source — a price shorthand, arithmetic on columns, or another factory's output.
Chart(prices, title="Composing expressions", max_bars=250).plot(
Candlesticks(),
SMA(20, src=(HIGH + LOW) / 2).alias("sma-20-typical"),
).pane("below").plot(
RSI(14).alias("rsi-14"),
EMA(9, src=RSI(14)).alias("ema-9"),
).show()
Boolean composition¶
Expressions compare natively — RSI(14) < 30 is just another pl.Expr — so condition-driven primitives like Stripes or Markers take the condition directly. (No as_expr() needed; that is the pandas-side bridge.)
Chart(prices, title="Boolean composition", max_bars=250).plot(
Candlesticks(),
Pane("below", yticks=(30, 50, 70)),
RSI(14),
Stripes(RSI(14) < 30, color="red"),
Stripes(RSI(14) > 70, color="green"),
).show()
Custom expressions¶
Any polars expression plots directly — use .alias(...) to set the legend label.
midrange = ((HIGH.rolling_max(20) + LOW.rolling_min(20)) / 2).alias("midrange-20")
Chart(prices, title="Custom expression", max_bars=250).plot(
Candlesticks(),
midrange,
).show()
The wrap_expression decorator turns a plain function into a factory like the built-ins: it accepts a leading pl.Expr as src, and aliases the result with the lowercase function name (e.g. zscore). Call .alias(...) when a more specific name is needed.
from mplchart.expressions import wrap_expression
@wrap_expression
def ZSCORE(period: int = 20, *, src=CLOSE):
"""Rolling z-score"""
return (src - src.rolling_mean(period)) / src.rolling_std(period)
Chart(prices, title="Custom expression factory", max_bars=250).plot(
Candlesticks(),
).pane("below").plot(
ZSCORE(20),
).show()