costs¶
PortPy assumes frictionless trading by default. turnover_from_weights/net_of_costs_returns layer a single blended cost rate back in; bid_ask_spreads, broker_commissions, fx_spread_costs, and tax_impact itemize that cost by source instead - see the Costs & taxes guide for how they compose.
costs ¶
Optional transaction-cost modeling.
PortPy assumes frictionless trading by default. These helpers let you layer
transaction costs back in from an explicit weight-history: an aggregate flat
rate (net_of_costs_returns, given turnover from turnover_from_weights),
or itemized by source (bid_ask_spreads, broker_commissions,
fx_spread_costs, tax_impact) - the itemized functions all return a
per-period return-drag Series in the same units, so they compose by plain
addition: total_drag = bid_ask_spreads(...) + broker_commissions(...) + ...,
then returns - total_drag.
Every function here is a flat, per-period approximation, not a trade-level execution or accounting engine - see each function's own caveats for exactly what it ignores.
turnover_from_weights ¶
Per-period portfolio turnover: half the sum of absolute weight changes, i.e. the fraction of the book traded each period.
The first row is treated as building the initial portfolio from cash, so its turnover equals half the sum of the initial absolute weights.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
weights_history
|
DataFrame
|
Portfolio weights per period, indexed by date with one column per asset. |
required |
Returns:
| Type | Description |
|---|---|
Series
|
Per-period turnover as a Series named "turnover", aligned to |
Series
|
|
Source code in src/portpy/metrics/costs.py
net_of_costs_returns ¶
Deduct a transaction-cost drag from returns, given per-period turnover
and a cost in basis points. Uses a flat, linear-in-turnover cost model.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
returns
|
Series
|
Periodic gross returns. |
required |
turnover
|
Series | float
|
Either a per-period turnover Series (see
|
required |
cost_bps
|
float
|
Round-trip cost per unit of notional traded, in basis points (e.g. 10 = 0.10% per unit turnover). |
required |
Returns:
| Type | Description |
|---|---|
Series
|
|
Source code in src/portpy/metrics/costs.py
bid_ask_spreads ¶
bid_ask_spreads(
weights_history: DataFrame,
spread_bps: float | dict[str, float] | Series,
) -> pd.Series
Per-period cost of crossing the bid-ask spread, from an explicit weight history.
A market order pays roughly half the quoted spread away from the mid price on each leg (buy or sell) - this charges that half-spread on every asset's traded notional each period, then sums across assets.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
weights_history
|
DataFrame
|
Portfolio weights per period, indexed by date with
one column per asset - same shape |
required |
spread_bps
|
float | dict[str, float] | Series
|
Quoted round-trip bid-ask spread, in basis points, either
one number applied to every asset or a |
required |
Returns:
| Type | Description |
|---|---|
Series
|
Per-period cost drag (a positive return-rate reduction) as a Series |
Series
|
named "bid_ask_spread_cost", aligned to |
Series
|
first row charges the full initial allocation, matching |
Series
|
|
Source code in src/portpy/metrics/costs.py
broker_commissions ¶
broker_commissions(
weights_history: DataFrame,
commission_bps: float | dict[str, float] | Series,
) -> pd.Series
Per-period broker commission cost, proportional to traded notional.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
weights_history
|
DataFrame
|
See |
required |
commission_bps
|
float | dict[str, float] | Series
|
Commission rate in basis points of traded notional,
either one number for every asset or a |
required |
Returns:
| Type | Description |
|---|---|
Series
|
Per-period cost drag as a Series named "broker_commission_cost", |
Series
|
aligned to |
Source code in src/portpy/metrics/costs.py
fx_spread_costs ¶
fx_spread_costs(
weights_history: DataFrame,
asset_currencies: dict[str, str],
base_currency: str,
fx_spread_bps: float | dict[str, float] | Series,
) -> pd.Series
Per-period cost of crossing the FX bid-ask spread when trading assets
denominated in a currency other than base_currency.
Every trade in a foreign-currency asset implicitly converts currency too
(buying a EUR-denominated stock from a USD book means buying EUR first) -
this charges the same half-spread convention as bid_ask_spreads, but
only on assets whose currency differs from base_currency, using an FX
spread rather than that asset's own equity/bond spread.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
weights_history
|
DataFrame
|
See |
required |
asset_currencies
|
dict[str, str]
|
Mapping of asset symbol -> currency code. Assets
already in |
required |
base_currency
|
str
|
The portfolio's base currency code. |
required |
fx_spread_bps
|
float | dict[str, float] | Series
|
Quoted FX bid-ask spread, in basis points, either one
number for every non-base currency or a |
required |
Returns:
| Type | Description |
|---|---|
Series
|
Per-period cost drag as a Series named "fx_spread_cost", aligned to |
Series
|
|
Series
|
|
Source code in src/portpy/metrics/costs.py
tax_impact ¶
tax_impact(
returns: Series,
turnover: Series | float,
capital_gains_tax_rate: float,
dividend_tax_rate: float = 0.0,
dividend_yield: float | Series = 0.0,
) -> pd.Series
Estimate a per-period tax drag on realized capital gains plus dividend income.
This is a first-order, turnover-proxy approximation, not a lot-level tax engine (see caveats) - it assumes the fraction of the book turned over each period approximates the fraction of that period's gain that gets realized (and therefore taxed) rather than deferred by staying invested, and taxes any assumed dividend yield separately (dividends are taxed whether or not you sell).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
returns
|
Series
|
Periodic gross returns. |
required |
turnover
|
Series | float
|
Either a per-period turnover Series (see
|
required |
capital_gains_tax_rate
|
float
|
Tax rate applied to the realized-gain estimate in periods with a positive return (e.g. 0.15 for a 15% long-term US federal rate, 0.37 for short-term-as-ordinary-income, or a blended local+foreign effective rate you've computed yourself). Losses are never taxed here (no loss-harvesting credit either - see caveats). |
required |
dividend_tax_rate
|
float
|
Tax rate applied to |
0.0
|
dividend_yield
|
float | Series
|
Assumed per-period dividend yield, as a return
fraction - a single constant or a Series aligned to |
0.0
|
Returns:
| Type | Description |
|---|---|
Series
|
|
This is a deliberately simplified, single-effective-rate approximation:
it has no per-lot cost basis, no distinction between short-term and
long-term holding periods beyond whatever single capital_gains_tax_rate
you pass in, no loss carryforward or tax-loss harvesting, no wash-sale
rule, and no jurisdiction-specific treatment of local vs. foreign
withholding beyond a blended rate you supply yourself. Using turnover as
a realization proxy is a common simplifying assumption in tax-drag
literature, not an actual accounting rule - a buy-and-hold book with zero
turnover pays zero estimated capital-gains tax here even though the
portfolio's value has genuinely appreciated (correctly reflecting that
unrealized gains aren't taxed yet, but only if you also track cost basis
yourself when gains are eventually realized outside this function).