Factor Evaluation, AFML Labeling & Vectorized Backtesting (evaluation, backtest)¶
polars-anta provides an end-to-end institutional validation and simulation suite combining AFML Path-Dependent Labeling, Alphalens-Style Cross-Sectional Factor Evaluation with Newey-West HAC Inference, Multiple-Testing Overfitting Controls (PSR & DSR), and an Execution-Realistic Vectorized Backtester with Round-Trip MAE/MFE Trade Analytics.
1. Quantitative Machine Learning Labeling (polars_anta.evaluation)¶
triple_barrier_labels(close, volatility=None, horizon=10, pt_mult=2.0, sl_mult=2.0, min_ret=0.0, vol_period=20, side=None)¶
Mathematical Formulation (López de Prado, 2018, Chapter 3):
At each bar \(t\) with local volatility \(\sigma_t\) (defaulting to the \(20\)-bar rolling standard deviation of returns when volatility=None) and optional primary model directional prediction \(s_t = \text{sign}(\text{side}_t) \in \{-1, +1\}\):
The horizontal Profit-Taking (\(U_t\)) and Stop-Loss (\(L_t\)) thresholds are:
Let \(\tau_{\text{PT}} = \min\{k \le H : r_{t, t+k}^{\text{dir}} \ge U_t\}\) and \(\tau_{\text{SL}} = \min\{k \le H : r_{t, t+k}^{\text{dir}} \le -L_t\}\) (with \(\min \emptyset = H + 1\)).
Output pl.Struct Schema (alias="triple_barrier", prefix="tb_" when unnest=True):
- label (Float64): \(+1.0\) if upper price barrier touched first, \(-1.0\) if lower price barrier touched first, or \(0.0\) if vertical expiration barrier \(H\) reached first.
- touch_bars (Int64): First-passage time \(\tau^* = \min(\tau_{\text{PT}}, \tau_{\text{SL}}, H) \in \{1, \dots, H\}\).
- realized_return (Float64): Raw price return \(r_{t, t+\tau^*}^{\text{raw}}\) at the touched barrier.
- meta_label (Float64): Binary \(\{0.0, 1.0\}\) equal to \(1.0\) if and only if the trade in direction \(s_t\) hit its Profit-Taking barrier (\(\tau_{\text{PT}} < \tau_{\text{SL}}\) and \(\tau_{\text{PT}} \le H\)).
df = df.anta.triple_barrier_labels(
close="close",
horizon=12,
pt_mult=2.0,
sl_mult=1.5,
side="primary_signal",
unnest=True,
group_by="symbol",
)
trend_scanning_labels(close, min_window=5, max_window=20, step=1)¶
Mathematical Formulation (López de Prado, 2020): Fits forward OLS regressions \(P_{t+i} = \hat{\beta}_0(L) + \hat{\beta}_1(L)\,i + \epsilon_i\) across all candidate horizons \(L \in \{W_{\min}, \dots, W_{\max}\}\):
Output pl.Struct Schema (alias="trend_scan", prefix="ts_label_" when unnest=True):
- label (Float64): Trend regime direction \(\text{sign}\left(t_{\hat{\beta}_1}(L^*_t)\right) \in \{-1.0, 0.0, +1.0\}\).
- t_value (Float64): Signed optimal OLS \(t\)-statistic \(t_{\hat{\beta}_1}(L^*_t)\) (usable as sample weights in ML training).
- optimal_window (Int64): Optimal lookahead window length \(L^*_t\).
- ret (Float64): Forward return \((P_{t + L^*_t - 1} / P_t) - 1\).
2. Institutional Factor Validation Suite (polars_anta.evaluation)¶
forward_returns(df, close_col="close", periods=(1, 5, 10), group_by=None, prefix="fwd_ret_")¶
Appends multi-horizon forward return columns \(r_{t \to t+p} = \frac{P_{t+p}}{P_t} - 1\) partitioned by group_by.
information_coefficient(df, factor_col, return_col, group_by=None, method="spearman")¶
Computes the cross-sectional Rank IC (method="spearman", default) or Pearson IC (method="pearson") time series:
ic_summary(df, factor_col, return_col, group_by=None, method="spearman", nw_lags=None)¶
Computes institutional IC summary metrics including Newey-West (1987) HAC autocorrelation-consistent \(t\)-statistics:
When nw_lags=None, automatically selects the Newey-West (1994) plug-in bandwidth \(L = \max\left(1, \lfloor 4(T/100)^{2/9}\rfloor\right)\).
| Returned Key | Formula / Meaning | Institutional Benchmark |
|---|---|---|
ic_mean |
\(\overline{\text{IC}} = \frac{1}{T}\sum_{t=1}^T \text{IC}_t\) | \(\vert \overline{\text{IC}}\vert \ge 0.03\) (Daily) |
ic_std |
Sample standard deviation \(\hat{\sigma}_{\text{IC}}\) | Lower indicates regime stability |
icir |
Information Ratio of IC (\(\overline{\text{IC}} / \hat{\sigma}_{\text{IC}}\)) | \(\vert \text{ICIR}\vert \ge 0.50\) |
t_stat |
i.i.d. Student \(t\)-statistic \(\overline{\text{IC}} / (\hat{\sigma}_{\text{IC}} / \sqrt{T})\) | \(\vert t\vert \ge 2.0\) (\(h = 1\) only) |
nw_t_stat |
Newey-West HAC \(t\)-statistic \(\overline{\text{IC}} / \sqrt{\hat{\Omega}_{\text{NW}} / T}\) | \(\vert t_{\text{NW}}\vert \ge 2.0\) (robust to overlapping \(h > 1\)) |
hit_rate |
Fraction of periods with \(\text{IC}_t > 0\) | \(\ge 55\%\) |
n_periods |
Total valid cross-sectional timestamps \(T\) | — |
ic_decay(df, factor_col, close_col="close", horizons=(1, 2, 3, 5, 10, 20), asset_col=None, group_by=None, method="spearman")¶
Evaluates ic_summary (with nw_lags = max(1, h - 1)) across all forward horizons \(h \in \text{horizons}\) and estimates the Alpha Half-Life (half_life column, in bars) via log-linear OLS regression \(\ln |\overline{\text{IC}}(h)| = a - \lambda h \implies t_{1/2} = \frac{\ln 2}{\hat{\lambda}}\).
quantile_analysis(df, factor_col, return_col, quantiles=5, group_by=None, annualization=252.0) & factor_turnover(df, factor_col, asset_col="ticker", time_col="timestamp", lag=1)¶
quantile_analysis: Partitions assets into \(Q\) cross-sectional factor buckets per timestamp and reportscount,mean_factor,mean_return,std_return,sharpe_ratio,win_rate, andcumulative_returnto verify monotonic return separation from \(Q_1\) to \(Q_Q\).factor_turnover: Computes cross-sectional rank autocorrelation \(\text{Corr}(\text{rank}_{t}, \text{rank}_{t-\text{lag}})\) and L1 weight turnover \(\sum_i |w_{i,t} - w_{i,t-\text{lag}}|\) to assess transaction cost drag before backtesting.
3. Strategy Performance & Overfitting-Adjusted Risk Expressions (pl.Expr)¶
All scalar risk metrics operate natively inside df.select(...), df.group_by("symbol").agg(...), or via .anta / .ta expression namespaces:
| Function Signature | Mathematical Definition |
|---|---|
drawdown_series(x, is_returns=True) |
\(\text{DD}_t = \frac{E_t - \max_{s \le t} E_s}{\max_{s \le t} E_s} \le 0.0\) |
max_drawdown(x, is_returns=True) |
\(\text{MDD} = \min_{t} \text{DD}_t\) |
sharpe_ratio(returns, risk_free_rate=0.0, annualization=252.0) |
\(\text{SR}_{\text{ann}} = \frac{\mathbb{E}[r - r_f]}{\sigma(r - r_f)} \sqrt{N_{\text{ann}}}\) |
sortino_ratio(returns, risk_free_rate=0.0, annualization=252.0) |
\(\text{Sortino}_{\text{ann}} = \frac{\mathbb{E}[r - r_f]}{\sqrt{\mathbb{E}[\min(0, r - r_f)^2]}} \sqrt{N_{\text{ann}}}\) |
calmar_ratio(returns, annualization=252.0) |
\(\text{Calmar} = \frac{\mathbb{E}[r] \cdot N_{\text{ann}}}{\vert \text{MDD}\vert }\) |
profit_factor(returns) |
\(\text{PF} = \frac{\sum_{r_t > 0} r_t}{\sum_{r_t < 0} \vert r_t\vert }\) |
win_rate(returns, include_zeros=False) |
\(\text{WR} = \frac{\sum \mathbb{I}_{\{r_t > 0\}}}{\sum \mathbb{I}_{\{r_t \neq 0\}}}\) |
probabilistic_sharpe_ratio(returns, benchmark_sr=0.0, ...) |
\(\text{PSR}(\text{SR}^{\ast}) = \Phi\left(\frac{\widehat{\text{SR}} - \text{SR}^{\ast}}{\sqrt{\frac{1 - \hat{\gamma}_3 \widehat{\text{SR}} + \frac{\hat{\gamma}_4 - 1}{4}\widehat{\text{SR}}^2}{T - 1}}}\right) \in [0, 1]\) |
deflated_sharpe_ratio(returns, n_trials=10, trials_sr_std=0.5, ...) |
\(\text{PSR}(\text{SR}^{\ast}_N)\) where \(\text{SR}^{\ast}_N = \sigma_{\{SR\}}\left[(1-\gamma)\Phi^{-1}(1-\frac{1}{N}) + \gamma\Phi^{-1}(1-\frac{1}{Ne})\right]\) |
4. Execution-Realistic Vectorized Backtesting & MAE/MFE Trade Ledger (polars_anta.backtest)¶
vectorized_backtest(df, ...) / df.anta.backtest(...)¶
Executes a single-asset or multi-asset vectorized backtest with full execution realism:
- Look-Ahead Prevention (signal_lag=1): Shifts target weights by 1 bar before multiplying by bar returns.
- Execution Price Benchmarks (execution_price):
- "close" (default): Close-to-Close bar returns.
- "open": Next-Open to Open execution returns.
- "twap": OHLC average \((O_t + H_t + L_t + C_t)/4\) execution returns.
- "vwap": Typical price \((H_t + L_t + C_t)/3\) execution returns.
- Intraday Session Flattening (flat_mask): Boolean expression forcing position = 0.0 on matching bars (e.g., closing intraday VN30F1M positions prior to the 14:30 ATC auction).
- Dynamic Volatility Targeting (vol_target, vol_lookback=20, max_leverage=2.0): Scales position size by \(\min\left(L_{\max},\, \frac{\sigma_{\text{target}}}{\hat{\sigma}_{t-1}\sqrt{N_{\text{ann}}}}\right)\).
- No-Trade Turnover Buffer (no_trade_buffer=0.0): Suppresses minor position adjustments where \(|w_t^{\text{target}} - w_{t-1}| \le \delta_{\text{buf}}\) unless exiting to 0.0.
BacktestResult.trades() & extract_trades(bt_df, ...)¶
Extracts a complete Round-Trip Trade Ledger with intra-trade excursion analytics:
- trade_id, side ("LONG" / "SHORT"), entry_index, exit_index, entry_time, exit_time, holding_bars, avg_position, entry_price, exit_price, gross_return, cost, net_return.
- mae (Maximum Adverse Excursion \(\le 0.0\)): Worst intra-trade cumulative drawdown from entry price.
- mfe (Maximum Favorable Excursion \(\ge 0.0\)): Highest intra-trade cumulative profit reached before exit.
parameter_sweep(df, signals, ...) / df.anta.parameter_sweep(...)¶
Evaluates a dictionary of candidate parameter configurations signals: Mapping[str, IntoExpr] in a single call, computes the empirical cross-trial Sharpe dispersion \(\sigma_{\{SR\}}\), and ranks all configurations with their multiple-testing Deflated Sharpe Ratio (deflated_sharpe_ratio).