Migrating from QuantStats to jquantstats¶
A heartfelt thank you to Ran Aroussi for creating QuantStats — a brilliant and widely loved library that has helped countless quants and portfolio managers understand their strategies better. This project would simply not exist without that foundation. We have enormous respect for the original work and encourage everyone to check it out.
jquantstats is a modern variation on the theme set by QuantStats — not a copy, not a
drop-in replacement, but a different take on the same problem space. Where QuantStats
offers a collection of standalone functions each operating on a return series,
jquantstats is built around a portfolio-centric entry point: you start from a
Portfolio object constructed from prices and positions, and analytics flow naturally
from there. This makes multi-asset analysis, cost modelling, and execution-lag studies
first-class citizens rather than afterthoughts.
This guide explains the key conceptual and API differences between the two libraries so you know what to expect, and provides concrete code translations to help you move your existing workflows over.
Why migrate¶
| QuantStats | jquantstats | |
|---|---|---|
| DataFrame engine | pandas | Polars (zero pandas at runtime) |
| Charts | static matplotlib/seaborn | interactive Plotly |
| Type annotations | partial | full (PEP 484, py.typed) |
| Multi-asset | single pd.Series per call |
multi-column DataFrame, one call |
| API style | functions (qs.stats.sharpe(r)) |
object methods (data.stats.sharpe()) |
| Portfolio analytics | not supported | prices + positions route via Portfolio |
| Execution-delay analysis | not supported | pf.lag(n) + lead-lag IR plot |
| Tilt / timing decomposition | not supported | pf.tilt, pf.timing, pf.tilt_timing_decomp |
| Turnover analytics | not supported | pf.turnover, pf.turnover_weekly() |
| Cost modelling | not supported | CostModel.per_unit() / CostModel.turnover_bps() |
| Python version | 3.7+ | 3.11+ |
Installation¶
pip install jquantstats
pip install jquantstats[plot] # adds kaleido for static image export
Uninstall QuantStats once you're done:
pip uninstall quantstats
Data format¶
QuantStats expects a pd.Series with a DatetimeIndex.
jquantstats expects a pl.DataFrame with a dedicated date column.
import pandas as pd
import polars as pl
# QuantStats — pd.Series with DatetimeIndex
returns_pd: pd.Series = ...
# jquantstats — convert from pandas
returns_pl = pl.from_pandas(returns_pd.rename("MyStrategy").reset_index())
# → DataFrame columns: ["Date", "MyStrategy"]
# jquantstats — build from scratch
from datetime import date
returns_pl = pl.DataFrame({
"Date": [date(2020, 1, 2), date(2020, 1, 3), ...],
"MyStrategy": [0.01, -0.005, ...],
})
Benchmarks¶
# QuantStats — benchmark passed to every function call
import quantstats as qs
qs.stats.information_ratio(returns_pd, benchmark=benchmark_pd)
# jquantstats — benchmark provided once at construction time
import jquantstats as jqs
data = jqs.Data.from_returns(
returns=returns_pl,
benchmark=benchmark_pl,
date_col="Date",
)
data.stats.information_ratio() # benchmark used automatically
Entry points¶
jquantstats has two entry points depending on what data you have.
Data — returns series (drop-in QuantStats replacement)¶
import jquantstats as jqs
data = jqs.Data.from_returns(
returns=returns_pl, # pl.DataFrame with date + return columns
benchmark=benchmark_pl, # optional
date_col="Date",
)
data.stats.sharpe()
data.plots.snapshot()
data.reports.summary()
You can also construct from prices:
data = jqs.Data.from_prices(prices=prices_pl, date_col="Date")
Portfolio — prices + positions (no QuantStats equivalent)¶
If you have asset prices and position sizes, use Portfolio to get
execution-delay analysis, tilt/timing decomposition, turnover analytics,
and cost modelling — none of which are available in QuantStats.
pf = jqs.Portfolio.from_cash_position(
prices=prices_df, # pl.DataFrame: date + asset columns
cash_position=pos_df, # pl.DataFrame: date + asset columns (£ amounts)
aum=1_000_000,
)
# All stats, plots, and reports are available
pf.stats.sharpe()
pf.plots.snapshot()
pf.report.to_html()
# Drop down to the returns-series API at any time
pf.data.stats.calmar()
Other factory methods:
# From share counts
pf = jqs.Portfolio.from_position(prices, position, aum)
# From risk-scaled positions
pf = jqs.Portfolio.from_risk_position(prices, risk_position, aum, vola=vola_df)
API mapping¶
All jquantstats analytics live on .stats, .plots, or .reports of a
Data (or Portfolio) instance. Methods return a dict keyed by
column name rather than a scalar.
# one-time setup
data = jqs.Data.from_returns(returns=returns_pl, benchmark=benchmark_pl)
Statistics¶
| QuantStats | jquantstats | Notes |
|---|---|---|
qs.stats.sharpe(r, periods=252) |
data.stats.sharpe(periods=252) |
|
qs.stats.smart_sharpe(r, periods=252) |
data.stats.smart_sharpe(periods=252) |
|
qs.stats.sortino(r, periods=252) |
data.stats.sortino(periods=252) |
|
qs.stats.smart_sortino(r) |
data.stats.smart_sortino() |
|
qs.stats.adjusted_sortino(r) |
data.stats.adjusted_sortino() |
|
qs.stats.calmar(r) |
data.stats.calmar() |
|
qs.stats.omega(r) |
data.stats.omega() |
|
qs.stats.treynor_ratio(r, benchmark=b) |
data.stats.treynor_ratio() |
|
qs.stats.volatility(r, periods=252) |
data.stats.volatility(periods=252) |
|
qs.stats.implied_volatility(r) |
data.stats.implied_volatility() |
|
qs.stats.skew(r) |
data.stats.skew() |
|
qs.stats.kurtosis(r) |
data.stats.kurtosis() |
|
qs.stats.max_drawdown(r) |
data.stats.max_drawdown() |
|
qs.stats.value_at_risk(r) |
data.stats.value_at_risk(alpha=0.05) |
|
qs.stats.conditional_value_at_risk(r, confidence=0.95) |
data.stats.conditional_value_at_risk(alpha=0.05) |
alpha = 1 − confidence; see note below |
qs.stats.win_rate(r) |
data.stats.win_rate() |
|
qs.stats.avg_return(r) |
data.stats.avg_return() |
|
qs.stats.avg_win(r) |
data.stats.avg_win() |
|
qs.stats.avg_loss(r) |
data.stats.avg_loss() |
|
qs.stats.best(r) |
data.stats.best() |
|
qs.stats.worst(r) |
data.stats.worst() |
|
qs.stats.consecutive_wins(r) |
data.stats.consecutive_wins() |
|
qs.stats.consecutive_losses(r) |
data.stats.consecutive_losses() |
|
qs.stats.profit_factor(r) |
data.stats.profit_factor() |
|
qs.stats.profit_ratio(r) |
data.stats.profit_ratio() |
|
qs.stats.payoff_ratio(r) |
data.stats.payoff_ratio() |
|
qs.stats.win_loss_ratio(r) |
data.stats.win_loss_ratio() |
|
qs.stats.gain_to_pain_ratio(r) |
data.stats.gain_to_pain_ratio() |
|
qs.stats.risk_return_ratio(r) |
data.stats.risk_return_ratio() |
|
qs.stats.cpc_index(r) |
data.stats.cpc_index() |
|
qs.stats.common_sense_ratio(r) |
data.stats.common_sense_ratio() |
|
qs.stats.tail_ratio(r) |
data.stats.tail_ratio() |
|
qs.stats.outlier_win_ratio(r) |
data.stats.outlier_win_ratio() |
|
qs.stats.outlier_loss_ratio(r) |
data.stats.outlier_loss_ratio() |
|
qs.stats.kelly_criterion(r) |
data.stats.kelly_criterion() |
|
qs.stats.exposure(r) |
data.stats.exposure() |
|
qs.stats.cagr(r) |
data.stats.cagr() |
|
qs.stats.rar(r) |
data.stats.rar() |
|
qs.stats.recovery_factor(r) |
data.stats.recovery_factor() |
|
qs.stats.risk_of_ruin(r) |
data.stats.risk_of_ruin() |
|
qs.stats.ulcer_index(r) |
data.stats.ulcer_index() |
|
qs.stats.ulcer_performance_index(r) |
data.stats.ulcer_performance_index() |
|
qs.stats.serenity_index(r) |
data.stats.serenity_index() |
|
qs.stats.information_ratio(r, benchmark=b) |
data.stats.information_ratio() |
jquantstats annualises; see note below |
qs.stats.r_squared(r, benchmark=b) |
data.stats.r_squared() |
|
qs.stats.greeks(r, benchmark=b) |
data.stats.greeks() |
|
qs.stats.probabilistic_sharpe_ratio(r) |
data.stats.probabilistic_sharpe_ratio() |
|
qs.stats.probabilistic_sortino_ratio(r) |
data.stats.probabilistic_sortino_ratio() |
|
qs.stats.probabilistic_adjusted_sortino_ratio(r) |
data.stats.probabilistic_adjusted_sortino_ratio() |
|
qs.stats.geometric_mean(r) |
data.stats.geometric_mean() |
|
qs.stats.ghpr(r) |
data.stats.ghpr() |
|
qs.stats.expected_return(r) |
data.stats.expected_return() |
|
qs.stats.outliers(r) |
data.stats.outliers() |
|
qs.stats.remove_outliers(r) |
data.stats.remove_outliers() |
|
qs.stats.drawdown_details(r) |
data.stats.drawdown_details() |
|
qs.stats.monthly_returns(r) |
data.stats.monthly_returns() |
|
qs.stats.compare(r, benchmark=b) |
data.stats.compare() |
|
qs.stats.rolling_sharpe(r) |
data.stats.rolling_sharpe() |
|
qs.stats.rolling_sortino(r) |
data.stats.rolling_sortino() |
|
qs.stats.rolling_volatility(r) |
data.stats.rolling_volatility() |
|
qs.stats.rolling_greeks(r, benchmark=b) |
data.stats.rolling_greeks() |
|
qs.stats.autocorr_penalty(r) |
data.stats.autocorr_penalty() |
|
qs.stats.pct_rank(r) |
data.stats.pct_rank() |
QuantStats functions with no jquantstats equivalent¶
| QuantStats | Notes |
|---|---|
qs.stats.montecarlo(r) |
Monte Carlo simulations not currently implemented |
qs.stats.montecarlo_sharpe(r) |
|
qs.stats.montecarlo_drawdown(r) |
|
qs.stats.montecarlo_cagr(r) |
jquantstats-only stats methods¶
These methods have no QuantStats equivalent and are unique to jquantstats.
| jquantstats | Description |
|---|---|
data.stats.acf(nlags=20) |
Full autocorrelation function series |
data.stats.annual_breakdown() |
Year-by-year performance table |
data.stats.autocorr(lag=1) |
Autocorrelation at a given lag (qs only has autocorr_penalty) |
data.stats.avg_drawdown() |
Average drawdown across all drawdown episodes |
data.stats.down_capture() |
Downside capture ratio vs benchmark |
data.stats.hhi_positive() |
Herfindahl-Hirschman concentration index for positive returns |
data.stats.hhi_negative() |
Herfindahl-Hirschman concentration index for negative returns |
data.stats.max_drawdown_duration() |
Duration of the maximum drawdown in periods |
data.stats.monthly_win_rate() |
Win rate computed on monthly aggregated returns |
data.stats.periods_per_year |
Inferred annualisation factor (property) |
data.stats.sharpe_variance() |
Variance-penalised Sharpe variant |
data.stats.summary() |
Composite stats summary as a pl.DataFrame |
data.stats.up_capture() |
Upside capture ratio vs benchmark |
data.stats.worst_n_periods(n=5) |
Worst N individual return periods |
Plots¶
| QuantStats | jquantstats |
|---|---|
qs.plots.snapshot(r) |
data.plots.snapshot() |
qs.plots.drawdown(r) |
data.plots.drawdown() |
qs.plots.returns(r) |
data.plots.returns() |
qs.plots.monthly_heatmap(r) |
data.plots.monthly_heatmap() |
qs.plots.distribution(r) |
data.plots.distribution() |
qs.plots.rolling_sharpe(r) |
data.plots.rolling_sharpe() |
qs.plots.rolling_volatility(r) |
data.plots.rolling_volatility() |
All data.plots.* methods return an interactive Plotly figure instead
of a static matplotlib figure.
Reports¶
# QuantStats
qs.reports.full(returns_pd, benchmark=benchmark_pd)
qs.reports.metrics(returns_pd)
# jquantstats
data.reports.summary()
data.reports.metrics()
data.reports.to_html() # full HTML report
Accessing results¶
QuantStats functions return scalars. jquantstats methods return a dict
keyed by column name.
# QuantStats — scalar
sharpe = qs.stats.sharpe(returns_pd) # 1.23
# jquantstats — dict
result = data.stats.sharpe() # {"MyStrategy": 1.23}
sharpe = result["MyStrategy"] # 1.23
Multi-asset results come back in the same call:
returns_multi = pl.DataFrame({
"Date": dates,
"AAPL": aapl_rets,
"MSFT": msft_rets,
})
data = jqs.Data.from_returns(returns=returns_multi)
data.stats.sharpe()
# → {"AAPL": 1.34, "MSFT": 0.91}
Behavioural differences¶
NaN / null handling¶
pandas (and QuantStats) silently drop NaN values in most calculations.
Polars propagates null by default — if any value in a column is null,
most statistics will return null instead of a numeric result.
Note: in Polars,
null(a missing entry, equivalent to pandasNaN) andNaN(IEEE-754 "Not a Number") are distinct. jquantstats treatsnullas missing;NaNis a numeric value that propagates through calculations.
You can clean data manually before constructing Data:
returns_pl = returns_pl.drop_nulls()
# or fill forward
returns_pl = returns_pl.with_columns(pl.all().forward_fill())
Or use the null_strategy parameter on Data.from_returns / Data.from_prices:
# Mirrors pandas / QuantStats behaviour: silently drop rows with nulls
data = jqs.Data.from_returns(returns=returns_pl, null_strategy="drop")
# Forward-fill nulls before computing statistics
data = jqs.Data.from_returns(returns=returns_pl, null_strategy="forward_fill")
# Raise an informative error if any null is found (useful during development)
data = jqs.Data.from_returns(returns=returns_pl, null_strategy="raise")
The default (null_strategy=None) passes nulls through unchanged.
No top-level functions¶
QuantStats exposes bare module-level functions. jquantstats has none.
# ❌ does not exist
jqs.stats.sharpe(returns)
# ✅ correct
data = jqs.Data.from_returns(returns=returns_pl)
data.stats.sharpe()
Portfolio-only features¶
These capabilities have no QuantStats equivalent and are only available
through the Portfolio entry point.
pf = jqs.Portfolio.from_cash_position(prices, positions, aum=1_000_000)
# Execution-delay analysis
pf_lagged = pf.lag(1) # shift positions forward by 1 day
pf.plots.lead_lag_ir_plot(max_lag=5) # information ratio across lags
# Tilt / timing decomposition
pf.tilt # constant-weight (allocation skill)
pf.timing # weight deviations (timing skill)
pf.tilt_timing_decomp # side-by-side NAV comparison
# Turnover analytics
pf.turnover # daily one-way turnover (fraction of AUM)
pf.turnover_weekly() # weekly aggregate
pf.turnover_summary() # {"mean_daily": ..., "mean_weekly": ..., "std": ...}
# Cost modelling
from jquantstats import CostModel
pf_a = jqs.Portfolio.from_cash_position(
prices, positions, aum,
cost_model=CostModel.per_unit(0.01), # £0.01 per share traded
)
pf_b = jqs.Portfolio.from_cash_position(
prices, positions, aum,
cost_model=CostModel.turnover_bps(5), # 5 bps per unit of AUM turnover
)
pf.trading_cost_impact(max_bps=20) # sweep cost sensitivity 0 → 20 bps
Minimal migration checklist¶
- Install
jquantstats(pip install jquantstats). - Convert your
pd.Seriesto apl.DataFramewith a date column. - Construct a
Dataobject once withData.from_returns(...). - Replace every
qs.stats.foo(r)call withdata.stats.foo()["col"]. - Drop any
NaN-filled rows before passing data in, or usenull_strategy.
Further reading¶
- API Reference — complete method signatures
- Architecture — how the library is structured
- Changelog — version history and new features
- API Stability — versioning and deprecation policy
- Quick Reference — one-page lookup table