beacon.analysis¶
Performance and risk analytics for indices, ETFs, and portfolios: ETF
tracking metrics (beacon.analysis.etf), performance attribution, and the
scalar risk measures covered on the Risk Model
concept page.
analysis ¶
The init.py for the 'analysis' module.
This module provides tools for analyzing the performance and risk characteristics of indices, ETFs, and portfolios.
Attribution ¶
Kept for the original portfolio-versus-benchmark helper.
simple_performance_attribution ¶
simple_performance_attribution(
portfolio_returns: Series, benchmark_returns: Series
) -> dict[str, float]
Total return difference between a portfolio and a benchmark.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
portfolio_returns
|
Series
|
Portfolio periodic returns. |
required |
benchmark_returns
|
Series
|
Benchmark periodic returns, same length. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
dict |
dict[str, float]
|
total_portfolio_return, total_benchmark_return and |
dict[str, float]
|
active_return. |
Raises:
| Type | Description |
|---|---|
TypeError
|
If either input is not a Series. |
ValueError
|
If the lengths differ or the inputs are empty. |
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/analysis/attribution.py
AttributionResult
dataclass
¶
AttributionResult(
start: str,
end: str,
periods: int,
total_return: float,
contributions: list[Contribution],
residual: float,
cap_drag: float | None = None,
cost_drag: float | None = None,
_weights: DataFrame | None = None,
)
A decomposition of one return into per-constituent contributions.
Attributes:
| Name | Type | Description |
|---|---|---|
start |
str
|
First date of the window, ISO 8601. |
end |
str
|
Last date, ISO 8601. |
periods |
int
|
Return periods decomposed. |
total_return |
float
|
The return being explained. |
contributions |
list[Contribution]
|
Per constituent, largest first. Sums to total_return up to residual. |
residual |
float
|
total_return minus the sum of contributions. Reported always, expected to be at machine epsilon after linking. It is never folded into a constituent. |
cap_drag |
float | None
|
Capped return minus uncapped return, when the index applies a cap. Negative when capping cost the index. None when uncapped. |
cost_drag |
float | None
|
Portfolio return minus its gross return, when a backtest is supplied. Negative by construction — costs only subtract. |
Contribution
dataclass
¶
One constituent's share of the total return.
Attributes:
| Name | Type | Description |
|---|---|---|
asset_id |
str
|
The constituent. |
contribution |
float
|
Its linked contribution. These sum to the total return. |
average_weight |
float
|
Mean weight across the window, for context — a large contribution from a small average weight is a different story from the same contribution from a large one. |
total_return |
float
|
The constituent's own return over the window. |
ConcentrationMetrics
dataclass
¶
ConcentrationMetrics(
assets: int,
herfindahl_index: float,
effective_assets: float,
largest_weight: float,
largest_asset_id: str | None,
)
How concentrated a set of weights is.
Attributes:
| Name | Type | Description |
|---|---|---|
assets |
int
|
Number of weighted positions. |
herfindahl_index |
float
|
Sum of squared weights. For weights summing to 1 this runs from 1/n (perfectly equal) to 1 (everything in one name). |
effective_assets |
float
|
|
largest_weight |
float
|
The biggest single weight. |
largest_asset_id |
str | None
|
Which asset holds it, or None when there are no positions. |
DriftMetrics
dataclass
¶
DriftMetrics(
per_asset: dict[str, float],
max_absolute: float,
max_absolute_asset_id: str | None,
total_absolute: float,
turnover: float,
)
How far current weights have moved from their targets.
Attributes:
| Name | Type | Description |
|---|---|---|
per_asset |
dict[str, float]
|
Current minus target for every asset in either set. Positive means overweight. |
max_absolute |
float
|
Largest absolute drift across assets. |
max_absolute_asset_id |
str | None
|
Which asset drifted most, or None when there is nothing to compare. |
total_absolute |
float
|
Sum of absolute drifts. |
turnover |
float
|
Half of total_absolute — the one-way trading needed to return to target, since every overweight funds an underweight. |
RiskMetricsCalculator ¶
A class to calculate various risk metrics. This class can be expanded to hold state or more complex configurations if needed.
Initializes the RiskMetricsCalculator.
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/analysis/risk.py
calculate_volatility ¶
Calculates annualized volatility from a price series.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
price_series
|
Series
|
A pandas Series of prices. |
required |
window
|
int
|
The number of trading periods in a year (e.g., 252 for daily). |
252
|
Returns:
| Type | Description |
|---|---|
float
|
The annualized volatility as a float. |
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/analysis/risk.py
calculate_sharpe_ratio ¶
calculate_sharpe_ratio(
returns: Series,
risk_free_rate: float,
periods_per_year: int = 252,
) -> float
Calculates the annualized Sharpe Ratio.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
returns
|
Series
|
A pandas Series of periodic returns. |
required |
risk_free_rate
|
float
|
The annualized risk-free rate. |
required |
periods_per_year
|
int
|
The number of return periods in a year (e.g., 252 for daily, 12 for monthly). |
252
|
Returns:
| Type | Description |
|---|---|
float
|
The annualized Sharpe Ratio as a float. |
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/analysis/risk.py
calculate_max_drawdown ¶
Calculates the maximum drawdown from a price series.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
price_series
|
Series
|
A pandas Series of prices. |
required |
Returns:
| Type | Description |
|---|---|
float
|
The maximum drawdown as a float (e.g., 0.2 for a 20% drawdown). |
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/analysis/risk.py
attribute ¶
attribute(
period_returns: Series,
weights: DataFrame,
asset_returns: DataFrame,
cap_drag: float | None = None,
cost_drag: float | None = None,
) -> AttributionResult
Decompose a return series into per-constituent contributions.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
period_returns
|
Series
|
The return being explained, per period. |
required |
weights
|
DataFrame
|
Weights per period, constituents on the columns. Aligned to period_returns; the weight used for a period is the one held at its start. |
required |
asset_returns
|
DataFrame
|
Constituent returns per period. |
required |
cap_drag
|
float | None
|
Optional capped-minus-uncapped return. |
None
|
cost_drag
|
float | None
|
Optional cost effect on the portfolio return. |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
AttributionResult |
AttributionResult
|
The decomposition, with contributions summing to the |
AttributionResult
|
compounded total return and a residual reported separately. |
Raises:
| Type | Description |
|---|---|
CalculationError
|
If the inputs cannot be aligned, or a period wipes out the index. |
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/analysis/attribution.py
cap_drag ¶
cap_drag(
capped_weights: dict[Timestamp, dict[str, float]],
uncapped_weights: dict[Timestamp, dict[str, float]],
prices: DataFrame,
) -> float
What capping cost, or gained, over the window.
The capped index's return minus the return of the same methodology left uncapped. Negative when the cap held back a name that went on to outperform, which is the usual case and the reason the number is worth reporting.
Both paths are built by drifting their own snapshots forward, so the comparison isolates the effect of the cap rather than of any other difference.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
capped_weights
|
dict[Timestamp, dict[str, float]]
|
Rebalance date -> capped weights. |
required |
uncapped_weights
|
dict[Timestamp, dict[str, float]]
|
Rebalance date -> weights before capping. |
required |
prices
|
DataFrame
|
Constituent prices over the window. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
float |
float
|
Capped total return minus uncapped total return. |
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/analysis/attribution.py
carino_factor ¶
The Carino coefficient for one period.
ln(1+R)/R, with the removable singularity at R = 0 filled in with its
limit of 1.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
period_return
|
float
|
The period's total return. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
float |
float
|
The coefficient. |
Raises:
| Type | Description |
|---|---|
CalculationError
|
If the return is -100% or worse, where the logarithm is undefined. An index that goes to zero cannot have its return attributed, and silently substituting a number would hide that. |
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/analysis/attribution.py
cost_drag ¶
Direct effect of transaction costs on a portfolio's return.
Costs paid as a fraction of starting capital, negated so it reads as a drag. This is the direct effect only: it excludes the compounding of the capital that was spent rather than invested, which is second-order but not zero over a long window. Reporting the direct figure keeps the number explainable — it is exactly the money that left the portfolio — and a caller wanting the full effect can difference a zero-cost run instead.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
total_costs
|
float
|
Sum of transaction costs paid. |
required |
initial_capital
|
float
|
Capital the portfolio started with. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
float |
float
|
A non-positive drag. |
Raises:
| Type | Description |
|---|---|
CalculationError
|
If initial_capital is not positive. |
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/analysis/attribution.py
drifted_weights ¶
Reconstruct the index's daily weights from its rebalance snapshots.
Weights are set at a rebalance and then drift with relative performance until the next one, because the index holds fixed units in between. Given the weight at a rebalance and prices since, the drifted weight is
w_i,t ∝ w_i,rebalance × (p_i,t / p_i,rebalance)
normalised across constituents. The unit scale cancels, so nothing beyond the snapshot and prices is needed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
snapshots
|
dict[Timestamp, dict[str, float]]
|
Rebalance date -> weights on that date. |
required |
prices
|
DataFrame
|
Dates on the index, constituents on the columns. |
required |
Returns:
| Type | Description |
|---|---|
DataFrame
|
pd.DataFrame: Weights for every date at or after the first rebalance, |
DataFrame
|
each row summing to 1. |
Raises:
| Type | Description |
|---|---|
CalculationError
|
If there are no snapshots to start from. |
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/analysis/attribution.py
link_contributions ¶
Scale per-period contributions so they sum to the compounded return.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
contributions
|
DataFrame
|
Periods on the index, constituents on the columns. Each row must sum to that period's return. |
required |
period_returns
|
Series
|
The total return of each period. |
required |
Returns:
| Type | Description |
|---|---|
Series
|
pd.Series: One linked contribution per constituent. Their sum equals |
Series
|
the compounded total return exactly. |
Raises:
| Type | Description |
|---|---|
CalculationError
|
If any period return is -100% or worse. |
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/analysis/attribution.py
simple_performance_attribution ¶
simple_performance_attribution(
portfolio_returns: Series, benchmark_returns: Series
) -> dict[str, float]
Total return difference between a portfolio and a benchmark.
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/analysis/attribution.py
drift_from_target ¶
Compare held weights against their targets.
Every asset appearing in either mapping is included, treating absence as a zero weight — a position that has been fully sold, or one the target wants but the portfolio does not hold, is precisely the drift worth seeing.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
current
|
dict[str, float]
|
Held weights. |
required |
target
|
dict[str, float]
|
Target weights. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
DriftMetrics |
DriftMetrics
|
The comparison. Empty inputs give zeros and a None asset |
DriftMetrics
|
rather than raising. |
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/analysis/concentration.py
drift_history ¶
drift_history(
weight_history: dict[str, dict[str, float]],
target: dict[str, float],
) -> dict[str, DriftMetrics]
Drift at each of several snapshots against one set of targets.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
weight_history
|
dict[str, dict[str, float]]
|
Mapping of snapshot label (typically an ISO date) to the weights held at that point. |
required |
target
|
dict[str, float]
|
The target weights to compare each snapshot against. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
dict |
dict[str, DriftMetrics]
|
Snapshot label -> DriftMetrics, in the order the labels sort. |
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/analysis/concentration.py
effective_number_of_assets ¶
Number of equally weighted positions with the same concentration.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
weights
|
dict[str, float]
|
Mapping of asset id to weight. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
float |
float
|
|
float
|
weight is zero — neither has a meaningful effective count, and |
|
float
|
returning 0.0 keeps callers from having to guard against a division by |
|
float
|
zero they cannot act on. |
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/analysis/concentration.py
herfindahl_index ¶
Sum of squared weights.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
weights
|
dict[str, float]
|
Mapping of asset id to weight, expected to sum to 1. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
float |
float
|
The index. 0.0 for no positions. Note that squaring makes this |
float
|
blind to sign, so a short position concentrates the measure exactly as |
|
float
|
a long one of the same size would. |
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/analysis/concentration.py
top_n_weight ¶
Combined weight of the count largest positions.
The measure a concentration limit is usually written against — "no more than 40% in the top five" — and not derivable from the Herfindahl index.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
weights
|
dict[str, float]
|
Mapping of asset id to weight. |
required |
count
|
int
|
How many of the largest positions to sum. Larger than the number of positions sums all of them. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
float |
float
|
The combined weight. |
Raises:
| Type | Description |
|---|---|
CalculationError
|
If count is not positive. |
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/analysis/concentration.py
average_daily_volume ¶
average_daily_volume(
market: DataFrame,
as_of: Timestamp,
months: int = TRAILING_MONTHS,
column: str = VOLUME_COLUMN,
) -> pd.Series
Mean daily volume over the trailing window, per identifier.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
market
|
DataFrame
|
Market data MultiIndexed by |
required |
as_of
|
Timestamp
|
End of the window, inclusive. |
required |
months
|
int
|
Length of the window in calendar months. |
TRAILING_MONTHS
|
column
|
str
|
Volume column to average. |
VOLUME_COLUMN
|
Returns:
| Type | Description |
|---|---|
Series
|
pd.Series: Indexed by identifier. Empty when the frame is empty or |
Series
|
carries no volume column — an absent column is a property of the |
Series
|
dataset, not a failure of the request, so it produces no answer rather |
Series
|
than an error. |
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/analysis/liquidity.py
calculate_max_drawdown ¶
Calculates the maximum drawdown from a price series.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
price_series
|
Series
|
A pandas Series of prices. |
required |
Returns:
| Type | Description |
|---|---|
float
|
The maximum drawdown as a float (e.g., 0.2 for a 20% drawdown). |
Raises:
| Type | Description |
|---|---|
ValueError
|
If price_series is empty or contains non-numeric data. |
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/analysis/risk.py
calculate_sharpe_ratio ¶
calculate_sharpe_ratio(
returns: Series,
risk_free_rate: float,
periods_per_year: int = 252,
) -> float
Calculates the annualized Sharpe Ratio.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
returns
|
Series
|
A pandas Series of periodic returns. |
required |
risk_free_rate
|
float
|
The annualized risk-free rate. |
required |
periods_per_year
|
int
|
The number of return periods in a year (e.g., 252 for daily, 12 for monthly). |
252
|
Returns:
| Type | Description |
|---|---|
float
|
The annualized Sharpe Ratio as a float. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If inputs are invalid. |
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/analysis/risk.py
calculate_volatility ¶
Calculates annualized volatility from a price series.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
price_series
|
Series
|
A pandas Series of prices. |
required |
window
|
int
|
The number of trading periods in a year (e.g., 252 for daily). |
252
|
Returns:
| Type | Description |
|---|---|
float
|
The annualized volatility as a float. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If price_series is empty or contains non-numeric data. |