Polars-native factor computation engine for quantitative research. All operators execute as Rust-backed Polars expressions with no Python loops in the hot path.
Panel-- Balanced(timestamp, symbol)container with strict alignment guarantees. Prevents look-ahead bias by construction.Factor-- Immutable signal vector. Every operator takesFactorand returnsFactorwith eager evaluation.
| Topic | Convention |
|---|---|
| Missing values | NaN and Inf are converted to null on Factor creation. The library operates on a single missing-value semantic (null only). Nulls propagate through all operations; boundary cases (constant window, insufficient data, zero denominator) return null explicitly. |
| Null in arithmetic | Default: 5.0 + null = null. The add, subtract, multiply functions accept filter=True to treat null as the identity element (0 for +/-, 1 for *). |
| Division by zero | All divisions guarded at abs(denominator) < 1e-10, returning null. Applies uniformly across divide, inverse, zscore, ts_zscore, ts_cv, ts_regression, and all neutralization operators. |
| Rank | Range (0, 1]. Does not pass through zero. Ties use average method. Nulls excluded from ranking. |
| Standard deviation | Population (ddof=0) for std, variance, zscore, normalize. |
| Correlation / Covariance | Sample (ddof=1) for ts_corr, ts_covariance, ts_autocorr. Identity corr(x,y) = cov(x,y) / (std(x) * std(y)) holds. |
| Rolling warmup | All ts_* operators require min_samples=window. First window-1 values per symbol are null. |
| ts_product | Correctly handles negative values and zeros. |
pip install elversfrom elvers import load, ts_rank, ts_regression, zscore, signal, group_neutralize panel = load("ohlcv.parquet") # or load() for built-in sample data close, volume = panel["close"], panel["volume"] momentum = ts_rank(close, 20) vol_adj = zscore(momentum) / zscore(ts_rank(volume, 20)) beta_resid = ts_regression(close, volume, window=60, rettype=0) alpha = signal(group_neutralize(vol_adj, panel["sector"]))Sub-daily data is supported via the interval parameter:
panel = load("hourly.parquet", interval="1h")70+ operators. All accept and return Factor.
Time-Series -- rolling window per symbol:
ts_delay ts_delta ts_mean ts_sum ts_std_dev ts_min ts_max ts_median ts_rank ts_skewness ts_kurtosis ts_zscore ts_corr ts_covariance ts_product ts_step ts_decay_linear ts_decay_exp_window days_from_last_change ts_av_diff ts_scale ts_percentile ts_quantile ts_cv ts_autocorr ts_count_nans ts_backfill kth_element last_diff_value inst_tvr ts_delta_limit ts_regression trade_when
Cross-Sectional -- across symbols at each timestamp:
rank zscore mean median scale normalize quantile signal winsorize truncate left_tail right_tail
Neutralization and Group -- sector/industry neutralization:
vector_neut regression_neut group_neutralize group_rank group_zscore group_scale group_normalize group_mean group_median group_backfill
Math:
log sqrt sign power signed_power inverse s_log_1p maximum minimum where
Arithmetic:
add subtract multiply divide reverse densify bucket and standard operators (+ - * / ** abs)
pip install -e ".[dev]" pytest tests/ -v ruff check elvers/See CLAUDE.md for full development standards.