スキル一覧に戻る
Yvictor

polars-backtest

by Yvictor

Blazingly fast portfolio backtesting for Polars

2🍴 2📅 2026年1月24日
GitHubで見るManusで実行

SKILL.md


name: polars-backtest description: Help users backtest trading strategies with polars-backtest library. Use when user asks about backtesting, portfolio simulation, trading strategy analysis, or working with polars-backtest.

Polars Backtest Usage Skill

Help users use polars-backtest to backtest their trading strategies efficiently.

Installation

pip install polars-backtest
# or
uv add polars-backtest

Data Format

Long format: one row per (date, symbol) pair.

import polars as pl
import polars_backtest as pl_bt

df = pl.DataFrame({
    "date": ["2024-01-01", "2024-01-01", "2024-01-02", "2024-01-02"],
    "symbol": ["2330", "2317", "2330", "2317"],
    "close": [100.0, 50.0, 102.0, 51.0],
    "weight": [0.6, 0.4, 0.6, 0.4],
})

Basic Usage

# DataFrame namespace
result = df.bt.backtest(trade_at_price="close", position="weight")

# Function API
result = pl_bt.backtest(df, trade_at_price="close", position="weight")

# With report (includes trades, stats)
report = df.bt.backtest_with_report(position="weight", resample="M")

Complete Parameter Reference

Column Mapping Parameters

ParameterDefaultTypeDescription
trade_at_price"close"str | ExprPrice column for trade execution. Use adjusted close for accurate returns.
position"weight"str | ExprPosition weight column. Can be float weights or boolean signals (True=buy). Null values are filled with 0.
date"date"str | ExprDate column. Must be sortable.
symbol"symbol"str | ExprStock symbol/ticker column.
open"open"str | ExprOpen price column. Required when touched_exit=True.
high"high"str | ExprHigh price column. Required when touched_exit=True.
low"low"str | ExprLow price column. Required when touched_exit=True.
factor"factor"strAdjustment factor column. Used to calculate raw price: raw_price = adj_price / factor. If column doesn't exist, defaults to 1.0. Useful for dividend/split adjusted prices.

Rebalancing Parameters

ParameterDefaultTypeDescription
resample"D"str | NoneRebalance frequency. Options: "D" (daily), "W" (weekly), "W-FRI" (weekly on Friday), "M" (monthly), "Q" (quarterly), "Y" (yearly), None (only when position changes).
resample_offsetNonestr | NoneDelay rebalance execution. Examples: "1d" (1 day delay), "2d", "1W". Useful for simulating delayed signal execution.

Cost Parameters

ParameterDefaultTypeDescription
fee_ratio0.001425floatTransaction fee rate (both buy and sell). Taiwan stock default is 0.1425%.
tax_ratio0.003floatTransaction tax rate (sell only). Taiwan stock default is 0.3%.

Risk Management Parameters

ParameterDefaultTypeDescription
stop_loss1.0floatStop loss threshold. Exit when loss reaches this ratio. 1.0 = disabled (100% loss). Example: -0.1 exits at 10% loss.
take_profitinffloatTake profit threshold. Exit when profit reaches this ratio. inf = disabled. Example: 0.2 exits at 20% profit.
trail_stopinffloatTrailing stop threshold. Exit when price drops this much from peak. inf = disabled. Example: 0.05 exits when 5% below peak.
position_limit1.0floatMaximum weight per single stock. 1.0 = no limit. Example: 0.1 caps each stock at 10% of portfolio.
touched_exitFalseboolUse intraday OHLC for stop detection. When True, checks if stop/take profit was touched during the day using high/low prices, not just close. Requires open/high/low columns.
stop_trading_next_periodTrueboolWhen stop is triggered, skip trading in the next period. Prevents immediate re-entry after stop.

Calculation Mode Parameters

ParameterDefaultDescription
finlab_modeFalse (backtest) / True (backtest_with_report)Use Finlab-compatible calculation. When True, boolean signals are converted to equal weights. Affects weight normalization behavior.
retain_cost_when_rebalanceFalseWhen rebalancing, retain the cost basis instead of resetting. Affects return calculation for partially sold positions.

Benchmark Parameters (backtest_with_report only)

ParameterDefaultTypeDescription
benchmarkNonestr | DataFrame | NoneBenchmark for alpha/beta calculation. str: symbol value in your data (e.g., "0050"), uses that symbol's price. DataFrame: must have date and creturn columns (cumulative return starting at 1.0).

Liquidity Metrics Parameters (backtest_with_report only)

ParameterDefaultTypeDescription
limit_up"limit_up"strColumn name for limit-up price. Used to calculate buyHigh metric (ratio of entries at limit-up).
limit_down"limit_down"strColumn name for limit-down price. Used to calculate sellLow metric (ratio of exits at limit-down).
trading_value"trading_value"strColumn for trading value (e.g., close * volume). Used to calculate capacity metric.

BacktestReport Object

report = df.bt.backtest_with_report(position="weight")

# Properties
report.creturn      # DataFrame with date, creturn columns
report.trades       # DataFrame with trade records (see Trades DataFrame section)
report.stats        # Statistics DataFrame (shortcut for get_stats())
report.fee_ratio    # Fee ratio used
report.tax_ratio    # Tax ratio used
report.stop_loss    # Stop loss threshold (None if disabled)
report.take_profit  # Take profit threshold (None if disabled)
report.trail_stop   # Trail stop threshold (None if disabled)
report.trade_at     # Trade timing (e.g., 'close')
report.resample     # Resample frequency
report.benchmark    # Benchmark DataFrame (can be set after creation)

# Set benchmark after creation
report.benchmark = benchmark_df

Trades DataFrame

The report.trades property returns a DataFrame with detailed trade records:

ColumnTypeDescription
stock_idstrStock symbol/ticker
entry_dateDateTrade entry execution date. null for pending entry trades.
exit_dateDateTrade exit execution date. null for open/pending positions.
entry_sig_dateDateDate when entry signal was generated.
exit_sig_dateDateDate when exit signal was generated. null if no exit signal yet.
positionf64Position weight at entry.
periodi32Number of trading days held.
returnf64Trade return ratio (e.g., 0.05 = 5% profit).
entry_pricef64Entry execution price (adjusted price, for return calculation).
exit_pricef64Exit execution price (adjusted price). null for open positions.
entry_raw_pricef64Entry raw price (unadjusted = entry_price / factor, for liquidity metrics).
exit_raw_pricef64Exit raw price (unadjusted = exit_price / factor). null for open positions.
maef64Maximum Adverse Excursion - worst drawdown during trade (negative value).
gmfef64Gross Maximum Favorable Excursion - best unrealized gain during trade.
bmfef64Before-MAE MFE - MFE at the time when MAE occurred.
mddf64Maximum drawdown during the trade.
pdaysi32Number of profitable days during the trade.

Pending Trades

Pending trades represent signals that exist but haven't been executed yet (T+1 execution model):

  • Pending entry: entry_date = null, entry_sig_date = latest signal date
    • Stock has a buy signal but trade hasn't executed yet
  • Pending exit: exit_date = null, exit_sig_date = latest signal date
    • Stock has a sell signal but trade hasn't closed yet
# Get pending entry trades
pending_entries = report.trades.filter(pl.col("entry_date").is_null())

# Get pending exit trades (open positions with exit signal)
pending_exits = report.trades.filter(
    pl.col("entry_date").is_not_null() &
    pl.col("exit_date").is_null() &
    pl.col("exit_sig_date").is_not_null()
)

# Get open positions (no exit signal yet)
open_positions = report.trades.filter(
    pl.col("entry_date").is_not_null() &
    pl.col("exit_date").is_null() &
    pl.col("exit_sig_date").is_null()
)

BacktestReport Methods

get_stats(riskfree_rate=0.02)

Get basic statistics as single-row DataFrame.

report.get_stats()  # or report.stats

Columns: start, end, rf, total_return, cagr, max_drawdown, avg_drawdown, daily_mean, daily_vol, daily_sharpe, daily_sortino, best_day, worst_day, calmar, win_ratio

get_monthly_stats(riskfree_rate=0.02)

Get monthly statistics.

report.get_monthly_stats()

Columns: monthly_mean, monthly_vol, monthly_sharpe, monthly_sortino, best_month, worst_month

get_return_table()

Get monthly return table pivoted by year x month.

report.get_return_table()

Returns DataFrame with year as rows and months (1-12) as columns.

get_metrics(sections=None, riskfree_rate=0.02)

Get structured metrics as single-row DataFrame.

metrics = report.get_metrics()  # All sections
metrics = report.get_metrics(sections=["profitability", "risk"])

current_trades()

Get active trades (positions without exit or exiting on last date).

report.current_trades()

actions()

Get trade actions for current positions with weights. Finlab-compatible logic.

report.actions()
# Returns DataFrame with columns: symbol, action, weight, next_weight, weight_date, next_weight_date

Output Columns

ColumnTypeDescription
symbolstrStock symbol/ticker
actionstrAction type: "enter", "exit", or "hold"
weightf64Current position weight (0 for enter, value for hold/exit)
next_weightf64Next period target weight (0 for exit, value for hold/enter)
weight_dateDateDate of current weights (signal date)
next_weight_dateDateDate of next weights (next rebalance date)

Action Types Summary

actionweightnext_weight說明
enter0>0待進場,尚未持有
exit>00待出場,將離開投資組合
hold>0>0持續持有

Weight Properties

  • sum(weight) ≤ 1.0 — 當前投資組合的總權重
  • sum(next_weight) ≤ 1.0 — 下期目標投資組合的總權重
  • hold + enter 的權重不會超過 1,因為:
    • weight 只計算當前持有的股票(hold + exit)
    • next_weight 是下期目標權重(hold + enter,已正規化)

Usage Example

report = df.bt.backtest_with_report(position="weight", resample="M")

# Get actions with weights and dates
actions = report.actions()
print(actions)
# shape: (115, 6)
# ┌────────┬────────┬──────────┬─────────────┬─────────────┬──────────────────┐
# │ symbol ┆ action ┆ weight   ┆ weight_date ┆ next_weight ┆ next_weight_date │
# │ str    ┆ str    ┆ f64      ┆ date        ┆ f64         ┆ date             │
# ╞════════╪════════╪══════════╪═════════════╪═════════════╪══════════════════╡
# │ 2330   ┆ hold   ┆ 0.023809 ┆ 2026-01-15  ┆ 0.017543    ┆ 2026-02-15       │
# │ 2317   ┆ exit   ┆ 0.023809 ┆ 2026-01-15  ┆ 0.0         ┆ null             │
# │ 2454   ┆ enter  ┆ 0.0      ┆ null        ┆ 0.017543    ┆ 2026-02-15       │
# └────────┴────────┴──────────┴─────────────┴─────────────┴──────────────────┘

# Filter by action type
entering = actions.filter(pl.col("action") == "enter")
exiting = actions.filter(pl.col("action") == "exit")
holding = actions.filter(pl.col("action") == "hold")

# Verify weight sums
print(f"Current portfolio weight: {actions['weight'].sum():.4f}")      # ≤ 1.0
print(f"Next portfolio weight: {actions['next_weight'].sum():.4f}")    # ≤ 1.0

Note: Closed trades (both entry_date and exit_date set) are excluded from actions.

weights()

Get current position weights (normalized). Finlab-compatible.

report.weights()
# Returns DataFrame with columns: symbol, weight, date
ColumnTypeDescription
symbolstrStock symbol/ticker
weightf64Normalized weight (sum ≤ 1.0)
dateDateDate of current weights (signal date)

Returns weights for currently held positions only (stocks with entry_date set, exit_date null).

next_weights()

Get next period target weights (normalized). Finlab-compatible.

report.next_weights()
# Returns DataFrame with columns: symbol, weight, date
ColumnTypeDescription
symbolstrStock symbol/ticker
weightf64Normalized weight (sum ≤ 1.0)
dateDateDate of next weights (next rebalance date)

Returns target weights for the next rebalancing period. Includes:

  • Hold stocks: continuing positions
  • Enter stocks: pending entry positions

Excludes stocks with pending exit signals.

is_stop_triggered()

Check if any trade was triggered by stop loss or take profit.

if report.is_stop_triggered():
    print("Stop was triggered")

daily_creturn()

Get daily resampled cumulative return DataFrame.

report.daily_creturn()

get_metrics Sections

SectionMetricsDescription
backteststartDate, endDate, feeRatio, taxRatio, freq, tradeAt, stopLoss, takeProfit, trailStopBacktest configuration
profitabilityannualReturn, avgNStock, maxNStock, alpha, betaReturns and benchmark comparison
riskmaxDrawdown, avgDrawdown, avgDrawdownDays, valueAtRisk, cvalueAtRiskRisk metrics
ratiosharpeRatio, sortinoRatio, calmarRatio, volatility, profitFactor, tailRatioRisk-adjusted ratios
winratewinRate, expectancy, mae, mfe, m12WinRateWin rate and trade analysis
liquiditybuyHigh, sellLow, capacityLiquidity metrics (requires columns)

Note: alpha, beta, m12WinRate require benchmark to be set.

Statistics Expressions

Use these expressions for custom calculations:

from polars_backtest import daily_returns, cumulative_returns, sharpe_ratio, max_drawdown

df.with_columns(
    ret=daily_returns("close"),
    creturn=cumulative_returns("ret"),
)

df.select(
    sharpe=sharpe_ratio("ret"),
    mdd=max_drawdown("creturn"),
)

Usage Examples

Momentum Strategy with Monthly Rebalancing

df = df.with_columns(
    pl.when(pl.col("close") >= pl.col("close").rolling_max(60).over("symbol"))
    .then(1.0)
    .otherwise(0.0)
    .alias("weight")
)
report = df.bt.backtest_with_report(position="weight", resample="M")

With Risk Management

report = df.bt.backtest_with_report(
    position="weight",
    stop_loss=-0.1,        # -10% stop loss
    take_profit=0.2,       # +20% take profit
    trail_stop=0.05,       # 5% trailing stop
    touched_exit=True,     # Use intraday OHLC for detection
)

With Benchmark Comparison

# Using a symbol in your data as benchmark
report = df.bt.backtest_with_report(
    position="weight",
    benchmark="0050",      # ETF ticker
)

# Or set after creation
report.benchmark = benchmark_df

# Get metrics with alpha, beta, m12WinRate
metrics = report.get_metrics(sections=["profitability", "winrate"])

With Factor Column (Adjusted Prices)

# When using adjusted prices, factor converts back to raw price
# raw_price = adj_close / factor
df = df.with_columns(
    (pl.col("close_raw") / pl.col("close_adj")).alias("factor")
)
report = df.bt.backtest_with_report(
    trade_at_price="close_adj",
    factor="factor",
)

With Liquidity Metrics

df = df.with_columns([
    pl.col("limit_up_price").alias("limit_up"),
    pl.col("limit_down_price").alias("limit_down"),
    (pl.col("close") * pl.col("volume")).alias("trading_value"),
])
report = df.bt.backtest_with_report(position="weight")
metrics = report.get_metrics(sections=["liquidity"])
# Returns: buyHigh, sellLow, capacity

Using Polars Expressions

# Pass expressions directly instead of column names
result = df.bt.backtest(
    trade_at_price=pl.col("adj_close"),
    position=pl.col("signal").cast(pl.Float64),
    resample="M",
)

Delayed Rebalancing

# Rebalance monthly, but execute 2 days after signal
report = df.bt.backtest_with_report(
    position="weight",
    resample="M",
    resample_offset="2d",
)

Analyzing Current Positions

report = df.bt.backtest_with_report(position="weight")

# Get current active trades
current = report.current_trades()

# Get recommended actions
actions = report.actions()  # enter/exit/hold per stock

# Get current and next weights (Finlab compatible)
weights = report.weights()        # Currently held positions (sum ≤ 1)
next_wts = report.next_weights()  # Target portfolio for next period (sum ≤ 1)

# Note: hold + enter positions are in next_weights
# This avoids double-counting weights when comparing current vs next portfolio

Resample Options

ValueDescription
NoneOnly rebalance when position changes
'D'Daily
'W'Weekly (Sunday)
'W-FRI'Weekly (Friday)
'M'Monthly
'Q'Quarterly
'Y'Yearly

Tips

  1. Weight normalization: Weights are automatically normalized to sum to 1.0 per date
  2. T+1 execution: Trades execute at next day's price (realistic simulation)
  3. Boolean signals: Pass boolean column as position, library converts True to equal weights
  4. Null handling: Null values in position column are filled with 0.0
  5. resample=None: Only rebalance when position changes (reduces turnover)
  6. Position limit: Use position_limit=0.1 to cap each stock at 10%

スコア

総合スコア

65/100

リポジトリの品質指標に基づく評価

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

+10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

0/15
最近の活動

1ヶ月以内に更新

+10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

+5

レビュー

💬

レビュー機能は近日公開予定です