From polars-backtest
Backtests trading strategies on Polars DataFrames using the Rust-powered polars-backtest library with Finlab-compatible semantics. Handles portfolio simulation, stop loss/take profit, rebalancing, and finlab migration.
How this skill is triggered — by the user, by Claude, or both
Slash command
/polars-backtest:polars-backtestThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
High-performance portfolio backtesting for Polars long-format DataFrames.
High-performance portfolio backtesting for Polars long-format DataFrames. Rust core, Finlab-compatible semantics (parity ~2e-16), T+1 execution.
pip install polars-backtest # or: uv add polars-backtest
| File | When to read |
|---|---|
| references/api.md | Full parameter reference, BacktestReport surface, trades schema, wide-format API, plugin expressions |
| references/execution-semantics.md | Exact timing/price/fee/stop mechanics — read before answering "when does it trade / at what price / how are fees applied" |
| references/finlab-migration.md | Porting finlab sim() code; param and Report mapping; unsupported params |
| references/recipes.md | Data prep, top-N ranking, masks, long/short, sweeps, live-trading actions |
Long format: one row per (date, symbol). Required columns (names remappable
via parameters, values can also be pl.Expr):
| Column | Dtype | Notes |
|---|---|---|
date | pl.Date preferred (ISO strings accepted) | Sort by date; unsorted input is auto-sorted but slower |
symbol | str | |
close (= trade_at_price) | Float64 | Use adjusted prices; NaN/≤0 treated as invalid |
weight (= position) | Boolean or Float64 | Bool → equal weights among True. Float → Finlab normalization. Nulls auto-filled with False/0.0 |
Optional columns picked up when present / when features need them:
factor — adjustment factor, raw_price = adj_price / factor. Missing column
silently means factor=1.0.open, high, low — required only when touched_exit=True.limit_up, limit_down, trading_value — enable liquidity metrics
(buyHigh, sellLow, capacity). Column names are configurable via the
limit_up/limit_down/trading_value parameters of backtest_with_report.import polars as pl
import polars_backtest as pl_bt # registers the df.bt namespace
# Boolean signal -> equal-weight portfolio, monthly rebalance
df = df.with_columns(
(pl.col("close") >= pl.col("close").rolling_max(60).over("symbol"))
.alias("weight")
)
report = df.bt.backtest_with_report(position="weight", resample="M")
report.stats # 1-row DataFrame: total_return, cagr, max_drawdown, daily_sharpe, ...
report.creturn # DataFrame [date, creturn]
report.trades # per-trade records with mae/gmfe/bmfe/mdd/pdays
# Equity curve only (faster, for optimization loops)
curve = df.bt.backtest(position="weight", resample="M") # DataFrame [date, creturn]
# Function form is identical: pl_bt.backtest(df, ...) / pl_bt.backtest_with_report(df, ...)
Exact signature — see references/api.md for full semantics.
| Parameter | Default | Meaning |
|---|---|---|
trade_at_price | "close" | Price column/Expr for valuation and execution |
position | "weight" | Weight column/Expr (bool signals or float weights) |
date, symbol | "date", "symbol" | Column names/Exprs |
open, high, low | "open"… | Used only with touched_exit=True |
factor | "factor" | str only; raw = adj / factor; missing → 1.0 |
resample | "D" | None, D, W, W-MON…W-SUN, M, ME, Q, QE, Y, YE, A. MS/QS raise ValueError (no start-of-period support). None = trade only on position change |
resample_offset | None | Delay rebalance by calendar days: "1d", "2d", "1W". Non-negative only; bad strings silently ignored |
fee_ratio | 0.001425 | Fee on buy AND sell notional (TW default) |
tax_ratio | 0.003 | Tax on sell side only (TW default) |
stop_loss | 1.0 = off | Positive fraction: 0.1 exits at −10% |
take_profit | inf = off | 0.2 exits at +20% |
trail_stop | inf = off | 0.08 exits 0.08 below peak cr since entry |
touched_exit | False | Intraday OHLC stop detection, same-day exit at the stop level |
stop_trading_next_period | True | Stopped stock excluded from the next rebalance |
position_limit | 1.0 | Max weight per stock (clip; float-weight excess → cash) |
retain_cost_when_rebalance | False | True: stops keep measuring from original entry across rebalances |
benchmark (report only) | None | Symbol string in your data, or DataFrame with date+creturn → enables alpha/beta/m12WinRate |
limit_up, limit_down (report only) | "limit_up", "limit_down" | Raw limit-price columns for liquidity metrics |
Full mechanics: references/execution-semantics.md.
trade_at_price. No same-bar fills. Last-date signals become pending trades
(null entry_date/exit_date in report.trades).target_value − current_value per
stock. Fees: fee_ratio on the traded amount both ways, + tax_ratio when
reducing/closing. Full round trip ≈ 0.585% at TW defaults.touched_exit=True, OHLC is checked intraday and the
exit happens same day at the touched level (priority open > high > low).Full surface: references/api.md.
report.get_stats(riskfree_rate=0.02) # = report.stats
report.get_monthly_stats()
report.get_return_table() # year x month returns
report.get_metrics(sections=["backtest", "profitability", "risk",
"ratio", "winrate", "liquidity"])
report.actions() # enter/exit/hold + weight/next_weight (live trading)
report.weights() # current holdings, normalized, sum <= 1
report.next_weights() # next-period targets, sum <= 1
report.current_trades() # open + pending + just-exited trades
report.is_stop_triggered()
report.benchmark = bm_df # attach later; needs date + creturn
stop_loss=0.1 for a 10% stop. The threshold
formula is 1 - stop_loss, so a negative value (e.g. -0.1) makes the
threshold > 1 and stops every position out immediately..shift(k).over("symbol") for time series and never rank on
same-day future info. The engine's T+1 handles execution lag, not your
feature construction.trade_at_price and supply factor so raw
prices are still available for trade records and liquidity metrics.w / w.abs().sum().over("date"), or just use a Boolean column.fill_null(False) on
the final signal.resample_offset (e.g. "-1d", "5days")
is ignored without error; a missing factor column means factor=1.0 without
error. Verify both when results look suspicious..unique(subset=["symbol", "date"]) before backtesting.
7b. Sparse weights + resample="D" liquidate: a symbol missing its row (or
null/NaN weight) on a day is sold the next day — weights are NOT
forward-filled. Emit weights on every held date or use a coarser resample.backtest_with_report returns an empty report (height-0 creturn/trades);
thin universes in sweeps may raise — wrap sweep iterations in try/except.fee_ratio=0, tax_ratio=0), then reduce turnover (resample="M" or
None) rather than dropping the cost assumptions.polars-backtest reproduces Finlab sim() bit-for-bit in parity tests. Wide
pandas position → long Boolean/float column; rolling → .over("symbol");
cross-sectional ops → .over("date"). Same parameter names for
fee/tax/stops/resample; mae_mfe_window*, fast_mode, and upload/notification
params are unsupported. Full mapping and verification recipe:
references/finlab-migration.md.
pl_bt.backtest_wide(prices, position, ...) and
pl_bt.backtest_with_report_wide(close, position, ...) accept Finlab-style wide
DataFrames (first column date, one column per symbol). Prefer long format —
wide exists mainly for Finlab parity. Details in
references/api.md.
npx claudepluginhub yvictor/polars_backtest_extension --plugin polars-backtestBuild robust backtesting systems for trading strategies with proper handling of look-ahead bias, survivorship bias, and transaction costs.
Builds robust backtesting systems for trading strategies, handling look-ahead bias, survivorship bias, and transaction costs. Use when developing trading algorithms or validating strategies.
Adds backtesting and performance analysis to Pine Script strategies and indicators. Covers win rate, drawdown, Sharpe ratio, Monte Carlo, and Pine Script v6 reporting features.