API Reference¶
jquantstats
¶
jQuantStats: Portfolio analytics for quants.
Two entry points¶
Entry point 1 — prices + positions (recommended for active portfolios):
Use :class:~jquantstats.portfolio.Portfolio when you have price series and
position sizes. Portfolio compiles the NAV curve from raw inputs and exposes
the full analytics suite via .stats, .plots, and .report.
from jquantstats import Portfolio
import polars as pl
pf = Portfolio.from_cash_position(
prices=prices_df,
cash_position=positions_df,
aum=1_000_000,
)
pf.stats.sharpe()
pf.plots.snapshot()
Entry point 2 — returns series (for arbitrary return streams):
Use :class:~jquantstats.data.Data when you already have a returns series
(e.g. downloaded from a data vendor) and want benchmark comparison or
factor analytics.
from jquantstats import Data
import polars as pl
data = Data.from_returns(returns=returns_df, benchmark=bench_df)
data.stats.sharpe()
data.plots.plot_snapshot(title="Performance")
The two APIs are layered: portfolio.data returns a :class:~jquantstats.data.Data
object so you can always drop into the returns-series API from a Portfolio.
For more information, visit the jQuantStats Documentation <https://tschm.github.io/jquantstats/book>_.
CostModel
dataclass
¶
Unified representation of a portfolio transaction-cost model.
Eliminates the implicit "pick one" contract between the two independent
cost parameters (cost_per_unit and cost_bps) on
:class:~jquantstats.portfolio.Portfolio. A CostModel
instance encapsulates one model at a time and can be passed to any
Portfolio factory method instead of specifying the raw float parameters.
Attributes:
| Name | Type | Description |
|---|---|---|
cost_per_unit |
float
|
One-way cost per unit of position change (Model A). Defaults to 0.0. |
cost_bps |
float
|
One-way cost in basis points of AUM turnover (Model B). Defaults to 0.0. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
>>> CostModel.per_unit(0.01)
CostModel(cost_per_unit=0.01, cost_bps=0.0)
>>> CostModel.turnover_bps(5.0)
CostModel(cost_per_unit=0.0, cost_bps=5.0)
>>> CostModel.zero()
CostModel(cost_per_unit=0.0, cost_bps=0.0)
per_unit(cost: float) -> CostModel
classmethod
¶
Create a Model A (position-delta) cost model.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cost
|
float
|
One-way cost per unit of position change. Must be non-negative. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
A |
CostModel
|
class: |
CostModel
|
|
Examples:
>>> CostModel.per_unit(0.01)
CostModel(cost_per_unit=0.01, cost_bps=0.0)
turnover_bps(bps: float) -> CostModel
classmethod
¶
Create a Model B (turnover-bps) cost model.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
bps
|
float
|
One-way cost in basis points of AUM turnover. Must be non-negative. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
A |
CostModel
|
class: |
CostModel
|
|
Examples:
>>> CostModel.turnover_bps(5.0)
CostModel(cost_per_unit=0.0, cost_bps=5.0)
Data
dataclass
¶
A container for financial returns data and an optional benchmark.
This class provides methods for analyzing and manipulating financial returns data, including converting returns to prices, calculating drawdowns, and resampling data to different time periods. It also provides access to statistical metrics through the stats property and visualization through the plots property.
Attributes:
| Name | Type | Description |
|---|---|---|
returns |
DataFrame
|
DataFrame containing returns data with assets as columns. |
benchmark |
DataFrame
|
DataFrame containing benchmark returns data. Defaults to None. |
index |
DataFrame
|
DataFrame containing the date index for the returns data. |
plots: DataPlots
property
¶
Provides access to visualization methods for the financial data.
Returns:
| Name | Type | Description |
|---|---|---|
DataPlots |
DataPlots
|
An instance of the DataPlots class initialized with this data. |
stats: Stats
property
¶
Provides access to statistical analysis methods for the financial data.
Returns:
| Name | Type | Description |
|---|---|---|
Stats |
Stats
|
An instance of the Stats class initialized with this data. |
reports: Reports
property
¶
Provides access to reporting methods for the financial data.
Returns:
| Name | Type | Description |
|---|---|---|
Reports |
Reports
|
An instance of the Reports class initialized with this data. |
date_col: list[str]
property
¶
Return the column names of the index DataFrame.
Returns:
| Type | Description |
|---|---|
list[str]
|
list[str]: List of column names in the index DataFrame, typically containing the date column name. |
assets: list[str]
property
¶
Return the combined list of asset column names from returns and benchmark.
Returns:
| Type | Description |
|---|---|
list[str]
|
list[str]: List of all asset column names from both returns and benchmark (if available). |
all: pl.DataFrame
property
¶
Combine index, returns, and benchmark data into a single DataFrame.
This property provides a convenient way to access all data in a single DataFrame, which is useful for analysis and visualization.
Returns:
| Type | Description |
|---|---|
DataFrame
|
pl.DataFrame: A DataFrame containing the index, all returns data, and benchmark data (if available) combined horizontally. |
__post_init__() -> None
¶
Validate the Data object after initialization.
from_returns(returns: NativeFrame, rf: NativeFrameOrScalar = 0.0, benchmark: NativeFrame | None = None, date_col: str = 'Date') -> Data
classmethod
¶
Create a Data object from returns and optional benchmark.
Parameters¶
returns : NativeFrame Financial returns data. First column should be the date column, remaining columns are asset returns.
float | NativeFrame, optional
Risk-free rate. Default is 0.0 (no risk-free rate adjustment).
- If float: Constant risk-free rate applied to all dates.
- If NativeFrame: Time-varying risk-free rate with dates matching returns.
NativeFrame | None, optional
Benchmark returns. Default is None (no benchmark). First column should be the date column, remaining columns are benchmark returns.
str, optional
Name of the date column in the DataFrames. Default is "Date".
Returns:¶
Data
Object containing excess returns and benchmark (if any), with methods for
analysis and visualization through the stats and plots properties.
Raises:¶
ValueError If there are no overlapping dates between returns and benchmark.
Examples:¶
Basic usage:
from jquantstats import Data
import polars as pl
returns = pl.DataFrame({
"Date": ["2023-01-01", "2023-01-02", "2023-01-03"],
"Asset1": [0.01, -0.02, 0.03]
}).with_columns(pl.col("Date").str.to_date())
data = Data.from_returns(returns=returns)
With benchmark and risk-free rate:
benchmark = pl.DataFrame({
"Date": ["2023-01-01", "2023-01-02", "2023-01-03"],
"Market": [0.005, -0.01, 0.02]
}).with_columns(pl.col("Date").str.to_date())
data = Data.from_returns(returns=returns, benchmark=benchmark, rf=0.0002)
from_prices(prices: NativeFrame, rf: NativeFrameOrScalar = 0.0, benchmark: NativeFrame | None = None, date_col: str = 'Date') -> Data
classmethod
¶
Create a Data object from prices and optional benchmark.
Converts price levels to returns via percentage change and delegates
to :meth:from_returns. The first row of each asset is dropped
because no prior price is available to compute a return.
Parameters¶
prices : NativeFrame Price-level data. First column should be the date column; remaining columns are asset prices.
float | NativeFrame, optional
Risk-free rate. Forwarded unchanged to :meth:from_returns.
Default is 0.0 (no risk-free rate adjustment).
NativeFrame | None, optional
Benchmark prices. Converted to returns in the same way as
prices before being forwarded to :meth:from_returns.
Default is None (no benchmark).
str, optional
Name of the date column in the DataFrames. Default is "Date".
Returns:¶
Data
Object containing excess returns derived from the supplied prices,
with methods for analysis and visualization through the stats
and plots properties.
Examples:¶
from jquantstats import Data
import polars as pl
prices = pl.DataFrame({
"Date": ["2023-01-01", "2023-01-02", "2023-01-03"],
"Asset1": [100.0, 101.0, 99.0]
}).with_columns(pl.col("Date").str.to_date())
data = Data.from_prices(prices=prices)
__repr__() -> str
¶
Return a string representation of the Data object.
resample(every: str = '1mo') -> Data
¶
Resamples returns and benchmark to a different frequency using Polars.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
every
|
str
|
Resampling frequency (e.g., '1mo', '1y'). Defaults to '1mo'. |
'1mo'
|
Returns:
| Name | Type | Description |
|---|---|---|
Data |
Data
|
Resampled data. |
describe() -> pl.DataFrame
¶
Return a tidy summary of shape, date range and asset names.
Returns:¶
pl.DataFrame One row per asset with columns: asset, start, end, rows, has_benchmark.
copy() -> Data
¶
Create a deep copy of the Data object.
Returns:
| Name | Type | Description |
|---|---|---|
Data |
Data
|
A new Data object with copies of the returns and benchmark. |
head(n: int = 5) -> Data
¶
Return the first n rows of the combined returns and benchmark data.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n
|
int
|
Number of rows to return. Defaults to 5. |
5
|
Returns:
| Name | Type | Description |
|---|---|---|
Data |
Data
|
A new Data object containing the first n rows of the combined data. |
tail(n: int = 5) -> Data
¶
Return the last n rows of the combined returns and benchmark data.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n
|
int
|
Number of rows to return. Defaults to 5. |
5
|
Returns:
| Name | Type | Description |
|---|---|---|
Data |
Data
|
A new Data object containing the last n rows of the combined data. |
truncate(start: object = None, end: object = None) -> Data
¶
Return a new Data object truncated to the inclusive [start, end] range.
When the index is temporal (Date/Datetime), truncation is performed by
comparing the date column against start and end values.
When the index is integer-based, row slicing is used instead, and
start and end must be non-negative integers. Passing
non-integer bounds to an integer-indexed Data raises :exc:TypeError.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
start
|
object
|
Optional lower bound (inclusive). A date/datetime value
when the index is temporal; a non-negative :class: |
None
|
end
|
object
|
Optional upper bound (inclusive). Same type rules as
|
None
|
Returns:
| Name | Type | Description |
|---|---|---|
Data |
Data
|
A new Data object filtered to the specified range. |
Raises:
| Type | Description |
|---|---|
TypeError
|
When the index is not temporal and a non-integer bound is supplied. |
items() -> Iterator[tuple[str, pl.Series]]
¶
Iterate over all assets and their corresponding data series.
This method provides a convenient way to iterate over all assets in the data, yielding each asset name and its corresponding data series.
Yields:
| Type | Description |
|---|---|
tuple[str, Series]
|
tuple[str, pl.Series]: A tuple containing the asset name and its data series. |
Portfolio
dataclass
¶
Portfolio analytics class for quant finance.
Stores the three raw inputs — cash positions, prices, and AUM — and exposes the standard derived data series, analytics facades, transforms, and attribution tools.
Derived data series:
- :attr:
profits— per-asset daily cash P&L - :attr:
profit— aggregate daily portfolio profit - :attr:
nav_accumulated— cumulative additive NAV - :attr:
nav_compounded— compounded NAV - :attr:
returns— daily returns (profit / AUM) - :attr:
monthly— monthly compounded returns - :attr:
highwater— running high-water mark - :attr:
drawdown— drawdown from high-water mark -
:attr:
all— merged view of all derived series -
Lazy composition accessors: :attr:
stats, :attr:plots, :attr:report - Portfolio transforms: :meth:
truncate, :meth:lag, :meth:smoothed_holding - Attribution: :attr:
tilt, :attr:timing, :attr:tilt_timing_decomp - Turnover: :attr:
turnover, :attr:turnover_weekly, :meth:turnover_summary - Cost analysis: :meth:
cost_adjusted_returns, :meth:trading_cost_impact - Utility: :meth:
correlation
Attributes:
| Name | Type | Description |
|---|---|---|
cashposition |
DataFrame
|
Polars DataFrame of positions per asset over time (includes date column if present). |
prices |
DataFrame
|
Polars DataFrame of prices per asset over time (includes date column if present). |
aum |
float
|
Assets under management used as base NAV offset. |
Analytics facades¶
.stats: delegates to the legacyStatspipeline via.data; all 50+ metrics available..plots: portfolio-specificPlots; NAV overlays, lead-lag IR, rolling Sharpe/vol, heatmaps..report: HTMLReport; self-contained portfolio performance report..data: bridge to the legacyData/Stats/DataPlotspipeline.
.plots and .report are intentionally not delegated to the legacy path: the legacy
path operates on a bare returns series, while the analytics path has access to raw prices,
positions, and AUM for richer portfolio-specific visualisations.
Cost models¶
Two independent cost models are provided. They are not interchangeable:
Model A — position-delta (stateful, set at construction):
cost_per_unit: float — one-way cost per unit of position change (e.g. 0.01 per share).
Used by .position_delta_costs and .net_cost_nav.
Best for: equity portfolios where cost scales with shares traded.
Model B — turnover-bps (stateless, passed at call time):
cost_bps: float — one-way cost in basis points of AUM turnover (e.g. 5 bps).
Used by .cost_adjusted_returns(cost_bps) and .trading_cost_impact(max_bps).
Best for: macro / fund-of-funds portfolios where cost scales with notional traded.
To sweep a range of cost assumptions use trading_cost_impact(max_bps=20) (Model B).
To compute a net-NAV curve set cost_per_unit at construction and read .net_cost_nav (Model A).
Date column requirement¶
Most analytics work with or without a date column. The following features require a
temporal date column (pl.Date or pl.Datetime):
portfolio.plots.correlation_heatmap()portfolio.plots.lead_lag_ir_plot()stats.monthly_win_rate()— returns NaN per column when no date is presentstats.annual_breakdown()— raisesValueErrorwhen no date is presentstats.max_drawdown_duration()— returns period count (int) instead of days
Portfolios without a date column (integer-indexed) are fully supported for
NAV, returns, Sharpe, drawdown, cost analytics, and most rolling metrics.
Examples:
>>> import polars as pl
>>> from datetime import date
>>> prices = pl.DataFrame({"date": [date(2020, 1, 1), date(2020, 1, 2)], "A": [100.0, 110.0]})
>>> pos = pl.DataFrame({"date": [date(2020, 1, 1), date(2020, 1, 2)], "A": [1000.0, 1000.0]})
>>> pf = Portfolio(prices=prices, cashposition=pos, aum=1e6)
>>> pf.assets
['A']
cost_model: CostModel
property
¶
assets: list[str]
property
¶
List the asset column names from prices (numeric columns).
Returns:
| Type | Description |
|---|---|
list[str]
|
list[str]: Names of numeric columns in prices; typically excludes |
list[str]
|
|
profits: pl.DataFrame
property
¶
Compute per-asset daily cash profits, preserving non-numeric columns.
Returns:
| Type | Description |
|---|---|
DataFrame
|
pl.DataFrame: Per-asset daily profit series along with any |
DataFrame
|
non-numeric columns (e.g., |
Examples:
>>> import polars as pl
>>> prices = pl.DataFrame({"A": [100.0, 110.0, 105.0]})
>>> pos = pl.DataFrame({"A": [1000.0, 1000.0, 1000.0]})
>>> pf = Portfolio(prices=prices, cashposition=pos, aum=1e6)
>>> pf.profits.columns
['A']
profit: pl.DataFrame
property
¶
Return total daily portfolio profit including the 'date' column.
Aggregates per-asset profits into a single 'profit' column and
validates that no day's total profit is NaN/null.
nav_accumulated: pl.DataFrame
property
¶
Compute cumulative additive NAV of the portfolio, preserving 'date'.
returns: pl.DataFrame
property
¶
Return daily returns as profit scaled by AUM, preserving 'date'.
The returned DataFrame contains the original 'date' column with the
'profit' column scaled by AUM (i.e., per-period returns), and also
an additional convenience column named 'returns' with the same
values for downstream consumers.
monthly: pl.DataFrame
property
¶
Return monthly compounded returns and calendar columns.
Aggregates daily returns (profit/AUM) by calendar month and computes the compounded monthly return: prod(1 + r_d) - 1. The resulting frame includes:
date: month-end label as a Polars Date (end of the grouping window)returns: compounded monthly returnNAV_accumulated: last NAV within the monthprofit: summed profit within the monthyear: integer year (e.g., 2020)month: integer month number (1-12)month_name: abbreviated month name (e.g.,"Jan","Feb")
Raises:
| Type | Description |
|---|---|
MissingDateColumnError
|
If the portfolio data has no |
nav_compounded: pl.DataFrame
property
¶
Compute compounded NAV from returns (profit/AUM), preserving 'date'.
highwater: pl.DataFrame
property
¶
Return the cumulative maximum of NAV as the high-water mark series.
The resulting DataFrame preserves the 'date' column and adds a
'highwater' column computed as the cumulative maximum of
'NAV_accumulated'.
drawdown: pl.DataFrame
property
¶
Return drawdown as the distance from high-water mark to current NAV.
Computes 'drawdown' = 'highwater' - 'NAV_accumulated' and
preserves the 'date' column alongside the intermediate columns.
all: pl.DataFrame
property
¶
Return a merged view of drawdown and compounded NAV.
When a 'date' column is present the two frames are joined on that
column to ensure temporal alignment. When the data is integer-indexed
(no 'date' column) the frames are stacked horizontally — they are
guaranteed to have identical row counts because both are derived from
the same source portfolio.
data: Data
property
¶
Build a legacy :class:~jquantstats._data.Data object from this portfolio's returns.
This bridges the two entry points: Portfolio compiles the NAV curve from
prices and positions; the returned :class:~jquantstats._data.Data object
gives access to the full legacy analytics pipeline (data.stats,
data.plots, data.reports).
Returns:
| Type | Description |
|---|---|
Data
|
class: |
Data
|
is the portfolio's daily return series and whose |
Data
|
column (or a synthetic integer index for date-free portfolios). |
Examples:
>>> import polars as pl
>>> from datetime import date
>>> prices = pl.DataFrame({"date": [date(2020, 1, 1), date(2020, 1, 2)], "A": [100.0, 110.0]})
>>> pos = pl.DataFrame({"date": [date(2020, 1, 1), date(2020, 1, 2)], "A": [1000.0, 1000.0]})
>>> pf = Portfolio(prices=prices, cashposition=pos, aum=1e6)
>>> d = pf.data
>>> "returns" in d.returns.columns
True
stats: Stats
property
¶
Return a Stats object built from the portfolio's daily returns.
Delegates to the legacy :class:~jquantstats._stats.Stats pipeline via
:attr:data, so all analytics (Sharpe, drawdown, summary, etc.) are
available through the shared implementation.
The result is cached after first access so repeated calls are O(1).
plots: PortfolioPlots
property
¶
Convenience accessor returning a PortfolioPlots facade for this portfolio.
Use this to create Plotly visualizations such as snapshots, lagged performance curves, and lead/lag IR charts.
Returns:
| Type | Description |
|---|---|
PortfolioPlots
|
class: |
PortfolioPlots
|
plotting methods. |
The result is cached after first access so repeated calls are O(1).
report: Report
property
¶
Convenience accessor returning a Report facade for this portfolio.
Use this to generate a self-contained HTML performance report containing statistics tables and interactive charts.
Returns:
| Type | Description |
|---|---|
Report
|
class: |
Report
|
report methods. |
The result is cached after first access so repeated calls are O(1).
tilt: Portfolio
property
¶
Return the 'tilt' portfolio with constant average weights.
Computes the time-average of each asset's cash position (ignoring nulls/NaNs) and builds a new Portfolio with those constant weights applied across time. Prices and AUM are preserved.
timing: Portfolio
property
¶
Return the 'timing' portfolio capturing deviations from the tilt.
Constructs weights as original cash positions minus the tilt's constant positions, per asset. This isolates timing (alloc-demeaned) effects. Prices and AUM are preserved.
tilt_timing_decomp: pl.DataFrame
property
¶
Return the portfolio's tilt/timing NAV decomposition.
When a 'date' column is present the three NAV series are joined on
it. When data is integer-indexed the frames are stacked horizontally.
turnover: pl.DataFrame
property
¶
Daily one-way portfolio turnover as a fraction of AUM.
Computes the sum of absolute position changes across all assets for each period, normalised by AUM. The first row is always zero because there is no prior position to form a difference against.
Returns:
| Type | Description |
|---|---|
DataFrame
|
pl.DataFrame: Frame with an optional |
DataFrame
|
|
Examples:
>>> import polars as pl
>>> from datetime import date
>>> _d = [date(2020, 1, 1), date(2020, 1, 2), date(2020, 1, 3)]
>>> prices = pl.DataFrame({"date": _d, "A": [100.0, 110.0, 121.0]})
>>> pos = pl.DataFrame({"date": prices["date"], "A": [1000.0, 1200.0, 900.0]})
>>> pf = Portfolio(prices=prices, cashposition=pos, aum=1e5)
>>> pf.turnover["turnover"].to_list()
[0.0, 0.002, 0.003]
turnover_weekly: pl.DataFrame
property
¶
Weekly aggregated one-way portfolio turnover as a fraction of AUM.
When a 'date' column is present, sums the daily turnover within
each calendar week (Monday-based group_by_dynamic). Without a
date column, a rolling 5-period sum with min_samples=5 is returned
(the first four rows will be null).
Returns:
| Type | Description |
|---|---|
DataFrame
|
pl.DataFrame: Frame with an optional |
DataFrame
|
start) and a |
DataFrame
|
the week). |
position_delta_costs: pl.DataFrame
property
¶
Daily trading cost using the position-delta model.
Computes the per-period cost as::
cost_t = sum_i( |x_{i,t} - x_{i,t-1}| ) * cost_per_unit
where x_{i,t} is the cash position in asset i at time t and
cost_per_unit is the one-way cost per unit of traded notional.
The first row is always zero because there is no prior position to
form a difference against.
Returns:
| Type | Description |
|---|---|
DataFrame
|
pl.DataFrame: Frame with an optional |
DataFrame
|
|
Examples:
>>> import polars as pl
>>> from datetime import date
>>> _d = [date(2020, 1, 1), date(2020, 1, 2), date(2020, 1, 3)]
>>> prices = pl.DataFrame({"date": _d, "A": [100.0, 110.0, 121.0]})
>>> pos = pl.DataFrame({"date": _d, "A": [1000.0, 1200.0, 900.0]})
>>> pf = Portfolio(prices=prices, cashposition=pos, aum=1e5, cost_per_unit=0.01)
>>> pf.position_delta_costs["cost"].to_list()
[0.0, 2.0, 3.0]
net_cost_nav: pl.DataFrame
property
¶
Net-of-cost cumulative additive NAV using the position-delta cost model.
Deducts :attr:position_delta_costs from daily portfolio profit and
computes the running cumulative sum offset by AUM. The result
represents the realised NAV path a strategy would achieve after paying
cost_per_unit on every unit of position change.
When cost_per_unit is zero the result equals :attr:nav_accumulated.
Returns:
| Type | Description |
|---|---|
DataFrame
|
pl.DataFrame: Frame with an optional |
DataFrame
|
|
Examples:
>>> import polars as pl
>>> from datetime import date
>>> _d = [date(2020, 1, 1), date(2020, 1, 2), date(2020, 1, 3)]
>>> prices = pl.DataFrame({"date": _d, "A": [100.0, 110.0, 121.0]})
>>> pos = pl.DataFrame({"date": _d, "A": [1000.0, 1200.0, 900.0]})
>>> pf = Portfolio(prices=prices, cashposition=pos, aum=1e5, cost_per_unit=0.0)
>>> net = pf.net_cost_nav
>>> list(net.columns)
['date', 'profit', 'cost', 'NAV_accumulated_net']
__post_init__() -> None
¶
Validate input types, shapes, and parameters post-initialization.
__repr__() -> str
¶
Return a string representation of the Portfolio object.
describe() -> pl.DataFrame
¶
Return a tidy summary of shape, date range and asset names.
Returns:¶
pl.DataFrame One row per asset with columns: asset, start, end, rows.
Examples:
>>> import polars as pl
>>> from datetime import date
>>> prices = pl.DataFrame({"date": [date(2020, 1, 1), date(2020, 1, 2)], "A": [100.0, 110.0]})
>>> pos = pl.DataFrame({"date": [date(2020, 1, 1), date(2020, 1, 2)], "A": [1000.0, 1000.0]})
>>> pf = Portfolio(prices=prices, cashposition=pos, aum=1e6)
>>> df = pf.describe()
>>> list(df.columns)
['asset', 'start', 'end', 'rows']
from_risk_position(prices: pl.DataFrame, risk_position: pl.DataFrame, aum: float, vola: int | dict[str, int] = 32, vol_cap: float | None = None, cost_per_unit: float = 0.0, cost_bps: float = 0.0, cost_model: CostModel | None = None) -> Self
classmethod
¶
Create a Portfolio from per-asset risk positions.
De-volatizes each risk position using an EWMA volatility estimate derived from the corresponding price series.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
prices
|
DataFrame
|
Price levels per asset over time (may include a date column). |
required |
risk_position
|
DataFrame
|
Risk units per asset aligned with prices. |
required |
vola
|
int | dict[str, int]
|
EWMA lookback (span-equivalent) used to estimate volatility.
Pass an |
32
|
vol_cap
|
float | None
|
Optional lower bound for the EWMA volatility estimate.
When provided, the vol series is clipped from below at this
value before dividing the risk position, preventing
position blow-up in calm, low-volatility regimes. For
example, |
None
|
aum
|
float
|
Assets under management used as the base NAV offset. |
required |
cost_per_unit
|
float
|
One-way trading cost per unit of position change. Defaults to 0.0 (no cost). Ignored when cost_model is given. |
0.0
|
cost_bps
|
float
|
One-way trading cost in basis points of AUM turnover. Defaults to 0.0 (no cost). Ignored when cost_model is given. |
0.0
|
cost_model
|
CostModel | None
|
Optional :class: |
None
|
Returns:
| Type | Description |
|---|---|
Self
|
A Portfolio instance whose cash positions are risk_position |
Self
|
divided by EWMA volatility. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If any span value in vola is ≤ 0, or if a key in a vola dict does not match any numeric column in prices, or if vol_cap is provided but is not positive. |
from_position(prices: pl.DataFrame, position: pl.DataFrame, aum: float, cost_per_unit: float = 0.0, cost_bps: float = 0.0, cost_model: CostModel | None = None) -> Self
classmethod
¶
Create a Portfolio from share/unit positions.
Converts position (number of units held per asset) to cash exposure
by multiplying element-wise with prices, then delegates to
:py:meth:from_cash_position.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
prices
|
DataFrame
|
Price levels per asset over time (may include a date column). |
required |
position
|
DataFrame
|
Number of units held per asset over time, aligned with
prices. Non-numeric columns (e.g. |
required |
aum
|
float
|
Assets under management used as the base NAV offset. |
required |
cost_per_unit
|
float
|
One-way trading cost per unit of position change. Defaults to 0.0 (no cost). Ignored when cost_model is given. |
0.0
|
cost_bps
|
float
|
One-way trading cost in basis points of AUM turnover. Defaults to 0.0 (no cost). Ignored when cost_model is given. |
0.0
|
cost_model
|
CostModel | None
|
Optional :class: |
None
|
Returns:
| Type | Description |
|---|---|
Self
|
A Portfolio instance whose cash positions equal position x prices. |
Examples:
>>> import polars as pl
>>> prices = pl.DataFrame({"A": [100.0, 110.0, 105.0]})
>>> pos = pl.DataFrame({"A": [10.0, 10.0, 10.0]})
>>> pf = Portfolio.from_position(prices=prices, position=pos, aum=1e6)
>>> pf.cashposition["A"].to_list()
[1000.0, 1100.0, 1050.0]
from_cash_position(prices: pl.DataFrame, cash_position: pl.DataFrame, aum: float, cost_per_unit: float = 0.0, cost_bps: float = 0.0, cost_model: CostModel | None = None) -> Self
classmethod
¶
Create a Portfolio directly from cash positions aligned with prices.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
prices
|
DataFrame
|
Price levels per asset over time (may include a date column). |
required |
cash_position
|
DataFrame
|
Cash exposure per asset over time. |
required |
aum
|
float
|
Assets under management used as the base NAV offset. |
required |
cost_per_unit
|
float
|
One-way trading cost per unit of position change. Defaults to 0.0 (no cost). Ignored when cost_model is given. |
0.0
|
cost_bps
|
float
|
One-way trading cost in basis points of AUM turnover. Defaults to 0.0 (no cost). Ignored when cost_model is given. |
0.0
|
cost_model
|
CostModel | None
|
Optional :class: |
None
|
Returns:
| Type | Description |
|---|---|
Self
|
A Portfolio instance with the provided cash positions. |
truncate(start: object = None, end: object = None) -> Portfolio
¶
Return a new Portfolio truncated to the inclusive [start, end] range.
When a 'date' column is present in both prices and cash positions,
truncation is performed by comparing the 'date' column against
start and end (which should be date/datetime values or strings
parseable by Polars).
When the 'date' column is absent, integer-based row slicing is
used instead. In this case start and end must be non-negative
integers representing 0-based row indices. Passing non-integer bounds
to an integer-indexed portfolio raises :exc:TypeError.
In all cases the aum value is preserved.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
start
|
object
|
Optional lower bound (inclusive). A date/datetime or
Polars-parseable string when a |
None
|
end
|
object
|
Optional upper bound (inclusive). Same type rules as
|
None
|
Returns:
| Type | Description |
|---|---|
Portfolio
|
A new Portfolio instance with prices and cash positions filtered |
Portfolio
|
to the specified range. |
Raises:
| Type | Description |
|---|---|
TypeError
|
When the portfolio has no |
lag(n: int) -> Portfolio
¶
Return a new Portfolio with cash positions lagged by n steps.
This method shifts the numeric asset columns in the cashposition
DataFrame by n rows, preserving the 'date' column and any
non-numeric columns unchanged. Positive n delays weights (moves
them down); negative n leads them (moves them up); n == 0
returns the current portfolio unchanged.
Notes
Missing values introduced by the shift are left as nulls; downstream profit computation already guards and treats nulls as zero when multiplying by returns.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n
|
int
|
Number of rows to shift (can be negative, zero, or positive). |
required |
Returns:
| Type | Description |
|---|---|
Portfolio
|
A new Portfolio instance with lagged cash positions and the same |
Portfolio
|
prices/AUM as the original. |
smoothed_holding(n: int) -> Portfolio
¶
Return a new Portfolio with cash positions smoothed by a rolling mean.
Applies a trailing window average over the last n steps for each
numeric asset column (excluding 'date'). The window length is
n + 1 so that:
- n=0 returns the original weights (no smoothing),
- n=1 averages the current and previous weights,
- n=k averages the current and last k weights.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n
|
int
|
Non-negative integer specifying how many previous steps to include. |
required |
Returns:
| Type | Description |
|---|---|
Portfolio
|
A new Portfolio with smoothed cash positions and the same |
Portfolio
|
prices/AUM. |
turnover_summary() -> pl.DataFrame
¶
Return a summary DataFrame of turnover statistics.
Computes three metrics from the daily turnover series:
mean_daily_turnover: mean of daily one-way turnover (fraction of AUM).mean_weekly_turnover: mean of weekly-aggregated turnover (fraction of AUM).turnover_std: standard deviation of daily turnover (fraction of AUM); complements the mean to detect regime switches.
Returns:
| Type | Description |
|---|---|
DataFrame
|
pl.DataFrame: One row per metric with columns |
DataFrame
|
|
Examples:
>>> import polars as pl
>>> from datetime import date, timedelta
>>> import numpy as np
>>> start = date(2020, 1, 1)
>>> dates = pl.date_range(start=start, end=start + timedelta(days=9), interval="1d", eager=True)
>>> prices = pl.DataFrame({"date": dates, "A": pl.Series(np.ones(10) * 100.0)})
>>> pos = pl.DataFrame({"date": dates, "A": pl.Series([float(i) * 100 for i in range(10)])})
>>> pf = Portfolio(prices=prices, cashposition=pos, aum=1e4)
>>> summary = pf.turnover_summary()
>>> list(summary["metric"])
['mean_daily_turnover', 'mean_weekly_turnover', 'turnover_std']
cost_adjusted_returns(cost_bps: float | None = None) -> pl.DataFrame
¶
Return daily portfolio returns net of estimated one-way trading costs.
Trading costs are modelled as a linear function of daily one-way
turnover: for every unit of AUM traded, the strategy incurs
cost_bps basis points (i.e. cost_bps / 10_000 fractional
cost). The daily cost deduction is therefore::
daily_cost = turnover * (cost_bps / 10_000)
where turnover is the fraction-of-AUM one-way turnover already
computed by :attr:turnover. The deduction is applied to the
returns column of :attr:returns, leaving all other columns
(including date) untouched.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cost_bps
|
float | None
|
One-way trading cost in basis points per unit of AUM
traded. Must be non-negative. Defaults to |
None
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
pl.DataFrame: Same schema as :attr: |
DataFrame
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
>>> import polars as pl
>>> from datetime import date
>>> _d = [date(2020, 1, 1), date(2020, 1, 2), date(2020, 1, 3)]
>>> prices = pl.DataFrame({"date": _d, "A": [100.0, 110.0, 121.0]})
>>> pos = pl.DataFrame({"date": _d, "A": [1000.0, 1200.0, 900.0]})
>>> pf = Portfolio(prices=prices, cashposition=pos, aum=1e5)
>>> adj = pf.cost_adjusted_returns(0.0)
>>> float(adj["returns"][1]) == float(pf.returns["returns"][1])
True
trading_cost_impact(max_bps: int = 20) -> pl.DataFrame
¶
Estimate the impact of trading costs on the Sharpe ratio.
Computes the annualised Sharpe ratio of cost-adjusted returns for
each integer cost level from 0 up to and including max_bps basis
points (1 bp = 0.01 %). The result lets you quickly assess at what
cost level the strategy's edge is eroded.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
max_bps
|
int
|
Maximum one-way trading cost to evaluate, in basis points. Defaults to 20 (i.e., evaluates 0, 1, 2, …, 20 bps). Must be a positive integer. |
20
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
pl.DataFrame: Frame with columns |
DataFrame
|
|
DataFrame
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
>>> import polars as pl
>>> from datetime import date, timedelta
>>> import numpy as np
>>> start = date(2020, 1, 1)
>>> dates = pl.date_range(
... start=start, end=start + timedelta(days=99), interval="1d", eager=True
... )
>>> rng = np.random.default_rng(0)
>>> prices = pl.DataFrame({
... "date": dates,
... "A": pl.Series(np.cumprod(1 + rng.normal(0.001, 0.01, 100)) * 100),
... })
>>> pos = pl.DataFrame({"date": dates, "A": pl.Series(np.ones(100) * 1000.0)})
>>> pf = Portfolio(prices=prices, cashposition=pos, aum=1e5)
>>> impact = pf.trading_cost_impact(max_bps=5)
>>> list(impact["cost_bps"])
[0, 1, 2, 3, 4, 5]
correlation(frame: pl.DataFrame, name: str = 'portfolio') -> pl.DataFrame
¶
Compute a correlation matrix of asset returns plus the portfolio.
Computes percentage changes for all numeric columns in frame,
appends the portfolio profit series under the provided name, and
returns the Pearson correlation matrix across all numeric columns.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
frame
|
DataFrame
|
A Polars DataFrame containing at least the asset price columns (and a date column which will be ignored if non-numeric). |
required |
name
|
str
|
The column name to use when adding the portfolio profit series to the input frame. |
'portfolio'
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
A square Polars DataFrame where each cell is the correlation |
DataFrame
|
between a pair of series (values in [-1, 1]). |