Analysis¶
A run's performance, portfolio, risk and attribution analytics, and the older helpers for concentration, liquidity and ETF tracking. See Performance, Portfolio, Risk analysis and Attribution.
analysis ¶
Analytics for indices, ETFs and portfolios.
Covers a run's performance (beacon.analysis.performance), risk
(beacon.analysis.risk) and attribution (attribute, Attribution, cost and
cap drag), concentration and drift from target weights, liquidity and ETF
tracking analytics (tracking difference, tracking error, premium or discount
to NAV), and RunData, everything a run's analysis reads, from a finished run
or a stored one.
RiskMetricsCalculator, calculate_volatility, calculate_sharpe_ratio,
calculate_max_drawdown and simple_performance_attribution are deprecated,
to be removed in 0.11.0: their figures are in
beacon.analysis.performance.formulas.
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: a compounded return, or a compounded return less a comparator's. |
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 one was
passed to |
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. Drifts equal to within floating-point noise count as tied, and a tie goes to the asset id that sorts first. |
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. |
ETFAnalytics ¶
ETF analytics as methods. Each method calls the module-level function of the same name and returns its result unchanged.
Initializes the ETFAnalytics calculator.
calculate_tracking_difference ¶
Cumulative ETF return minus cumulative index return over the period.
See :func:calculate_tracking_difference.
calculate_tracking_error ¶
calculate_tracking_error(
etf_returns: Series,
index_returns: Series,
periods_per_year: int = 252,
) -> float
Annualised standard deviation of the ETF-minus-index return differences.
See :func:calculate_tracking_error.
calculate_premium_discount ¶
Market price divided by NAV, minus one.
See :func:calculate_premium_discount.
RiskMetricsCalculator ¶
Risk metrics as methods. Each method calls the module-level function of the same name and returns its result unchanged.
Deprecated: use the functions of beacon.analysis.performance.formulas.
Initializes the RiskMetricsCalculator.
calculate_volatility ¶
Calculates annualized volatility from a price series.
See :func:calculate_volatility.
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. |
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
|
See :func:calculate_sharpe_ratio.
Returns:
| Type | Description |
|---|---|
float
|
The annualized Sharpe Ratio as a float. |
calculate_max_drawdown ¶
Calculates the maximum drawdown from a price series.
See :func:calculate_max_drawdown.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
price_series
|
Series
|
A pandas Series of prices. |
required |
Returns:
| Type | Description |
|---|---|
float
|
The maximum drawdown as a non-positive float (e.g., -0.2 for a |
float
|
20% drawdown). |
attribute ¶
attribute(
period_returns: Series,
weights: DataFrame,
asset_returns: DataFrame,
cap_drag: float | None = None,
cost_drag: float | None = None,
comparator: Series | 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
|
comparator
|
Series | None
|
A comparator's return per period. When given, weights are active weights (the portfolio's less the comparator's), and the return explained is the compounded period_returns less the compounded comparator's. |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
AttributionResult |
AttributionResult
|
The decomposition, with contributions summing to the |
AttributionResult
|
compounded total return (or active return) and a residual reported |
|
AttributionResult
|
separately. |
Raises:
| Type | Description |
|---|---|
CalculationError
|
If the inputs cannot be aligned, or a period wipes out the index. |
cap_drag ¶
cap_drag(
capped_weights: dict[Timestamp, dict[str, float]],
uncapped_weights: dict[Timestamp, dict[str, float]],
prices: DataFrame,
window: Index | None = None,
) -> 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, from the first rebalance on. Weights drift from the rebalance before each date, so the prices have to reach back to it even when window starts later. |
required |
window
|
Index | None
|
The dates to measure over, as for :func: |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
float |
float
|
Capped total return minus uncapped total return. |
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. |
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. |
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. |
link_contributions ¶
link_contributions(
contributions: DataFrame,
period_returns: Series,
comparator: Series | None = None,
) -> pd.Series
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 (or, with comparator, to that period's return less the comparator's). |
required |
period_returns
|
Series
|
The total return of each period. |
required |
comparator
|
Series | None
|
A comparator's return each period. When given, the contributions are linked by the active factor and sum to the compounded return less the comparator's. |
None
|
Returns:
| Type | Description |
|---|---|
Series
|
pd.Series: One linked contribution per constituent. Their sum equals |
Series
|
the compounded total return (or the compounded active return) |
Series
|
exactly. |
Raises:
| Type | Description |
|---|---|
CalculationError
|
If any period return is -100% or worse. |
simple_performance_attribution ¶
simple_performance_attribution(
portfolio_returns: Series, benchmark_returns: Series
) -> dict[str, float]
Total return difference between a portfolio and a benchmark.
Deprecated, and to be removed in 0.11.0: use
beacon.analysis.performance.formulas.compounded_return on each series,
or a run's Attribution.active.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
portfolio_returns
|
Series
|
Portfolio periodic returns. |
required |
benchmark_returns
|
Series
|
Benchmark periodic returns, same length. |
required |
Each total return compounds its own periodic returns,
prod(1 + r) - 1; the two series are not aligned by date.
Returns:
| Name | Type | Description |
|---|---|---|
dict |
dict[str, float]
|
total_portfolio_return, total_benchmark_return and |
dict[str, float]
|
active_return (portfolio minus benchmark). |
Raises:
| Type | Description |
|---|---|
TypeError
|
If either input is not a Series. |
ValueError
|
If the lengths differ or the inputs are empty. |
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. |
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. |
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. |
herfindahl_index ¶
Sum of squared weights.
Logs a warning when the weights do not sum to 1, since the measure then scales with the square of their sum.
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. |
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. |
calculate_premium_discount ¶
Calculates the premium/discount of an ETF's market price relative to its NAV.
Premium/Discount = (ETF Market Price / NAV) - 1.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
etf_price
|
float
|
The current market price of the ETF. |
required |
nav_price
|
float
|
The current Net Asset Value (NAV) per share of the ETF. |
required |
Returns:
| Type | Description |
|---|---|
float
|
The premium/discount as a float (e.g., 0.01 for 1% premium, -0.005 for 0.5% discount). |
Raises:
| Type | Description |
|---|---|
TypeError
|
If either input is not an int or float. |
ValueError
|
If nav_price is zero. |
calculate_tracking_difference ¶
Calculates the tracking difference between ETF returns and index returns.
Tracking difference is the ETF's cumulative return minus the index's
cumulative return over the whole period, where each cumulative return
compounds its periodic returns: prod(1 + r) - 1. It is not
annualised. Each Series is compounded on its own, so the two are not
aligned by date; NaN returns are skipped.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
etf_returns
|
Series
|
A pandas Series of ETF periodic returns. |
required |
index_returns
|
Series
|
A pandas Series of benchmark index periodic returns. Must be of the same frequency and length as etf_returns. |
required |
Returns:
| Type | Description |
|---|---|
float
|
The tracking difference over the period, as a float (e.g. -0.002 |
float
|
when the ETF returned 0.2 percentage points less than the index). |
Raises:
| Type | Description |
|---|---|
TypeError
|
If either input is not a pandas Series. |
ValueError
|
If the input Series are not of the same length or are empty. |
calculate_tracking_error ¶
calculate_tracking_error(
etf_returns: Series,
index_returns: Series,
periods_per_year: int = 252,
) -> float
Calculates the annualized tracking error between ETF returns and index returns.
Tracking error is the sample standard deviation of the periodic return
differences (ETF minus index), multiplied by sqrt(periods_per_year).
The difference is taken after aligning the two Series on their index, so
they should share the same dates: a date present in only one Series
produces NaN and is skipped, and fewer than two common dates gives NaN.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
etf_returns
|
Series
|
A pandas Series of ETF periodic returns. |
required |
index_returns
|
Series
|
A pandas Series of benchmark index periodic returns. Must be of the same frequency and length as etf_returns. |
required |
periods_per_year
|
int
|
The number of periods in a year (e.g., 252 for daily). |
252
|
Returns:
| Type | Description |
|---|---|
float
|
The annualized tracking error as a float. |
Raises:
| Type | Description |
|---|---|
TypeError
|
If either input is not a pandas Series. |
ValueError
|
If the input Series are not of the same length or are empty, or periods_per_year is not positive. |
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, |
Series
|
carries no volume column, or has no rows in the window (the last |
Series
|
case logs a warning). An absent column is a property of the dataset, |
Series
|
not a failure of the request, so it produces no answer rather than |
Series
|
an error. |
calculate_max_drawdown ¶
Calculates the maximum drawdown from a price series.
Deprecated: use beacon.analysis.performance.formulas.max_drawdown,
max_drawdown(prices).
The largest fall from a running peak, as a fraction of that peak.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
price_series
|
Series
|
A pandas Series of prices. |
required |
Returns:
| Type | Description |
|---|---|
float
|
The maximum drawdown as a non-positive float (e.g., -0.2 for a 20% |
float
|
drawdown, 0.0 when the price never falls below a previous peak). |
Raises:
| Type | Description |
|---|---|
TypeError
|
If price_series is not a pandas Series. |
ValueError
|
If price_series is empty or contains non-numeric data. |
calculate_sharpe_ratio ¶
calculate_sharpe_ratio(
returns: Series,
risk_free_rate: float,
periods_per_year: int = 252,
) -> float
Calculates the annualized Sharpe Ratio.
Deprecated: use beacon.analysis.performance.formulas.sharpe_ratio,
sharpe_ratio(returns, periods_per_year, risk_free_rate).
The mean return in excess of the risk-free rate over its sample standard
deviation, multiplied by sqrt(periods_per_year): the Sharpe ratio a
backtest reports, beacon.analysis.performance.formulas.sharpe_ratio.
The annual risk-free rate is taken as the period rate that compounds to
it. 0 when the returns do not vary.
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 |
|---|---|
TypeError
|
If returns is not a pandas Series or risk_free_rate is not a number. |
ValueError
|
If returns is empty or non-numeric, or periods_per_year is not positive. |
calculate_volatility ¶
Calculates annualized volatility from a price series.
Deprecated: use beacon.analysis.performance.formulas.volatility on the
returns, volatility(prices.pct_change().dropna(), periods_per_year).
The sample standard deviation of the simple period returns, multiplied by
sqrt(window). Despite its name, window is the annualisation factor,
not a rolling window: the whole series is used.
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 |
|---|---|
TypeError
|
If price_series is not a pandas Series. |
ValueError
|
If price_series is empty or contains non-numeric data, or window is not positive. |
attribution ¶
Performance attribution: which names, groups and decisions produced a return.
beacon.analysis.attribution.contribution decomposes one return into its
names' contributions, linked over many periods (attribute), with the cap and
cost drags beside it. Attribution does it for a run: by name or by group, for
the fund or a comparator, and against the index or the benchmark.
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: a compounded return, or a compounded return less a comparator's. |
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 one was
passed to |
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. |
attribute ¶
attribute(
period_returns: Series,
weights: DataFrame,
asset_returns: DataFrame,
cap_drag: float | None = None,
cost_drag: float | None = None,
comparator: Series | 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
|
comparator
|
Series | None
|
A comparator's return per period. When given, weights are active weights (the portfolio's less the comparator's), and the return explained is the compounded period_returns less the compounded comparator's. |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
AttributionResult |
AttributionResult
|
The decomposition, with contributions summing to the |
AttributionResult
|
compounded total return (or active return) and a residual reported |
|
AttributionResult
|
separately. |
Raises:
| Type | Description |
|---|---|
CalculationError
|
If the inputs cannot be aligned, or a period wipes out the index. |
cap_drag ¶
cap_drag(
capped_weights: dict[Timestamp, dict[str, float]],
uncapped_weights: dict[Timestamp, dict[str, float]],
prices: DataFrame,
window: Index | None = None,
) -> 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, from the first rebalance on. Weights drift from the rebalance before each date, so the prices have to reach back to it even when window starts later. |
required |
window
|
Index | None
|
The dates to measure over, as for :func: |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
float |
float
|
Capped total return minus uncapped total return. |
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. |
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. |
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. |
link_contributions ¶
link_contributions(
contributions: DataFrame,
period_returns: Series,
comparator: Series | None = None,
) -> pd.Series
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 (or, with comparator, to that period's return less the comparator's). |
required |
period_returns
|
Series
|
The total return of each period. |
required |
comparator
|
Series | None
|
A comparator's return each period. When given, the contributions are linked by the active factor and sum to the compounded return less the comparator's. |
None
|
Returns:
| Type | Description |
|---|---|
Series
|
pd.Series: One linked contribution per constituent. Their sum equals |
Series
|
the compounded total return (or the compounded active return) |
Series
|
exactly. |
Raises:
| Type | Description |
|---|---|
CalculationError
|
If any period return is -100% or worse. |
simple_performance_attribution ¶
simple_performance_attribution(
portfolio_returns: Series, benchmark_returns: Series
) -> dict[str, float]
Total return difference between a portfolio and a benchmark.
Deprecated, and to be removed in 0.11.0: use
beacon.analysis.performance.formulas.compounded_return on each series,
or a run's Attribution.active.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
portfolio_returns
|
Series
|
Portfolio periodic returns. |
required |
benchmark_returns
|
Series
|
Benchmark periodic returns, same length. |
required |
Each total return compounds its own periodic returns,
prod(1 + r) - 1; the two series are not aligned by date.
Returns:
| Name | Type | Description |
|---|---|---|
dict |
dict[str, float]
|
total_portfolio_return, total_benchmark_return and |
dict[str, float]
|
active_return (portfolio minus benchmark). |
Raises:
| Type | Description |
|---|---|
TypeError
|
If either input is not a Series. |
ValueError
|
If the lengths differ or the inputs are empty. |
analytics ¶
A run's attribution: what each name, or each group, contributed to a book's return, or to the fund's return over a comparator.
Each day a name contributes its weight at the close before times its return
that day (the run's own daily total return for it, in the book's currency),
and the days are linked over the window by Carino's factors so the
contributions add up to the compounded return (see
beacon.analysis.attribution.contribution). A group contributes what its
names did, each in the group it was in at that close.
Active attribution takes the fund's weights less the comparator's, both as they drift, against the fund's daily return less the comparator's, linked by the active Carino factor so the contributions add up to the fund's compounded return less the comparator's, the same total Brinson-Fachler, currency, factor and implementation attribution explain. What the names do not explain (cash, costs, fees, futures, the timing of dividends) is the residual, reported with every result and never folded into a name.
Attribution ¶
What produced a run's returns, by name or group.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
run
|
RunData
|
The run's data, with its weights, name returns and reference. |
required |
contribution ¶
contribution(
book: str = FUND,
field: str | None = None,
start: str | Timestamp | None = None,
end: str | Timestamp | None = None,
) -> AttributionResult
Each name's (or, with field, each group's) contribution to a
book's return (fund, index, optimised or benchmark)
over the window,
the whole run by default.
active ¶
active(
against: str = INDEX,
field: str | None = None,
start: str | Timestamp | None = None,
end: str | Timestamp | None = None,
) -> AttributionResult
Each name's (or group's) contribution to the fund's return over against, from its active weight, over the window: the contributions add up to the fund's compounded return less against's.
brinson ¶
brinson(
field: str,
against: str = INDEX,
start: str | Timestamp | None = None,
end: str | Timestamp | None = None,
) -> BrinsonResult
The fund's Brinson-Fachler attribution against against by the grouping field field, over the window, the whole run by default: each group's allocation, selection and interaction, cash a group of its own, linked over the days.
brinson_by_period ¶
The Brinson-Fachler effects over each year (Y) or month
(M).
currency ¶
currency(
against: str | None = None,
by_currency: bool = False,
start: str | Timestamp | None = None,
end: str | Timestamp | None = None,
) -> EffectsResult
The fund's return (or, against a comparator, its active return) split into its names' local returns, their currencies' moves and the cross term; with by_currency, each by the names' currencies too.
factors ¶
factors(
analytics: FactorAnalytics,
against: str | None = None,
method: str = factor_model.HOLDINGS,
start: str | Timestamp | None = None,
end: str | Timestamp | None = None,
) -> EffectsResult
The fund's return (or its active return against a comparator)
explained by the run's factors: from its holdings' exposures
(holdings: market, each factor, specific) or from its returns'
betas (returns: risk-free, alpha, each factor, residual).
Raises:
| Type | Description |
|---|---|
ValueError
|
If the method is neither. |
implementation ¶
implementation(
against: str = INDEX,
start: str | Timestamp | None = None,
end: str | Timestamp | None = None,
) -> EffectsResult
The fund's tracking difference against against split into the cap and screens, drift, cash drag, equitisation, each cost, fees, withholding tax, creation fees and dividend treatment, from the run's own records, linked over the window; the rest is the residual.
weights ¶
A book's daily weights, by name: fund, index,
optimised or benchmark.
Raises:
| Type | Description |
|---|---|
KeyError
|
If the book is none of those, or the run kept no weights for it. |
brinson ¶
Brinson-Fachler attribution: how much of the fund's return over a comparator came from how it weighted each group (allocation), from what it held within each (selection), and from the two together (interaction).
Each day, with w and b the fund's and the comparator's weights at the close before, R and R^b their returns, by group g and in all:
allocation_g = (w_g - b_g) (R^b_g - R^b)
selection_g = b_g (R_g - R^b_g)
interaction_g = (w_g - b_g) (R_g - R^b_g)
They add up, over the groups, to the holdings' active return that day. A group the fund does not hold takes the comparator's group return as its own, so it has no selection or interaction; one the comparator does not hold takes the comparator's whole return, so its bet is all interaction. Cash is a group of its own: the fund's weight in it is one less its names', and its return the interest it earned over the cash it held.
The days are linked over the window by Carino's factors for an active return,
(ln(1 + R_t) - ln(1 + R^b_t)) / (R_t - R^b_t) over the same for the
window, so the effects add up to the holdings' compounded active return. What
the holdings do not explain of the books' own returns (costs, fees, dividends
not yet paid, a price index's missing dividends) is the residual.
BrinsonResult
dataclass
¶
BrinsonResult(
start: str,
end: str,
periods: int,
fund_return: float,
comparator_return: float,
active_return: float,
explained: float,
residual: float,
groups: DataFrame,
names: DataFrame = pd.DataFrame(),
)
The fund's return over a comparator, by group and effect.
Attributes:
| Name | Type | Description |
|---|---|---|
start |
str
|
The first day attributed. |
end |
str
|
The last. |
periods |
int
|
The days. |
fund_return |
float
|
The fund's own compounded return. |
comparator_return |
float
|
The comparator's. |
active_return |
float
|
The fund's less the comparator's. |
explained |
float
|
The holdings' active return the effects add up to. |
residual |
float
|
The active return less what the effects explain. |
groups |
DataFrame
|
By group: the mean weights and compounded returns of the fund's and the comparator's holdings, and each effect, linked. |
names |
DataFrame
|
By group and name: each name's mean weights in the two books and its share of its group's selection and interaction, linked; a group's names add up to its own. |
daily_effects ¶
daily_effects(
fund: DataFrame,
comparator: DataFrame,
fund_earned: DataFrame,
comparator_earned: DataFrame,
) -> dict[str, pd.DataFrame]
Each day's effects by group, from each book's group weights at the close before and what each group earned (weight times return).
carino_factors ¶
Each day's Carino factor over the window's, which link the days' effects so they add up to the compounded active return.
linked ¶
Each effect by group over the days, scaled by Carino's factors so they add up to the compounded active return of fund over comparator.
brinson ¶
brinson(
run: RunData,
weights: DataFrame,
comparator_weights: DataFrame,
comparator_levels: Series,
name_returns: DataFrame,
field: str,
days: DatetimeIndex,
) -> BrinsonResult
The fund's Brinson-Fachler attribution against a comparator, by field, over days after the first, whose close the first day's weights are held at.
by_period ¶
by_period(
days: DatetimeIndex,
compute: Callable[[DatetimeIndex], BrinsonResult],
frequency: str = "Y",
) -> pd.DataFrame
The effects over each year (Y) or month (M), each period's
days linked within it, from the close before its first: a row a period,
the effects and the residual.
brinson_names ¶
Brinson-Fachler selection and interaction split across each group's names, so a group's row opens into the names that make it up.
Each day, with W_g and B_g the fund's and the comparator's weights in group g at the close before, w_i and b_i a name's, r_i its return and R^b_g the comparator's group return:
d_i = (w_i / W_g - b_i / B_g) (r_i - R^b_g)
selection_i = B_g d_i
interaction_i = (W_g - B_g) d_i
The d_i of a group add up to R_g - R^b_g, so its names' selections and interactions add up to its own. A name the comparator holds and the fund does not has a negative share, so it shows what not holding it cost. A group the fund does not hold takes the comparator's return, so its names have none; in one the comparator does not hold, a name's share is its share of the fund's, measured against the comparator's whole return, as the group's is. The days are linked with the group effects' Carino factors. A name that changed group has a row in each.
name_effects ¶
name_effects(
fund_lagged: DataFrame,
comparator_lagged: DataFrame,
returns: DataFrame,
groups: dict[Timestamp, Series],
fund_held: DataFrame,
comparator_held: DataFrame,
comparator_earned: DataFrame,
factors: Series,
) -> pd.DataFrame
Each name's linked selection and interaction within each group it was in, and its mean weights in each book, over the days of factors (the days' Carino factors over the window's).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
fund_lagged
|
DataFrame
|
The fund's weights at the close before each day. |
required |
comparator_lagged
|
DataFrame
|
The comparator's. |
required |
returns
|
DataFrame
|
Each name's return each day. |
required |
groups
|
dict[Timestamp, Series]
|
Each rebalance's group of every name. |
required |
fund_held
|
DataFrame
|
The fund's weight in each group each day. |
required |
comparator_held
|
DataFrame
|
The comparator's. |
required |
comparator_earned
|
DataFrame
|
What each group earned the comparator each day. |
required |
factors
|
Series
|
The linking factor of each day attributed. |
required |
Returns:
| Type | Description |
|---|---|
DataFrame
|
pd.DataFrame: Indexed by group and name, with |
DataFrame
|
|
contribution ¶
Performance attribution: which constituents produced an index's return.
The identity, and why it needs care¶
Within a single period the decomposition is exact:
R_t = Σ_i w_{i,t-1} × r_{i,t}
where w are the index's own drifting weights. For a Beacon index this
holds to better than 1e-12 for every weighting scheme, on rebalance days as
well as ordinary ones.
Over multiple periods it does not carry over. Returns compound while contributions add, so the arithmetic sum of daily contributions falls short of the compounded total: by about one percentage point over a 130-day window in a modest example, which is far too large to write off as a residual.
The fix is Carino linking. Scale each period's contributions by k_t / K
where k_t = ln(1+R_t)/R_t and K = ln(1+R)/R for the total return R.
Then
Σ_i Σ_t (k_t/K) c_{i,t} = (1/K) Σ_t ln(1+R_t) = ln(1+R)/K = R
exactly. The residual is reported regardless, and should sit at machine epsilon; a residual that is not tiny means an assumption has broken.
A return over a comparator links the same way with the active factor
k_t = (ln(1+R_t) - ln(1+B_t)) / (R_t - B_t), and K the same for the
window, so the contributions add up to R - B: the fund's compounded return
less the comparator's, as Brinson-Fachler explains it
(beacon.analysis.attribution.brinson).
What the drags are, and what they are not¶
Cap drag and cost drag are comparisons, not terms in the identity above. Each is the difference between two returns (the index as built versus a counterfactual), so adding them to a decomposition of a single return would be mixing two different questions. They are reported alongside, each named for the counterfactual it implies.
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. |
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: a compounded return, or a compounded return less a comparator's. |
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 one was
passed to |
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. |
active_carino ¶
The linking factor for an active return: (ln(1 + R) - ln(1 + R^b))
/ (R - R^b), 1 / (1 + R) where the two are equal.
link_contributions ¶
link_contributions(
contributions: DataFrame,
period_returns: Series,
comparator: Series | None = None,
) -> pd.Series
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 (or, with comparator, to that period's return less the comparator's). |
required |
period_returns
|
Series
|
The total return of each period. |
required |
comparator
|
Series | None
|
A comparator's return each period. When given, the contributions are linked by the active factor and sum to the compounded return less the comparator's. |
None
|
Returns:
| Type | Description |
|---|---|
Series
|
pd.Series: One linked contribution per constituent. Their sum equals |
Series
|
the compounded total return (or the compounded active return) |
Series
|
exactly. |
Raises:
| Type | Description |
|---|---|
CalculationError
|
If any period return is -100% or worse. |
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. |
attribute ¶
attribute(
period_returns: Series,
weights: DataFrame,
asset_returns: DataFrame,
cap_drag: float | None = None,
cost_drag: float | None = None,
comparator: Series | 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
|
comparator
|
Series | None
|
A comparator's return per period. When given, weights are active weights (the portfolio's less the comparator's), and the return explained is the compounded period_returns less the compounded comparator's. |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
AttributionResult |
AttributionResult
|
The decomposition, with contributions summing to the |
AttributionResult
|
compounded total return (or active return) and a residual reported |
|
AttributionResult
|
separately. |
Raises:
| Type | Description |
|---|---|
CalculationError
|
If the inputs cannot be aligned, or a period wipes out the index. |
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. |
cap_drag ¶
cap_drag(
capped_weights: dict[Timestamp, dict[str, float]],
uncapped_weights: dict[Timestamp, dict[str, float]],
prices: DataFrame,
window: Index | None = None,
) -> 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, from the first rebalance on. Weights drift from the rebalance before each date, so the prices have to reach back to it even when window starts later. |
required |
window
|
Index | None
|
The dates to measure over, as for :func: |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
float |
float
|
Capped total return minus uncapped total return. |
simple_performance_attribution ¶
simple_performance_attribution(
portfolio_returns: Series, benchmark_returns: Series
) -> dict[str, float]
Total return difference between a portfolio and a benchmark.
Deprecated, and to be removed in 0.11.0: use
beacon.analysis.performance.formulas.compounded_return on each series,
or a run's Attribution.active.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
portfolio_returns
|
Series
|
Portfolio periodic returns. |
required |
benchmark_returns
|
Series
|
Benchmark periodic returns, same length. |
required |
Each total return compounds its own periodic returns,
prod(1 + r) - 1; the two series are not aligned by date.
Returns:
| Name | Type | Description |
|---|---|---|
dict |
dict[str, float]
|
total_portfolio_return, total_benchmark_return and |
dict[str, float]
|
active_return (portfolio minus benchmark). |
Raises:
| Type | Description |
|---|---|
TypeError
|
If either input is not a Series. |
ValueError
|
If the lengths differ or the inputs are empty. |
currency ¶
Currency attribution: how much of a book's return came from its names' own markets and how much from the exchange rates into the book's currency.
A name's daily return in the book's currency r is its return in its own
l and the move of its currency x: 1 + r = (1 + l)(1 + x), so
r = l + x + l x
and each name contributes its weight at the close before times each part: the local return, the currency effect, and the cross term on a line of its own. Local returns are worked out from the run's own closes and corporate actions in each name's currency, as the book's are.
local_returns ¶
Each name's daily total return in its own currency, on the days of the run's name returns.
currency_parts ¶
Each name's daily contribution from each part: its weight at the close before times its local return, its currency effect and the cross term.
by_currency ¶
Each part's daily contributions added up by the names' currencies.
effects ¶
A return split into named effects each day (local, currency and cross term; market, factors and specific), linked over the days so the effects add up to the compounded return, with what they leave unexplained as the residual.
A book's own return links by Carino's factor ln(1 + R) / R; a return over
a comparator by the active factor (ln(1 + R) - ln(1 + R^b)) / (R - R^b),
as Brinson-Fachler links (beacon.analysis.attribution.brinson).
EffectsResult
dataclass
¶
EffectsResult(
start: str,
end: str,
periods: int,
total_return: float,
effects: Series,
explained: float,
residual: float,
table: DataFrame,
)
A return by effect, linked over the days.
Attributes:
| Name | Type | Description |
|---|---|---|
start |
str
|
The first day attributed. |
end |
str
|
The last. |
periods |
int
|
The days. |
total_return |
float
|
The return explained: a book's compounded return, or the fund's less a comparator's. |
effects |
Series
|
Each effect, linked, largest first. |
explained |
float
|
What the effects add up to. |
residual |
float
|
The return less what the effects explain. |
table |
DataFrame
|
By group where one was asked for (each currency, say), each effect linked; else empty. |
reconciles ¶
Whether the effects account for the return.
linked_effects ¶
linked_effects(
daily: DataFrame,
ours: Series,
theirs: Series | None = None,
table: dict[str, DataFrame] | None = None,
) -> EffectsResult
Each of daily's effects (days by effect) linked over its days, as a share of ours' compounded return or, with theirs, of the return over it; table's groups (effect -> days by group) linked the same way.
groups ¶
Which group each name was in on each day, and a table's columns added up by it, for attribution by group.
A name's weight at a close earns the next day's return, so a day's contributions belong to the groups the names were in at the close before: the groups of the last rebalance before the day. Memberships change only at rebalances, so the days sharing one are grouped at once.
memberships ¶
Each rebalance's group of every name by field, by date.
Raises:
| Type | Description |
|---|---|
KeyError
|
If the run kept no such field. |
by_group ¶
table's columns (names) added up by group, each day's by the groups
of the last rebalance before it, or of the first for a day at or before
it; Unknown for names with none.
implementation ¶
The implementation breakdown: where a fund's tracking difference came from, day by day from the run's own records, linked over the run.
Each day, with V the base the fund's return per unit is measured on (the
NAV at the close before, grown by any units dealt that day: today's units at
yesterday's NAV per unit), w and b the fund's and the index's
weights in the names at the close before (the fund's scaled onto V, so
money dealt that day is held as cash), c = 1 - sum w the rest of the
fund (its cash) and r the names' total returns:
- cap and screens:
sum g^R r, whereg^R = w^R - (1 - c^R) b^Ris the gap between the fund's invested weights and the index's at the last rebalance: what the cap, the screens, the smallest position and rounding left the fund holding differently; - drift:
sum (g - g^R) r, how that gap has moved since, as flows, dividends and the cash band traded the fund and not the index; - cash drag:
-c sum b rplus the interest earned overV: the return the cash gave up against the index, less what it earned; - equitisation: the futures' profit and loss before their costs, over
V, which wins back the cash drag; - fixed costs, market impact, futures rolls and futures
trading: each day's cost over
V; - fees: the management fee charged over
V; - withholding tax: the tax withheld from dividends over
V; - creation fees: what dealing investors paid into the fund over
V; - dividend treatment:
sum b rless the index's own return: the dividends a price index leaves out, or a total return index's own timing.
They add up to the fund's return from its names, cash and futures, less its costs and fees, less the index's; what else moved the NAV (a currency hedge, trades dealt away from the close) is the residual.
daily_parts ¶
daily_parts(
run: RunData,
fund: DataFrame,
index: DataFrame,
name_returns: DataFrame,
index_returns: Series,
days: DatetimeIndex,
) -> pd.DataFrame
Each day's parts of the tracking difference, by day and part: from the run's first day, whose close before is its starting capital all in cash, or else from the second of days.
model ¶
Factor attribution: how much of a book's return its factor exposures explain,
from the run's factors (beacon.analysis.risk.factors).
Holdings-based. Each day every name's return is the market's, plus its exposures at the last rebalance before the day times the factor returns, plus what is its own:
r_i = f_market + sum_k z_ik f_k + e_i
with e_i defined as what is left, so the split is exact. A book, holding
w at the close before, earns (sum w) f_market from the market,
X_k f_k from each factor (X_k = sum w_i z_ik) and sum w_i e_i of
its own: the specific return.
Returns-based. The book's daily return over the risk-free rate,
regressed on the factor returns over the window (alpha + sum beta_k f_k +
e), splits into the risk-free return, the alpha, each beta times its
factor's return, and the regression's residual.
On the active return the same holds on the active weights, or the active return's regression.
holdings_parts ¶
holdings_parts(
factors: FactorAnalytics,
lagged: DataFrame,
name_returns: DataFrame,
) -> pd.DataFrame
Each day's market, factor and specific contributions of a book held at lagged (its weights at the close before), by day and part.
Worked out a stretch between rebalances at a time, over which the exposures are fixed, as matrix products over its days.
returns_parts ¶
returns_parts(
target: Series,
factor_returns: DataFrame,
rate: float,
periods_per_year: int,
) -> pd.DataFrame
Each day's risk-free, alpha, factor and residual parts of target (a book's daily return), from its regression on the factor returns.
daily_rate ¶
The risk-free rate a day.
schedule ¶
Contributions of an index's names over a window, from its weights at each rebalance and its names' prices, with what its cap and its costs took.
The weights drift from each rebalance with prices, so a rebalance before the window still sets the weights inside it: prices should cover the whole history, and the window is chosen afterwards from the drifted weights' days.
attribute_schedule ¶
attribute_schedule(
capped: dict[Timestamp, dict[str, float]],
uncapped: dict[Timestamp, dict[str, float]],
prices: DataFrame,
start: str | None,
end: str | None,
total_costs: float = 0.0,
initial_capital: float = 0.0,
) -> AttributionResult
Each name's contribution to the index's return between start and end.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
capped
|
dict[Timestamp, dict[str, float]]
|
The weights set at each rebalance, as applied. |
required |
uncapped
|
dict[Timestamp, dict[str, float]]
|
The same before the cap; equal to capped when nothing was capped, and then no cap drag is given. |
required |
prices
|
DataFrame
|
Every name's prices, names on the columns, in the index's currency. |
required |
start
|
str | None
|
The window's first date; the first priced day when None. |
required |
end
|
str | None
|
Its last; the last priced day when None. |
required |
total_costs
|
float
|
Transaction costs paid; no cost drag is given at 0. |
0.0
|
initial_capital
|
float
|
What the costs are measured against. |
0.0
|
Raises:
| Type | Description |
|---|---|
DataNotFoundError
|
If the window has no days. A total return of zero over an empty window would read as a flat index rather than as a question that could not be answered. |
class_panels ¶
The tables a run with share classes keeps of each class: its days, its flows
and its hedge's trades, a row each, with the class's name in CLASS.
Amounts are in the class's own currency, as its book records them, except
BOOK_NET_ASSETS and the hedge's CARRY and COST, which are in the
book's; a day's net assets in both give the rate between them.
class_tables ¶
The three tables, by name; none for a run without share classes.
concentration ¶
Concentration and drift measures for a set of index or portfolio weights.
Two questions an index owner asks constantly: how concentrated is this thing, and how far has it wandered from its targets. Weights are passed as a mapping of asset id to weight.
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. Drifts equal to within floating-point noise count as tied, and a tie goes to the asset id that sorts first. |
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. |
herfindahl_index ¶
Sum of squared weights.
Logs a warning when the weights do not sum to 1, since the measure then scales with the square of their sum.
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. |
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. |
concentration ¶
Summarise how concentrated a set of weights is.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
weights
|
dict[str, float]
|
Mapping of asset id to weight. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
ConcentrationMetrics |
ConcentrationMetrics
|
The summary. An empty mapping yields zeros and a |
ConcentrationMetrics
|
None largest asset rather than raising, since "no positions" is a |
|
ConcentrationMetrics
|
legitimate state for an index between its inception and base date. |
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. |
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. |
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. |
data ¶
Everything a run's analysis reads, from a finished run or a stored one.
data = RunData.from_result(result)
data.write(folder)
again = RunData.from_record(folder)
Every analysis of a run is built from a RunData, so it works the same on a
run just finished and on one stored months ago: the stored one is read back
exactly as it was written, whatever has happened to the data store since.
A run stored before its tables were kept loads with what it has, and
missing says, for each table it lacks, why.
Written with a data store's shared folder (beacon.analysis.shared_inputs),
a run keeps its market inputs there, once for every run on that store, and
only the list of pieces it used itself; it reads them back from there, and
says so when the store has since been deleted.
RunData
dataclass
¶
RunData(
currency: str,
returns: Series,
nav: Series,
units: Series,
initial_capital: float,
periods_per_year: int,
risk_free_rate: float,
comparators: Mapping[str, Series] = dict(),
tables: Mapping[str, DataFrame] = dict(),
missing: Mapping[str, str] = dict(),
performance: Series | None = None,
performance_start: float | None = None,
investor_flows: Series | None = None,
warnings: tuple[str, ...] = (),
)
A run, as its analysis reads it.
Attributes:
| Name | Type | Description |
|---|---|---|
currency |
str
|
The book's currency, which every amount is in. |
returns |
Series
|
The fund's daily returns, as its metrics measure them: of a unit when the run had flows, with cash paid out added back. |
nav |
Series
|
The fund's net assets at the end of each day, net of fees owed. |
units |
Series
|
Units outstanding at the end of each day. |
initial_capital |
float
|
What the fund started with. |
periods_per_year |
int
|
How many daily returns make a year. |
risk_free_rate |
float
|
The annual rate excess returns are measured over. |
comparators |
Mapping[str, Series]
|
Each comparator's levels by name: |
tables |
Mapping[str, DataFrame]
|
The run's tables by name ( |
missing |
Mapping[str, str]
|
For each table or series a stored run lacks, why. |
performance |
Series | None
|
The level the fund's total return and drawdown are measured along: its NAV, or, when it had flows or paid cash out, what it started with grown by its returns. None for a run stored before it was kept, whose NAV is used. |
performance_start |
float | None
|
What performance starts from: the initial capital, or a unit's launch price. |
investor_flows |
Series | None
|
The investors' cash flows, dated, from their side (money put in negative, the final NAV positive), for a run with flows; None for any other. |
warnings |
tuple[str, ...]
|
What a stored run says as it opens: that the data store its market inputs came from was deleted, say. |
from_result
classmethod
¶
A finished run's data, with every table it keeps, built the first time any is asked for.
of_returns
classmethod
¶
of_returns(
returns: Series,
periods_per_year: int,
risk_free_rate: float = 0.0,
comparators: Mapping[str, Series] | None = None,
currency: str = "",
) -> RunData
A run known only by its daily returns (the first from its start) and its comparators' levels, for the figures that need nothing more: its headline figures, measured from a start of 1, its comparisons and its figures by period. It has no tables.
latest ¶
The last rows rows of table name. A stored run's daily weights are worked out for those days alone, unless already read whole, and are those rows of the whole table exactly.
write ¶
write(
folder: Path,
shared: SharedInputs | None = None,
store: Mapping[str, Any] | None = None,
) -> None
Keep this run in folder, replacing whatever it held: its tables
and series as beacon.analysis.panels writes them, and the rest in
run.json.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
folder
|
Path
|
Where the run is kept. |
required |
shared
|
SharedInputs | None
|
The shared folder of the data store the run read, which keeps its market inputs instead of folder; None to keep them in folder. |
None
|
store
|
Mapping[str, Any] | None
|
The data store the run read ( |
None
|
from_record
classmethod
¶
A stored run, as it was written.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
folder
|
Path
|
Where the run was written. |
required |
record
|
Mapping[str, Any] | None
|
For a run stored before its data was kept this way, its
JSON record (a backtest record's |
None
|
Raises:
| Type | Description |
|---|---|
ConfigurationError
|
If neither the folder nor record holds the run, or the folder was written by a newer version. |
etf ¶
Analytics for ETFs: tracking difference, tracking error and premium or discount to NAV.
analytics ¶
Analytics specific to Exchange Traded Funds (ETFs): tracking difference, tracking error, and the premium or discount of market price to NAV.
Each is available as a plain function and as a method of ETFAnalytics.
ETFAnalytics ¶
ETF analytics as methods. Each method calls the module-level function of the same name and returns its result unchanged.
Initializes the ETFAnalytics calculator.
calculate_tracking_difference ¶
Cumulative ETF return minus cumulative index return over the period.
See :func:calculate_tracking_difference.
calculate_tracking_error ¶
calculate_tracking_error(
etf_returns: Series,
index_returns: Series,
periods_per_year: int = 252,
) -> float
Annualised standard deviation of the ETF-minus-index return differences.
See :func:calculate_tracking_error.
calculate_premium_discount ¶
Market price divided by NAV, minus one.
See :func:calculate_premium_discount.
calculate_tracking_difference ¶
Calculates the tracking difference between ETF returns and index returns.
Tracking difference is the ETF's cumulative return minus the index's
cumulative return over the whole period, where each cumulative return
compounds its periodic returns: prod(1 + r) - 1. It is not
annualised. Each Series is compounded on its own, so the two are not
aligned by date; NaN returns are skipped.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
etf_returns
|
Series
|
A pandas Series of ETF periodic returns. |
required |
index_returns
|
Series
|
A pandas Series of benchmark index periodic returns. Must be of the same frequency and length as etf_returns. |
required |
Returns:
| Type | Description |
|---|---|
float
|
The tracking difference over the period, as a float (e.g. -0.002 |
float
|
when the ETF returned 0.2 percentage points less than the index). |
Raises:
| Type | Description |
|---|---|
TypeError
|
If either input is not a pandas Series. |
ValueError
|
If the input Series are not of the same length or are empty. |
calculate_tracking_error ¶
calculate_tracking_error(
etf_returns: Series,
index_returns: Series,
periods_per_year: int = 252,
) -> float
Calculates the annualized tracking error between ETF returns and index returns.
Tracking error is the sample standard deviation of the periodic return
differences (ETF minus index), multiplied by sqrt(periods_per_year).
The difference is taken after aligning the two Series on their index, so
they should share the same dates: a date present in only one Series
produces NaN and is skipped, and fewer than two common dates gives NaN.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
etf_returns
|
Series
|
A pandas Series of ETF periodic returns. |
required |
index_returns
|
Series
|
A pandas Series of benchmark index periodic returns. Must be of the same frequency and length as etf_returns. |
required |
periods_per_year
|
int
|
The number of periods in a year (e.g., 252 for daily). |
252
|
Returns:
| Type | Description |
|---|---|
float
|
The annualized tracking error as a float. |
Raises:
| Type | Description |
|---|---|
TypeError
|
If either input is not a pandas Series. |
ValueError
|
If the input Series are not of the same length or are empty, or periods_per_year is not positive. |
calculate_premium_discount ¶
Calculates the premium/discount of an ETF's market price relative to its NAV.
Premium/Discount = (ETF Market Price / NAV) - 1.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
etf_price
|
float
|
The current market price of the ETF. |
required |
nav_price
|
float
|
The current Net Asset Value (NAV) per share of the ETF. |
required |
Returns:
| Type | Description |
|---|---|
float
|
The premium/discount as a float (e.g., 0.01 for 1% premium, -0.005 for 0.5% discount). |
Raises:
| Type | Description |
|---|---|
TypeError
|
If either input is not an int or float. |
ValueError
|
If nav_price is zero. |
holding_replay ¶
The fund's holdings worked out again from its trades.
A holding changes only when the fund trades the name or a split changes its
share count, and the books add each trade to the holding before it, in
order. So the fund's holdings on the days they changed (as
beacon.analysis.holdings.holding_changes gives them) are worked out again
by replaying the trades and the splits in the books' own arithmetic: a split
multiplies the holding first, then each buy adds to it and each sell takes
from it, a holding below 1e-9 ending.
What the replay does not give back to the last bit is kept: a patch of the recorded rows it gets wrong or misses, and the rows it gives that were not recorded (a split on a day nothing was recorded, say). A stored run keeps those and its trades rather than the holdings, and checks on writing that they come back exactly.
replayed ¶
The fund's holdings on the days they changed, from its trades (a run's trades table) and the actions' splits, by day and then name.
pack_holdings ¶
pack_holdings(
changes: DataFrame,
trades: DataFrame,
actions: DataFrame,
) -> dict[str, pd.DataFrame] | None
What keeps changes given the trades and actions: the recorded rows
the replay gets wrong or misses (patch) and the rows it gives that
were not recorded (drops); None when they would not give changes
back exactly.
unpack_holdings ¶
unpack_holdings(
trades: DataFrame,
actions: DataFrame,
kept: Mapping[str, DataFrame],
dtypes: Mapping[str, object] | None = None,
) -> pd.DataFrame
The fund's holdings on the days they changed, from the replay with kept's rows dropped and patched, by day and then name.
holdings ¶
Daily weights worked out again from what was held and the closes.
A book's daily weights repeat information its holdings and the prices already carry: a weight is a holding's value over the day's total, and a holding changes only when the book trades or the index rebalances. So a stored run keeps the holdings on the days they change, and the weights are worked out again from them and the closes, in the same arithmetic as the code that first recorded them.
Wherever a weight worked out this way is not the recorded one to the last bit, the recorded value is kept in a patch and put back, so the weights always come back exactly. The fund's weights need no patch; an index's, recorded at single precision from units that were themselves rounded to single precision, need one for about a quarter of their cells.
wide_of ¶
A long frame as dates (its index column) by name (its columns column) of values, each pair's value as it is, missing where a pair has none; what a pivot gives for pairs that occur once, without its cost.
holding_changes ¶
The days each name's holding changed, from amounts (dates by name, missing where not held).
Returns:
| Type | Description |
|---|---|
DataFrame
|
pd.DataFrame: Indexed by date, with |
DataFrame
|
the day a holding ends) and |
DataFrame
|
its holding began, changed or ended. |
held_amounts ¶
changes as amounts held on every one of dates, by name in names, missing where a name was not held; a holding that began before the first of dates is carried into them.
fund_weights_from ¶
fund_weights_from(
changes: DataFrame,
totals: Series,
prices: DataFrame,
dates: DatetimeIndex,
names: list[str],
) -> pd.DataFrame
The fund's daily weights: each holding's quantity times its price in the book's currency, over the day's total, as the books recorded them.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
changes
|
DataFrame
|
The fund's quantities on the days they changed. |
required |
totals
|
Series
|
The books' total value (holdings and cash) on each day. |
required |
prices
|
DataFrame
|
Each name's close in the book's currency, carried over gaps. |
required |
dates
|
DatetimeIndex
|
The days the books held anything. |
required |
names
|
list[str]
|
The names, in the recorded weights' order. |
required |
index_weights_from ¶
index_weights_from(
changes: DataFrame,
closes: DataFrame,
rates: DataFrame,
dates: DatetimeIndex,
names: list[str],
dtype: str,
) -> pd.DataFrame
An index's daily weights: each holding's units times its close times its rate, over the day's total, in the index's own order of operations and the precision it records them at, then in dtype.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
changes
|
DataFrame
|
The index's units on the days they changed. |
required |
closes
|
DataFrame
|
Each name's close in its own currency, carried over gaps. |
required |
rates
|
DataFrame
|
Each name's rate into the book's currency, by date and name. |
required |
dates
|
DatetimeIndex
|
The days the index held anything. |
required |
names
|
list[str]
|
The names, in the recorded weights' order. |
required |
dtype
|
str
|
The weights table's dtype. |
required |
rate_grid ¶
rate_grid(
currencies: DataFrame,
rates: DataFrame,
dates: DatetimeIndex,
names: list[str],
) -> pd.DataFrame
Each name's rate into the book's currency on each of dates: its currency's kept rate, or 1 for a name in the book's own currency.
patch_of ¶
Every cell where rebuilt is not recorded to the last bit, with the
recorded value: indexed by date, with NAME and WEIGHT.
patched ¶
rebuilt with patch's recorded values put back.
liquidity ¶
Liquidity measures computed from held market data.
Average daily volume (ADV) is the figure a universe table shows for every name.
The trailing window is calendar months, not a day count¶
"ADV 3M" means the last three calendar months, so the window is
as_of - 3 months < date <= as_of. The two differ by several days across a
quarter, and quoting a 63-trading-day average as "3M" makes two vendors
disagree for no reason anybody can see. This matches the trailing-twelve-month
dividend convention in beacon.data.corporate_actions, including the
half-open boundary: a day exactly three months old has rolled out.
Missing data is NaN, not zero¶
A name whose volume is missing on every day in the window gets NaN rather
than 0.0, and a name with no rows in the window is left out of the result.
Zero is a claim (that it traded and nobody bought) and a liquidity screen
reading it would exclude the name for the wrong reason. NaN says the question
has no answer here, and serialises to null.
trailing_start ¶
The first day of the trailing months ending on end.
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, |
Series
|
carries no volume column, or has no rows in the window (the last |
Series
|
case logs a warning). An absent column is a property of the dataset, |
Series
|
not a failure of the request, so it produces no answer rather than |
Series
|
an error. |
packing ¶
A run's market inputs, kept small, and the data source they stand in for.
A run keeps the closes it read, in each name's own currency, rather than the tables worked out from them: a close is quoted to a few decimals, so it packs into a small whole number, where a return or a weight is a full-precision float that barely compresses. The tables are worked out again from these when first asked for, by the same code a finished run used, on the same numbers, so they come back exactly.
- Closes are kept per name as whole numbers at the fewest decimals that hold every one of that name's closes exactly (whole cents for most), with the scale beside them. A name whose closes need more than six decimals is kept as it is.
StoredSourceserves the kept closes' currencies, exchange rates and corporate actions to the code that reads them from a data store.
StoredSource ¶
The parts of a data store a run's kept market inputs stand in for:
each name's currency, the exchange rates on the kept days and the
corporate actions, for the code that reads them from a DataFetcher.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
currencies
|
DataFrame
|
Each name's currency, indexed by name, in a |
required |
rates
|
DataFrame
|
Each currency's rate into the book's currency on the kept days, a column per currency. |
required |
actions
|
DataFrame
|
The kept names' corporate actions. |
required |
book
|
str
|
The book's currency. |
required |
fetch_market_data ¶
fetch_market_data(
identifier: str | list[str],
start_date: str | None = None,
end_date: str | None = None,
columns: list[str] | None = None,
) -> pd.DataFrame
No market data: the kept closes are handed to the calculation directly, so this is never what it reads.
fetch_reference_data ¶
fetch_reference_data(
identifier: str | list[str],
date: str | None = None,
columns: list[str] | None = None,
) -> pd.DataFrame
Each kept name's currency, as reference data gives it.
fx_rates_on ¶
The kept rate from from_currency into the book's currency on days, or None when none was kept.
decimals_of ¶
The fewest decimals that hold every one of values exactly, or None
when more than MAX_DECIMALS would be needed.
pack_closes ¶
closes (dates by name) as the tables that keep them.
Returns:
| Name | Type | Description |
|---|---|---|
dict |
dict[str, DataFrame]
|
|
dict[str, DataFrame]
|
scale; |
|
dict[str, DataFrame]
|
|
unpack_closes ¶
The closes pack_closes kept, exactly as they were, names in order
(or packed names first).
market_inputs ¶
What a run keeps beside its closes so its returns can be worked out again: each name's currency, the rates the conversion read, and the names' corporate actions.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
Any
|
The data the run used. |
required |
closes
|
DataFrame
|
The closes kept, dates by name. |
required |
currency
|
str
|
The book's currency. |
required |
panels ¶
The large tables a run's analysis reads, kept as Parquet files in a folder.
A run's record is a JSON document: its summary and its small series. The tables that grow with the number of names and days, such as each day's weights in every name held, are too large for JSON and slow to read back from it, so they are kept beside the record as Parquet files, one per table, with a small manifest saying what is there.
Each table is wide, dates by name, with a date index. It reads back exactly: the same values, names and dates. A record kept before tables were written has no folder, and reading one gives no tables rather than an error.
run_panels ¶
The tables a finished run keeps beside its record, by name:
fund_weights: the fund's weight in each name held on each day it was recorded, unabridged.name_returns: each day's total return, in the book's currency, of every name the fund held or its index or benchmark held, asbeacon.data.returns.name_returnsmeasures it. Left out, with a warning, when the run kept no data to read them from or they cannot be measured (a currency with no rate into the book's).market: for an ETF, its quote on the exchange each day: NAV per share, premium, spread, market price, bid, ask, and what creating and redeeming cost.cash_band_events: for a fund with a cash band, each time its cash left the band and was brought back, as the run recorded it.class_days,class_flowsandclass_hedges: for a run with share classes, each class's days, flows and hedge trades, asbeacon.analysis.class_panels.class_tablesgives them.futures_tradesandfutures_days: for a fund holding index futures, each trade of them with its cost, and each day's position, price, fair value and index level.trades: every trade, why it was made and its tags, asbeacon.backtest.breakdown.tradesgives them.costs: what the run cost each day, by kind, asbeacon.backtest.breakdown.daily_costsgives it.cash: its cash, income, fees and flows each day, asbeacon.backtest.breakdown.daily_cashgives them.index_weights,optimised_weightsandbenchmark_weights: each comparator's daily weights in its names, for those the run has and that hold names (a benchmark given as a bare level series has none).reference: the grouping fields (sector, industry, region, country, currency, exchange) of every name held, as they stood at each rebalance; seebeacon.analysis.snapshots.characteristics: their market cap, P/E, P/B, dividend yield and momentum at each rebalance, asbeacon.analysis.snapshots.characteristics_snapshotsgives them; left out, with a warning, when they cannot be measured.closes,currencies,ratesandactions: the closes the name returns are worked out from, in each name's own currency, and what converts them and accounts for splits and cash; seebeacon.analysis.packing.traded_values: their average daily traded value at each rebalance and month end, as the liquidity screens measure it (beacon.analysis.snapshots.traded_value_snapshots).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
result
|
BacktestResult
|
The finished run. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
dict |
dict[str, DataFrame]
|
Each table by name, as above. |
name_returns_from ¶
name_returns_from(
tables: Mapping[str, DataFrame],
currency: str,
days: DatetimeIndex,
) -> pd.DataFrame
The name returns, worked out from a run's kept closes, currencies, rates and corporate actions, on its NAV days from the first kept close: a year before the fund's first holding, enough for a risk model's window.
The same calculation as beacon.data.returns.name_returns on the data
store, on the same numbers, so a finished run and a stored one give the
same table to the last bit.
on_sessions ¶
returns, measured on every day a name has a close, as returns from one of days to the next: a day with a close but no session (a holiday's print) is compounded into the session after it, rather than dropped with its move. Days that follow no such day are as they were.
weights_inputs ¶
weights_inputs(
result: BacktestResult, tables: Mapping[str, DataFrame]
) -> dict[str, pd.DataFrame]
What each book's daily weights are worked out again from: the fund's quantities and daily totals, each index's units, and for every book its days, its names and the cells that differ from what was recorded.
weights_from ¶
weights_from(
name: str,
tables: Mapping[str, DataFrame],
currency: str,
dtype: str,
last: int | None = None,
) -> pd.DataFrame
A book's daily weights, worked out again from what was kept, with the recorded cells put back, so they are exactly what was recorded; for its last days alone when given, which are those days of the whole table.
held_names ¶
Every name the fund, its index (both books) or its benchmark held at any point in the run, sorted.
write_panels ¶
Write panels to folder, replacing whatever it held.
The tables are written to a new folder beside it, which then takes its place, so a failure part way leaves the previous tables as they were.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
folder
|
Path
|
Where the tables go. |
required |
panels
|
Mapping[str, DataFrame]
|
Each table by name: dates by name, with a date index. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
dict |
dict[str, Any]
|
The manifest written beside them. |
Raises:
| Type | Description |
|---|---|
ConfigurationError
|
If a table's name is not lower-case letters, digits and underscores, or its columns are not text. |
read_manifest ¶
What folder holds, or None when there is no such folder.
Raises:
| Type | Description |
|---|---|
ConfigurationError
|
If the manifest cannot be read, or was written by a newer version than this one reads. |
read_panels ¶
Read tables back from folder.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
folder
|
Path
|
Where the tables were written. |
required |
names
|
Iterable[str] | None
|
The tables wanted; every one there when None. |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
dict |
dict[str, DataFrame]
|
Each table by name, as it was written. Empty when there is no |
dict[str, DataFrame]
|
folder, as for a record kept before tables were. |
Raises:
| Type | Description |
|---|---|
DataNotFoundError
|
If a table asked for by name is not there. |
ConfigurationError
|
If the folder cannot be read. |
performance ¶
A run's performance: its returns, risk and figures against its comparators.
PerformanceAnalytics measures a run's figures from its RunData, with the
AnalysisSettings they are measured with; RelativeAnalytics its figures
against one comparator; CrisisAnalytics how it held up in crises and
RegimeAnalytics in each kind of market; PortfolioAnalytics what it held.
beacon.analysis.performance.formulas holds the formulas, one definition
each, that every figure is computed with.
AnalysisSettings
dataclass
¶
AnalysisSettings(
start: Timestamp | str | None = None,
end: Timestamp | str | None = None,
periods_per_year: int | None = None,
risk_free_rate: float | None = None,
tracked: str | None = None,
)
How a run's figures are measured. Each setting left as None takes the run's own.
Attributes:
| Name | Type | Description |
|---|---|---|
start |
Timestamp | str | None
|
The first day measured; the run's first when None. A later start measures from the previous day's close. |
end |
Timestamp | str | None
|
The last day measured; the run's last when None. |
periods_per_year |
int | None
|
How many daily returns make a year. |
risk_free_rate |
float | None
|
The annual rate the Sharpe ratio is in excess of. |
tracked |
str | None
|
The comparator tracking is measured against, by name
( |
analytics ¶
A run's performance figures, from its data and the settings it is measured with.
analytics = PerformanceAnalytics(RunData.from_record(folder))
analytics.summary()
Every figure is one of beacon.analysis.performance.formulas, so a finished
run and a stored one give the same figures, and a backtest's summary is
these.
PerformanceAnalytics ¶
A run's performance figures.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
run
|
RunData
|
The run's data. |
required |
settings
|
AnalysisSettings | None
|
What the figures are measured over and against; the run's own when None. |
None
|
before
property
¶
The run's close before the days measured, when they start after the run's first day; None when they start with it.
tracked
property
¶
The levels of the comparator tracking is measured against, or None when the run has none.
headline ¶
The five headline figures: total and annualised return, volatility, Sharpe ratio and maximum drawdown.
Over the whole run, the total return and drawdown run from what the fund started with, so the cost of buying in on the first day counts; over a window, from the close before its first day.
hit_rate_monthly ¶
The share of calendar months with a positive return.
tracking_error ¶
The annualised spread of the daily returns less the tracked comparator's; None without one or with fewer than two days.
tracking_difference ¶
The compounded return less the tracked comparator's; None without one.
information_ratio ¶
The mean daily return less the tracked comparator's, over its spread, annualised; None without one or without active risk.
money_weighted_return ¶
The annual internal rate of return of the investors' money; None for a run without flows, or when no rate exists.
comparison ¶
The run against a comparator, given by name or by its levels, over the days both have: the comparator's own figures and the run's relative to it. None for a comparator the run does not have, or one sharing fewer than two days with it.
periods ¶
The run's figures in each calendar year, quarter or month, and its comparators' in each, by name: the index and the benchmark, of those the run has, unless comparators names others.
Raises:
| Type | Description |
|---|---|
ValueError
|
If the frequency is not |
trailing_returns ¶
The returns over the last 1, 3 and 6 months and 1, 3, 5 and 10
years, for the month, quarter and year to date, and since inception,
by name (1M ... 10Y, MTD, QTD, YTD,
inception). Each is annualised as well when it is longer than a
year. None for a window longer than the run.
Since inception is the headline's total and annualised return, so over the whole run it starts from what the fund started with.
best_and_worst ¶
best_and_worst() -> dict[
str,
tuple[
return_figures.PeriodReturn,
return_figures.PeriodReturn,
]
| None,
]
The best and worst day, week, month, quarter and year, by
frequency (daily ... annual), with their dates and whether
the run covered all of each.
positive_shares ¶
The share of days, months and years with a positive return, by frequency.
gains_and_losses ¶
The average gain and loss over days (or months, ...), and their ratio.
longest_streaks ¶
The longest run of months that all gained, and of months that all lost.
means ¶
The arithmetic and geometric mean daily return, and each a year.
levels ¶
The level the figures are measured along, dated, from its start: over the whole run what the fund started with, then its NAV or a unit's value; over a window, 1 grown by its returns. The start is dated the business day before the first return.
drawdowns ¶
The run's drawdowns: the deepest worst with their peak, trough and recovery dates, the current one, and the figures over all of them. The deepest is the headline's maximum drawdown.
ratios ¶
ratios(
sharpe_benchmark: float = 0.0,
trials: int | None = None,
trial_variance: float | None = None,
) -> dict[str, float | None]
The risk-adjusted ratios beyond the Sharpe ratio, by name.
sortino, kappa_3 (daily), upside_potential and omega
measure return against the shortfall below the risk-free rate;
calmar, martin, sterling and burke against drawdowns;
gain_to_pain against losing months. probabilistic_sharpe is the
probability the true Sharpe ratio is above sharpe_benchmark (a
year); deflated_sharpe the same against the best of trials
strategies whose annual Sharpe ratios vary by trial_variance, None
unless both are given; lo_sharpe the Sharpe ratio corrected for
serial correlation.
distribution ¶
The shape of the daily returns, by name: downside_deviation
(below the risk-free rate) and semi_variance (below the mean),
both annualised; skew and excess_kurtosis; tail_ratio;
serial_correlation by lag in days; and a histogram of bins
bins with the normal distribution to compare.
bootstrap ¶
bootstrap(
samples: int = shape.SAMPLES,
confidence: float = shape.CONFIDENCE,
block: int = 1,
seed: int = shape.SEED,
) -> dict[str, shape.Interval] | None
How far the annualised return and the Sharpe ratio could have come
out otherwise, from runs made by drawing the run's own days again;
see beacon.analysis.performance.distribution.bootstrap.
rolling ¶
Each day's return, volatility, Sharpe ratio, Sortino ratio and maximum drawdown over the window days ending that day, blank until the first full window; with a comparator (by name or levels), its relative return, tracking error, information ratio, beta and correlation too, on the days both have.
rolling_outcomes ¶
rolling_outcomes(
window: int = moving.WINDOWS[1],
comparator: str | Series | None = None,
) -> dict[str, float | None]
Over every full window: the worst, middle and best return, the share that gained, and the share ahead of the comparator when one is given.
monthly_grid ¶
The calendar grid of monthly returns, a row a year, with each
year's: of the fund, of a comparator by name, or of the fund less a
comparator (difference index).
Raises:
| Type | Description |
|---|---|
KeyError
|
If the run has no such comparator. |
period_table ¶
Each year's (or quarter's, month's) return, volatility, Sharpe ratio and maximum drawdown for the fund, with its share of positive days, for its index and benchmark, and the fund's less each's.
trailing_table ¶
The trailing returns (trailing_returns) of the fund, its index
and benchmark and the fund's less each's, a row a window.
month_of_year ¶
The fund's mean return in each calendar month over the years.
summary ¶
The headline figures, the monthly hit rate, and, when the run has them, the tracking error and difference against the tracked comparator and the money-weighted return.
cash_management ¶
How a fund managed its cash: each time the cash left its band and was raised or spent back down to its target, how often and when that happened, what it cost and how close it brought the cash to its target; and every trade made to raise cash or spend it, whatever it was raised or spent for.
An event's gap is how far the cash stood from its target; the share of the gap closed is one less the gap after over the gap before, so 1 is a cash brought exactly to its target.
band_events ¶
Each event as the run recorded it, with the gap to the target before and after, the share of it closed, and the cost in basis points of the value traded.
band_summary ¶
For raises, spend-downs and both: how many, the value traded, the total and mean cost, the cost in basis points of the value traded, and the mean share of the gap closed and gap left, a row each.
band_calendar ¶
How many raises and spend-downs each month (M) or year (Y)
that had one, and what they cost.
cash_trading ¶
Every trade made to raise cash or spend it, by the tag and the reason it was made for (a redemption, a distribution, the band...): how many, the value traded and its cost.
comparisons ¶
A run against a comparator, and its figures by calendar period.
A run is given as its daily returns, the first running from its starting
point (a backtest's initial capital), and a comparator as its levels. Over
the run's whole window the comparator's first day returns nothing, so its
figures run from its level that day, as a run's tracking does; over a later
window both start there. Every figure is one of
beacon.analysis.performance.formulas, so a comparison against a run's own
index agrees with its tracking error and tracking difference.
Figures by period split the same returns into calendar years, quarters or months, each period from the previous period's close and the first from the start, so the periods compound to the whole window's figures.
Comparison
dataclass
¶
Comparison(
start: Timestamp,
end: Timestamp,
observations: int,
level: Series,
own: dict[str, float | None],
relative: dict[str, float | None],
)
A run against one comparator over the window they share.
Attributes:
| Name | Type | Description |
|---|---|---|
start |
Timestamp
|
The window's first day. |
end |
Timestamp
|
Its last day. |
observations |
int
|
Its days. |
level |
Series
|
The comparator's level over it, rebased to 100 on its first day. |
own |
dict[str, float | None]
|
The comparator's figures: total_return, annualised_return, volatility, sharpe_ratio, max_drawdown and hit_rate_monthly. |
relative |
dict[str, float | None]
|
The run against it: relative_return, annualised_relative_return, tracking_error, information_ratio, correlation, beta and hit_rate_monthly. |
Period
dataclass
¶
Period(
period: str,
start: Timestamp,
end: Timestamp,
returns: Series,
figures: dict[str, float],
against: dict[
str, dict[str, dict[str, float | None]] | None
] = dict(),
)
A run's figures in one calendar period.
Attributes:
| Name | Type | Description |
|---|---|---|
period |
str
|
Its name: |
start |
Timestamp
|
Its first day. |
end |
Timestamp
|
Its last day. |
returns |
Series
|
The run's returns in it. |
figures |
dict[str, float]
|
The run's headline figures in it, and the share of its days
with a positive return ( |
against |
dict[str, dict[str, dict[str, float | None]] | None]
|
Each comparator's |
comparison_of ¶
comparison_of(
returns: Series,
comparator_levels: Series,
periods_per_year: int,
risk_free_rate: float = 0.0,
) -> Comparison | None
A run against a comparator over the days both have; None when they share fewer than two.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
returns
|
Series
|
The run's daily returns, the first from its starting point. |
required |
comparator_levels
|
Series
|
The comparator's levels. |
required |
periods_per_year
|
int
|
How many daily returns make a year. |
required |
risk_free_rate
|
float
|
The annual rate Sharpe ratios are in excess of. |
0.0
|
aligned ¶
aligned(
returns: Series,
comparator_levels: Series,
before: Timestamp | None = None,
) -> tuple[pd.Series, pd.Series]
A run's and a comparator's daily returns on the days both have, empty when they share fewer than two: the comparator's first day returns nothing from its level that day, and so, when the comparator starts after the run, does the run's.
For returns over a window that starts after the run, before is the run's close before the window's first day: the comparator's first return is then from its own close on or before it, as the run's is.
periods_of ¶
periods_of(
returns: Series,
frequency: str,
periods_per_year: int,
risk_free_rate: float = 0.0,
comparators: Mapping[str, Series] | None = None,
) -> list[Period]
A run's figures in each calendar period, and each comparator's in it.
Each period's returns are its own days', so the first runs from the run's starting point and each later one from the previous close.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
returns
|
Series
|
The run's daily returns, the first from its starting point. |
required |
frequency
|
str
|
|
required |
periods_per_year
|
int
|
How many daily returns make a year. |
required |
risk_free_rate
|
float
|
The annual rate the Sharpe ratio is in excess of. |
0.0
|
comparators
|
Mapping[str, Series] | None
|
Each comparator's daily returns, by name, its first day's nothing. |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If the frequency is not one of the three. |
period_against ¶
period_against(
ours: Series,
theirs: Series,
periods_per_year: int,
risk_free_rate: float = 0.0,
) -> dict[str, dict[str, float | None]] | None
A comparator's own figures and the run's relative ones over a
period, from both's daily returns in it; None with no common days.
own_figures ¶
own_figures(
returns: Series,
path: Series,
periods_per_year: int,
risk_free_rate: float,
) -> dict[str, float | None]
A series' own figures, from its returns and the level they move.
relative_figures ¶
The run against the comparator, over the same days.
costs ¶
What a run cost: its costs by kind (the fixed cost and the market impact of its trades, the cost of rolling and of trading futures) and the creation fees dealing investors paid into it, in money, in basis points of its assets a year and per unit it traded, over the run, each year and by why it traded.
Creation fees are received by the fund, so they are set against its costs: the net cost is the costs less the fees.
cost_summary ¶
Each kind's total over the run and in basis points of the mean NAV a year, a row a kind.
yearly_costs ¶
Each calendar year's costs by kind, and the net in basis points of the year's mean NAV (for the year's days alone, not annualised), a row a year.
per_traded ¶
The fixed cost, the market impact and both, in basis points of the value traded; None without trading.
by_reason ¶
The trading costs by why the trade was made: the cost, its market impact, the value traded and the cost in basis points of it, a row a reason, largest cost first. They add up to the run's trading costs.
crises ¶
How a run held up in crises: each crisis window's returns, outperformance, drawdowns, beta and alpha, what came after the trough and how long the fund took to recover; and the figures across every crisis.
crises = CrisisAnalytics(RunData.from_record(folder), market=sp500_levels)
crises.crises(), crises.summary()
The windows come from a broad market series by default: each fall of the market deeper than a threshold (15%), from its peak to its trough. Or they are the user's own named episodes, each a start and an end date, instead. A window is measured over the days after its start through its end; one the run covers only part of is marked partial.
Episode
dataclass
¶
A crisis window: a name, its start (the market's peak) and its end (its trough).
Crisis
dataclass
¶
Crisis(
name: str,
start: Timestamp,
end: Timestamp,
partial: bool,
days: int,
fund: float,
comparator: float,
outperformance: float,
fund_drawdown: float,
comparator_drawdown: float,
beta: float,
alpha: float,
after: dict[str, float | None],
recovery_days: int | None,
)
How the fund held up in one crisis.
Attributes:
| Name | Type | Description |
|---|---|---|
name |
str
|
The episode's name, or the market's peak and trough dates. |
start |
Timestamp
|
The window's start (the market's peak). |
end |
Timestamp
|
Its end (the trough). |
partial |
bool
|
Whether the run covered only part of it. |
days |
int
|
The days measured. |
fund |
float
|
The fund's return over it. |
comparator |
float
|
The comparator's. |
outperformance |
float
|
The fund's less the comparator's. |
fund_drawdown |
float
|
The fund's deepest fall inside it. |
comparator_drawdown |
float
|
The comparator's. |
beta |
float
|
The fund's beta to the comparator over it. |
alpha |
float
|
The fund's return beyond what the whole run's beta predicts. |
after |
dict[str, float | None]
|
The fund's and the comparator's returns over the months after
the end, by |
recovery_days |
int | None
|
Trading days from the end until the fund was back at its value at the start; None if it had not been by the run's end. |
CrisisAnalytics ¶
CrisisAnalytics(
run: RunData,
comparator: str | Series = INDEX,
market: str | Series | None = None,
threshold: float = THRESHOLD,
named: Sequence[Episode] | None = None,
settings: AnalysisSettings | None = None,
)
How a run held up in crises, against one comparator.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
run
|
RunData
|
The run's data. |
required |
comparator
|
str | Series
|
What the fund is measured against in each crisis, by name or levels; the run's index by default. |
INDEX
|
market
|
str | Series | None
|
The broad market series the crises are found on, by name of one of the run's comparators or by its levels; the run's benchmark when it has one, else its index. |
None
|
threshold
|
float
|
The market's fall that makes a crisis. |
THRESHOLD
|
named
|
Sequence[Episode] | None
|
The user's own episodes, used instead of the market's falls. |
None
|
settings
|
AnalysisSettings | None
|
What the figures are measured over and with. |
None
|
Raises:
| Type | Description |
|---|---|
KeyError
|
If the run has no comparator of a name given. |
market_crises ¶
Each fall of the market deeper than threshold, peak to trough.
crisis_of ¶
How the fund held up over one episode, on the days the run has; None when the run has none of them.
across ¶
across(
crises: Sequence[Crisis],
ours: Series,
theirs: Series,
run_beta: float,
periods_per_year: int,
) -> dict[str, float | None]
The figures across every crisis: the average outperformance, the share outperformed, crisis capture, and the pooled crisis alpha (the mean daily return beyond the whole run's beta over every crisis day, a year).
dealing ¶
What dealing in a fund cost: each subscription or redemption (each creation or redemption, for an ETF), and each share class's costs.
A flow's cost to the fund is the trading it caused, as fund_figures
attributes it (every trading cost of a day with flows and no rebalance; on a
rebalance day, the flow's share of the day's trading), less what the dealing
investors paid into the fund toward it through the price or a levy. For an
ETF, its cost to the AP is the flow times the quote's cost of creating or
redeeming that day: the AP's fee, and the basket's trading or the creation
fee.
A class's costs are its fees; its hedge's trading costs, less the carry its forwards earned (each forward's locked-in carry, for the share of its term it was held); the trading its own flows caused, less what its dealing investors paid; and its share of the rest of the fund's trading (rebalances, dividends, the cash band, futures rolls), by its share of the fund's assets at the close before. All are in the book's currency, so the classes' trading adds up to the fund's.
dealing_costs ¶
dealing_costs(
trades: DataFrame,
cash: DataFrame,
costs: DataFrame,
market: DataFrame | None = None,
) -> pd.DataFrame
Each day's flow: whether money came in or went out, its size, the trading it caused, what the dealing investors paid, the net cost to the fund and in basis points of the flow, and, for an ETF, its cost to the AP.
dealing_summary ¶
The flows' costs added up by kind, over the run (run) or each
year too (year): how many, their size, the trading they caused, what
dealing investors paid, the net cost and in basis points of the flows,
and the AP's cost.
class_costs ¶
class_costs(
tables: dict[str, DataFrame],
trades: DataFrame,
cash: DataFrame,
costs: DataFrame,
periods_per_year: int,
by: str = "run",
) -> pd.DataFrame
Each class's costs in the book's currency, over the run (run,
with the net in basis points of its mean assets a year) or each year
(year, with the net in basis points of the year's mean assets).
distribution ¶
The shape of a run's daily returns: their spread below a target and below their mean, their skew and tails, how each day's return relates to the days before, a histogram to draw, and, by resampling, how far its annualised return and Sharpe ratio could have come out otherwise.
Histogram
dataclass
¶
The daily returns counted into bins, with the normal distribution of the same mean and spread to compare them with.
Attributes:
| Name | Type | Description |
|---|---|---|
edges |
list[float]
|
The bins' edges, one more than the bins. |
counts |
list[int]
|
The returns in each bin. |
normal |
list[float]
|
The returns a normal distribution would put in each bin. |
Interval
dataclass
¶
A figure and the range the resampled runs put it in.
Attributes:
| Name | Type | Description |
|---|---|---|
value |
float
|
The figure on the run's own returns. |
low |
float
|
The lower end of the interval. |
high |
float
|
The upper end. |
confidence |
float
|
The share of resampled runs between them. |
downside_deviation ¶
The root mean square of the shortfalls below the daily risk-free rate, every day counted, annualised: the Sortino ratio's risk.
semi_variance ¶
The mean square of the shortfalls below the mean return, every day counted, annualised.
skew ¶
The sample skew, bias-corrected as a spreadsheet's is; None with fewer than three returns or no spread.
excess_kurtosis ¶
The sample kurtosis over a normal distribution's, bias-corrected as a spreadsheet's is; None with fewer than four returns or no spread.
tail_ratio ¶
The 95th percentile of the returns over the size of the 5th: how much larger the right tail is than the left.
serial_correlation ¶
Each day's return's correlation with the return lag days before, for lags 1 to lags.
histogram ¶
The daily returns counted into bins bins of equal width.
bootstrap ¶
bootstrap(
returns: Series,
periods_per_year: int,
risk_free_rate: float = 0.0,
samples: int = SAMPLES,
confidence: float = CONFIDENCE,
block: int = 1,
seed: int = SEED,
) -> dict[str, Interval] | None
How far the annualised return and the Sharpe ratio could have come out otherwise: each over samples runs of the same length made by drawing the run's own days again, in blocks of block days to keep any dependence between neighbouring days, with the interval holding confidence of them. The same seed draws the same runs. None with fewer than two blocks' worth of returns.
drawdowns ¶
A run's drawdowns: each fall from a peak, how deep it went, how long it took to reach its trough and to recover, and the figures over all of them.
A drawdown starts on a peak (the level's high so far), reaches its trough (the lowest level before it recovers) and ends when the level first gets back to the peak. One still open on the last day has no recovery date: it is the current drawdown. Lengths are counted in trading days, from the peak.
Drawdown
dataclass
¶
Drawdown(
peak: Timestamp,
trough: Timestamp,
recovery: Timestamp | None,
depth: float,
to_trough: int,
to_recover: int | None,
length: int,
)
One fall from a peak.
Attributes:
| Name | Type | Description |
|---|---|---|
peak |
Timestamp
|
The day of the high it fell from. |
trough |
Timestamp
|
The day of its lowest level. |
recovery |
Timestamp | None
|
The first day the level got back to the peak; None when it had not by the end. |
depth |
float
|
The fall to the trough, as a fraction of the peak (negative). |
to_trough |
int
|
Trading days from the peak to the trough. |
to_recover |
int | None
|
Trading days from the trough to the recovery; None while open. |
length |
int
|
Trading days from the peak to the recovery, or to the last day while open. |
DrawdownFigures
dataclass
¶
DrawdownFigures(
worst: list[Drawdown],
current: float,
current_drawdown: Drawdown | None,
average_depth: float,
average_length: float,
time_below_peak: float,
longest_below_peak: int,
recovery_factor: float | None,
ulcer_index: float,
pain_index: float,
)
The figures over a run's drawdowns.
Attributes:
| Name | Type | Description |
|---|---|---|
worst |
list[Drawdown]
|
The deepest drawdowns, deepest first. |
current |
float
|
How far below its peak the level ended (0 at a high). |
current_drawdown |
Drawdown | None
|
The drawdown open on the last day; None at a high. |
average_depth |
float
|
The mean depth of every drawdown; 0 without any. |
average_length |
float
|
Their mean length in trading days; 0 without any. |
time_below_peak |
float
|
The share of days spent below a peak. |
longest_below_peak |
int
|
The longest drawdown, in trading days. |
recovery_factor |
float | None
|
The total return over the size of the deepest drawdown; None without a drawdown. |
ulcer_index |
float
|
The root mean square of the daily drawdown. |
pain_index |
float
|
The mean size of the daily drawdown. |
episodes ¶
Every drawdown of a dated level series, in order.
drawdown_figures ¶
The figures over a level series' drawdowns.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
levels
|
Series
|
The level, dated, from its start. |
required |
total_return
|
float
|
The return over the whole series, for the recovery factor. |
required |
worst
|
int
|
How many of the deepest drawdowns to list. |
WORST
|
formulas ¶
The formulas behind a run's performance figures: pure functions of returns and levels, one definition each.
Returns are simple daily returns, and a level series is what they move. A
comparator's returns over a run start from its level on the first day, so
its first return is nothing (returns_from_start), as a run's first return
runs from its starting capital. Figures annualise over periods_per_year
returns a year.
A figure that cannot be measured (too few returns) is None, unless its definition says otherwise. The ratios of a mean return to its spread, the Sharpe and information ratios, are the mean daily return over its standard deviation, annualised by the square root of periods_per_year: both halves from the same daily returns, so the ratio does not change with leverage, and times the square root of the years it is the t-statistic of the mean.
returns_from_start ¶
Daily returns of a level series, the first day's being nothing: its returns over a run, measured from its level on the first day.
compounded_return ¶
The return of a run of returns, compounded.
annualised_return ¶
total, earned over periods returns, as a rate a year; 0 over no time.
volatility ¶
The annualised standard deviation of returns; 0 with fewer than two.
daily_rate ¶
The rate a period that compounds to annual_rate over a year.
sharpe_ratio ¶
The mean daily return in excess of the risk-free rate, over its
standard deviation, annualised; 0 with fewer than two returns, or when
they do not vary (NEGLIGIBLE_SPREAD).
The annual risk_free_rate is taken as the daily rate that compounds to
it (daily_rate).
drawdown ¶
How far below its running peak a level series stands each day.
max_drawdown ¶
The largest fall of path from a running peak; 0 for no path.
path_of ¶
The level a run of returns moves, from 1 before its first.
headline_figures ¶
headline_figures(
returns: Series,
total: float,
path: Series,
periods_per_year: int,
risk_free_rate: float = 0.0,
) -> dict[str, float]
The five headline figures of a performance.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
returns
|
Series
|
The daily returns. |
required |
total
|
float
|
The return over the whole performance. |
required |
path
|
Series
|
The level the drawdown is measured along, from its start. |
required |
periods_per_year
|
int
|
How many returns make a year. |
required |
risk_free_rate
|
float
|
The annual rate the Sharpe ratio is in excess of. |
0.0
|
Returns:
| Name | Type | Description |
|---|---|---|
dict |
dict[str, float]
|
total_return, annualised_return, volatility, sharpe_ratio and |
dict[str, float]
|
max_drawdown. |
period_returns ¶
Returns compounded within each calendar year, quarter or month, by
its name (2024, 2024 Q1, 2024-01), in order.
grouped ¶
returns split into calendar periods, each named, in order.
Raises:
| Type | Description |
|---|---|
ValueError
|
If the frequency is not one of |
hit_rate_monthly ¶
The share of calendar months whose compounded return was positive.
relative_hit_rate_monthly ¶
The share of calendar months in which ours compounded to more than theirs, over the same days.
tracking_error ¶
The annualised standard deviation of the active returns (ours less theirs, day by day); None with fewer than two.
tracking_difference ¶
Our compounded return less theirs.
annualised_relative_return ¶
annualised_relative_return(
our_total: float,
their_total: float,
periods: int,
periods_per_year: int,
) -> float
Our annualised growth less theirs, each total earned over periods returns; 0 over no time.
information_ratio ¶
The mean active return over its standard deviation, annualised; None
with fewer than two, or when they do not vary (NEGLIGIBLE_SPREAD), as
there is no active risk to measure it against.
correlation ¶
The correlation of two return series over the same days; 0 with
fewer than two, or when either does not vary (NEGLIGIBLE_SPREAD), as
nothing relates them.
beta ¶
The sensitivity of ours to theirs: their covariance over the
variance of theirs; 0 with fewer than two, or when theirs does not
vary (NEGLIGIBLE_SPREAD), as there is nothing to be sensitive to.
market_return ¶
An exchange investor's return: bought at first_ask, sold at last_bid.
money_weighted_return ¶
The annual rate that discounts an investor's cash flows to zero.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
flows
|
list[tuple[Timestamp, float]]
|
(date, amount) from the investor's side: money put in is negative, money taken out (and the final value) positive. |
required |
Returns:
| Type | Description |
|---|---|
float | None
|
float or None: The internal rate of return a year, over calendar days |
float | None
|
(ACT/365), or None when the flows do not change sign, so no rate |
float | None
|
exists. |
fund_figures ¶
A fund's own figures: what it held looked like (its characteristics against its index's), what it earned (its income), how big it was and how money moved in and out (its AUM and flows), and, for an ETF, how its price on the exchange stood against its NAV.
Characteristics are weighted over the names with a value, the weights scaled to them: market cap and dividend yield as weighted means, P/E and P/B as weighted harmonic means, through earnings and book yields, which a loss or a tiny book does not blow up.
weighted_characteristics ¶
A book's characteristics on one day, from its weights by name and
each name's characteristics (a row a name, IDENTIFIER and a column
each).
characteristics_over_time ¶
characteristics_over_time(
characteristics: DataFrame, books: dict[str, DataFrame]
) -> pd.DataFrame
Each book's characteristics at each date the characteristics were taken (the rebalances), from its weights at the end of that day: a row a date, columns by book and characteristic.
trailing_yields ¶
Each day's distribution yield (what was paid out over the year to it, over that day's NAV) and trailing dividend yield (the dividends the fund became owed over the year to it, gross, over its mean NAV over the year), from a year after the run's start.
dividend_growth ¶
Each calendar year's dividends (gross, as the fund became owed them) and their growth on the year before's.
net_flows ¶
Each month's (M) or year's (Y) net flows (money in less money
out, as dealt) and their share of the AUM at the close before it began.
flow_costs ¶
The cost of trading each day's flows: on a day with flows and no rebalance, every trading cost that day; on a rebalance day, the share of the day's trading costs the flow's size is of what was traded; with the creation fees dealing investors paid set against it. A row a day with flows.
premium_figures ¶
An ETF's premium (its price over its NAV, less one) on the exchange: its mean, its standard deviation, and its widest above and below; and the mean bid-ask spread.
futures_figures ¶
How a fund holding index futures traded them, and the basis risk of holding them.
Trading, by what each trade did (opened, rolled, resized): how many trades, the contracts and notional traded, the cost, the cost per contract and in basis points of the notional, and the notional traded a year over the fund's mean assets.
The basis each day: the futures' price over the level of the index they are on, less one, which splits into the carry (fair value over the level) and the mispricing (the price over fair value).
Basis risk splits the gap between the futures' daily return and the fund's index's into two: the pricing basis, the futures against their own index (carry and any mispricing), and the underlying mismatch, their index against the fund's, which brings tracking error even at fair value with no costs. A day the position rolled is left out of the futures' returns, since its price moved to another contract.
futures_trading ¶
The futures trading by kind (opened, rolled, resized), between rolls, and all of it: the trades, the contracts and notional traded, the cost, per contract and in basis points of the notional, and the notional a year over the mean NAV.
daily_basis ¶
Each day's basis (the price over the level, less one), its carry (fair value over the level) and its mispricing (the price over fair value).
basis_summary ¶
The basis, carry and mispricing in basis points: their mean, standard deviation, and lowest and highest, a row each.
basis_returns ¶
Each day's pricing basis (the futures' return less their index's), underlying mismatch (their index's less the fund's) and both, on days the contract held did not change.
basis_risk ¶
For the pricing basis, the underlying mismatch and both: the mean a year and the tracking error each causes (its daily standard deviation, annualised), and the days measured.
levels ¶
Level series as they are shown: rebased to 100, their daily returns, their returns by calendar year, and several on one axis over the dates they share.
LevelComparison
dataclass
¶
LevelComparison(
window: DatetimeIndex,
levels: dict[str, Series],
total_returns: dict[str, float],
)
Several level series over the dates they all have, each rebased to 100 on the first.
Attributes:
| Name | Type | Description |
|---|---|---|
window |
DatetimeIndex
|
The shared dates. |
levels |
dict[str, Series]
|
Each series over them, rebased. |
total_returns |
dict[str, float]
|
Each series' return over them. |
rebased ¶
series rebased to base at its first value; as it is when empty or when that value is 0.
daily_returns ¶
Each day's return of levels, from the day before; none for the first.
total_return ¶
The last level over the first, less one; 0 when there is none or the first is 0.
headline ¶
The five headline figures of a level series alone; see
formulas.headline_figures.
annual_returns ¶
Return per calendar year, keyed by the year.
Each year runs from the previous year's closing level to its own, so
the product of (1 + r) telescopes to last / first - 1. The first
value is the starting point, not a year of its own: a run that starts on
2 January begins from the capital held on the eve, which may fall in the
year before.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
level
|
Series
|
The level series, indexed by date. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
dict |
dict[str, float]
|
Year (as a string) -> return for that year. |
compared ¶
levels on one axis, over the dates every one of them has.
Intersected rather than clipped to the latest start and earliest end, so a gap in one series drops that date for all of them instead of leaving a hole only one line has.
Raises:
| Type | Description |
|---|---|
DataNotFoundError
|
If they share no date. |
portfolio ¶
What a run held: how many names, and its weight in each group of a grouping field (sector, region, currency, ...) each day, against its index's and its benchmark's.
portfolio = PortfolioAnalytics(RunData.from_record(folder))
portfolio.group_weights("SECTOR")
A name's group on a day is the one it had at the latest rebalance on or
before that day, as the run's reference table recorded it. A name with no
group recorded is in Unknown. A book's group weights add up to its
invested weight each day: the fund's cash is in none of them.
PortfolioAnalytics ¶
What a run held, and in which groups.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
run
|
RunData
|
The run's data. |
required |
weights ¶
A book's daily weights, dates by name: fund, index or
benchmark; None for a book the run did not keep weights for (a
benchmark given as a level series holds no names).
holdings_count ¶
How many names a book held each day.
group_weights ¶
A book's weight in each group of field each day, dates by group, the groups sorted; None for a book without weights.
Raises:
| Type | Description |
|---|---|
KeyError
|
If the run kept no reference data, or none for field. |
active_group_weights ¶
The fund's weight in each group less against's (index or
benchmark), on the days the fund has; None when the comparator
holds no names.
turnover ¶
The fund's buys, sells, number of trades and their mean value, and
its one-way turnover (the lesser of buys and sells) and two-way
turnover (all of it), over its assets: each trading day (day,
with whether it was a rebalance), each rebalance's own trades
(rebalance) or each year (year).
Raises:
| Type | Description |
|---|---|
KeyError
|
If the run kept no trades (one stored before 0.10.0). |
ValueError
|
If by is none of the three. |
trading_by_reason ¶
The fund's trading by why it was made, a row a reason.
costs ¶
The fund's costs by kind (fixed, impact, futures rolls and futures trading), the
creation fees it received and the net: over the run (run), with
each in basis points of its mean NAV a year, or each year
(year), with the net in basis points of the year's mean NAV.
Raises:
| Type | Description |
|---|---|
ValueError
|
If by is neither. |
cost_per_traded ¶
The trading costs in basis points of the value traded.
characteristics ¶
The weighted market cap, P/E, P/B and dividend yield of the fund, its index and its benchmark (when it holds names) at each rebalance: a row a rebalance, columns by book and figure.
income ¶
Each day's distribution yield and trailing dividend yield, over the year to it.
dividend_growth ¶
Each calendar year's dividends and their growth.
net_flows ¶
Each month's (M) or year's (Y) net flows and their share of
the AUM it began with.
flow_costs ¶
The cost of trading each day's flows, less the creation fees received.
etf_premium ¶
An ETF's premium to NAV on the exchange: its mean, volatility and widest each way, and the mean spread; None for another vehicle.
cash_band_events ¶
Each time the cash left its band and was raised or spent back to its target: the cash before and after, the target and band, the gap closed, what was traded and its cost. Empty for a run without a band.
cash_band_summary ¶
The raises, the spend-downs and both: how many, what they traded and cost, and how close they brought the cash to its target.
cash_band_calendar ¶
How many raises and spend-downs each month (M) or year (Y)
had, and their cost.
cash_trading ¶
Every trade made to raise or spend cash, by tag and reason.
dealing_costs ¶
Each day's subscriptions or redemptions (an ETF's creations or redemptions): the trading they caused, what the dealing investors paid toward it, the net cost to the fund, and for an ETF the cost to the AP.
dealing_summary ¶
The flows' costs added up by kind, over the run (run) or each
year (year).
class_costs ¶
Each share class's fees, hedging costs less the carry earned, the
trading its flows caused less what its dealing investors paid, and
its share of the rest of the fund's trading, in the book's currency:
over the run (run) or each year (year).
Raises:
| Type | Description |
|---|---|
KeyError
|
If the run had no share classes. |
futures_trading ¶
The fund's futures trading by why each trade was made, between rolls and all of it: trades, contracts, notional, cost per contract and in basis points, and turnover.
Raises:
| Type | Description |
|---|---|
KeyError
|
If the run held no index futures. |
futures_basis ¶
The futures' basis to their index, its carry and its mispricing: each day (daily), or their mean, volatility and extremes in basis points.
futures_basis_risk ¶
The tracking error the futures bring, split into the pricing basis (against their own index) and the underlying mismatch (their index against the fund's).
ratios ¶
Risk-adjusted ratios beyond the Sharpe ratio: return over the downside (Sortino, Kappa 3, upside potential, Omega), over drawdowns (Calmar, Martin, Sterling, Burke) and over losing months (gain to pain), and the probability that a Sharpe ratio is real (probabilistic and deflated), with the Sharpe ratio corrected for serial correlation (Lo).
Excess returns are over the daily rate that compounds to the risk-free rate, as the Sharpe ratio's are. A ratio over a risk that is not there (no day below the target, no drawdown) is None.
excess_returns ¶
returns less the daily rate that compounds to risk_free_rate.
downside_deviation ¶
The root mean square of the shortfalls below 0, over every day.
sortino ¶
The mean excess return over the downside deviation, annualised.
kappa_3 ¶
The mean daily excess return over the cube root of the third lower moment: Sortino with the shortfalls cubed, which weighs the larger ones more. Daily: a cube root does not annualise by a square root.
upside_potential ¶
The mean gain above 0 over the downside deviation.
omega ¶
The sum of the gains above 0 over the sum of the shortfalls below it.
gain_to_pain ¶
The sum of the monthly returns over the size of the losing months' sum.
calmar ¶
The annualised return over the size of the deepest drawdown.
martin ¶
The annualised mean excess return over the ulcer index.
sterling ¶
The annualised return over the mean size of the deepest drawdowns
(STERLING_DRAWDOWNS of them).
burke ¶
The annualised return over the root of the sum of every drawdown's depth squared.
moments ¶
The skew and kurtosis (not excess kurtosis) of the daily excess returns, from their population moments.
probabilistic_sharpe ¶
probabilistic_sharpe(
excess: Series,
periods_per_year: int,
benchmark: float = 0.0,
) -> float | None
The probability that the true Sharpe ratio is above benchmark (a year), allowing for the skew and fat tails of the daily returns (Bailey and Lopez de Prado). None with fewer than three returns or no spread.
expected_best_sharpe ¶
The largest Sharpe ratio (in the units of variance's) expected from trials independent strategies with no skill, whose Sharpe ratios vary by variance (Bailey and Lopez de Prado).
deflated_sharpe ¶
deflated_sharpe(
excess: Series,
periods_per_year: int,
trials: int | None,
trial_variance: float | None,
) -> float | None
The probabilistic Sharpe ratio against the best Sharpe ratio trials strategies would show by luck, their annual Sharpe ratios varying by trial_variance. None unless both are given and there were at least two trials.
lo_sharpe ¶
The Sharpe ratio a year corrected for the serial correlation of the daily returns (Lo, 2002), which annualising by the square root of the days assumes away. Correlations up to lags days are counted.
regimes ¶
How a run did in each kind of market: months the market rose and fell, calm and volatile stretches, and bear markets and corrections.
regimes = RegimeAnalytics(RunData.from_record(folder), market=sp500_levels)
regimes.regimes()["volatile"]
Each regime is a set of days, found on a broad market series as the crises are; over its days the fund is measured against its comparator. A regime's return compounds its days, as if invested on those days alone, and is annualised from their mean so regimes of different lengths compare.
Regime
dataclass
¶
Regime(
days: int,
fund: float,
comparator: float,
difference: float,
fund_annual: float,
comparator_annual: float,
beta: float,
alpha: float,
)
The fund against its comparator over one regime's days.
Attributes:
| Name | Type | Description |
|---|---|---|
days |
int
|
The regime's days the run had. |
fund |
float
|
The fund's return compounded over them. |
comparator |
float
|
The comparator's. |
difference |
float
|
The fund's less the comparator's. |
fund_annual |
float
|
The fund's mean daily return, a year. |
comparator_annual |
float
|
The comparator's. |
beta |
float
|
The fund's beta to the comparator over them. |
alpha |
float
|
The fund's mean daily return beyond its beta, in excess of the risk-free rate, a year (Jensen's alpha over the regime). |
RegimeAnalytics ¶
RegimeAnalytics(
run: RunData,
comparator: str | Series = INDEX,
market: str | Series | None = None,
settings: AnalysisSettings | None = None,
)
How a run did in each kind of market, against one comparator.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
run
|
RunData
|
The run's data. |
required |
comparator
|
str | Series
|
What the fund is measured against, by name or levels; the run's index by default. |
INDEX
|
market
|
str | Series | None
|
The broad market series the regimes are found on, by name of one of the run's comparators or by its levels; the run's benchmark when it has one, else its index. |
None
|
settings
|
AnalysisSettings | None
|
What the figures are measured over and with. |
None
|
Raises:
| Type | Description |
|---|---|
KeyError
|
If the run has no comparator of a name given. |
up_and_down_months ¶
Which days fall in a month the market rose, and in one it fell, from its daily returns.
calm_and_volatile ¶
calm_and_volatile(
market: Series,
window: int = VOLATILITY_WINDOW,
fifth: float = FIFTH,
) -> tuple[pd.Series, pd.Series]
Which days the market's rolling volatility was in its lowest fifth, and in its highest; days before the first full window are neither.
falls ¶
Which days fall in one of the market's declines, peak to trough, of more than deeper_than (and at most no_deeper_than).
regime_of ¶
regime_of(
ours: Series,
theirs: Series,
days: Series,
daily_rate: float,
periods_per_year: int,
) -> Regime | None
The fund against its comparator over days; None without any.
relative_analytics ¶
A run against one comparator, its index or its benchmark: how much of its return the comparator explains and what is left (alpha, beta, R-squared, and the t-statistics that say whether the rest is more than luck), how it behaved when the comparator rose and fell, how often and by how much it got ahead, and its stretches of falling behind.
relative = RelativeAnalytics(RunData.from_record(folder), "benchmark")
relative.figures()
The two are compared on the days both have, as the run's comparisons are: the comparator's first day returns nothing from its level that day, and so, when the comparator starts after the run, does the run's.
Regression
dataclass
¶
Regression(
alpha: float,
beta: float,
r_squared: float,
alpha_t: float | None,
alpha_p: float | None,
residual_risk: float,
)
The fund's daily excess returns regressed on the comparator's.
Attributes:
| Name | Type | Description |
|---|---|---|
alpha |
float
|
The intercept, a year: the return the comparator does not explain. |
beta |
float
|
The slope: the fund's sensitivity to the comparator. |
r_squared |
float
|
The share of the fund's variance the comparator explains. |
alpha_t |
float | None
|
Alpha over its standard error. |
alpha_p |
float | None
|
The two-sided probability of an alpha this far from 0 by chance. |
residual_risk |
float
|
The spread of what the comparator does not explain, a year. |
YearDifference
dataclass
¶
One calendar year's returns and their difference.
Attributes:
| Name | Type | Description |
|---|---|---|
year |
str
|
The year. |
fund |
float
|
The fund's return in it. |
comparator |
float
|
The comparator's. |
difference |
float
|
The fund's less the comparator's, as a fraction (0.019 is 1.9 percentage points). |
RelativeAnalytics ¶
RelativeAnalytics(
run: RunData,
comparator: str | Series = INDEX,
settings: AnalysisSettings | None = None,
)
A run against one comparator.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
run
|
RunData
|
The run's data. |
required |
comparator
|
str | Series
|
The comparator, by name ( |
INDEX
|
settings
|
AnalysisSettings | None
|
What the figures are measured over and with; the run's own when None. |
None
|
Raises:
| Type | Description |
|---|---|
KeyError
|
If the run has no comparator of that name. |
regression ¶
Jensen's regression of the fund's daily excess returns on the
comparator's; None with fewer than three days. Against a comparator
that does not vary there is nothing to be sensitive to, so the beta
is 0, as beacon.analysis.performance.formulas.beta gives it, and
the alpha is the fund's mean excess return.
treynor ¶
The annual mean excess return per unit of beta; None without a beta.
information_ratio_t ¶
The information ratio's t-statistic, IR times the root of the years, and its two-sided probability by chance.
capture ¶
Up capture, down capture and their ratio, on months: the fund's geometric mean monthly return in the months the comparator rose (fell) over the comparator's in them.
batting_average ¶
The share of months the fund's return beat the comparator's.
relative_levels ¶
The fund's wealth over the comparator's, from 1 the business day before the first day compared.
relative_drawdowns ¶
The drawdowns of the fund's wealth relative to the comparator's: each a stretch of falling behind.
downside_tracking_error ¶
The root mean square of the days the fund fell behind, every day counted, a year.
tracking_difference_by_year ¶
Each calendar year's return less the comparator's, and the standard deviation of those differences (None with fewer than two years).
figures ¶
Every relative figure, by name: today's comparison figures (relative return, tracking error, information ratio, correlation, beta, monthly hit rate) and the rest.
return_figures ¶
A run's return figures: trailing returns and returns to date, its best and worst periods, how often it gained, its average gain and loss, its longest runs of winning and losing months, and its mean returns.
Every figure is worked out from daily returns, the first from the run's starting point. A period's return compounds its days. A trailing return runs from the close on or before the same date that many months or years before the last day; a window the run is too short for has none. Returns to date run from the close before the month, quarter or year began, or from the run's start when it began within it.
WindowReturn
dataclass
¶
WindowReturn(
start: Timestamp,
end: Timestamp,
days: int,
total: float,
annualised: float | None,
)
A return over a window ending on the run's last day.
Attributes:
| Name | Type | Description |
|---|---|---|
start |
Timestamp
|
The window's first day with a return. |
end |
Timestamp
|
Its last day. |
days |
int
|
Its returns. |
total |
float
|
Its compounded return. |
annualised |
float | None
|
The total as a rate a year, for a window longer than a year (3 years and more, or since inception over more than a calendar year); None otherwise, as a shorter return annualised overstates what was earned. |
PeriodReturn
dataclass
¶
One calendar period's compounded return.
Attributes:
| Name | Type | Description |
|---|---|---|
period |
str
|
Its name: a date, |
start |
Timestamp
|
Its first day with a return. |
end |
Timestamp
|
Its last. |
value |
float
|
Its compounded return. |
partial |
bool
|
Whether the run covered only part of it, at its start or end. |
GainsAndLosses
dataclass
¶
GainsAndLosses(
average_gain: float | None,
average_loss: float | None,
ratio: float | None,
gains: int,
losses: int,
)
The average gain and loss over periods of one length.
Attributes:
| Name | Type | Description |
|---|---|---|
average_gain |
float | None
|
The mean of the positive returns; None without any. |
average_loss |
float | None
|
The mean of the negative returns; None without any. |
ratio |
float | None
|
The average gain over the size of the average loss; None without both. |
gains |
int
|
How many returns were positive. |
losses |
int
|
How many were negative. |
Streak
dataclass
¶
A run of consecutive months that all gained, or all lost.
Attributes:
| Name | Type | Description |
|---|---|---|
months |
int
|
Its length. |
start |
str
|
Its first month, |
end |
str
|
Its last month. |
total |
float
|
Its compounded return. |
window_return ¶
window_return(
returns: Series,
start: Timestamp,
periods_per_year: int,
annualise: bool = False,
) -> WindowReturn | None
The return over returns from start (the first day counted) to their end, annualised as well when annualise; None when there are no such days.
longer_than_a_year ¶
Whether a window from start to end spans more than a calendar year.
trailing_returns ¶
The returns over the last 1, 3 and 6 months and 1, 3, 5 and 10 years, by name; None for a window longer than the run.
returns_to_date ¶
The returns for the month, quarter and year to date, by name.
period_returns ¶
Each calendar day, week, month, quarter or year's compounded return, in order, with whether the run covered all of it.
best_and_worst ¶
The best and the worst of frequency's periods; None with none. The first of equals is taken.
positive_share ¶
The share of frequency's periods with a positive return; None with none.
gains_and_losses ¶
The average gain and average loss over frequency's periods, and their ratio.
longest_streaks ¶
The longest run of months that all gained, and of months that all lost; the earlier of equals, and None without such a month. A month with no return breaks both.
means ¶
The arithmetic and geometric mean daily return, and each a year: the arithmetic one times the days a year, the geometric one compounded over them, which is the annualised return.
rolling ¶
A run's figures over a window that moves a day at a time: each day's figure is the one its formula gives over the window ending that day, blank until the first full window. And over every window, the worst, middle and best return, and how often it gained or got ahead.
rolling_figures ¶
rolling_figures(
returns: Series,
window: int,
periods_per_year: int,
risk_free_rate: float = 0.0,
) -> pd.DataFrame
Each day's return, volatility, Sharpe ratio, Sortino ratio and maximum drawdown over the window days ending that day.
rolling_relative ¶
rolling_relative(
ours: Series,
theirs: Series,
window: int,
periods_per_year: int,
) -> pd.DataFrame
Each day's relative return, tracking error, information ratio, beta and correlation against a comparator over the window days ending that day, the two on the same days.
outcomes ¶
outcomes(
window_returns: Series,
relative_returns: Series | None = None,
) -> dict[str, float | None]
Over every full window: the worst, middle and best return, the share of windows that gained, and the share ahead of the comparator.
settings ¶
What a run's analysis is measured over and against.
AnalysisSettings
dataclass
¶
AnalysisSettings(
start: Timestamp | str | None = None,
end: Timestamp | str | None = None,
periods_per_year: int | None = None,
risk_free_rate: float | None = None,
tracked: str | None = None,
)
How a run's figures are measured. Each setting left as None takes the run's own.
Attributes:
| Name | Type | Description |
|---|---|---|
start |
Timestamp | str | None
|
The first day measured; the run's first when None. A later start measures from the previous day's close. |
end |
Timestamp | str | None
|
The last day measured; the run's last when None. |
periods_per_year |
int | None
|
How many daily returns make a year. |
risk_free_rate |
float | None
|
The annual rate the Sharpe ratio is in excess of. |
tracked |
str | None
|
The comparator tracking is measured against, by name
( |
tables ¶
A run's figures laid out as tables: the calendar grid of monthly returns with each year's, the figures for each quarter or year, and the trailing returns, each for the fund, its comparators and the differences.
Differences are the fund's figure less the comparator's, as fractions like the figures themselves (0.019 is 1.9 percentage points). A period's return compounds its days, so a year's months compound to the year, and the years to the whole run.
monthly_grid ¶
Each month's compounded return, a row a year and a column a month,
with the year's own return (to date, for the last) in Year. A month
the run did not cover is blank.
difference_grid ¶
The fund's monthly grid less a comparator's, cell by cell, the two on the same days.
period_table ¶
period_table(
returns: Series,
frequency: str,
periods_per_year: int,
risk_free_rate: float,
comparators: Mapping[str, Series],
) -> pd.DataFrame
Each quarter's or year's figures (return, volatility, Sharpe ratio, maximum drawdown) for the fund, with its share of positive days, and for each comparator (given by its daily returns, its first day's nothing), and the fund's less each comparator's: a row a period, columns by whom and figure.
trailing_table ¶
trailing_table(
fund: Mapping[str, WindowReturn | None],
comparators: Mapping[
str, Mapping[str, WindowReturn | None]
],
) -> pd.DataFrame
The trailing returns as a table: a row a window, the total and annualised return for the fund, each comparator and the fund's less each comparator's. A difference is given only when the two cover the same days: a comparator that starts after the run has a shorter inception.
month_of_year ¶
The mean return of each calendar month over the years, January to December; blank for a month the run never covered.
comparator_returns ¶
comparator_returns(
returns: Series, levels: Mapping[str, Series]
) -> dict[str, tuple[pd.Series, pd.Series]]
For each comparator, the fund's and its daily returns on the days both
have (beacon.analysis.performance.comparisons.aligned); a comparator
sharing fewer than two days is left out.
trading ¶
How much a run traded: its turnover each trading day, at each rebalance and each year, and its trading by why it was made.
One-way turnover is the lesser of what was bought and what was sold, over the fund's assets: the SEC's measure, which money arriving or leaving does not inflate, as it is bought or sold on one side only. Two-way turnover is all of it, bought and sold, over the same assets. A day's are over the NAV at the close before its trades; a year's over its mean daily NAV.
nav_before ¶
The NAV at the close before each day: the day before's, the first's the starting capital.
daily_turnover ¶
Each trading day's buys, sells, number of trades and their mean value, one-way and two-way turnover, and whether it was a rebalance.
rebalance_turnover ¶
Each rebalance's own trades: its buys, sells, number of trades and their mean value, and its one-way and two-way turnover.
yearly_turnover ¶
Each calendar year's buys, sells, number of trades and their mean value, and its one-way and two-way turnover over its mean daily NAV.
by_reason ¶
The run's trading by why it was made: the trades, what was bought and sold, and the share of all it traded, a row a reason, largest first. Each trade has one reason, so the rows add up to the run's trading.
relative ¶
Performance of one level series relative to another.
BacktestResult.get_tracking_error compares a portfolio against the index it
was built to track, which is the right question for a replication mandate. It
is not the only question: a portfolio is also measured against benchmarks it
was never trying to replicate. That comparison needs a general pair of level
series rather than a target IndexResult, which is what lives here.
Alignment is the part that matters. Two series rarely cover exactly the same dates (a benchmark may start earlier, or trade on a holiday the portfolio does not), and silently comparing mismatched observations produces numbers that look fine and mean nothing. Every comparison here uses only the dates both series share.
RelativeMetrics
dataclass
¶
RelativeMetrics(
observations: int,
start: str,
end: str,
total_return: float,
benchmark_return: float,
excess_return: float,
tracking_error: float,
correlation: float,
beta: float,
)
How one level series performed against another.
Attributes:
| Name | Type | Description |
|---|---|---|
observations |
int
|
Aligned dates the comparison used, which may be fewer than either input carried. |
start |
str
|
First aligned date, ISO 8601. |
end |
str
|
Last aligned date, ISO 8601. |
total_return |
float
|
Portfolio return over the aligned window. |
benchmark_return |
float
|
Benchmark return over the same window. |
excess_return |
float
|
Portfolio minus benchmark. Also known as the tracking difference: both names mean the arithmetic difference of two total returns, and each is idiomatic in a different context. |
tracking_error |
float
|
Annualised sample standard deviation of the per-period return differences. |
correlation |
float
|
Correlation of the two return series. 0.0 when either series barely varies (standard deviation at or below 1e-12). |
beta |
float
|
Sensitivity of portfolio returns to benchmark returns: covariance over benchmark variance. 0.0 when the benchmark's returns barely vary. |
align_on_common_window ¶
Restrict two level series to the dates they share.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
portfolio
|
Series
|
Level series of the portfolio. |
required |
benchmark
|
Series
|
Level series of the benchmark. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
tuple |
tuple[Series, Series]
|
The two series over their shared dates, in ascending order. |
Raises:
| Type | Description |
|---|---|
CalculationError
|
If either series is empty, or the two share fewer than MINIMUM_ALIGNED_OBSERVATIONS (3) dates. Comparing series that barely overlap would return a number rather than an error, and nothing downstream would reveal that it rests on two data points. |
relative_metrics ¶
relative_metrics(
portfolio: Series,
benchmark: Series,
periods_per_year: int = PERIODS_PER_YEAR,
) -> RelativeMetrics
Compare a portfolio level series against a benchmark's.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
portfolio
|
Series
|
Level series of the portfolio. Need not be rebased: returns are scale-invariant, so only the shape matters. |
required |
benchmark
|
Series
|
Level series of the benchmark. |
required |
periods_per_year
|
int
|
Annualisation factor for the tracking error. |
PERIODS_PER_YEAR
|
Returns:
| Name | Type | Description |
|---|---|---|
RelativeMetrics |
RelativeMetrics
|
The comparison, computed over the shared window only. |
Raises:
| Type | Description |
|---|---|
CalculationError
|
If the series cannot be aligned, or a level of zero makes a return undefined. |
reports ¶
A run's analysis as reports: the statistics, the reports they are read in, and every choice they make.
catalogue ¶
Every statistic a run's analysis gives, in the order the categories are read.
from beacon.analysis.reports.catalogue import CATALOGUE
CATALOGUE["sharpe_ratio"].label # "Sharpe ratio"
attribution ¶
The statistics of where the fund's return came from: by name and group, against the tracked comparator, by Brinson-Fachler effect, by currency, by factor, and the implementation breakdown of the tracking difference.
build ¶
The shapes most statistics take: a figure of each book's returns, a figure of the fund against each comparator, and tables from one call.
per_book ¶
A statistic of each book's returns: the fund's, each comparator's and each share class's; with active, the fund's less the tracked comparator's too.
per_comparator ¶
A statistic of the fund against each comparator the run has.
memo ¶
build's answer, worked out once for analytics (a book's or a comparator's), so statistics read from the same call share it.
fund_only ¶
A statistic of the fund alone.
frame ¶
table with two-level column and row names joined with /, ready
to send.
performance ¶
The statistics of a run's own returns: returns, risk, drawdowns, the shape of the distribution and the risk-adjusted ratios, each given for the fund, its comparators and its share classes.
portfolio ¶
The statistics of what the fund held and did: its holdings and groups, its trading and costs, its cash and flows, its income, and an ETF's premium and a fund's futures.
relative ¶
The statistics of the fund against its comparators, the spread of its rolling returns, and how it held up in crises and regimes.
statistics ¶
The relative, rolling, crisis and regime statistics, in order.
risk ¶
The statistics of the fund's risk: its factors, its value at risk, where its risk comes from, how concentrated it is, and how fast it could be sold.
The factor, expected-risk and liquidity figures read the data store the run was made from; without it they are unavailable.
context ¶
What a run's statistics are worked out from: the run, the choices the analysis makes, and each analysis of it, built the first time a statistic asks for it.
context = Context(run, options, source)
context.book("index").headline()
context.relative("benchmark").regression()
A statistic asks the context for what it needs; one the run cannot give,
such as a comparator it does not have, raises Unavailable saying why.
Context ¶
Context(
run: RunData,
options: AnalysisOptions | None = None,
source: DataFetcher | None = None,
factors: Sequence[Factor] | None = None,
folder: Path | None = None,
)
A run and every analysis of it a statistic can read.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
run
|
RunData
|
The run. |
required |
options
|
AnalysisOptions | None
|
The choices the analysis makes. |
None
|
source
|
DataFetcher | None
|
The data store the run was made from, which factor, liquidity and risk figures read; None when it is not loaded. |
None
|
factors
|
Sequence[Factor] | None
|
The factors risk and attribution use; the defaults when None. |
None
|
folder
|
Path | None
|
Where a stored run is kept: its factor work is read from there when it was kept for these factors. |
None
|
tracked
cached
property
¶
The comparator relative figures are measured against: the one asked for, else the optimised index when the run solved one, else the index; None when the run has none of them.
classes
cached
property
¶
The run's share classes, by name; none for a run without them.
regimes
cached
property
¶
The run's regimes, against the tracked comparator.
factor_analytics
cached
property
¶
The run's factor exposures, returns and betas: as kept with a stored run for these factors, else worked out when first read.
loss
cached
property
¶
The run's value at risk, expected shortfall and stress windows.
concentration
cached
property
¶
The run's concentration; by risk only when the data store is loaded.
kept ¶
What build gives, worked out once under key, so statistics read from the same call share it.
require_tracked ¶
The tracked comparator, or Unavailable when the run has none.
books ¶
Every book a figure of returns is given for: the fund, each comparator and each share class.
book ¶
The performance of book name: the fund, a comparator, or a
share class (class/<name>).
relative ¶
The fund against comparator: the tracked one when None.
require_source ¶
The data store, or Unavailable when it is not loaded.
keep_work ¶
Keep the factor work this context did with the run in folder, so a later analysis of it does not do it again; False when it did none, or its factors cannot be kept.
definitions ¶
The reports a run's analysis is read in, each at two levels: core, the
statistics shown by default with others offered, and extended, everything
the report covers.
REPORTS["factsheet"].level("core").included
A level lists its statistics in sections, by category, in the order the
categories are read; each statistic is included (shown by default) or
optional (offered, off by default), with the time view it is shown in by
default: point, rolling or both. Everything in a report's
extended view is optional for its core.
Entry
dataclass
¶
One statistic in a report level.
Attributes:
| Name | Type | Description |
|---|---|---|
id |
str
|
The statistic's id. |
state |
str
|
|
view |
str
|
The time view it is shown in by default. |
Section
dataclass
¶
A level's statistics of one category, in order.
Level
dataclass
¶
A report at one level of detail.
entry ¶
statistic_id's entry, or None when the level does not cover it.
Report
dataclass
¶
One report, with its two levels.
level ¶
The report at name (core or extended).
Raises:
| Type | Description |
|---|---|
ValueError
|
If name is neither. |
load_reports ¶
The reports kept in path, in order.
options ¶
Every modelling choice a run's analysis makes, in sections, each with its default, and described well enough for a form to draw.
options = AnalysisOptions(rolling=RollingOptions(window=126))
A section left out takes its defaults. describe gives each section's
settings in the shape GET /backtest/options describes a backtest's.
RollingOptions
dataclass
¶
How rolling figures are measured.
Attributes:
| Name | Type | Description |
|---|---|---|
window |
int
|
The trailing window, in days. |
step |
str | None
|
How often a window ends: every day or every month end; by each statistic's cost when None (month end for the heavy ones). |
ReturnsOptions
dataclass
¶
ReturnsOptions(
worst_drawdowns: int = 5,
gains_frequency: str = "daily",
sharpe_benchmark: float = 0.0,
trials: int | None = None,
trial_variance: float | None = None,
bootstrap_samples: int = 1000,
bootstrap_confidence: float = 0.95,
bootstrap_block: int = 1,
bootstrap_seed: int = 0,
histogram_bins: int = 50,
)
Choices the return, drawdown and distribution figures make.
Attributes:
| Name | Type | Description |
|---|---|---|
worst_drawdowns |
int
|
How many of the deepest drawdowns are listed. |
gains_frequency |
str
|
The returns average gains and losses are measured on. |
sharpe_benchmark |
float
|
The Sharpe ratio the probabilistic Sharpe ratio is the chance of beating. |
trials |
int | None
|
How many strategies were tried, for the deflated Sharpe ratio; the run alone when None. |
trial_variance |
float | None
|
The variance of the tried strategies' Sharpe ratios; the run's own when None. |
bootstrap_samples |
int
|
How many resamples the ranges are drawn from. |
bootstrap_confidence |
float
|
The share of resamples the ranges hold. |
bootstrap_block |
int
|
Consecutive days drawn together. |
bootstrap_seed |
int
|
The random seed, so a range is the same each time. |
histogram_bins |
int
|
How many bins the return histogram has. |
CrisesOptions
dataclass
¶
How crises and regimes are found.
Attributes:
| Name | Type | Description |
|---|---|---|
market |
str | None
|
The comparator crises and regimes are read from: the benchmark when the run has one, else the index, when None. |
threshold |
float
|
The fall from a peak that makes a crisis. |
LossOptions
dataclass
¶
How value at risk and expected shortfall are given.
Attributes:
| Name | Type | Description |
|---|---|---|
levels |
tuple[float, ...]
|
The confidence levels. |
horizons |
tuple[int, ...]
|
The horizons, in days; longer ones scale the day's by the square root of time. |
RiskModelOptions
dataclass
¶
RiskModelOptions(
window: int = 252,
structure: str = "auto",
country_field: str = "COUNTRY_DOMICILE",
minimum_names: int = 10,
sectors: bool = True,
currencies: bool = True,
weekly: bool = True,
weeks: int = 104,
)
How expected risk is worked out.
Attributes:
| Name | Type | Description |
|---|---|---|
window |
int
|
The days of returns before each rebalance the factor model is fitted to, when it is fitted on daily returns. |
structure |
str
|
|
country_field |
str
|
The reference field each name's country is read from; empty for no country factors. |
minimum_names |
int
|
The fewest names a country needs for a factor of its own; one with fewer joins its region's. |
sectors |
bool
|
Whether sectors are factors in the global model. |
currencies |
bool
|
Whether currencies are factors in the global model. |
weekly |
bool
|
Whether the global model is fitted on weekly returns. |
weeks |
int
|
The weeks of returns before each rebalance the weekly model is fitted to. |
settings ¶
These choices as the risk model's settings
(beacon.analysis.risk.model.RiskModelSettings).
LiquidityOptions
dataclass
¶
LiquidityOptions(
participation: float = 0.2,
lookback_days: int = 20,
horizons: tuple[int, ...] = (1, 5, 20),
)
How fast the book could be sold.
Attributes:
| Name | Type | Description |
|---|---|---|
participation |
float
|
The share of a name's daily traded value the book could sell. |
lookback_days |
int
|
The days a name's average traded value is taken over. |
horizons |
tuple[int, ...]
|
The days the sellable share is given at. |
AttributionOptions
dataclass
¶
How returns are attributed.
Attributes:
| Name | Type | Description |
|---|---|---|
grouping |
str
|
The reference field names are grouped by. |
frequency |
str
|
The periods attribution is also given by: |
AnalysisOptions
dataclass
¶
AnalysisOptions(
window: AnalysisSettings = AnalysisSettings(),
rolling: RollingOptions = RollingOptions(),
returns: ReturnsOptions = ReturnsOptions(),
crises: CrisesOptions = CrisesOptions(),
value_at_risk: LossOptions = LossOptions(),
risk_model: RiskModelOptions = RiskModelOptions(),
liquidity: LiquidityOptions = LiquidityOptions(),
attribution: AttributionOptions = AttributionOptions(),
)
Every choice a run's analysis makes, by section.
Described
dataclass
¶
Described(
label: str,
help: str,
unit: str | None = None,
choices: tuple[Any, ...] | None = None,
choice_labels: dict[str, str] | None = None,
suggestions: tuple[Any, ...] | None = None,
minimum: float | None = None,
maximum: float | None = None,
items: str | None = None,
items_unit: str | None = None,
)
How one setting is shown: what describe adds to its name, type and
default.
describe ¶
Each section's settings, in order, with their defaults from options (the defaults when None).
rerun ¶
A stored run's analysis asked for again: with statistics added, a time view changed, or other choices.
block = rerun(run, request, source, folder, previous=stored_block)
With the same choices as the block it is given, only the statistics that block lacks (or lacks in a view now wanted) are worked out, and the answer is the whole block, the earlier statistics and reports with the new ones. With other choices everything asked for is worked out afresh. The run's factor work is read from its folder when it was kept for these factors, and kept there when it is worked out.
rerun ¶
rerun(
run: RunData,
request: AnalysisRequest,
source: DataFetcher | None,
folder: Path | None = None,
previous: Mapping[str, Any] | None = None,
progress: Callable[[str], None] | None = None,
) -> dict[str, Any]
The analysis request asks for of a stored run.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
run
|
RunData
|
The run. |
required |
request
|
AnalysisRequest
|
What to give. |
required |
source
|
DataFetcher | None
|
The data store, which factor, liquidity and risk figures read; None when it is not loaded. |
required |
folder
|
Path | None
|
Where the run is kept, which its factor work is read from and kept in. |
None
|
previous
|
Mapping[str, Any] | None
|
The run's block so far, whose statistics are kept when it was made with the same choices. |
None
|
progress
|
Callable[[str], None] | None
|
Told as each part starts. |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
dict |
dict[str, Any]
|
The block, as |
same_choices ¶
Whether previous was made with the choices request makes: the same options and factors.
reusable ¶
The statistics of previous that request wants and need not be worked out again: made with the same choices, in every view now wanted.
rolling ¶
Statistics over each trailing window: the same calculation as each one's figure over the whole window, run on each window in turn, so a rolling figure is the point figure of its window.
tables = rolling_all([CATALOGUE["sharpe_ratio"], CATALOGUE["beta"]], context)
A window ends every day, or every month end; a statistic that is heavy to roll steps month end unless asked otherwise. A window is a number of daily returns, measured from the close before its first.
The most read figures (return, volatility, Sharpe and Sortino ratios, maximum drawdown, against each comparator the tracking difference and error, information ratio, beta and correlation, and value at risk and expected shortfall) roll in one pass over the whole series instead, which the tests check gives the window-by-window figures. Every other statistic is worked out window by window, all of them together, so those that read the same calculation share it.
step_of ¶
How often statistic's windows end: as asked, else by its cost.
step_for ¶
How often statistic's windows end under options: as they ask, else by its cost.
window_ends ¶
The positions in days each full window ends at.
rolling ¶
statistic's figures over each trailing window: dates by figure name, empty when the window is longer than the run.
Raises:
| Type | Description |
|---|---|
ValueError
|
If the statistic cannot roll. |
rolling_all ¶
Each of statistics over each trailing window, by id.
Raises:
| Type | Description |
|---|---|
ValueError
|
If one of them cannot roll. |
window_by_window ¶
window_by_window(
statistics: Sequence[Statistic],
context: Context,
step: str,
) -> dict[str, pd.DataFrame]
statistics worked out on each window in turn, sharing each window's analyses.
rolling_loss ¶
Value at risk and expected shortfall over each trailing window, every window
at once: the windows stacked as rows, each method worked out along them, as
beacon.analysis.risk.var works it out for one window.
rolling_loss ¶
Each day's value at risk (var) or expected shortfall (es) over
the window ending that day, by book, method, level and horizon.
statistics ¶
What a statistic is: its id and label, what it measures, the kind of value it gives, how it is worked out, and whether it can roll.
A statistic gives one of three kinds of value:
figures: named numbers, by book (fund,index,benchmark,class/<name>), by comparator, or by the parts of a compound name joined with/(historical/0.95/1);series: one series, dated or not;table: one table.
One that cannot be worked out for a run raises Unavailable with a short
code and a sentence saying why.
Unavailable ¶
Bases: Exception
Why a statistic cannot be worked out for a run.
Attributes:
| Name | Type | Description |
|---|---|---|
code |
One of |
|
reason |
A plain sentence saying why, fit to show as it is. |
Column
dataclass
¶
One column of a table statistic or a large table.
Attributes:
| Name | Type | Description |
|---|---|---|
name |
str
|
As it is keyed. |
label |
str
|
As it is shown. |
unit |
str | None
|
What it is measured in; None for text. |
labels |
Mapping[str, str] | None
|
For a column of coded text, each value's label. |
Statistic
dataclass
¶
Statistic(
id: str,
label: str,
category: str,
kind: str,
part: str,
description: str,
compute: Callable[[Any], Any],
unit: str | None = None,
decimals: int | None = None,
rolling: str | None = None,
settings: tuple[str, ...] = (),
columns: tuple[Column, ...] = (),
figure_names: tuple[str, ...] = ("book",),
roll: Callable[[Any], DataFrame] | None = None,
signed: tuple[str, ...] = (),
)
One statistic.
Attributes:
| Name | Type | Description |
|---|---|---|
id |
str
|
Its id, which requests and stored choices name it by. |
label |
str
|
Its name, as shown. |
category |
str
|
What it is a figure of, which reports are sectioned by. |
kind |
str
|
|
part |
str
|
The analysis that works it out: |
description |
str
|
One line saying what it measures. |
compute |
Callable[[Any], Any]
|
Works it out from a |
unit |
str | None
|
What its figures (or series) are measured in. |
decimals |
int | None
|
How many decimals it is shown to; by unit when None. |
rolling |
str | None
|
What rolling it costs, when it can roll; None when it cannot. |
settings |
tuple[str, ...]
|
The settings it reads, as |
columns |
tuple[Column, ...]
|
For a table, its columns in order. |
figure_names |
tuple[str, ...]
|
What its figure names are made of, part by part. |
roll |
Callable[[Any], DataFrame] | None
|
Works out its rolling figures itself, for a statistic that cannot be worked out window by window; dates by figure name. |
signed |
tuple[str, ...]
|
The names of its figures that are returns or differences,
whose sign says better or worse: the last part of a figure's,
a column's or a summary figure's name; |
Summarised
dataclass
¶
A table statistic's table, with the figures that sum it up (its totals, its residual).
Catalogue
dataclass
¶
Every statistic, by id, in order.
decimals_of ¶
How many decimals statistic is shown to.
figures_of ¶
value as named figures: a mapping or a dataclass flattened, nested
names joined with /, each figure a number, text or None.
scalar ¶
One figure as plain data: a number, text, true or false, or None for
a missing one; a date as YYYY-MM-DD.
wire ¶
A statistic's value as plain data, ready to send as JSON: figures as a map of
names to numbers, a series as {index, name, data}, a table as
{index, columns, data} (rows), with dates as YYYY-MM-DD and missing
values as None.
request ¶
What a run's analysis should give: the reports wanted, each at a level, with statistics added to or taken from them, the time view each is shown in, and every choice the analysis makes.
Backtest(...).run(definition, start, end,
analysis=AnalysisRequest(reports={"factsheet": "core"},
add={"factsheet": ["sortino_ratio"]}))
The run then carries the analytics block (analyse): each statistic once,
however many reports show it, and the reports as lists of statistic ids. Only
what the statistics asked for need is worked out, so a factsheet's core never
builds the risk model.
AnalysisRequest
dataclass
¶
AnalysisRequest(
reports: Mapping[str, str] = (
lambda: {"factsheet": "core"}
)(),
add: Mapping[str, Sequence[str]] = dict(),
remove: Mapping[str, Sequence[str]] = dict(),
time_views: Mapping[str, Mapping[str, str]] = dict(),
statistics: Sequence[str] = (),
options: AnalysisOptions = AnalysisOptions(),
factors: Sequence[Factor] | None = None,
)
What a run's analysis should give.
Attributes:
| Name | Type | Description |
|---|---|---|
reports |
Mapping[str, str]
|
Each report wanted, by id, with its level: |
add |
Mapping[str, Sequence[str]]
|
By report, optional statistics to show. |
remove |
Mapping[str, Sequence[str]]
|
By report, included statistics not to show. |
time_views |
Mapping[str, Mapping[str, str]]
|
By report, statistic ids with the time view to show
them in: |
statistics |
Sequence[str]
|
Statistics wanted outside any report, over the window. |
options |
AnalysisOptions
|
Every choice the analysis makes; the defaults when left out. |
factors |
Sequence[Factor] | None
|
The factors risk and attribution use; the defaults (size, value, momentum) when None. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If a report, level or statistic is unknown, a statistic added is not optional at the report's level or one taken out is not in it, or a time view is unknown or rolls a statistic that does not roll. |
analyse ¶
analyse(
result: Any,
request: AnalysisRequest,
progress: Callable[[str], None] | None = None,
) -> dict[str, Any]
The analytics block request asks for, of the finished run result.
Each statistic is worked out once, part by part, with a message as each
part starts; one that cannot be worked out for the run is named under
unavailable with a code and a reason.
Returns:
| Name | Type | Description |
|---|---|---|
dict |
dict[str, Any]
|
|
dict[str, Any]
|
by id, with their views), |
|
dict[str, Any]
|
|
|
dict[str, Any]
|
table's |
|
dict[str, Any]
|
|
|
dict[str, Any]
|
can group by) and |
keep_analysis_work ¶
Keep the factor work a finished run's analysis did in folder, where the run is stored, so a later analysis of it does not do it again; False when there was none.
analyse_run ¶
analyse_run(
context: Any,
request: AnalysisRequest,
progress: Callable[[str], None] | None = None,
skip: Collection[str] = (),
) -> dict[str, Any]
analyse on a run's Context, for a stored run as for a finished
one; the statistics in skip are left out, as already worked out.
risk ¶
Risk analysis of a run, and the single-series risk metrics.
beacon.analysis.risk.model chooses and sets up the risk model an analysis
uses, and estimates it once at each rebalance. The single-series metrics
(volatility, Sharpe ratio, maximum drawdown) in
beacon.analysis.risk.single_series, importable from here as before, are
deprecated: use beacon.analysis.performance.formulas.
RiskMetricsCalculator ¶
Risk metrics as methods. Each method calls the module-level function of the same name and returns its result unchanged.
Deprecated: use the functions of beacon.analysis.performance.formulas.
Initializes the RiskMetricsCalculator.
calculate_volatility ¶
Calculates annualized volatility from a price series.
See :func:calculate_volatility.
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. |
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
|
See :func:calculate_sharpe_ratio.
Returns:
| Type | Description |
|---|---|
float
|
The annualized Sharpe Ratio as a float. |
calculate_max_drawdown ¶
Calculates the maximum drawdown from a price series.
See :func:calculate_max_drawdown.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
price_series
|
Series
|
A pandas Series of prices. |
required |
Returns:
| Type | Description |
|---|---|
float
|
The maximum drawdown as a non-positive float (e.g., -0.2 for a |
float
|
20% drawdown). |
calculate_max_drawdown ¶
Calculates the maximum drawdown from a price series.
Deprecated: use beacon.analysis.performance.formulas.max_drawdown,
max_drawdown(prices).
The largest fall from a running peak, as a fraction of that peak.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
price_series
|
Series
|
A pandas Series of prices. |
required |
Returns:
| Type | Description |
|---|---|
float
|
The maximum drawdown as a non-positive float (e.g., -0.2 for a 20% |
float
|
drawdown, 0.0 when the price never falls below a previous peak). |
Raises:
| Type | Description |
|---|---|
TypeError
|
If price_series is not a pandas Series. |
ValueError
|
If price_series is empty or contains non-numeric data. |
calculate_sharpe_ratio ¶
calculate_sharpe_ratio(
returns: Series,
risk_free_rate: float,
periods_per_year: int = 252,
) -> float
Calculates the annualized Sharpe Ratio.
Deprecated: use beacon.analysis.performance.formulas.sharpe_ratio,
sharpe_ratio(returns, periods_per_year, risk_free_rate).
The mean return in excess of the risk-free rate over its sample standard
deviation, multiplied by sqrt(periods_per_year): the Sharpe ratio a
backtest reports, beacon.analysis.performance.formulas.sharpe_ratio.
The annual risk-free rate is taken as the period rate that compounds to
it. 0 when the returns do not vary.
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 |
|---|---|
TypeError
|
If returns is not a pandas Series or risk_free_rate is not a number. |
ValueError
|
If returns is empty or non-numeric, or periods_per_year is not positive. |
calculate_volatility ¶
Calculates annualized volatility from a price series.
Deprecated: use beacon.analysis.performance.formulas.volatility on the
returns, volatility(prices.pct_change().dropna(), periods_per_year).
The sample standard deviation of the simple period returns, multiplied by
sqrt(window). Despite its name, window is the annualisation factor,
not a rolling window: the whole series is used.
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 |
|---|---|
TypeError
|
If price_series is not a pandas Series. |
ValueError
|
If price_series is empty or contains non-numeric data, or window is not positive. |
analytics ¶
A run's risk analytics in one place: its factors, value at risk, risk over time, concentration and liquidity, each built when first asked for and kept.
RiskAnalytics ¶
RiskAnalytics(
run: RunData,
source: DataFetcher,
factors: Sequence[Factor] | None = None,
settings: AnalysisSettings | None = None,
risk_model: RiskModelSettings | None = None,
participation: float = PARTICIPATION,
lookback_days: int = LOOKBACK_DAYS,
)
A run's risk.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
run
|
RunData
|
The run's data. |
required |
source
|
DataFetcher
|
The data the run read, which the factors and traded values are measured from. |
required |
factors
|
Sequence[Factor] | None
|
The factors; |
None
|
settings
|
AnalysisSettings | None
|
The window and conventions the value at risk is measured with. |
None
|
risk_model
|
RiskModelSettings | None
|
How expected risk is worked out; the defaults when None. |
None
|
participation
|
float
|
The share of a name's daily traded value the liquidity figures assume could be sold. |
PARTICIPATION
|
lookback_days
|
int
|
The days a name's average traded value is taken over. |
LOOKBACK_DAYS
|
factors
cached
property
¶
The run's factor exposures, factor returns and betas.
loss
cached
property
¶
The run's value at risk, expected shortfall and stress windows.
over_time
cached
property
¶
The run's expected and measured risk, and where it came from.
concentration
cached
property
¶
The run's concentration by weight, risk and outcome.
liquidity
cached
property
¶
The run's liquidity at each rebalance and month end.
concentration ¶
How concentrated a book is: in its weights, in its risk and in what earned its return.
By weight, over the names held (weight above 0), scaled to add to 1:
- the Herfindahl index
H = sum w_i^2, and its form normalised between an equally weighted book (0) and a single name (1),(H - 1/N) / (1 - 1/N); - the effective number of holdings
1 / H; - the largest weight and the top 5, 10 and 20 together;
- the fewest names making 50%, 80% and 90% of the book;
- optionally, the Gini coefficient from the Lorenz curve (0 equal, toward 1
all in one name) and the entropy
-sum w_i ln w_i, with its effective numberexp(E).
The same figures on group weights (a book's weight in each sector, say) measure how concentrated it is across the groups.
By risk, from each name's share of the expected volatility at each
rebalance (beacon.analysis.risk.contributions): the Herfindahl of the
shares and the effective number of risk sources 1 / sum p_i^2, the top
names' share of the risk, the diversification ratio and the average
correlation; optionally the absorption ratio, the share of the held names'
total variance in the risk model's largest eigenvalues, one per factor.
By outcome, over a window: each name's contribution to the return (its weight at the close before times its return, added up day by day), the top contributors' share of the total, and breadth, the share of the names held that added to it.
Drift is how far the weights have moved since the last rebalance: half the sum of the gaps, the share of the book that would trade to put it back.
The concentration curve takes the names largest first, position by position: the k-th name's weight, the cumulative weight of the top k (as a share of the book), and the concentration of the top k on their own, their Herfindahl and effective number once their weights are scaled to add to 1, with how much adding the k-th changed each.
ConcentrationAnalytics ¶
ConcentrationAnalytics(
run: RunData,
risk: RiskOverTime | None = None,
settings: AnalysisSettings | None = None,
)
A run's concentration by weight, risk and outcome.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
run
|
RunData
|
The run's data. |
required |
risk
|
RiskOverTime | None
|
The run's risk over time, for the figures by risk. |
None
|
settings
|
AnalysisSettings | None
|
The window the figures in crises are measured over, and the conventions the crises are found with. |
None
|
weights ¶
A book's daily weights, by name or, with field, by group.
by_weight ¶
by_weight(
book: str = "fund",
field: str | None = None,
daily: bool = False,
optional: bool = False,
) -> pd.DataFrame
The book's concentration by weight at each rebalance's close, or each day; across groups with field.
against ¶
The fund's figures by weight less other's, on the days both have.
curve ¶
The concentration curve of a book's weights (or, with field, its group weights) at the close of date: names largest first, with cumulative weight and the top k's concentration.
by_period ¶
Each month's (M) or year's (Y) mean daily figures by weight.
in_crises ¶
in_crises(
book: str = "fund",
field: str | None = None,
market: str | Series | None = None,
threshold: float = THRESHOLD,
named: Sequence[Episode] | None = None,
) -> pd.DataFrame
The mean daily figures by weight inside each crisis and on the calm days outside all of them, within the analysis window.
by_risk ¶
The book's concentration by risk at each rebalance the risk model covers, with its diversification ratio and average correlation.
Raises:
| Type | Description |
|---|---|
ValueError
|
If no risk was given. |
absorption_ratio ¶
The share of the total variance of the names held at the rebalance on date in the risk model's components largest eigenvalues: by default one per independent factor, so it reads how much of the risk the portfolio's common drivers absorb.
Over a sample covariance, which has no factors, it counts the largest fifth of the eigenvalues instead.
by_outcome ¶
by_outcome(
start: str | Timestamp | None = None,
end: str | Timestamp | None = None,
top: int = 10,
) -> dict[str, float | None]
The top contributors' share of the fund's return from its names over the window (the whole run by default), and breadth.
weight_figures ¶
The concentration figures of each row of weights (a book's weights by date and name), a row each.
lorenz ¶
The Lorenz curve of the names held: the share of the names, smallest first, against the share of the book they make together.
concentration_curve ¶
The names held, largest first, a row a position from 1: each name, its weight (a share of the names held), the cumulative weight to it, and the top k's Herfindahl and effective number on their own, with the change the k-th made to each.
risk_figures ¶
The concentration of a book's risk, from each name's share of it.
outcome_figures ¶
The top contributors' share of the return and breadth, from each name's contribution to it over a window.
contributions ¶
Where a run's risk came from: expected at each rebalance, from the factor model, and measured over the run, from its returns.
Expected. At each rebalance the factor model (see
beacon.analysis.risk.model) is estimated on the year of daily returns
before it, on the factors' exposures that day, and the book is held at its
weights at that day's close. Its covariance is B F B' + D, so the
book's variance splits exactly into a factor part, x' F x with
x = B' w, and a specific part, w' D w; so does the active variance,
on the active weights. Each name's marginal contribution is (S w)_i / sigma
and its contribution w_i times that, which add up to the volatility;
a group's is the sum of its names'.
The diversification ratio is the weighted sum of the names' volatilities over the book's, and the average correlation the one the weights and volatilities imply:
DR = sum w_i s_i / s, rho = (s^2 - sum w_i^2 s_i^2) / ((sum w_i s_i)^2 - sum w_i^2 s_i^2)
Measured. Each name's daily contribution to the fund's return is its
weight at the close before times its return that day; whatever else moved the
fund (cash, costs, futures) is other. Each one's share of the fund's
measured variance is the covariance of its contributions with the fund's
returns over their variance, and the shares add up to 1 exactly.
RiskOverTime ¶
RiskOverTime(
factors: FactorAnalytics,
window: int = 252,
settings: RiskModelSettings | None = None,
)
A run's expected risk at each rebalance and its measured risk over the run.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
factors
|
FactorAnalytics
|
The run's factors, which the risk model is fitted on. |
required |
window
|
int
|
The sessions of daily returns before each rebalance the model is estimated on. |
252
|
settings
|
RiskModelSettings | None
|
How the factor model is estimated, its structure among
them (see |
None
|
dates ¶
The rebalances with enough returns before them for their model.
weights ¶
A book's weights at the close of date, by name.
contributions ¶
contributions(
date: str | Timestamp,
book: str = "fund",
against: str | None = None,
) -> pd.DataFrame
Each name's weight, marginal contribution, contribution and share of the book's expected volatility (or, against a comparator, of its expected tracking error) at the rebalance on date. A tracking error below a millionth of the book's own volatility is rounding in the weights, and its shares are given as 0.
group_contributions ¶
group_contributions(
date: str | Timestamp,
field: str,
book: str = "fund",
against: str | None = None,
) -> pd.DataFrame
The contributions added up by the group each name was in on the day, by the grouping field field.
expected ¶
The book's expected risk at the rebalance on date: its volatility, split into factor and specific; its tracking error against against, split the same way; its diversification ratio and average correlation; and the share of its weight the model covers.
summary ¶
expected at every rebalance with a year of returns before it.
forecast_check ¶
Each rebalance's expected volatility, and tracking error against
against (none when None), beside the ones measured over the days
until the next rebalance, and the measured over the expected: see
beacon.analysis.risk.forecast.
bias_statistics ¶
The bias statistic of the expected volatility, and of the
expected tracking error against against: the standard deviation of
the daily returns over the forecast in force, 1 when the forecasts
were right on average. See beacon.analysis.risk.forecast.
measured ¶
Each name's (or, with group, each group's) share of the fund's
measured variance over the run, with other for the rest of its
return, and its contribution to its measured volatility a year.
factor_store ¶
A run's factor work kept beside it, so it is worked out once: each rebalance's exposures, the market caps they were centred and fitted with, the groups a small group fell back to, and the daily factor returns.
keep(analytics, folder) # after working them out
restore(analytics, folder) # before, on a stored run
What is kept is keyed by the factors' definitions and the universe they were standardised over, so a run asked about other factors works them out again. Factors that read a Python function cannot be told apart that way and are never kept.
fingerprint ¶
A key for factors standardised over universe; None when one of them reads something that cannot be described, such as a function.
keep ¶
Keep what analytics has worked out under folder; False, keeping nothing, when its factors cannot be keyed or it has worked out nothing.
restore ¶
Give analytics the work kept under folder for its factors; False, leaving it to work them out, when nothing was kept for them.
factors ¶
A run's factors: each name's exposures at each rebalance, the factors' daily returns, and the betas of the fund, its comparators and its active return to them.
Exposures are each factor's scores (see beacon.strategy.Factor) at
each rebalance, for every name the fund, its index or its benchmark held,
standardised over the universe: the index's names that day by default, or
names given.
Factor returns are estimated each day by a cross-sectional regression of the universe's returns on their exposures at the last rebalance before the day, with a market intercept, each name weighted by its market cap:
r_i = f_market + sum_k z_ik f_k + e_i
so a factor's return is what a unit of exposure earned that day with the
others held fixed. A factor naming its own series (returns) takes that
series' daily change instead.
Betas regress a book's daily return over the risk-free rate on the factor returns, with an intercept, by ordinary least squares; the active return (the fund's less its index's) is regressed on the same factors.
Holdings-based exposures are a book's weights times its names' exposures: at each rebalance, from its weights at the close that day, or each day, from that day's weights and the last rebalance's exposures. Cash and names without exposures add nothing. A book's active exposure is its exposure less its index's, or another comparator's.
FactorRegression
dataclass
¶
FactorRegression(
coefficients: DataFrame,
r_squared: float,
observations: int,
periods_per_year: int,
)
A book's returns regressed on the factors.
Attributes:
| Name | Type | Description |
|---|---|---|
coefficients |
DataFrame
|
For the intercept ( |
r_squared |
float
|
The share of the returns' variance the factors explain. |
observations |
int
|
The days fitted on. |
periods_per_year |
int
|
The days in the run's year. |
FactorAnalytics ¶
FactorAnalytics(
run: RunData,
source: DataFetcher,
factors: Sequence[Factor] | None = None,
universe: Sequence[str] | None = None,
)
A run's factor exposures, factor returns and betas.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
run
|
RunData
|
The run's data, with its name returns, weights and reference. |
required |
source
|
DataFetcher
|
The data the run read, which the factors are scored from. |
required |
factors
|
Sequence[Factor] | None
|
The factors; |
None
|
universe
|
Sequence[str] | None
|
The names exposures are standardised over; the index's names at each rebalance when None. |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If two factors share a name, or one is called market. |
exposures ¶
Each held name's exposure to each factor at the rebalance on date: names on the index, factors on the columns.
caps ¶
Each held name's market cap at the rebalance on date, which its exposures were centred with.
fallbacks ¶
For each factor centred within groups, the groups at the rebalance on date too small to centre on, and the wider group used.
universe_on ¶
The names standardised over at the rebalance on date.
factor_returns ¶
Each day's factor returns, the market's first, from the day after the first rebalance.
regression ¶
The book's returns (fund, a comparator's name such as
index, or active) regressed on the factor returns over the
whole run.
rolling_betas ¶
Each factor's beta over the window days to each day.
holdings_exposures ¶
A book's exposure to each factor (fund, index or
benchmark): at each rebalance, or each day when daily.
Raises:
| Type | Description |
|---|---|
KeyError
|
If the run kept no weights for the book. |
active_exposures ¶
The fund's exposures less a comparator's, on the days both have.
rolling_fits ¶
target regressed with an intercept on factors over each trailing window of days: each factor's beta, a row a window's last day.
Every window's cross-products are summed at once and its normal equations solved, rather than each window fitted by least squares; a window whose equations are too near singular to solve that way is fitted on its own.
regressed ¶
target regressed on factors with an intercept, by ordinary least squares.
forecast ¶
How well each rebalance's expected risk forecast what followed.
At each rebalance with a model (RiskOverTime.dates), the expected
volatility and tracking error (RiskOverTime.expected) are set beside the
ones measured over the days after it, up to and including the next
rebalance (or the run's last day): the standard deviation of the daily
returns (or of the fund's less the comparator's) over those days, a year.
Their ratio is above 1 where the model under-forecast.
The bias statistic pools the days: each day's return is divided by the daily risk forecast in force at the close before (the expected figure over the root of the days in a year), and the bias is the standard deviation of those standardised returns. It is 1 when the forecasts were right on average, above 1 when the risk was under-forecast. With few days it is noisy: its standard error is about one over the root of twice the days.
book_returns ¶
A book's daily returns: the fund's, or a comparator's from its levels.
active_returns ¶
A book's daily returns less against's, on the days both have.
forecast_check ¶
forecast_check(
expected: dict[Timestamp, dict[str, float]],
returns: Series,
active: Series | None,
periods_per_year: int,
) -> pd.DataFrame
Each rebalance's expected volatility (and tracking error, when active is given) beside the ones measured until the next rebalance.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
expected
|
dict[Timestamp, dict[str, float]]
|
Each rebalance's expected figures, as
|
required |
returns
|
Series
|
The book's daily returns. |
required |
active
|
Series | None
|
Its daily returns less the comparator's; None for no tracking error. |
required |
periods_per_year
|
int
|
The days in a year the figures are annualised by. |
required |
bias_statistic ¶
The standard deviation of the daily returns after the first forecast, each over the daily forecast in force at the close before; None with fewer than two such days or a forecast of 0.
global_model ¶
The global factor model of a run whose names span more than one country:
the styles, with country, sector and currency factors beside them, fitted on
weekly returns (see beacon.risk.model.global_fit).
At each rebalance the model is fitted on the weeks whole weeks before it, each ending on a Friday, for the names held whose returns cover every one of them. Each name's country is its reference field (its domicile by default) as it stood on the rebalance, its sector likewise, and its currency the one its closes were converted from. Turned off, weekly returns give way to the daily returns of the model's window, and countries, sectors or currencies to no factor at all.
GlobalModels ¶
GlobalModels(
run: RunData,
country_field: str | None,
minimum_names: int,
sectors: bool,
currencies: bool,
weekly: bool,
)
A run's returns and the reference it is grouped by, read once for every rebalance's global model.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
run
|
RunData
|
The run's data, with its name returns, reference, currencies and rates. |
required |
country_field
|
str | None
|
The reference field each name's country is read from, or None for no country factors. |
required |
minimum_names
|
int
|
The fewest names a country needs for a factor of its own; one with fewer joins its region's. |
required |
sectors
|
bool
|
Whether sectors are factors. |
required |
currencies
|
bool
|
Whether currencies are factors. |
required |
weekly
|
bool
|
Whether the model is fitted on weekly returns rather than daily. |
required |
reference_on ¶
The names' reference fields at the rebalance on day, by name.
spans_countries ¶
Whether names were in more than one country on day.
periods_before ¶
The returns of the periods that ended before day: whole weeks, or sessions.
estimate ¶
estimate(
day: Timestamp,
names: list[str],
periods: int,
styles: Loadings | None,
caps: Caps | None,
) -> tuple[FactorRiskModel, list[str]]
The global model at the rebalance on day over its last periods periods, and the names held left out of it for want of returns.
Raises:
| Type | Description |
|---|---|
CalculationError
|
If fewer than periods periods come before day, or no name's returns cover them. |
liquidity ¶
How quickly a book could be sold: each holding's days to sell, and the share of the book sellable within 1, 5 and 20 days, at each rebalance and month end.
A holding worth V is sold at no more than participation of the name's
average daily traded value ADV a day, so it takes
days = V / (participation x ADV)
and within d days the book can sell sum min(V, d x participation x ADV)
of itself. A name with no traded value in the window cannot be sold at all
and is counted apart.
The average daily traded value is the close times the volume, in the book's
currency, over the name's last lookback_days days with a volume, as the
liquidity screens measure it (beacon.analysis.snapshots), read from the
data the run used. Without it, a stored run's own traded values are used,
which the run kept over the screens' 63 days.
LiquidityAnalytics ¶
LiquidityAnalytics(
run: RunData,
source: DataFetcher | None = None,
participation: float = PARTICIPATION,
lookback_days: int = LOOKBACK_DAYS,
)
A run's liquidity at each rebalance and month end.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
run
|
RunData
|
The run's data. |
required |
source
|
DataFetcher | None
|
The data the run read, which traded values are measured from; without it, the run's own stored traded values. |
None
|
participation
|
float
|
The share of a name's daily traded value sold a day. |
PARTICIPATION
|
lookback_days
|
int
|
The days with a volume its traded value is averaged over, when measured from source. |
LOOKBACK_DAYS
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If participation is not above 0 and at most 1. |
traded_values ¶
Each name's average daily traded value on each date, dates by name.
days_to_sell ¶
Each holding's value, average daily traded value and days to sell on date, slowest first; infinite for a name with no traded value.
profile ¶
At each date: the share of the book sellable within each horizon, the days to sell all of it but the names with no traded value, the value-weighted mean days, and the share with no traded value.
model ¶
The risk model an analysis of a run uses: which kind, estimated over how long, and estimated once at each rebalance for every figure that needs it.
Kinds. sample is the sample covariance of the names' daily returns,
shrunk toward a structured target (beacon.risk.estimate_risk_model).
factor is the factor model fitted by cross-sectional regression on the
names' exposures (beacon.risk.fit_factor_model), which splits risk into
factor and specific parts; its exposures at each rebalance are given by the
caller.
Structure. A factor model is global or styles. A global model
adds country, sector and currency factors to the styles and is fitted on
weekly returns (beacon.analysis.risk.global_model); a styles model is the
market and the styles alone, on daily returns. auto, the default, takes
the global model at a rebalance whose names span more than one country and
the styles model otherwise.
Each rebalance's model is estimated on the run's daily name returns over
the window sessions before that day, never including it, for every name
the fund, its index or its benchmark held on it. A name whose returns do not
cover the whole window (one that listed during it, say) is left out of that
rebalance's model rather than shortening the window for every name, and is
reported by uncovered.
RiskModelSettings
dataclass
¶
RiskModelSettings(
kind: str = SAMPLE,
window: int = DEFAULT_WINDOW,
target: str = CONSTANT_CORRELATION,
intensity: float | None = None,
structure: str = AUTO,
country_field: str | None = DOMICILE,
minimum_names: int = MINIMUM_NAMES,
sectors: bool = True,
currencies: bool = True,
weekly: bool = True,
weeks: int = DEFAULT_WEEKS,
)
How an analysis estimates its risk model.
Attributes:
| Name | Type | Description |
|---|---|---|
kind |
str
|
|
window |
int
|
The sessions of daily returns before each rebalance it is estimated on. |
target |
str
|
For a sample model, the structured target it is shrunk
toward; see |
intensity |
float | None
|
For a sample model, the weight on the target, from 0 to 1; None takes the heuristic from the panel's shape. |
structure |
str
|
For a factor model, |
country_field |
str | None
|
For a global model, the reference field each name's
country is read from: its domicile by default, its listing
( |
minimum_names |
int
|
For a global model, the fewest names a country needs for a factor of its own; one with fewer joins its region's. |
sectors |
bool
|
For a global model, whether sectors are factors. |
currencies |
bool
|
For a global model, whether currencies are factors. |
weekly |
bool
|
For a global model, whether it is fitted on weekly returns; on the window's daily returns when not. |
weeks |
int
|
For a global model on weekly returns, the weeks before each rebalance it is estimated on. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the kind, target or structure is unknown, the window is shorter than 20 sessions or the weeks than 8, the minimum names below 1, or the intensity outside 0 to 1. |
RebalanceRiskModels ¶
RebalanceRiskModels(
run: RunData,
settings: RiskModelSettings | None = None,
exposures: Exposures | None = None,
caps: Caps | None = None,
)
A run's risk model at each rebalance, each estimated once when first asked for.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
run
|
RunData
|
The run's data, with its name returns. |
required |
settings
|
RiskModelSettings | None
|
How the models are estimated. |
None
|
exposures
|
Exposures | None
|
For a factor model, each rebalance's style exposures of the names asked about. |
None
|
caps
|
Caps | None
|
For a global model, each rebalance's market caps of the names asked about, which weight its regressions; every name the same when None. |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If a factor model is asked for without exposures. |
at ¶
The model estimated for the rebalance on date.
Raises:
| Type | Description |
|---|---|
KeyError
|
If date is not a rebalance. |
CalculationError
|
If fewer than the window's sessions come before it, or no name's returns cover them. |
uncovered ¶
The names held on the rebalance on date whose returns did not cover the window, and so are not in its model.
is_global ¶
Whether the model at the rebalance on date is the global one.
has_history ¶
Whether enough returns come before the rebalance on date for its model: the weeks of a weekly global model, the window's sessions otherwise.
held ¶
Every name the fund, its index or its benchmark held on date.
read_once ¶
A data source read once for a run's factor analytics: every name's closes over the run, and each feature as of every rebalance, cut to what each rebalance asks for, which is what the source itself would give.
FeaturesReadOnce ¶
A source's features, whose as-of tables for a set of names on a set of dates are read once a field: a read of fewer names on some of the dates is cut from it, which is the same answer, since a name's value on a date reads only that name's rows; every other read goes to the source.
table_as_of ¶
table_as_of(
identifiers: list[str],
field: str,
dates: list[Timestamp],
feature_type: str | None = None,
max_age_days: int | None = MAX_AGE_DAYS,
) -> pd.DataFrame
The table, cut from the one read when it covers the names and dates.
PricesReadOnce ¶
PricesReadOnce(
source: DataFetcher,
names: Sequence[str],
start: Timestamp,
end: Timestamp,
currency: str,
dates: Sequence[Timestamp] = (),
)
A data source whose closes for a set of names, in one currency, over
one window, are read once; a read of fewer names or a shorter window is
cut from it as a read of its own would find them (only the days and
names with a price in it), and every other read goes to the source.
Its features' as-of tables on the given dates are read once a field
too (FeaturesReadOnce).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
DataFetcher
|
The data. |
required |
names
|
Sequence[str]
|
The names read once. |
required |
start
|
Timestamp
|
The window's first day. |
required |
end
|
Timestamp
|
Its last. |
required |
currency
|
str
|
The currency the closes are in. |
required |
dates
|
Sequence[Timestamp]
|
The dates features are read as of once. |
()
|
fetch_prices ¶
fetch_prices(
identifiers: list[str],
start_date: str | None = None,
end_date: str | None = None,
currency: str | None = None,
column: str = "CLOSE",
) -> pd.DataFrame
The closes, cut from the one read when it covers them.
single_series ¶
Risk metrics for a price or return series: annualised volatility, Sharpe ratio and maximum drawdown.
Deprecated, and to be removed in 0.11.0: each figure is in
beacon.analysis.performance.formulas (volatility, sharpe_ratio,
max_drawdown), and a run's are its analytics' headline().
RiskMetricsCalculator ¶
Risk metrics as methods. Each method calls the module-level function of the same name and returns its result unchanged.
Deprecated: use the functions of beacon.analysis.performance.formulas.
Initializes the RiskMetricsCalculator.
calculate_volatility ¶
Calculates annualized volatility from a price series.
See :func:calculate_volatility.
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. |
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
|
See :func:calculate_sharpe_ratio.
Returns:
| Type | Description |
|---|---|
float
|
The annualized Sharpe Ratio as a float. |
calculate_max_drawdown ¶
Calculates the maximum drawdown from a price series.
See :func:calculate_max_drawdown.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
price_series
|
Series
|
A pandas Series of prices. |
required |
Returns:
| Type | Description |
|---|---|
float
|
The maximum drawdown as a non-positive float (e.g., -0.2 for a |
float
|
20% drawdown). |
calculate_volatility ¶
Calculates annualized volatility from a price series.
Deprecated: use beacon.analysis.performance.formulas.volatility on the
returns, volatility(prices.pct_change().dropna(), periods_per_year).
The sample standard deviation of the simple period returns, multiplied by
sqrt(window). Despite its name, window is the annualisation factor,
not a rolling window: the whole series is used.
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 |
|---|---|
TypeError
|
If price_series is not a pandas Series. |
ValueError
|
If price_series is empty or contains non-numeric data, or window is not positive. |
calculate_sharpe_ratio ¶
calculate_sharpe_ratio(
returns: Series,
risk_free_rate: float,
periods_per_year: int = 252,
) -> float
Calculates the annualized Sharpe Ratio.
Deprecated: use beacon.analysis.performance.formulas.sharpe_ratio,
sharpe_ratio(returns, periods_per_year, risk_free_rate).
The mean return in excess of the risk-free rate over its sample standard
deviation, multiplied by sqrt(periods_per_year): the Sharpe ratio a
backtest reports, beacon.analysis.performance.formulas.sharpe_ratio.
The annual risk-free rate is taken as the period rate that compounds to
it. 0 when the returns do not vary.
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 |
|---|---|
TypeError
|
If returns is not a pandas Series or risk_free_rate is not a number. |
ValueError
|
If returns is empty or non-numeric, or periods_per_year is not positive. |
calculate_max_drawdown ¶
Calculates the maximum drawdown from a price series.
Deprecated: use beacon.analysis.performance.formulas.max_drawdown,
max_drawdown(prices).
The largest fall from a running peak, as a fraction of that peak.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
price_series
|
Series
|
A pandas Series of prices. |
required |
Returns:
| Type | Description |
|---|---|
float
|
The maximum drawdown as a non-positive float (e.g., -0.2 for a 20% |
float
|
drawdown, 0.0 when the price never falls below a previous peak). |
Raises:
| Type | Description |
|---|---|
TypeError
|
If price_series is not a pandas Series. |
ValueError
|
If price_series is empty or contains non-numeric data. |
var ¶
Value at risk and expected shortfall, and how each book fared in the run's stress windows.
Both are losses, reported as positive numbers: a one-day 99% value at risk of 0.02 says a day's loss is worse than 2% one day in a hundred.
- Historical: from the returns themselves. Value at risk is minus their
1 - levelquantile (linearly interpolated); expected shortfall is minus the mean of the returns at or below that quantile. - Normal: from their mean and standard deviation, as if normal.
VaR = -(mean + z sd)withzthe normal's1 - levelquantile, andES = -mean + sd phi(z) / (1 - level). - Cornish-Fisher: the normal's, with
zmoved by the returns' skew S and excess kurtosis K,z + (z^2 - 1) S / 6 + (z^3 - 3z) K / 24 - (2z^3 - 5z) S^2 / 36, so fat tails and a lean to one side count. The expansion holds for moderate tails (a skew within about 1 either side and an excess kurtosis up to about 6); far beyond them it can fall below even the normal's figure, and the historical one is the better guide.
Ten days is the square root of ten times one day, the Basel convention.
Stress windows are the run's crises (beacon.analysis.performance.crises:
the market's falls of 15% or more, or named episodes), and each book's return
and deepest fall over each.
RiskOfLoss ¶
A run's value at risk, expected shortfall and stress windows.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
run
|
RunData
|
The run's data. |
required |
settings
|
AnalysisSettings | None
|
The window and conventions figures are measured with. |
None
|
books ¶
The books the run can be measured on: the fund, each comparator, and the active return when it has an index.
returns_of ¶
A book's daily returns over the window: fund, a comparator by
name, or active (the fund's less its index's).
Raises:
| Type | Description |
|---|---|
KeyError
|
If the run has no such book. |
table ¶
table(
book: str = FUND,
levels: Sequence[float] = LEVELS,
horizons: Sequence[int] = HORIZONS,
) -> pd.DataFrame
The book's value at risk and expected shortfall, every method, level and horizon.
stress_windows ¶
stress_windows(
market: str | Series | None = None,
threshold: float = THRESHOLD,
named: Sequence[Episode] | None = None,
) -> pd.DataFrame
Each crisis the run saw: its window, and each book's return and deepest fall over it, a row a crisis.
value_at_risk ¶
value_at_risk(
returns: Series,
level: float = 0.95,
method: str = HISTORICAL,
horizon: int = 1,
) -> float | None
The loss worse than which a horizon-day period falls with
probability 1 - level; None with fewer than 20 returns.
Raises:
| Type | Description |
|---|---|
ValueError
|
If the method is unknown or the level is not between 0 and 1. |
expected_shortfall ¶
expected_shortfall(
returns: Series,
level: float = 0.95,
method: str = HISTORICAL,
horizon: int = 1,
) -> float | None
The mean loss on the periods worse than the value at risk; None with fewer than 20 returns.
Raises:
| Type | Description |
|---|---|
ValueError
|
If the method is unknown or the level is not between 0 and 1. |
risk_of_loss ¶
risk_of_loss(
returns: Series,
levels: Sequence[float] = LEVELS,
horizons: Sequence[int] = HORIZONS,
) -> pd.DataFrame
Every value at risk and expected shortfall of returns: a row a measure and method, a column a level and horizon.
shared_indices ¶
An index's results within a run, kept once for every run that tracked it.
Strategies tracking the same index on the same data store over the same days
work out the same index: its target book's holdings, days, names and patch
(beacon.analysis.holdings), and its optimised book's when it has one. Kept
in the store's shared folder (beacon.analysis.shared_inputs) under
indices/<fingerprint>, they are kept once. The fingerprint is of the
tables themselves, so a changed definition, other data or other days give
another entry, and an older run keeps the index it ran against.
index_tables ¶
The tables of tables that are the index's results.
fingerprint ¶
A fingerprint of tables, their names, columns, dtypes, index and every value.
keep ¶
Keep tables (an index's results) in the shared folder, unless an entry with their fingerprint is there already; the fingerprint.
read ¶
The index's results kept under key.
Raises:
| Type | Description |
|---|---|
KeyError
|
If the folder holds no such entry. |
prune ¶
Remove the entries no run uses, sparing those written in the last
GRACE_SECONDS; those removed.
shared_inputs ¶
A data store's market inputs, kept once for every run on it.
A run keeps the market inputs it read (closes, exchange rates, corporate actions, currencies, traded values, characteristics and reference data), and runs on the same data read mostly the same ones. Kept here, each is kept once:
- Pieces. Each table is cut into pieces of one name's year (a currency's for the exchange rates; a name's whole entry for its currency), each named by a fingerprint of its contents. A run keeps the list of pieces it used. A repeat run on the same data adds nothing; a longer run adds the earlier years; a wider one the new names; a revised price is a new piece, so an older run keeps the numbers it ran on.
- On disk. One folder per data store: a catalogue (table, name, year,
fingerprint, file, first row, rows) and a few files per table. Everything a
run adds goes into one new file per table.
mergerewrites the files as one per table, keeping only the pieces still in use, beside the old ones, and swaps the catalogue in; runs name fingerprints, not files, so a merge does not touch them.
A table read back from its pieces equals the one written, to the last bit: the run keeps its days and names, or the order of its rows, beside its list.
SharedInputs ¶
A data store's shared folder of market inputs.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
folder
|
Path
|
The folder; created when the first pieces are added. |
required |
add ¶
Keep the pieces of tables (those of SHARED) the folder does
not hold yet.
Returns:
| Name | Type | Description |
|---|---|---|
tuple |
DataFrame
|
The pieces the tables are made of ( |
dict[str, Any]
|
|
|
tuple[DataFrame, dict[str, Any]]
|
them (its days and names, or the order of its rows), for the run |
|
tuple[DataFrame, dict[str, Any]]
|
to keep. |
read ¶
table put back together from its pieces fingerprints, as the run that kept layout wrote it.
Raises:
| Type | Description |
|---|---|
KeyError
|
If a piece is not in the folder. |
catalogue ¶
Every piece the folder holds: its table, name, year, fingerprint, file, first row and rows.
merge ¶
Rewrite the folder as one file per table holding only the pieces
in_use (table and fingerprint), and those in files written in the
last GRACE_SECONDS: written beside the old files, the catalogue
swapped in, then the old files removed.
mark_deleted ¶
Mark the folder as kept for a data store that was deleted.
deleted ¶
The deleted data store this folder was kept for, or None.
folder_lock ¶
The lock that runs adding to, merging or reading folder take.
deleted_warning ¶
The warning a run on the deleted data store store opens with.
to_long ¶
frame as rows of a name, a day and its values, and what puts it back: its days and names, or how its rows are ordered, and its dtypes.
from_long ¶
The table to_long was given, from its rows and layout.
shared_runs ¶
The stored runs that keep their market inputs in a data store's shared
folder (beacon.analysis.shared_inputs): which runs read a store, the pieces
they use, what deleting the store leaves, and moving a run that keeps its
inputs itself into the shared layout.
Each function takes the stored runs as a mapping of a name (a strategy, say) to the folder the run was written to.
shared_folder ¶
The shared folder of the data store store_id under root.
store_of ¶
The data store whose shared folder the run in folder keeps its inputs in, or None for a run that keeps them itself.
runs_on ¶
The runs that keep their inputs in store_id's folder.
in_use ¶
Every piece (table and fingerprint) the runs on store_id use.
indices_in_use ¶
Every index result the runs on store_id use.
merge_if_due ¶
Merge shared, keeping the pieces runs use, when it holds too many files, and remove the index results no run uses when it holds too many of those; whether it did either.
forget_store ¶
forget_store(
runs: Mapping[str, Path],
root: Path,
store: Mapping[str, Any],
cascade: bool = False,
) -> dict[str, Any]
What deleting the data store store leaves of its shared folder: with runs on it, kept and marked as for a deleted store, unless cascade, which removes it, as does no run using it.
Returns:
| Name | Type | Description |
|---|---|---|
dict |
dict[str, Any]
|
|
dict[str, Any]
|
under cascade), and |
warnings_of ¶
What the run in folder says as it opens, without opening it: that its data store was deleted, say.
migrate ¶
Move each of runs that keeps its market inputs itself to the shared
layout, under unattributed (the run did not record its store),
checking every table reads back the same first; the runs moved.
snapshots ¶
What a run's names looked like at each rebalance, kept with the run.
A rerun of a run's analysis months later should give the same answer even if the data store has been refreshed since, so what the analysis groups and weighs names by is read as it stood at each rebalance and kept with the run.
Each snapshot is a long table indexed by date, a row per name per rebalance,
with an IDENTIFIER column and a column per field. A name with no record in
force on a date has no row for it. Between rebalances, the latest snapshot on
or before a day is the one in force.
rebalance_dates ¶
Every day the fund or its index rebalanced, within the run, sorted.
reference_snapshots ¶
reference_snapshots(
source: DataFetcher,
names: list[str],
dates: list[Timestamp],
) -> pd.DataFrame
The grouping fields of names as they stood on each of dates.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
DataFetcher
|
The data the run used. |
required |
names
|
list[str]
|
The names to describe. |
required |
dates
|
list[Timestamp]
|
The days to describe them on. |
required |
Returns:
| Type | Description |
|---|---|
DataFrame
|
pd.DataFrame: Indexed by date, with |
DataFrame
|
|
DataFrame
|
name has no value for a field. |
characteristics_snapshots ¶
characteristics_snapshots(
source: DataFetcher,
names: list[str],
dates: list[Timestamp],
currency: str,
market: dict[str, DataFrame] | None = None,
) -> pd.DataFrame
Each name's characteristics as they stood on each of dates.
MARKET_CAP: the close times shares outstanding, in currency.PE_RATIOandPB_RATIO: thepe_ratioandpb_ratiofeatures in force on the date, byDataFetcher.fetch_feature's rules.DIVIDEND_YIELD: ordinary dividends over the trailing year over the close, asDataFetcher.fetch_trailing_dividend_yieldgives it.MOMENTUM: the total return, in the name's own currency, from 252 sessions before the date to 21 sessions before it.
A value the data does not give is missing. A name only has a row while it trades: one whose last close is more than five sessions old by the date has stopped, and is left out.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
DataFetcher
|
The data the run used. |
required |
names
|
list[str]
|
The names to describe. |
required |
dates
|
list[Timestamp]
|
The days to describe them on. |
required |
currency
|
str
|
The currency market caps are in. |
required |
market
|
dict[str, DataFrame] | None
|
|
None
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
pd.DataFrame: Indexed by date, with |
DataFrame
|
|
market_history ¶
market_history(
source: DataFetcher,
names: list[str],
dates: list[Timestamp],
until: Timestamp | None = None,
) -> dict[str, pd.DataFrame]
The market data characteristics_snapshots reads, by column: closes,
shares outstanding and volumes from 400 days before the first of dates
to the last of them, or to until when later, in one read.
liquidity_dates ¶
Each rebalance and the last day of each month of the run, sorted.
traded_value_snapshots ¶
traded_value_snapshots(
source: DataFetcher,
names: list[str],
dates: list[Timestamp],
currency: str,
market: dict[str, DataFrame] | None = None,
lookback_days: int = DEFAULT_LIQUIDITY_DAYS,
) -> pd.DataFrame
Each name's average daily traded value on each of dates, as the liquidity screens measure it.
The close times the volume, in currency converted day by day, averaged
over the name's last lookback_days days with a volume in the window the
screens read (beacon.backtest.screens.average_traded).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
DataFetcher
|
The data the run used. |
required |
names
|
list[str]
|
The names to describe. |
required |
dates
|
list[Timestamp]
|
The days to describe them on. |
required |
currency
|
str
|
The currency traded values are in. |
required |
market
|
dict[str, DataFrame] | None
|
|
None
|
lookback_days
|
int
|
How many days with a volume to average. |
DEFAULT_LIQUIDITY_DAYS
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
pd.DataFrame: Indexed by date, with |
DataFrame
|
|
stored_tables ¶
A stored run's tables: each read from its folder, from its data store's
shared folder, or worked out again from what it kept, the first time it is
asked for. beacon.analysis.data.RunData writes and opens a run with these.
StoredTables ¶
Bases: Mapping[str, DataFrame]
A stored run's tables, each read from its folder, or worked out again from the ones it kept, the first time it is asked for.
add ¶
add(
name: str,
rebuild: Callable[[], DataFrame],
window: Callable[[int], DataFrame] | None = None,
) -> None
Have name read or worked out the first time it is asked for; window works out only its last rows, when it can.
latest ¶
The last rows rows of name: worked out for those alone when it is worked out again and has not been already.
relative_folder ¶
target relative to start, so a run and its store's folder can move together; absolute when the two are on different drives.
add_shared ¶
add_shared(
tables: StoredTables,
folder: Path,
inputs: Mapping[str, Any] | None,
) -> tuple[str, ...]
Add to tables the market inputs a stored run keeps in its data store's shared folder, each read when first asked for; the warnings the run opens with.
packed_trades ¶
packed_trades(
tables: Mapping[str, DataFrame], currency: str
) -> tuple[dict[str, pd.DataFrame], float] | None
A run's trades packed, and the fixed rate; None when the run has no trades or lacks what they are worked out from, or they would not come back exactly.
packed_holdings ¶
The fund's holdings as what its trades do not give back; None when the run lacks them or what they are worked out from, or they would not come back exactly.
holdings_from ¶
holdings_from(
folder: Path,
tables: Mapping[str, DataFrame],
run: Mapping[str, Any],
) -> pd.DataFrame
A stored run's fund holdings, worked out again from its trades.
trades_from ¶
trades_from(
folder: Path,
tables: Mapping[str, DataFrame],
run: Mapping[str, Any],
) -> pd.DataFrame
A stored run's trades, worked out again from what it kept.
rebuilds_of ¶
rebuilds_of(
tables: Mapping[str, DataFrame], run: Mapping[str, Any]
) -> dict[str, Callable[[], pd.DataFrame]]
How each table a stored run worked out again is rebuilt from the ones it kept.
window_of ¶
window_of(
name: str,
tables: Mapping[str, DataFrame],
run: Mapping[str, Any],
) -> Callable[[int], pd.DataFrame] | None
How a stored run's daily weights are worked out for their last days alone; None for any other table.
latest_weights ¶
latest_weights(
name: str,
tables: Mapping[str, DataFrame],
currency: str,
dtype: str,
rows: int,
) -> pd.DataFrame
name's daily weights on its last rows days.
trade_packing ¶
A run's trades, kept small: what was traded, and the rest worked out again.
A trade's price is its name's close in the book's currency that day, its value is quantity times price, and its cost is the run's fixed rate on that value, with any market impact. So a stored run keeps each trade's date, name, side, quantity, reason and tags, and the fixed rate; the price, value, cost and impact are worked out again from the closes it keeps, in the arithmetic that first recorded them.
Wherever a value worked out this way is not the recorded one to the last bit, the recorded value is kept in a patch and put back, so the trades come back exactly. A column the patch would hold for most trades, such as the costs of a run that modelled market impact, is kept whole instead.
fixed_rate ¶
The fixed rate, in basis points, the run's rebalance trades paid: the middle of their cost over value, among those with no market impact.
pack_trades ¶
pack_trades(
trades: DataFrame,
tables: Mapping[str, DataFrame],
currency: str,
) -> tuple[dict[str, pd.DataFrame], float] | None
trades as the tables that keep them, and the fixed rate; None when they would not come back exactly, so they are kept as they are.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
trades
|
DataFrame
|
The run's trades ( |
required |
tables
|
Mapping[str, DataFrame]
|
The run's closes, currencies, rates and actions. |
required |
currency
|
str
|
The book's currency. |
required |
unpack_trades ¶
unpack_trades(
packed: Mapping[str, DataFrame],
tables: Mapping[str, DataFrame],
currency: str,
rate: float,
order: list[str],
) -> pd.DataFrame
The trades pack_trades kept, exactly as they were, in the columns of
order.
book_prices ¶
Each trade's name's close on its day, carried over gaps, in the book's currency, as the books priced it; missing where the run kept none.
same_trades ¶
Whether two trade tables are the same: index, columns and dtypes, text equal and every number to the last bit.
A run's data¶
Everything a run's analysis reads, from a finished run or a stored one, read back exactly as it was written.
data ¶
Everything a run's analysis reads, from a finished run or a stored one.
data = RunData.from_result(result)
data.write(folder)
again = RunData.from_record(folder)
Every analysis of a run is built from a RunData, so it works the same on a
run just finished and on one stored months ago: the stored one is read back
exactly as it was written, whatever has happened to the data store since.
A run stored before its tables were kept loads with what it has, and
missing says, for each table it lacks, why.
Written with a data store's shared folder (beacon.analysis.shared_inputs),
a run keeps its market inputs there, once for every run on that store, and
only the list of pieces it used itself; it reads them back from there, and
says so when the store has since been deleted.
RunData
dataclass
¶
RunData(
currency: str,
returns: Series,
nav: Series,
units: Series,
initial_capital: float,
periods_per_year: int,
risk_free_rate: float,
comparators: Mapping[str, Series] = dict(),
tables: Mapping[str, DataFrame] = dict(),
missing: Mapping[str, str] = dict(),
performance: Series | None = None,
performance_start: float | None = None,
investor_flows: Series | None = None,
warnings: tuple[str, ...] = (),
)
A run, as its analysis reads it.
Attributes:
| Name | Type | Description |
|---|---|---|
currency |
str
|
The book's currency, which every amount is in. |
returns |
Series
|
The fund's daily returns, as its metrics measure them: of a unit when the run had flows, with cash paid out added back. |
nav |
Series
|
The fund's net assets at the end of each day, net of fees owed. |
units |
Series
|
Units outstanding at the end of each day. |
initial_capital |
float
|
What the fund started with. |
periods_per_year |
int
|
How many daily returns make a year. |
risk_free_rate |
float
|
The annual rate excess returns are measured over. |
comparators |
Mapping[str, Series]
|
Each comparator's levels by name: |
tables |
Mapping[str, DataFrame]
|
The run's tables by name ( |
missing |
Mapping[str, str]
|
For each table or series a stored run lacks, why. |
performance |
Series | None
|
The level the fund's total return and drawdown are measured along: its NAV, or, when it had flows or paid cash out, what it started with grown by its returns. None for a run stored before it was kept, whose NAV is used. |
performance_start |
float | None
|
What performance starts from: the initial capital, or a unit's launch price. |
investor_flows |
Series | None
|
The investors' cash flows, dated, from their side (money put in negative, the final NAV positive), for a run with flows; None for any other. |
warnings |
tuple[str, ...]
|
What a stored run says as it opens: that the data store its market inputs came from was deleted, say. |
from_result
classmethod
¶
A finished run's data, with every table it keeps, built the first time any is asked for.
of_returns
classmethod
¶
of_returns(
returns: Series,
periods_per_year: int,
risk_free_rate: float = 0.0,
comparators: Mapping[str, Series] | None = None,
currency: str = "",
) -> RunData
A run known only by its daily returns (the first from its start) and its comparators' levels, for the figures that need nothing more: its headline figures, measured from a start of 1, its comparisons and its figures by period. It has no tables.
latest ¶
The last rows rows of table name. A stored run's daily weights are worked out for those days alone, unless already read whole, and are those rows of the whole table exactly.
write ¶
write(
folder: Path,
shared: SharedInputs | None = None,
store: Mapping[str, Any] | None = None,
) -> None
Keep this run in folder, replacing whatever it held: its tables
and series as beacon.analysis.panels writes them, and the rest in
run.json.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
folder
|
Path
|
Where the run is kept. |
required |
shared
|
SharedInputs | None
|
The shared folder of the data store the run read, which keeps its market inputs instead of folder; None to keep them in folder. |
None
|
store
|
Mapping[str, Any] | None
|
The data store the run read ( |
None
|
from_record
classmethod
¶
A stored run, as it was written.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
folder
|
Path
|
Where the run was written. |
required |
record
|
Mapping[str, Any] | None
|
For a run stored before its data was kept this way, its
JSON record (a backtest record's |
None
|
Raises:
| Type | Description |
|---|---|
ConfigurationError
|
If neither the folder nor record holds the run, or the folder was written by a newer version. |
A run's tables¶
The large tables a run keeps beside its record, such as the fund's daily weights in every name, as Parquet files.
panels ¶
The large tables a run's analysis reads, kept as Parquet files in a folder.
A run's record is a JSON document: its summary and its small series. The tables that grow with the number of names and days, such as each day's weights in every name held, are too large for JSON and slow to read back from it, so they are kept beside the record as Parquet files, one per table, with a small manifest saying what is there.
Each table is wide, dates by name, with a date index. It reads back exactly: the same values, names and dates. A record kept before tables were written has no folder, and reading one gives no tables rather than an error.
run_panels ¶
The tables a finished run keeps beside its record, by name:
fund_weights: the fund's weight in each name held on each day it was recorded, unabridged.name_returns: each day's total return, in the book's currency, of every name the fund held or its index or benchmark held, asbeacon.data.returns.name_returnsmeasures it. Left out, with a warning, when the run kept no data to read them from or they cannot be measured (a currency with no rate into the book's).market: for an ETF, its quote on the exchange each day: NAV per share, premium, spread, market price, bid, ask, and what creating and redeeming cost.cash_band_events: for a fund with a cash band, each time its cash left the band and was brought back, as the run recorded it.class_days,class_flowsandclass_hedges: for a run with share classes, each class's days, flows and hedge trades, asbeacon.analysis.class_panels.class_tablesgives them.futures_tradesandfutures_days: for a fund holding index futures, each trade of them with its cost, and each day's position, price, fair value and index level.trades: every trade, why it was made and its tags, asbeacon.backtest.breakdown.tradesgives them.costs: what the run cost each day, by kind, asbeacon.backtest.breakdown.daily_costsgives it.cash: its cash, income, fees and flows each day, asbeacon.backtest.breakdown.daily_cashgives them.index_weights,optimised_weightsandbenchmark_weights: each comparator's daily weights in its names, for those the run has and that hold names (a benchmark given as a bare level series has none).reference: the grouping fields (sector, industry, region, country, currency, exchange) of every name held, as they stood at each rebalance; seebeacon.analysis.snapshots.characteristics: their market cap, P/E, P/B, dividend yield and momentum at each rebalance, asbeacon.analysis.snapshots.characteristics_snapshotsgives them; left out, with a warning, when they cannot be measured.closes,currencies,ratesandactions: the closes the name returns are worked out from, in each name's own currency, and what converts them and accounts for splits and cash; seebeacon.analysis.packing.traded_values: their average daily traded value at each rebalance and month end, as the liquidity screens measure it (beacon.analysis.snapshots.traded_value_snapshots).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
result
|
BacktestResult
|
The finished run. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
dict |
dict[str, DataFrame]
|
Each table by name, as above. |
name_returns_from ¶
name_returns_from(
tables: Mapping[str, DataFrame],
currency: str,
days: DatetimeIndex,
) -> pd.DataFrame
The name returns, worked out from a run's kept closes, currencies, rates and corporate actions, on its NAV days from the first kept close: a year before the fund's first holding, enough for a risk model's window.
The same calculation as beacon.data.returns.name_returns on the data
store, on the same numbers, so a finished run and a stored one give the
same table to the last bit.
on_sessions ¶
returns, measured on every day a name has a close, as returns from one of days to the next: a day with a close but no session (a holiday's print) is compounded into the session after it, rather than dropped with its move. Days that follow no such day are as they were.
weights_inputs ¶
weights_inputs(
result: BacktestResult, tables: Mapping[str, DataFrame]
) -> dict[str, pd.DataFrame]
What each book's daily weights are worked out again from: the fund's quantities and daily totals, each index's units, and for every book its days, its names and the cells that differ from what was recorded.
weights_from ¶
weights_from(
name: str,
tables: Mapping[str, DataFrame],
currency: str,
dtype: str,
last: int | None = None,
) -> pd.DataFrame
A book's daily weights, worked out again from what was kept, with the recorded cells put back, so they are exactly what was recorded; for its last days alone when given, which are those days of the whole table.
held_names ¶
Every name the fund, its index (both books) or its benchmark held at any point in the run, sorted.
write_panels ¶
Write panels to folder, replacing whatever it held.
The tables are written to a new folder beside it, which then takes its place, so a failure part way leaves the previous tables as they were.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
folder
|
Path
|
Where the tables go. |
required |
panels
|
Mapping[str, DataFrame]
|
Each table by name: dates by name, with a date index. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
dict |
dict[str, Any]
|
The manifest written beside them. |
Raises:
| Type | Description |
|---|---|
ConfigurationError
|
If a table's name is not lower-case letters, digits and underscores, or its columns are not text. |
read_manifest ¶
What folder holds, or None when there is no such folder.
Raises:
| Type | Description |
|---|---|
ConfigurationError
|
If the manifest cannot be read, or was written by a newer version than this one reads. |
read_panels ¶
Read tables back from folder.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
folder
|
Path
|
Where the tables were written. |
required |
names
|
Iterable[str] | None
|
The tables wanted; every one there when None. |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
dict |
dict[str, DataFrame]
|
Each table by name, as it was written. Empty when there is no |
dict[str, DataFrame]
|
folder, as for a record kept before tables were. |
Raises:
| Type | Description |
|---|---|
DataNotFoundError
|
If a table asked for by name is not there. |
ConfigurationError
|
If the folder cannot be read. |
Shared market inputs¶
A data store's market inputs and index results, kept once for every run on it, and the runs that use them.
shared_inputs ¶
A data store's market inputs, kept once for every run on it.
A run keeps the market inputs it read (closes, exchange rates, corporate actions, currencies, traded values, characteristics and reference data), and runs on the same data read mostly the same ones. Kept here, each is kept once:
- Pieces. Each table is cut into pieces of one name's year (a currency's for the exchange rates; a name's whole entry for its currency), each named by a fingerprint of its contents. A run keeps the list of pieces it used. A repeat run on the same data adds nothing; a longer run adds the earlier years; a wider one the new names; a revised price is a new piece, so an older run keeps the numbers it ran on.
- On disk. One folder per data store: a catalogue (table, name, year,
fingerprint, file, first row, rows) and a few files per table. Everything a
run adds goes into one new file per table.
mergerewrites the files as one per table, keeping only the pieces still in use, beside the old ones, and swaps the catalogue in; runs name fingerprints, not files, so a merge does not touch them.
A table read back from its pieces equals the one written, to the last bit: the run keeps its days and names, or the order of its rows, beside its list.
SharedInputs ¶
A data store's shared folder of market inputs.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
folder
|
Path
|
The folder; created when the first pieces are added. |
required |
add ¶
Keep the pieces of tables (those of SHARED) the folder does
not hold yet.
Returns:
| Name | Type | Description |
|---|---|---|
tuple |
DataFrame
|
The pieces the tables are made of ( |
dict[str, Any]
|
|
|
tuple[DataFrame, dict[str, Any]]
|
them (its days and names, or the order of its rows), for the run |
|
tuple[DataFrame, dict[str, Any]]
|
to keep. |
read ¶
table put back together from its pieces fingerprints, as the run that kept layout wrote it.
Raises:
| Type | Description |
|---|---|
KeyError
|
If a piece is not in the folder. |
catalogue ¶
Every piece the folder holds: its table, name, year, fingerprint, file, first row and rows.
merge ¶
Rewrite the folder as one file per table holding only the pieces
in_use (table and fingerprint), and those in files written in the
last GRACE_SECONDS: written beside the old files, the catalogue
swapped in, then the old files removed.
mark_deleted ¶
Mark the folder as kept for a data store that was deleted.
deleted ¶
The deleted data store this folder was kept for, or None.
folder_lock ¶
The lock that runs adding to, merging or reading folder take.
deleted_warning ¶
The warning a run on the deleted data store store opens with.
to_long ¶
frame as rows of a name, a day and its values, and what puts it back: its days and names, or how its rows are ordered, and its dtypes.
from_long ¶
The table to_long was given, from its rows and layout.
shared_runs ¶
The stored runs that keep their market inputs in a data store's shared
folder (beacon.analysis.shared_inputs): which runs read a store, the pieces
they use, what deleting the store leaves, and moving a run that keeps its
inputs itself into the shared layout.
Each function takes the stored runs as a mapping of a name (a strategy, say) to the folder the run was written to.
shared_folder ¶
The shared folder of the data store store_id under root.
store_of ¶
The data store whose shared folder the run in folder keeps its inputs in, or None for a run that keeps them itself.
runs_on ¶
The runs that keep their inputs in store_id's folder.
in_use ¶
Every piece (table and fingerprint) the runs on store_id use.
indices_in_use ¶
Every index result the runs on store_id use.
merge_if_due ¶
Merge shared, keeping the pieces runs use, when it holds too many files, and remove the index results no run uses when it holds too many of those; whether it did either.
forget_store ¶
forget_store(
runs: Mapping[str, Path],
root: Path,
store: Mapping[str, Any],
cascade: bool = False,
) -> dict[str, Any]
What deleting the data store store leaves of its shared folder: with runs on it, kept and marked as for a deleted store, unless cascade, which removes it, as does no run using it.
Returns:
| Name | Type | Description |
|---|---|---|
dict |
dict[str, Any]
|
|
dict[str, Any]
|
under cascade), and |
warnings_of ¶
What the run in folder says as it opens, without opening it: that its data store was deleted, say.
migrate ¶
Move each of runs that keeps its market inputs itself to the shared
layout, under unattributed (the run did not record its store),
checking every table reads back the same first; the runs moved.
shared_indices ¶
An index's results within a run, kept once for every run that tracked it.
Strategies tracking the same index on the same data store over the same days
work out the same index: its target book's holdings, days, names and patch
(beacon.analysis.holdings), and its optimised book's when it has one. Kept
in the store's shared folder (beacon.analysis.shared_inputs) under
indices/<fingerprint>, they are kept once. The fingerprint is of the
tables themselves, so a changed definition, other data or other days give
another entry, and an older run keeps the index it ran against.
index_tables ¶
The tables of tables that are the index's results.
fingerprint ¶
A fingerprint of tables, their names, columns, dtypes, index and every value.
keep ¶
Keep tables (an index's results) in the shared folder, unless an entry with their fingerprint is there already; the fingerprint.
read ¶
The index's results kept under key.
Raises:
| Type | Description |
|---|---|
KeyError
|
If the folder holds no such entry. |
prune ¶
Remove the entries no run uses, sparing those written in the last
GRACE_SECONDS; those removed.
holding_replay ¶
The fund's holdings worked out again from its trades.
A holding changes only when the fund trades the name or a split changes its
share count, and the books add each trade to the holding before it, in
order. So the fund's holdings on the days they changed (as
beacon.analysis.holdings.holding_changes gives them) are worked out again
by replaying the trades and the splits in the books' own arithmetic: a split
multiplies the holding first, then each buy adds to it and each sell takes
from it, a holding below 1e-9 ending.
What the replay does not give back to the last bit is kept: a patch of the recorded rows it gets wrong or misses, and the rows it gives that were not recorded (a split on a day nothing was recorded, say). A stored run keeps those and its trades rather than the holdings, and checks on writing that they come back exactly.
replayed ¶
The fund's holdings on the days they changed, from its trades (a run's trades table) and the actions' splits, by day and then name.
pack_holdings ¶
pack_holdings(
changes: DataFrame,
trades: DataFrame,
actions: DataFrame,
) -> dict[str, pd.DataFrame] | None
What keeps changes given the trades and actions: the recorded rows
the replay gets wrong or misses (patch) and the rows it gives that
were not recorded (drops); None when they would not give changes
back exactly.
unpack_holdings ¶
unpack_holdings(
trades: DataFrame,
actions: DataFrame,
kept: Mapping[str, DataFrame],
dtypes: Mapping[str, object] | None = None,
) -> pd.DataFrame
The fund's holdings on the days they changed, from the replay with kept's rows dropped and patched, by day and then name.
Snapshots at each rebalance¶
What a run's names looked like at each rebalance, such as their sector and country, kept with the run so a later rerun of its analysis gives the same answer.
snapshots ¶
What a run's names looked like at each rebalance, kept with the run.
A rerun of a run's analysis months later should give the same answer even if the data store has been refreshed since, so what the analysis groups and weighs names by is read as it stood at each rebalance and kept with the run.
Each snapshot is a long table indexed by date, a row per name per rebalance,
with an IDENTIFIER column and a column per field. A name with no record in
force on a date has no row for it. Between rebalances, the latest snapshot on
or before a day is the one in force.
rebalance_dates ¶
Every day the fund or its index rebalanced, within the run, sorted.
reference_snapshots ¶
reference_snapshots(
source: DataFetcher,
names: list[str],
dates: list[Timestamp],
) -> pd.DataFrame
The grouping fields of names as they stood on each of dates.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
DataFetcher
|
The data the run used. |
required |
names
|
list[str]
|
The names to describe. |
required |
dates
|
list[Timestamp]
|
The days to describe them on. |
required |
Returns:
| Type | Description |
|---|---|
DataFrame
|
pd.DataFrame: Indexed by date, with |
DataFrame
|
|
DataFrame
|
name has no value for a field. |
characteristics_snapshots ¶
characteristics_snapshots(
source: DataFetcher,
names: list[str],
dates: list[Timestamp],
currency: str,
market: dict[str, DataFrame] | None = None,
) -> pd.DataFrame
Each name's characteristics as they stood on each of dates.
MARKET_CAP: the close times shares outstanding, in currency.PE_RATIOandPB_RATIO: thepe_ratioandpb_ratiofeatures in force on the date, byDataFetcher.fetch_feature's rules.DIVIDEND_YIELD: ordinary dividends over the trailing year over the close, asDataFetcher.fetch_trailing_dividend_yieldgives it.MOMENTUM: the total return, in the name's own currency, from 252 sessions before the date to 21 sessions before it.
A value the data does not give is missing. A name only has a row while it trades: one whose last close is more than five sessions old by the date has stopped, and is left out.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
DataFetcher
|
The data the run used. |
required |
names
|
list[str]
|
The names to describe. |
required |
dates
|
list[Timestamp]
|
The days to describe them on. |
required |
currency
|
str
|
The currency market caps are in. |
required |
market
|
dict[str, DataFrame] | None
|
|
None
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
pd.DataFrame: Indexed by date, with |
DataFrame
|
|
market_history ¶
market_history(
source: DataFetcher,
names: list[str],
dates: list[Timestamp],
until: Timestamp | None = None,
) -> dict[str, pd.DataFrame]
The market data characteristics_snapshots reads, by column: closes,
shares outstanding and volumes from 400 days before the first of dates
to the last of them, or to until when later, in one read.
liquidity_dates ¶
Each rebalance and the last day of each month of the run, sorted.
traded_value_snapshots ¶
traded_value_snapshots(
source: DataFetcher,
names: list[str],
dates: list[Timestamp],
currency: str,
market: dict[str, DataFrame] | None = None,
lookback_days: int = DEFAULT_LIQUIDITY_DAYS,
) -> pd.DataFrame
Each name's average daily traded value on each of dates, as the liquidity screens measure it.
The close times the volume, in currency converted day by day, averaged
over the name's last lookback_days days with a volume in the window the
screens read (beacon.backtest.screens.average_traded).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
DataFetcher
|
The data the run used. |
required |
names
|
list[str]
|
The names to describe. |
required |
dates
|
list[Timestamp]
|
The days to describe them on. |
required |
currency
|
str
|
The currency traded values are in. |
required |
market
|
dict[str, DataFrame] | None
|
|
None
|
lookback_days
|
int
|
How many days with a volume to average. |
DEFAULT_LIQUIDITY_DAYS
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
pd.DataFrame: Indexed by date, with |
DataFrame
|
|
Performance¶
A run's performance, its figures against its comparators, in crises and in each kind of market, and the formulas each figure is computed with. See Performance.
analytics ¶
A run's performance figures, from its data and the settings it is measured with.
analytics = PerformanceAnalytics(RunData.from_record(folder))
analytics.summary()
Every figure is one of beacon.analysis.performance.formulas, so a finished
run and a stored one give the same figures, and a backtest's summary is
these.
PerformanceAnalytics ¶
A run's performance figures.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
run
|
RunData
|
The run's data. |
required |
settings
|
AnalysisSettings | None
|
What the figures are measured over and against; the run's own when None. |
None
|
before
property
¶
The run's close before the days measured, when they start after the run's first day; None when they start with it.
tracked
property
¶
The levels of the comparator tracking is measured against, or None when the run has none.
headline ¶
The five headline figures: total and annualised return, volatility, Sharpe ratio and maximum drawdown.
Over the whole run, the total return and drawdown run from what the fund started with, so the cost of buying in on the first day counts; over a window, from the close before its first day.
hit_rate_monthly ¶
The share of calendar months with a positive return.
tracking_error ¶
The annualised spread of the daily returns less the tracked comparator's; None without one or with fewer than two days.
tracking_difference ¶
The compounded return less the tracked comparator's; None without one.
information_ratio ¶
The mean daily return less the tracked comparator's, over its spread, annualised; None without one or without active risk.
money_weighted_return ¶
The annual internal rate of return of the investors' money; None for a run without flows, or when no rate exists.
comparison ¶
The run against a comparator, given by name or by its levels, over the days both have: the comparator's own figures and the run's relative to it. None for a comparator the run does not have, or one sharing fewer than two days with it.
periods ¶
The run's figures in each calendar year, quarter or month, and its comparators' in each, by name: the index and the benchmark, of those the run has, unless comparators names others.
Raises:
| Type | Description |
|---|---|
ValueError
|
If the frequency is not |
trailing_returns ¶
The returns over the last 1, 3 and 6 months and 1, 3, 5 and 10
years, for the month, quarter and year to date, and since inception,
by name (1M ... 10Y, MTD, QTD, YTD,
inception). Each is annualised as well when it is longer than a
year. None for a window longer than the run.
Since inception is the headline's total and annualised return, so over the whole run it starts from what the fund started with.
best_and_worst ¶
best_and_worst() -> dict[
str,
tuple[
return_figures.PeriodReturn,
return_figures.PeriodReturn,
]
| None,
]
The best and worst day, week, month, quarter and year, by
frequency (daily ... annual), with their dates and whether
the run covered all of each.
positive_shares ¶
The share of days, months and years with a positive return, by frequency.
gains_and_losses ¶
The average gain and loss over days (or months, ...), and their ratio.
longest_streaks ¶
The longest run of months that all gained, and of months that all lost.
means ¶
The arithmetic and geometric mean daily return, and each a year.
levels ¶
The level the figures are measured along, dated, from its start: over the whole run what the fund started with, then its NAV or a unit's value; over a window, 1 grown by its returns. The start is dated the business day before the first return.
drawdowns ¶
The run's drawdowns: the deepest worst with their peak, trough and recovery dates, the current one, and the figures over all of them. The deepest is the headline's maximum drawdown.
ratios ¶
ratios(
sharpe_benchmark: float = 0.0,
trials: int | None = None,
trial_variance: float | None = None,
) -> dict[str, float | None]
The risk-adjusted ratios beyond the Sharpe ratio, by name.
sortino, kappa_3 (daily), upside_potential and omega
measure return against the shortfall below the risk-free rate;
calmar, martin, sterling and burke against drawdowns;
gain_to_pain against losing months. probabilistic_sharpe is the
probability the true Sharpe ratio is above sharpe_benchmark (a
year); deflated_sharpe the same against the best of trials
strategies whose annual Sharpe ratios vary by trial_variance, None
unless both are given; lo_sharpe the Sharpe ratio corrected for
serial correlation.
distribution ¶
The shape of the daily returns, by name: downside_deviation
(below the risk-free rate) and semi_variance (below the mean),
both annualised; skew and excess_kurtosis; tail_ratio;
serial_correlation by lag in days; and a histogram of bins
bins with the normal distribution to compare.
bootstrap ¶
bootstrap(
samples: int = shape.SAMPLES,
confidence: float = shape.CONFIDENCE,
block: int = 1,
seed: int = shape.SEED,
) -> dict[str, shape.Interval] | None
How far the annualised return and the Sharpe ratio could have come
out otherwise, from runs made by drawing the run's own days again;
see beacon.analysis.performance.distribution.bootstrap.
rolling ¶
Each day's return, volatility, Sharpe ratio, Sortino ratio and maximum drawdown over the window days ending that day, blank until the first full window; with a comparator (by name or levels), its relative return, tracking error, information ratio, beta and correlation too, on the days both have.
rolling_outcomes ¶
rolling_outcomes(
window: int = moving.WINDOWS[1],
comparator: str | Series | None = None,
) -> dict[str, float | None]
Over every full window: the worst, middle and best return, the share that gained, and the share ahead of the comparator when one is given.
monthly_grid ¶
The calendar grid of monthly returns, a row a year, with each
year's: of the fund, of a comparator by name, or of the fund less a
comparator (difference index).
Raises:
| Type | Description |
|---|---|
KeyError
|
If the run has no such comparator. |
period_table ¶
Each year's (or quarter's, month's) return, volatility, Sharpe ratio and maximum drawdown for the fund, with its share of positive days, for its index and benchmark, and the fund's less each's.
trailing_table ¶
The trailing returns (trailing_returns) of the fund, its index
and benchmark and the fund's less each's, a row a window.
month_of_year ¶
The fund's mean return in each calendar month over the years.
summary ¶
The headline figures, the monthly hit rate, and, when the run has them, the tracking error and difference against the tracked comparator and the money-weighted return.
settings ¶
What a run's analysis is measured over and against.
AnalysisSettings
dataclass
¶
AnalysisSettings(
start: Timestamp | str | None = None,
end: Timestamp | str | None = None,
periods_per_year: int | None = None,
risk_free_rate: float | None = None,
tracked: str | None = None,
)
How a run's figures are measured. Each setting left as None takes the run's own.
Attributes:
| Name | Type | Description |
|---|---|---|
start |
Timestamp | str | None
|
The first day measured; the run's first when None. A later start measures from the previous day's close. |
end |
Timestamp | str | None
|
The last day measured; the run's last when None. |
periods_per_year |
int | None
|
How many daily returns make a year. |
risk_free_rate |
float | None
|
The annual rate the Sharpe ratio is in excess of. |
tracked |
str | None
|
The comparator tracking is measured against, by name
( |
relative_analytics ¶
A run against one comparator, its index or its benchmark: how much of its return the comparator explains and what is left (alpha, beta, R-squared, and the t-statistics that say whether the rest is more than luck), how it behaved when the comparator rose and fell, how often and by how much it got ahead, and its stretches of falling behind.
relative = RelativeAnalytics(RunData.from_record(folder), "benchmark")
relative.figures()
The two are compared on the days both have, as the run's comparisons are: the comparator's first day returns nothing from its level that day, and so, when the comparator starts after the run, does the run's.
Regression
dataclass
¶
Regression(
alpha: float,
beta: float,
r_squared: float,
alpha_t: float | None,
alpha_p: float | None,
residual_risk: float,
)
The fund's daily excess returns regressed on the comparator's.
Attributes:
| Name | Type | Description |
|---|---|---|
alpha |
float
|
The intercept, a year: the return the comparator does not explain. |
beta |
float
|
The slope: the fund's sensitivity to the comparator. |
r_squared |
float
|
The share of the fund's variance the comparator explains. |
alpha_t |
float | None
|
Alpha over its standard error. |
alpha_p |
float | None
|
The two-sided probability of an alpha this far from 0 by chance. |
residual_risk |
float
|
The spread of what the comparator does not explain, a year. |
YearDifference
dataclass
¶
One calendar year's returns and their difference.
Attributes:
| Name | Type | Description |
|---|---|---|
year |
str
|
The year. |
fund |
float
|
The fund's return in it. |
comparator |
float
|
The comparator's. |
difference |
float
|
The fund's less the comparator's, as a fraction (0.019 is 1.9 percentage points). |
RelativeAnalytics ¶
RelativeAnalytics(
run: RunData,
comparator: str | Series = INDEX,
settings: AnalysisSettings | None = None,
)
A run against one comparator.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
run
|
RunData
|
The run's data. |
required |
comparator
|
str | Series
|
The comparator, by name ( |
INDEX
|
settings
|
AnalysisSettings | None
|
What the figures are measured over and with; the run's own when None. |
None
|
Raises:
| Type | Description |
|---|---|
KeyError
|
If the run has no comparator of that name. |
regression ¶
Jensen's regression of the fund's daily excess returns on the
comparator's; None with fewer than three days. Against a comparator
that does not vary there is nothing to be sensitive to, so the beta
is 0, as beacon.analysis.performance.formulas.beta gives it, and
the alpha is the fund's mean excess return.
treynor ¶
The annual mean excess return per unit of beta; None without a beta.
information_ratio_t ¶
The information ratio's t-statistic, IR times the root of the years, and its two-sided probability by chance.
capture ¶
Up capture, down capture and their ratio, on months: the fund's geometric mean monthly return in the months the comparator rose (fell) over the comparator's in them.
batting_average ¶
The share of months the fund's return beat the comparator's.
relative_levels ¶
The fund's wealth over the comparator's, from 1 the business day before the first day compared.
relative_drawdowns ¶
The drawdowns of the fund's wealth relative to the comparator's: each a stretch of falling behind.
downside_tracking_error ¶
The root mean square of the days the fund fell behind, every day counted, a year.
tracking_difference_by_year ¶
Each calendar year's return less the comparator's, and the standard deviation of those differences (None with fewer than two years).
figures ¶
Every relative figure, by name: today's comparison figures (relative return, tracking error, information ratio, correlation, beta, monthly hit rate) and the rest.
crises ¶
How a run held up in crises: each crisis window's returns, outperformance, drawdowns, beta and alpha, what came after the trough and how long the fund took to recover; and the figures across every crisis.
crises = CrisisAnalytics(RunData.from_record(folder), market=sp500_levels)
crises.crises(), crises.summary()
The windows come from a broad market series by default: each fall of the market deeper than a threshold (15%), from its peak to its trough. Or they are the user's own named episodes, each a start and an end date, instead. A window is measured over the days after its start through its end; one the run covers only part of is marked partial.
Episode
dataclass
¶
A crisis window: a name, its start (the market's peak) and its end (its trough).
Crisis
dataclass
¶
Crisis(
name: str,
start: Timestamp,
end: Timestamp,
partial: bool,
days: int,
fund: float,
comparator: float,
outperformance: float,
fund_drawdown: float,
comparator_drawdown: float,
beta: float,
alpha: float,
after: dict[str, float | None],
recovery_days: int | None,
)
How the fund held up in one crisis.
Attributes:
| Name | Type | Description |
|---|---|---|
name |
str
|
The episode's name, or the market's peak and trough dates. |
start |
Timestamp
|
The window's start (the market's peak). |
end |
Timestamp
|
Its end (the trough). |
partial |
bool
|
Whether the run covered only part of it. |
days |
int
|
The days measured. |
fund |
float
|
The fund's return over it. |
comparator |
float
|
The comparator's. |
outperformance |
float
|
The fund's less the comparator's. |
fund_drawdown |
float
|
The fund's deepest fall inside it. |
comparator_drawdown |
float
|
The comparator's. |
beta |
float
|
The fund's beta to the comparator over it. |
alpha |
float
|
The fund's return beyond what the whole run's beta predicts. |
after |
dict[str, float | None]
|
The fund's and the comparator's returns over the months after
the end, by |
recovery_days |
int | None
|
Trading days from the end until the fund was back at its value at the start; None if it had not been by the run's end. |
CrisisAnalytics ¶
CrisisAnalytics(
run: RunData,
comparator: str | Series = INDEX,
market: str | Series | None = None,
threshold: float = THRESHOLD,
named: Sequence[Episode] | None = None,
settings: AnalysisSettings | None = None,
)
How a run held up in crises, against one comparator.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
run
|
RunData
|
The run's data. |
required |
comparator
|
str | Series
|
What the fund is measured against in each crisis, by name or levels; the run's index by default. |
INDEX
|
market
|
str | Series | None
|
The broad market series the crises are found on, by name of one of the run's comparators or by its levels; the run's benchmark when it has one, else its index. |
None
|
threshold
|
float
|
The market's fall that makes a crisis. |
THRESHOLD
|
named
|
Sequence[Episode] | None
|
The user's own episodes, used instead of the market's falls. |
None
|
settings
|
AnalysisSettings | None
|
What the figures are measured over and with. |
None
|
Raises:
| Type | Description |
|---|---|
KeyError
|
If the run has no comparator of a name given. |
market_crises ¶
Each fall of the market deeper than threshold, peak to trough.
crisis_of ¶
How the fund held up over one episode, on the days the run has; None when the run has none of them.
across ¶
across(
crises: Sequence[Crisis],
ours: Series,
theirs: Series,
run_beta: float,
periods_per_year: int,
) -> dict[str, float | None]
The figures across every crisis: the average outperformance, the share outperformed, crisis capture, and the pooled crisis alpha (the mean daily return beyond the whole run's beta over every crisis day, a year).
regimes ¶
How a run did in each kind of market: months the market rose and fell, calm and volatile stretches, and bear markets and corrections.
regimes = RegimeAnalytics(RunData.from_record(folder), market=sp500_levels)
regimes.regimes()["volatile"]
Each regime is a set of days, found on a broad market series as the crises are; over its days the fund is measured against its comparator. A regime's return compounds its days, as if invested on those days alone, and is annualised from their mean so regimes of different lengths compare.
Regime
dataclass
¶
Regime(
days: int,
fund: float,
comparator: float,
difference: float,
fund_annual: float,
comparator_annual: float,
beta: float,
alpha: float,
)
The fund against its comparator over one regime's days.
Attributes:
| Name | Type | Description |
|---|---|---|
days |
int
|
The regime's days the run had. |
fund |
float
|
The fund's return compounded over them. |
comparator |
float
|
The comparator's. |
difference |
float
|
The fund's less the comparator's. |
fund_annual |
float
|
The fund's mean daily return, a year. |
comparator_annual |
float
|
The comparator's. |
beta |
float
|
The fund's beta to the comparator over them. |
alpha |
float
|
The fund's mean daily return beyond its beta, in excess of the risk-free rate, a year (Jensen's alpha over the regime). |
RegimeAnalytics ¶
RegimeAnalytics(
run: RunData,
comparator: str | Series = INDEX,
market: str | Series | None = None,
settings: AnalysisSettings | None = None,
)
How a run did in each kind of market, against one comparator.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
run
|
RunData
|
The run's data. |
required |
comparator
|
str | Series
|
What the fund is measured against, by name or levels; the run's index by default. |
INDEX
|
market
|
str | Series | None
|
The broad market series the regimes are found on, by name of one of the run's comparators or by its levels; the run's benchmark when it has one, else its index. |
None
|
settings
|
AnalysisSettings | None
|
What the figures are measured over and with. |
None
|
Raises:
| Type | Description |
|---|---|
KeyError
|
If the run has no comparator of a name given. |
up_and_down_months ¶
Which days fall in a month the market rose, and in one it fell, from its daily returns.
calm_and_volatile ¶
calm_and_volatile(
market: Series,
window: int = VOLATILITY_WINDOW,
fifth: float = FIFTH,
) -> tuple[pd.Series, pd.Series]
Which days the market's rolling volatility was in its lowest fifth, and in its highest; days before the first full window are neither.
falls ¶
Which days fall in one of the market's declines, peak to trough, of more than deeper_than (and at most no_deeper_than).
regime_of ¶
regime_of(
ours: Series,
theirs: Series,
days: Series,
daily_rate: float,
periods_per_year: int,
) -> Regime | None
The fund against its comparator over days; None without any.
formulas ¶
The formulas behind a run's performance figures: pure functions of returns and levels, one definition each.
Returns are simple daily returns, and a level series is what they move. A
comparator's returns over a run start from its level on the first day, so
its first return is nothing (returns_from_start), as a run's first return
runs from its starting capital. Figures annualise over periods_per_year
returns a year.
A figure that cannot be measured (too few returns) is None, unless its definition says otherwise. The ratios of a mean return to its spread, the Sharpe and information ratios, are the mean daily return over its standard deviation, annualised by the square root of periods_per_year: both halves from the same daily returns, so the ratio does not change with leverage, and times the square root of the years it is the t-statistic of the mean.
returns_from_start ¶
Daily returns of a level series, the first day's being nothing: its returns over a run, measured from its level on the first day.
compounded_return ¶
The return of a run of returns, compounded.
annualised_return ¶
total, earned over periods returns, as a rate a year; 0 over no time.
volatility ¶
The annualised standard deviation of returns; 0 with fewer than two.
daily_rate ¶
The rate a period that compounds to annual_rate over a year.
sharpe_ratio ¶
The mean daily return in excess of the risk-free rate, over its
standard deviation, annualised; 0 with fewer than two returns, or when
they do not vary (NEGLIGIBLE_SPREAD).
The annual risk_free_rate is taken as the daily rate that compounds to
it (daily_rate).
drawdown ¶
How far below its running peak a level series stands each day.
max_drawdown ¶
The largest fall of path from a running peak; 0 for no path.
path_of ¶
The level a run of returns moves, from 1 before its first.
headline_figures ¶
headline_figures(
returns: Series,
total: float,
path: Series,
periods_per_year: int,
risk_free_rate: float = 0.0,
) -> dict[str, float]
The five headline figures of a performance.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
returns
|
Series
|
The daily returns. |
required |
total
|
float
|
The return over the whole performance. |
required |
path
|
Series
|
The level the drawdown is measured along, from its start. |
required |
periods_per_year
|
int
|
How many returns make a year. |
required |
risk_free_rate
|
float
|
The annual rate the Sharpe ratio is in excess of. |
0.0
|
Returns:
| Name | Type | Description |
|---|---|---|
dict |
dict[str, float]
|
total_return, annualised_return, volatility, sharpe_ratio and |
dict[str, float]
|
max_drawdown. |
period_returns ¶
Returns compounded within each calendar year, quarter or month, by
its name (2024, 2024 Q1, 2024-01), in order.
grouped ¶
returns split into calendar periods, each named, in order.
Raises:
| Type | Description |
|---|---|
ValueError
|
If the frequency is not one of |
hit_rate_monthly ¶
The share of calendar months whose compounded return was positive.
relative_hit_rate_monthly ¶
The share of calendar months in which ours compounded to more than theirs, over the same days.
tracking_error ¶
The annualised standard deviation of the active returns (ours less theirs, day by day); None with fewer than two.
tracking_difference ¶
Our compounded return less theirs.
annualised_relative_return ¶
annualised_relative_return(
our_total: float,
their_total: float,
periods: int,
periods_per_year: int,
) -> float
Our annualised growth less theirs, each total earned over periods returns; 0 over no time.
information_ratio ¶
The mean active return over its standard deviation, annualised; None
with fewer than two, or when they do not vary (NEGLIGIBLE_SPREAD), as
there is no active risk to measure it against.
correlation ¶
The correlation of two return series over the same days; 0 with
fewer than two, or when either does not vary (NEGLIGIBLE_SPREAD), as
nothing relates them.
beta ¶
The sensitivity of ours to theirs: their covariance over the
variance of theirs; 0 with fewer than two, or when theirs does not
vary (NEGLIGIBLE_SPREAD), as there is nothing to be sensitive to.
market_return ¶
An exchange investor's return: bought at first_ask, sold at last_bid.
money_weighted_return ¶
The annual rate that discounts an investor's cash flows to zero.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
flows
|
list[tuple[Timestamp, float]]
|
(date, amount) from the investor's side: money put in is negative, money taken out (and the final value) positive. |
required |
Returns:
| Type | Description |
|---|---|
float | None
|
float or None: The internal rate of return a year, over calendar days |
float | None
|
(ACT/365), or None when the flows do not change sign, so no rate |
float | None
|
exists. |
Portfolio¶
What a run held and traded, and what it cost. See Portfolio.
portfolio ¶
What a run held: how many names, and its weight in each group of a grouping field (sector, region, currency, ...) each day, against its index's and its benchmark's.
portfolio = PortfolioAnalytics(RunData.from_record(folder))
portfolio.group_weights("SECTOR")
A name's group on a day is the one it had at the latest rebalance on or
before that day, as the run's reference table recorded it. A name with no
group recorded is in Unknown. A book's group weights add up to its
invested weight each day: the fund's cash is in none of them.
PortfolioAnalytics ¶
What a run held, and in which groups.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
run
|
RunData
|
The run's data. |
required |
weights ¶
A book's daily weights, dates by name: fund, index or
benchmark; None for a book the run did not keep weights for (a
benchmark given as a level series holds no names).
holdings_count ¶
How many names a book held each day.
group_weights ¶
A book's weight in each group of field each day, dates by group, the groups sorted; None for a book without weights.
Raises:
| Type | Description |
|---|---|
KeyError
|
If the run kept no reference data, or none for field. |
active_group_weights ¶
The fund's weight in each group less against's (index or
benchmark), on the days the fund has; None when the comparator
holds no names.
turnover ¶
The fund's buys, sells, number of trades and their mean value, and
its one-way turnover (the lesser of buys and sells) and two-way
turnover (all of it), over its assets: each trading day (day,
with whether it was a rebalance), each rebalance's own trades
(rebalance) or each year (year).
Raises:
| Type | Description |
|---|---|
KeyError
|
If the run kept no trades (one stored before 0.10.0). |
ValueError
|
If by is none of the three. |
trading_by_reason ¶
The fund's trading by why it was made, a row a reason.
costs ¶
The fund's costs by kind (fixed, impact, futures rolls and futures trading), the
creation fees it received and the net: over the run (run), with
each in basis points of its mean NAV a year, or each year
(year), with the net in basis points of the year's mean NAV.
Raises:
| Type | Description |
|---|---|
ValueError
|
If by is neither. |
cost_per_traded ¶
The trading costs in basis points of the value traded.
characteristics ¶
The weighted market cap, P/E, P/B and dividend yield of the fund, its index and its benchmark (when it holds names) at each rebalance: a row a rebalance, columns by book and figure.
income ¶
Each day's distribution yield and trailing dividend yield, over the year to it.
dividend_growth ¶
Each calendar year's dividends and their growth.
net_flows ¶
Each month's (M) or year's (Y) net flows and their share of
the AUM it began with.
flow_costs ¶
The cost of trading each day's flows, less the creation fees received.
etf_premium ¶
An ETF's premium to NAV on the exchange: its mean, volatility and widest each way, and the mean spread; None for another vehicle.
cash_band_events ¶
Each time the cash left its band and was raised or spent back to its target: the cash before and after, the target and band, the gap closed, what was traded and its cost. Empty for a run without a band.
cash_band_summary ¶
The raises, the spend-downs and both: how many, what they traded and cost, and how close they brought the cash to its target.
cash_band_calendar ¶
How many raises and spend-downs each month (M) or year (Y)
had, and their cost.
cash_trading ¶
Every trade made to raise or spend cash, by tag and reason.
dealing_costs ¶
Each day's subscriptions or redemptions (an ETF's creations or redemptions): the trading they caused, what the dealing investors paid toward it, the net cost to the fund, and for an ETF the cost to the AP.
dealing_summary ¶
The flows' costs added up by kind, over the run (run) or each
year (year).
class_costs ¶
Each share class's fees, hedging costs less the carry earned, the
trading its flows caused less what its dealing investors paid, and
its share of the rest of the fund's trading, in the book's currency:
over the run (run) or each year (year).
Raises:
| Type | Description |
|---|---|
KeyError
|
If the run had no share classes. |
futures_trading ¶
The fund's futures trading by why each trade was made, between rolls and all of it: trades, contracts, notional, cost per contract and in basis points, and turnover.
Raises:
| Type | Description |
|---|---|
KeyError
|
If the run held no index futures. |
futures_basis ¶
The futures' basis to their index, its carry and its mispricing: each day (daily), or their mean, volatility and extremes in basis points.
futures_basis_risk ¶
The tracking error the futures bring, split into the pricing basis (against their own index) and the underlying mismatch (their index against the fund's).
Risk¶
A run's factors, value at risk, risk over time, concentration and liquidity. See Risk analysis.
analytics ¶
A run's risk analytics in one place: its factors, value at risk, risk over time, concentration and liquidity, each built when first asked for and kept.
RiskAnalytics ¶
RiskAnalytics(
run: RunData,
source: DataFetcher,
factors: Sequence[Factor] | None = None,
settings: AnalysisSettings | None = None,
risk_model: RiskModelSettings | None = None,
participation: float = PARTICIPATION,
lookback_days: int = LOOKBACK_DAYS,
)
A run's risk.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
run
|
RunData
|
The run's data. |
required |
source
|
DataFetcher
|
The data the run read, which the factors and traded values are measured from. |
required |
factors
|
Sequence[Factor] | None
|
The factors; |
None
|
settings
|
AnalysisSettings | None
|
The window and conventions the value at risk is measured with. |
None
|
risk_model
|
RiskModelSettings | None
|
How expected risk is worked out; the defaults when None. |
None
|
participation
|
float
|
The share of a name's daily traded value the liquidity figures assume could be sold. |
PARTICIPATION
|
lookback_days
|
int
|
The days a name's average traded value is taken over. |
LOOKBACK_DAYS
|
factors
cached
property
¶
The run's factor exposures, factor returns and betas.
loss
cached
property
¶
The run's value at risk, expected shortfall and stress windows.
over_time
cached
property
¶
The run's expected and measured risk, and where it came from.
concentration
cached
property
¶
The run's concentration by weight, risk and outcome.
liquidity
cached
property
¶
The run's liquidity at each rebalance and month end.
model ¶
The risk model an analysis of a run uses: which kind, estimated over how long, and estimated once at each rebalance for every figure that needs it.
Kinds. sample is the sample covariance of the names' daily returns,
shrunk toward a structured target (beacon.risk.estimate_risk_model).
factor is the factor model fitted by cross-sectional regression on the
names' exposures (beacon.risk.fit_factor_model), which splits risk into
factor and specific parts; its exposures at each rebalance are given by the
caller.
Structure. A factor model is global or styles. A global model
adds country, sector and currency factors to the styles and is fitted on
weekly returns (beacon.analysis.risk.global_model); a styles model is the
market and the styles alone, on daily returns. auto, the default, takes
the global model at a rebalance whose names span more than one country and
the styles model otherwise.
Each rebalance's model is estimated on the run's daily name returns over
the window sessions before that day, never including it, for every name
the fund, its index or its benchmark held on it. A name whose returns do not
cover the whole window (one that listed during it, say) is left out of that
rebalance's model rather than shortening the window for every name, and is
reported by uncovered.
RiskModelSettings
dataclass
¶
RiskModelSettings(
kind: str = SAMPLE,
window: int = DEFAULT_WINDOW,
target: str = CONSTANT_CORRELATION,
intensity: float | None = None,
structure: str = AUTO,
country_field: str | None = DOMICILE,
minimum_names: int = MINIMUM_NAMES,
sectors: bool = True,
currencies: bool = True,
weekly: bool = True,
weeks: int = DEFAULT_WEEKS,
)
How an analysis estimates its risk model.
Attributes:
| Name | Type | Description |
|---|---|---|
kind |
str
|
|
window |
int
|
The sessions of daily returns before each rebalance it is estimated on. |
target |
str
|
For a sample model, the structured target it is shrunk
toward; see |
intensity |
float | None
|
For a sample model, the weight on the target, from 0 to 1; None takes the heuristic from the panel's shape. |
structure |
str
|
For a factor model, |
country_field |
str | None
|
For a global model, the reference field each name's
country is read from: its domicile by default, its listing
( |
minimum_names |
int
|
For a global model, the fewest names a country needs for a factor of its own; one with fewer joins its region's. |
sectors |
bool
|
For a global model, whether sectors are factors. |
currencies |
bool
|
For a global model, whether currencies are factors. |
weekly |
bool
|
For a global model, whether it is fitted on weekly returns; on the window's daily returns when not. |
weeks |
int
|
For a global model on weekly returns, the weeks before each rebalance it is estimated on. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the kind, target or structure is unknown, the window is shorter than 20 sessions or the weeks than 8, the minimum names below 1, or the intensity outside 0 to 1. |
RebalanceRiskModels ¶
RebalanceRiskModels(
run: RunData,
settings: RiskModelSettings | None = None,
exposures: Exposures | None = None,
caps: Caps | None = None,
)
A run's risk model at each rebalance, each estimated once when first asked for.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
run
|
RunData
|
The run's data, with its name returns. |
required |
settings
|
RiskModelSettings | None
|
How the models are estimated. |
None
|
exposures
|
Exposures | None
|
For a factor model, each rebalance's style exposures of the names asked about. |
None
|
caps
|
Caps | None
|
For a global model, each rebalance's market caps of the names asked about, which weight its regressions; every name the same when None. |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If a factor model is asked for without exposures. |
at ¶
The model estimated for the rebalance on date.
Raises:
| Type | Description |
|---|---|
KeyError
|
If date is not a rebalance. |
CalculationError
|
If fewer than the window's sessions come before it, or no name's returns cover them. |
uncovered ¶
The names held on the rebalance on date whose returns did not cover the window, and so are not in its model.
is_global ¶
Whether the model at the rebalance on date is the global one.
has_history ¶
Whether enough returns come before the rebalance on date for its model: the weeks of a weekly global model, the window's sessions otherwise.
held ¶
Every name the fund, its index or its benchmark held on date.
forecast ¶
How well each rebalance's expected risk forecast what followed.
At each rebalance with a model (RiskOverTime.dates), the expected
volatility and tracking error (RiskOverTime.expected) are set beside the
ones measured over the days after it, up to and including the next
rebalance (or the run's last day): the standard deviation of the daily
returns (or of the fund's less the comparator's) over those days, a year.
Their ratio is above 1 where the model under-forecast.
The bias statistic pools the days: each day's return is divided by the daily risk forecast in force at the close before (the expected figure over the root of the days in a year), and the bias is the standard deviation of those standardised returns. It is 1 when the forecasts were right on average, above 1 when the risk was under-forecast. With few days it is noisy: its standard error is about one over the root of twice the days.
book_returns ¶
A book's daily returns: the fund's, or a comparator's from its levels.
active_returns ¶
A book's daily returns less against's, on the days both have.
forecast_check ¶
forecast_check(
expected: dict[Timestamp, dict[str, float]],
returns: Series,
active: Series | None,
periods_per_year: int,
) -> pd.DataFrame
Each rebalance's expected volatility (and tracking error, when active is given) beside the ones measured until the next rebalance.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
expected
|
dict[Timestamp, dict[str, float]]
|
Each rebalance's expected figures, as
|
required |
returns
|
Series
|
The book's daily returns. |
required |
active
|
Series | None
|
Its daily returns less the comparator's; None for no tracking error. |
required |
periods_per_year
|
int
|
The days in a year the figures are annualised by. |
required |
bias_statistic ¶
The standard deviation of the daily returns after the first forecast, each over the daily forecast in force at the close before; None with fewer than two such days or a forecast of 0.
Attribution¶
A run's attribution: by name and group, Brinson-Fachler, currency, factors and implementation. See Attribution.
analytics ¶
A run's attribution: what each name, or each group, contributed to a book's return, or to the fund's return over a comparator.
Each day a name contributes its weight at the close before times its return
that day (the run's own daily total return for it, in the book's currency),
and the days are linked over the window by Carino's factors so the
contributions add up to the compounded return (see
beacon.analysis.attribution.contribution). A group contributes what its
names did, each in the group it was in at that close.
Active attribution takes the fund's weights less the comparator's, both as they drift, against the fund's daily return less the comparator's, linked by the active Carino factor so the contributions add up to the fund's compounded return less the comparator's, the same total Brinson-Fachler, currency, factor and implementation attribution explain. What the names do not explain (cash, costs, fees, futures, the timing of dividends) is the residual, reported with every result and never folded into a name.
Attribution ¶
What produced a run's returns, by name or group.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
run
|
RunData
|
The run's data, with its weights, name returns and reference. |
required |
contribution ¶
contribution(
book: str = FUND,
field: str | None = None,
start: str | Timestamp | None = None,
end: str | Timestamp | None = None,
) -> AttributionResult
Each name's (or, with field, each group's) contribution to a
book's return (fund, index, optimised or benchmark)
over the window,
the whole run by default.
active ¶
active(
against: str = INDEX,
field: str | None = None,
start: str | Timestamp | None = None,
end: str | Timestamp | None = None,
) -> AttributionResult
Each name's (or group's) contribution to the fund's return over against, from its active weight, over the window: the contributions add up to the fund's compounded return less against's.
brinson ¶
brinson(
field: str,
against: str = INDEX,
start: str | Timestamp | None = None,
end: str | Timestamp | None = None,
) -> BrinsonResult
The fund's Brinson-Fachler attribution against against by the grouping field field, over the window, the whole run by default: each group's allocation, selection and interaction, cash a group of its own, linked over the days.
brinson_by_period ¶
The Brinson-Fachler effects over each year (Y) or month
(M).
currency ¶
currency(
against: str | None = None,
by_currency: bool = False,
start: str | Timestamp | None = None,
end: str | Timestamp | None = None,
) -> EffectsResult
The fund's return (or, against a comparator, its active return) split into its names' local returns, their currencies' moves and the cross term; with by_currency, each by the names' currencies too.
factors ¶
factors(
analytics: FactorAnalytics,
against: str | None = None,
method: str = factor_model.HOLDINGS,
start: str | Timestamp | None = None,
end: str | Timestamp | None = None,
) -> EffectsResult
The fund's return (or its active return against a comparator)
explained by the run's factors: from its holdings' exposures
(holdings: market, each factor, specific) or from its returns'
betas (returns: risk-free, alpha, each factor, residual).
Raises:
| Type | Description |
|---|---|
ValueError
|
If the method is neither. |
implementation ¶
implementation(
against: str = INDEX,
start: str | Timestamp | None = None,
end: str | Timestamp | None = None,
) -> EffectsResult
The fund's tracking difference against against split into the cap and screens, drift, cash drag, equitisation, each cost, fees, withholding tax, creation fees and dividend treatment, from the run's own records, linked over the window; the rest is the residual.
weights ¶
A book's daily weights, by name: fund, index,
optimised or benchmark.
Raises:
| Type | Description |
|---|---|
KeyError
|
If the book is none of those, or the run kept no weights for it. |
Analysis requests¶
What a run's analysis should give: the reports, the statistics added to or taken from them, and every choice the analysis makes. See Reports.
request ¶
What a run's analysis should give: the reports wanted, each at a level, with statistics added to or taken from them, the time view each is shown in, and every choice the analysis makes.
Backtest(...).run(definition, start, end,
analysis=AnalysisRequest(reports={"factsheet": "core"},
add={"factsheet": ["sortino_ratio"]}))
The run then carries the analytics block (analyse): each statistic once,
however many reports show it, and the reports as lists of statistic ids. Only
what the statistics asked for need is worked out, so a factsheet's core never
builds the risk model.
AnalysisRequest
dataclass
¶
AnalysisRequest(
reports: Mapping[str, str] = (
lambda: {"factsheet": "core"}
)(),
add: Mapping[str, Sequence[str]] = dict(),
remove: Mapping[str, Sequence[str]] = dict(),
time_views: Mapping[str, Mapping[str, str]] = dict(),
statistics: Sequence[str] = (),
options: AnalysisOptions = AnalysisOptions(),
factors: Sequence[Factor] | None = None,
)
What a run's analysis should give.
Attributes:
| Name | Type | Description |
|---|---|---|
reports |
Mapping[str, str]
|
Each report wanted, by id, with its level: |
add |
Mapping[str, Sequence[str]]
|
By report, optional statistics to show. |
remove |
Mapping[str, Sequence[str]]
|
By report, included statistics not to show. |
time_views |
Mapping[str, Mapping[str, str]]
|
By report, statistic ids with the time view to show
them in: |
statistics |
Sequence[str]
|
Statistics wanted outside any report, over the window. |
options |
AnalysisOptions
|
Every choice the analysis makes; the defaults when left out. |
factors |
Sequence[Factor] | None
|
The factors risk and attribution use; the defaults (size, value, momentum) when None. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If a report, level or statistic is unknown, a statistic added is not optional at the report's level or one taken out is not in it, or a time view is unknown or rolls a statistic that does not roll. |
analyse ¶
analyse(
result: Any,
request: AnalysisRequest,
progress: Callable[[str], None] | None = None,
) -> dict[str, Any]
The analytics block request asks for, of the finished run result.
Each statistic is worked out once, part by part, with a message as each
part starts; one that cannot be worked out for the run is named under
unavailable with a code and a reason.
Returns:
| Name | Type | Description |
|---|---|---|
dict |
dict[str, Any]
|
|
dict[str, Any]
|
by id, with their views), |
|
dict[str, Any]
|
|
|
dict[str, Any]
|
table's |
|
dict[str, Any]
|
|
|
dict[str, Any]
|
can group by) and |
keep_analysis_work ¶
Keep the factor work a finished run's analysis did in folder, where the run is stored, so a later analysis of it does not do it again; False when there was none.
analyse_run ¶
analyse_run(
context: Any,
request: AnalysisRequest,
progress: Callable[[str], None] | None = None,
skip: Collection[str] = (),
) -> dict[str, Any]
analyse on a run's Context, for a stored run as for a finished
one; the statistics in skip are left out, as already worked out.