Getting Started¶
jQuantStats provides two entry points depending on what data you have:
| You have… | Use… | Why |
|---|---|---|
| Prices and positions | Portfolio |
Unlocks execution-delay analysis, turnover analytics, and cost modelling |
| Returns (or prices only) | Data |
Lighter-weight path; easiest migration from QuantStats |
Both routes expose the same stats, plots, and report API.
Installation¶
Python version
Python 3.11+ is required.
Optional extras:
pip install jquantstats[plot] # static chart export via kaleido
pip install jquantstats[web] # FastAPI web server
Route A: Portfolio¶
Use Portfolio when you have prices and positions. This unlocks
position-level analytics — turnover, cost modelling, execution-delay analysis —
that are impossible from returns alone.
Build a Portfolio¶
from datetime import date
import polars as pl
from jquantstats import Portfolio
prices = pl.DataFrame({
"date": [date(2020, 1, 2), date(2020, 1, 3), date(2020, 1, 6)],
"AAPL": [75.09, 74.36, 75.80],
"MSFT": [160.62, 158.96, 159.03],
})
# Dollar amount held per asset each day
positions = pl.DataFrame({
"date": [date(2020, 1, 2), date(2020, 1, 3), date(2020, 1, 6)],
"AAPL": [500_000.0, 500_000.0, 600_000.0],
"MSFT": [300_000.0, 300_000.0, 300_000.0],
})
pf = Portfolio.from_cash_position(
prices=prices,
cash_position=positions,
aum=1_000_000,
)
Core series¶
pf.returns # daily portfolio returns → pl.Series
pf.nav_compounded # compounded NAV curve → pl.Series
pf.drawdown # drawdown from HWM → pl.Series
Stats¶
pf.stats.sharpe() # (1)
pf.stats.max_drawdown()
pf.stats.volatility()
pf.stats.summary() # full metrics table → pl.DataFrame
- Returns a
dictkeyed by column name, e.g.{'AAPL': 1.34, 'MSFT': 0.91, 'portfolio': 1.21}
Plots¶
fig = pf.plots.snapshot() # NAV + drawdown dashboard
fig = pf.plots.rolling_sharpe(window=60)
fig.show() # opens in browser / notebook
Report¶
Route B: Data¶
Use Data when you already have a return series (or just prices without
positions). This is the lighter-weight path and accepts pandas DataFrames too.
from datetime import date
import polars as pl
from jquantstats import Data
returns = pl.DataFrame({
"Date": [date(2020, 1, 2), date(2020, 1, 3), date(2020, 1, 6)],
"Strategy": [0.012, -0.009, 0.005],
"Benchmark": [0.004, -0.003, 0.002],
})
data = Data.from_returns(
returns=returns,
benchmark="Benchmark", # column name to use as benchmark
rf=0.0, # risk-free rate (float or time-varying DataFrame)
)
Stats¶
data.stats.sharpe()
data.stats.sortino()
data.stats.cagr()
data.stats.annual_breakdown() # pl.DataFrame: year | return | sharpe | ...
Plots¶
fig = data.plots.snapshot()
fig = data.plots.monthly_heatmap()
fig = data.plots.returns_distribution()
fig.show()
Report¶
Execution-Delay Analysis¶
Portfolio route only
Execution-delay analysis requires the Portfolio entry point.
A return series does not carry enough information to reconstruct
what happens under different execution assumptions.
Simulate what happens if signals are executed one or more days late:
pf_t0 = pf # ideal T+0
pf_t1 = pf.lag(1) # T+1 fill — signal today, fills tomorrow
pf_t2 = pf.lag(2) # T+2 fill
print(pf_t0.stats.sharpe()) # {"portfolio": 1.34}
print(pf_t1.stats.sharpe()) # {"portfolio": 1.28}
print(pf_t2.stats.sharpe()) # {"portfolio": 1.19}
# Visualise the full lead/lag sweep as a single chart
fig = pf.plots.lead_lag_ir_plot(start=-5, end=10)
fig.show()
pf.lag(n) returns a new Portfolio with positions shifted by n periods.
All downstream accessors — .stats, .plots, .report — recompute on the
shifted positions, so a single call gives you the full analytics picture
under a different execution assumption.
Transaction Costs¶
Portfolio route only
Cost modelling requires the Portfolio entry point.
See Cost Models for the full reference. Quick example:
from jquantstats import CostModel, Portfolio
pf = Portfolio.from_cash_position(
prices=prices,
cash_position=positions,
aum=1_000_000,
cost_model=CostModel.turnover_bps(5.0), # 5 bps one-way cost on AUM turnover
)
# Sweep Sharpe ratio across 0 → 20 bps — how robust is the strategy?
impact = pf.trading_cost_impact(max_bps=20)
print(impact)
NaN / null handling¶
Polars vs pandas null semantics
Unlike pandas, Polars propagates null by default — if any value in a column
is null, most statistics return null instead of a numeric result.
Use the null_strategy parameter to control this behaviour:
# Mirrors pandas / QuantStats — silently drop rows with nulls
data = Data.from_returns(returns=returns_pl, null_strategy="drop")
# Forward-fill nulls before computing statistics
data = Data.from_returns(returns=returns_pl, null_strategy="forward_fill")
# Raise an error if any null is found (useful during development)
data = Data.from_returns(returns=returns_pl, null_strategy="raise")
The default (null_strategy=None) passes nulls through unchanged.
Next steps¶
- Cost Models — model transaction costs
- Migration from QuantStats — complete API mapping
- API Reference — full method signatures