Indices¶
Defining an index and calculating it: IndexDefinition holds the rules, the methodology module has the selection rules and weighting schemes, IndexCalculator walks the calendar's sessions, and trading calendars live in beacon.index.schedule. See Methodology.
index ¶
Index methodologies and calculation.
Defining an index (IndexDefinition, eligibility rules and weighting
schemes), selecting its constituents, weighting them, and computing its levels
(IndexCalculator, which returns an IndexResult).
IndexAssetView ¶
IndexAssetView(
asset_id: str,
data_fetcher: DataFetcher,
weight_snapshots: dict[Timestamp, dict[str, float]],
index_levels: Series,
)
Bases: AssetView
AssetView with index weight history and contribution analysis.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
asset_id
|
str
|
The identifier used to look up data in the DataFetcher. |
required |
data_fetcher
|
DataFetcher
|
The data provider instance. |
required |
weight_snapshots
|
dict[Timestamp, dict[str, float]]
|
Mapping of rebalance date -> dict of {asset_id: weight} from the parent IndexResult. |
required |
index_levels
|
Series
|
Index level time series from the parent IndexResult. |
required |
weight_on_date ¶
Get this asset's index weight on a specific date.
Finds the most recent rebalance on or before date and returns
the asset's weight. Returns None if the asset was not a
constituent at that point.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
date
|
Timestamp
|
The query date. |
required |
Returns:
| Type | Description |
|---|---|
float | None
|
float or None |
weight_series ¶
Return a Series of this asset's weight at each rebalance date.
Returns:
| Type | Description |
|---|---|
Series
|
pd.Series: Indexed by rebalance date. Rebalance dates where the |
Series
|
asset was not a constituent are excluded. |
contribution ¶
Calculate this asset's contribution to index returns.
Contribution on day t = weight_{t-1} * asset_return_t.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
start
|
str
|
Start date (YYYY-MM-DD). |
required |
end
|
str
|
End date (YYYY-MM-DD). |
required |
price_column
|
str
|
Column name for return calculation. |
'CLOSE'
|
Returns:
| Type | Description |
|---|---|
Series
|
pd.Series: Contribution series indexed by date. |
IndexCalculator ¶
IndexCalculator(
index_definition: IndexDefinition,
data_provider: DataFetcher,
price_column: str = "CLOSE",
)
Bases: MarketValuesMixin, DeletionMixin, TotalReturnMixin, CorporateActionsMixin
Calculates an index from its definition and a data source.
Stateless between runs: it holds only its configuration (the definition,
the data source, the price column and the index context), and everything
a run produces is returned in its :class:IndexResult. It provides
constituent selection, weighting, index level calculation, and
corporate-action, deletion and distribution adjustments.
Initializes the IndexCalculator.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
index_definition
|
IndexDefinition
|
The IndexDefinition object that specifies the index rules. |
required |
data_provider
|
DataFetcher
|
A DataFetcher instance to access market and asset data. |
required |
price_column
|
str
|
Market-data column read as the constituent price when
computing market values. Defaults to |
'CLOSE'
|
resolve_universe ¶
Resolve the definition's universe identifiers into Asset objects.
The public entry point for universe resolution, for callers outside
the calculation loop (the constituent preview, for one). The reference
data is read once for the whole universe, and each resolved name
becomes an :class:~beacon.asset.equity.Equity built from its own
row. An identifier the reference data does not know is skipped with a
warning.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
date
|
Timestamp
|
Point-in-time date for the reference-data lookup. |
required |
Returns:
| Type | Description |
|---|---|
list[Asset]
|
list[Asset]: Assets for every identifier that resolved, in the |
list[Asset]
|
order the definition names them. |
Raises:
| Type | Description |
|---|---|
CalculationError
|
If the definition names no universe at all. |
select_constituents ¶
Select index constituents from a universe by the eligibility rules.
A thin projection of :meth:select_with_provenance: the survivors, with
the record of which rule removed each excluded name discarded. Callers
wanting that record (the preview waterfall, anything answering "why is
this name missing") should use the fuller method rather than repeating
the walk.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
universe
|
list[Asset]
|
A list of potential Asset objects to consider for inclusion. |
required |
current_date
|
Timestamp
|
The date for which selection is being made. |
required |
Returns:
| Type | Description |
|---|---|
list[Asset]
|
A list of Asset objects that are eligible for the index. |
select_with_provenance ¶
Select constituents, keeping the record of how the universe narrowed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
universe
|
list[Asset]
|
A list of potential Asset objects to consider for inclusion. |
required |
current_date
|
Timestamp
|
The date for which selection is being made. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
SelectionResult |
SelectionResult
|
Survivors, one step per rule, and the position of |
SelectionResult
|
the rule that excluded each removed asset. |
calculate_constituent_weights ¶
calculate_constituent_weights(
constituents: list[Asset], current_date: Timestamp
) -> dict[Asset, float]
Weight the constituents by the index's weighting scheme.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
constituents
|
list[Asset]
|
A list of Asset objects that are part of the index. |
required |
current_date
|
Timestamp
|
The date for which weights are calculated. |
required |
Returns:
| Type | Description |
|---|---|
dict[Asset, float]
|
A dictionary mapping each Asset to its weight, summing to 1. Empty |
dict[Asset, float]
|
(with a warning) when constituents is empty. |
Raises:
| Type | Description |
|---|---|
CalculationError
|
If the scheme refuses (an unpriced constituent, an unknown share count, a market cap of zero), in which case it propagates exactly as the scheme raised it, remedy and all. Also if the scheme's weights do not sum to 1: rescaling them would publish an allocation the scheme did not produce under the scheme's own name. |
UnexpectedCalculationError
|
If the scheme raises anything else. A crash, not a decision, and it carries its own published code so a client does not read it as a refusal. |
cap_weights ¶
Apply the definition's cap, returning the weights and a report.
Capping happens here rather than inside a weighting scheme so that it
composes with every scheme, and it returns its report rather than
storing one so the calculator stays stateless and run() stays
idempotent.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
weights
|
dict[Asset, float]
|
Normalised weights keyed by Asset. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
tuple |
dict[Asset, float]
|
The capped weights and a CapReport. With no cap configured |
CapReport
|
the weights are returned unchanged and the report is empty. |
initialize_divisor ¶
Calculate the index's initial divisor on its base date.
divisor = initial_total_market_value / base_value.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
initial_total_market_value
|
float
|
The sum of (price * shares * fx_rate * free_float_if_applicable) for all base constituents on the base_date, expressed in index currency. |
required |
Returns:
| Type | Description |
|---|---|
float
|
The initial divisor as a float. |
Raises:
| Type | Description |
|---|---|
CalculationError
|
If initial_total_market_value or the definition's base value is not positive. |
adjust_divisor_for_rebalance
staticmethod
¶
adjust_divisor_for_rebalance(
old_divisor: float,
old_market_value: float,
new_market_value: float,
) -> float
Adjust the divisor to maintain index level continuity across a rebalance.
When index composition or weights change, the total market value shifts. To prevent an artificial jump in the index level the divisor is scaled:
new_divisor = old_divisor * (new_market_value / old_market_value)
This guarantees: level_before == level_after.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
old_divisor
|
float
|
The divisor in effect before the rebalance. |
required |
old_market_value
|
float
|
Aggregate market value under the old composition. |
required |
new_market_value
|
float
|
Aggregate market value under the new composition. |
required |
Returns:
| Type | Description |
|---|---|
float
|
The adjusted divisor. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If old_divisor, old_market_value or new_market_value is zero or negative. |
run ¶
Run the full index calculation over a date range.
Iterates the index's own trading sessions (the definition's calendar, not Monday to Friday) from start_date to end_date, handling three day types:
- Base date: resolve universe, select constituents, compute weights, apply the cap, initialise divisor, set level = base_value. Rolled forward to the first session when the base date itself was not one.
- Rebalance date: reinvest the day's distributions (total-return indices), then reconstitute (re-resolve universe, re-select, re-weight, re-cap) and adjust the divisor for continuity. With an announcement lag, the composition is selected and weighted as of the announcement date and applied on the effective date.
- Regular day: drop any holding that stopped being listed, reinvest distributions (total-return indices), and compute the level from the units held.
The method is idempotent: it carries no state between calls. When the
calendar covers only part of the window, the run is calculated over
the covered part and the result's calendar_coverage says so; a
window with no sessions at all (a single weekend, say) gives an empty
result.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
start_date
|
str | None
|
First calculation date (YYYY-MM-DD). Defaults to
|
None
|
end_date
|
str | None
|
Last calculation date (YYYY-MM-DD). Required. |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
An |
IndexResult
|
class: |
IndexResult
|
constituent snapshots, weight snapshots, cap reports, announcement |
|
IndexResult
|
dates and the daily weights panel (one row per constituent per |
|
IndexResult
|
day, recorded as the loop goes, since the state it holds each day |
|
IndexResult
|
is path-dependent and cannot be reconstructed from the rebalance |
|
IndexResult
|
snapshots afterwards). The result is bound to the data source. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If end_date is not provided or precedes the base date. |
CalculationError
|
If the dataset lacks a column the definition reads (checked before any work, rather than discovered at the first read and reported as one company's problem), if the calendar can cover none of the window, if the index holds nothing on its base date, or if a rule, scheme or valuation refuses. |
require_columns ¶
Refuse up front if the dataset cannot support this definition.
Public because the constituent preview runs a definition without
calling run, and deserves the same answer: a preview of a
market-cap index over a store with no share counts should say so,
not fail on the first name it tries to price.
Raises:
| Type | Description |
|---|---|
CalculationError
|
Naming each missing column and what needs it. |
run_daily_calculation ¶
run_daily_calculation(
current_date: Timestamp,
constituents: list[Asset],
weights: dict[Asset, float],
previous_index_level: float,
previous_divisor: float,
) -> tuple[float, float]
Runs a single day's index calculation process.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
current_date
|
Timestamp
|
The date for which to perform calculations. |
required |
constituents
|
list[Asset]
|
Current index constituents. |
required |
weights
|
dict[Asset, float]
|
Current constituent weights. |
required |
previous_index_level
|
float
|
Index level from the previous period. |
required |
previous_divisor
|
float
|
Divisor from the previous period. |
required |
Returns:
| Type | Description |
|---|---|
tuple[float, float]
|
Tuple of (new_index_level, new_divisor). |
IndexDefinition ¶
IndexDefinition(
index_id: str,
index_name: str,
base_date: str,
base_value: float,
currency: str,
eligibility_rules: list[EligibilityRuleBase],
weighting_scheme: WeightingSchemeBase,
rebalancing_frequency: str,
calendar: str,
description: str | None = None,
universe_identifiers: list[str] | None = None,
max_constituent_weight: float | None = None,
rebalance_day_rule: str = DEFAULT_DAY_RULE,
return_type: str = PRICE,
withholding_tax_rate: float = 0.0,
effective_lag_sessions: int = 0,
)
Defines the static characteristics and rules for constructing an index.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
index_id
|
str
|
A unique identifier for the index. |
required |
index_name
|
str
|
The common name of the index. |
required |
base_date
|
str
|
The date from which the index calculation begins (YYYY-MM-DD). |
required |
base_value
|
float
|
The initial value of the index on its base_date. Must be positive. |
required |
currency
|
str
|
The currency of the index (stored upper-cased). |
required |
eligibility_rules
|
list[EligibilityRuleBase]
|
A list of EligibilityRuleBase objects that define criteria for constituent selection. An empty list is allowed but logged as a warning. |
required |
weighting_scheme
|
WeightingSchemeBase
|
A WeightingSchemeBase object that defines how constituents are weighted. |
required |
rebalancing_frequency
|
str
|
How often the index is rebalanced:
|
required |
calendar
|
str
|
Exchange MIC backing trading-day arithmetic, e.g.
|
required |
description
|
str | None
|
Optional textual description of the index. |
None
|
universe_identifiers
|
list[str] | None
|
Optional list of string identifiers (e.g. tickers, ISINs) defining the asset universe from which constituents are selected. When given, it must not be empty. An index calculated with none is refused. |
None
|
max_constituent_weight
|
float | None
|
Optional cap on any single constituent's weight, as a fraction (0.1 is 10%), in (0, 1]. Applied after the weighting scheme and iterated until no constituent breaches it. None means uncapped. |
None
|
rebalance_day_rule
|
str
|
Which day of a scheduled month the rebalance
falls on: |
DEFAULT_DAY_RULE
|
return_type
|
str
|
|
PRICE
|
withholding_tax_rate
|
float
|
Fraction of each distribution withheld, for a net index, in [0, 1). Ignored unless the return type is NET_TOTAL_RETURN, so a definition carrying a rate it does not use cannot quietly apply it. |
0.0
|
effective_lag_sessions
|
int
|
Sessions between a composition being announced and its weights taking effect. Zero (the default) is same-day. Must not be negative. |
0
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If a required argument is empty or out of range, or the day rule or return type is not supported. |
get_rebalance_dates ¶
Every rebalance date within [start_date, end_date].
Follows the index's rebalancing frequency, day rule and calendar (see
beacon.index.schedule). The calendar is always a real exchange
calendar, so a date this returns is always a date the exchange has a
session for. The cadence is anchored on the first scheduled date in
the range.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
start_date
|
str
|
Start of the range (YYYY-MM-DD), inclusive. |
required |
end_date
|
str
|
End of the range (YYYY-MM-DD), inclusive. |
required |
Returns:
| Type | Description |
|---|---|
list[Timestamp]
|
A chronologically sorted list of rebalance dates, each a session on |
list[Timestamp]
|
the index's calendar. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the rebalancing frequency is unsupported. |
next_rebalance ¶
The first rebalance strictly after a date.
Anchored on the base date, as a calculation run from the base date is, so the answer names a day the index would genuinely rebalance on.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
as_of
|
str
|
The date being asked from, YYYY-MM-DD. |
required |
Returns:
| Type | Description |
|---|---|
Timestamp | None
|
The date, or None if none falls within the lookahead window. |
OptimisedIndexDefinition ¶
OptimisedIndexDefinition(
index_id: str,
index_name: str,
source: AnyIndexDefinition,
objective: str = MIN_TRACKING_ERROR,
constraints: Sequence[Constraint] = (),
base_date: str | None = None,
base_value: float | None = None,
currency: str | None = None,
description: str | None = None,
risk_model: RiskModel | None = None,
)
An optimised index: a derivation on a source index, plus identity.
The source stays first-class (referenced, never copied), so editing the parent changes its optimised children at their next calculation, which is what "optimise the index I built" means. Chained optimisation (a source that is itself optimised) falls out of the recursion for free.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
index_id
|
str
|
A unique identifier for the derived index. |
required |
index_name
|
str
|
The common name of the derived index. |
required |
source
|
AnyIndexDefinition
|
The parent: a plain :class: |
required |
objective
|
str
|
What to minimise. Only |
MIN_TRACKING_ERROR
|
constraints
|
Sequence[Constraint]
|
What the solved weights must satisfy, as
:class: |
()
|
base_date
|
str | None
|
First calculation date (YYYY-MM-DD). None inherits the source's, which is the usual case: the child lives on the parent's calendar. |
None
|
base_value
|
float | None
|
The level the chained path starts at. None inherits the source's. |
None
|
currency
|
str | None
|
The derived index's currency. None inherits the source's. |
None
|
description
|
str | None
|
Optional textual description. |
None
|
risk_model
|
RiskModel | None
|
Reserved: carried but unused, mirroring
:class: |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If index_id or index_name is empty, source is None, or base_value is given and not positive. |
base_date
property
¶
The first calculation date: own when given, else the source's.
calendar
property
¶
The trading calendar, which is always the source's.
No override, unlike the currency or the base date: the derivation reallocates on exactly the parent's rebalance dates, so a calendar of its own could only disagree with the days it actually has weights for.
universe_identifiers
property
¶
The investable universe, which is always the source's.
The derivation holds no universe of its own (it reallocates over exactly the names the parent published), so the answer resolves through the chain to the root definition's.
from_config
classmethod
¶
from_config(
index_id: str,
index_name: str,
source: AnyIndexDefinition,
config: OptimisationConfig,
) -> OptimisedIndexDefinition
The derivation an :class:OptimisationConfig describes.
One vocabulary for ad-hoc and stored runs: the config is the stored
derivation minus the source, so an ad-hoc Backtest.run builds an
ephemeral definition through here and calculates it exactly as a
stored one would be. The config's objective, constraints and risk
model are carried over; base date, base value and currency inherit
the source's.
ExpressionRule ¶
ExpressionRule(
expression: dict[str, Any],
on_missing: str = EXCLUDE,
max_age_days: int | None = MAX_AGE_DAYS,
)
Bases: EligibilityRuleBase
Select instruments that satisfy an expression.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
expression
|
dict[str, Any]
|
The serialised expression tree ( |
required |
on_missing
|
str
|
|
EXCLUDE
|
max_age_days
|
int | None
|
How old a feature value may be and still count. None means no limit. |
MAX_AGE_DAYS
|
Raises:
| Type | Description |
|---|---|
InvalidRuleError
|
If on_missing is not recognised, or expression is not a valid tree. |
required_columns ¶
The market columns the expression reads, derived from its tree.
An expression's needs are whatever it references, so they come from
the fields in the tree. A derived field is expanded into what it is
computed from: a screen on market_cap needs CLOSE and
SHARES_OUTSTANDING, not a column called MARKET_CAP that no store has.
Reference, action and feature fields read other tables and add
nothing here.
from_expression
classmethod
¶
from_expression(
expression: Expression,
on_missing: str = EXCLUDE,
max_age_days: int | None = MAX_AGE_DAYS,
) -> ExpressionRule
Build from a live expression rather than from its serialised form.
What a user writing Python calls. The stored params are identical
either way, which is the point: one representation, two front doors.
is_eligible ¶
is_eligible(
asset: Asset,
current_date: Timestamp,
market_data_provider: DataFetcher,
context: IndexContext | None = None,
) -> bool
Whether the asset passes, as of current_date.
The date is the rebalance date and is passed straight through to the point-in-time reads. A value published after it is invisible.
FeatureRule ¶
FeatureRule(
field: str,
comparison: str = "gt",
threshold: float = 0.0,
feature_type: str | None = None,
on_missing: str = EXCLUDE,
max_age_days: int | None = MAX_AGE_DAYS,
)
Bases: EligibilityRuleBase
Select instruments whose feature value passes a threshold.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
field
|
str
|
The feature to read, e.g. |
required |
comparison
|
str
|
How the value is tested against threshold: |
'gt'
|
threshold
|
float
|
The value compared against. |
0.0
|
feature_type
|
str | None
|
Which feature dataset ( |
None
|
on_missing
|
str
|
|
EXCLUDE
|
max_age_days
|
int | None
|
How old a value may be and still count. None means no limit. |
MAX_AGE_DAYS
|
Raises:
| Type | Description |
|---|---|
InvalidRuleError
|
If comparison or on_missing is not recognised, or field is empty. |
required_columns ¶
No market columns: a feature is read from the features table.
Stated rather than inherited, so the absence is visibly a decision. Whether the named feature exists is a real question with the same shape as a missing column, but it is asked of a different table, and the up-front column check covers market data only.
is_eligible ¶
is_eligible(
asset: Asset,
current_date: Timestamp,
market_data_provider: DataFetcher,
context: IndexContext | None = None,
) -> bool
Whether the asset passes, as of current_date.
The date is the rebalance date, and it is passed straight through to the point-in-time accessor. A value published after it is invisible.
EligibilityRuleBase ¶
Bases: ABC
Abstract base class for an eligibility rule.
Eligibility rules determine whether an asset can be part of an index.
Subclasses implement is_eligible, and may override required_columns
and prepare.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
rule_name
|
str
|
The rule's name, used in logs and error messages. |
required |
required_columns ¶
The market-data columns this rule reads, declared up front.
Checked against the dataset before a run does any work, so a store with no SHARES_OUTSTANDING column is refused at the start as "this rule needs SHARES_OUTSTANDING and the dataset has none", rather than at the first rebalance as "N0 has no SHARES_OUTSTANDING on 2024-01-02", which is true, and sends a reader to inspect one company whose data is fine.
Empty by default rather than abstract, so a rule written outside this package keeps working. It is then simply not checked up front, and fails at the first read instead. Every rule shipped here declares its own.
prepare ¶
prepare(
candidates: list[Asset],
current_date: Timestamp,
market_data_provider: DataFetcher,
context: IndexContext | None = None,
) -> None
Read in one go whatever this rule is about to read per name.
Called once with the whole candidate set before is_eligible is asked
about any of them. It decides nothing and returns nothing: a rule must
give exactly the same answers whether or not it prepared, because this
is a hint about how to read rather than about what is eligible.
Doing nothing is therefore the right default, and it is the base
implementation.
It exists because is_eligible is a predicate over one asset, so a
rule reading market data reads it a name at a time, and each read
slices a frame whose size is the whole store: the cost of one lookup
grows with the universe around it rather than with the row it wants.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
candidates
|
list[Asset]
|
Everything that reached this rung, in order. A rule that ranks rather than screens would want this set too; that is not what this is for, but it is the same set. |
required |
current_date
|
Timestamp
|
The date selection is being made at. |
required |
market_data_provider
|
DataFetcher
|
The data source the reads will go to. |
required |
context
|
IndexContext | None
|
What the index settles for its rules, as for
:meth: |
None
|
is_eligible
abstractmethod
¶
is_eligible(
asset: Asset,
current_date: Timestamp,
market_data_provider: DataFetcher,
context: IndexContext | None = None,
) -> bool
Checks if a given asset is eligible based on this rule.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
asset
|
Asset
|
The asset to check. |
required |
current_date
|
Timestamp
|
The date on which eligibility is being assessed. |
required |
market_data_provider
|
DataFetcher
|
A DataFetcher instance to get necessary market data (e.g., market cap, trading volume). |
required |
context
|
IndexContext | None
|
What the index the rule is running inside reports in and settles. None when the rule is evaluated outside an index, in which case nothing here may assume a currency it was not told. |
None
|
Returns:
| Type | Description |
|---|---|
bool
|
True if the asset is eligible, False otherwise. |
EqualWeighted ¶
Bases: WeightingSchemeBase
Equal weighting: every constituent gets 1 / n.
Reads no market data. An empty constituent list gets empty weights.
required_columns ¶
Nothing: equal weights are decided without reading the market.
Stated rather than inherited, so the absence is a decision a reader can see. The index still needs a price column to value its holdings daily, but that is the calculator's requirement, not this scheme's.
LiquidityRule ¶
LiquidityRule(
min_avg_daily_volume: int | None = None,
min_avg_daily_value: float | None = None,
lookback_days: int = 60,
)
Bases: EligibilityRuleBase
Eligibility by trading liquidity: average daily volume or value.
The averages are taken over the last lookback_days rows of market data on or before the date asked about. A name with fewer than 80% of that many rows, or with the needed column missing or empty, is excluded (and logged as a warning). Values are in the currency the name trades in, not converted.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
min_avg_daily_volume
|
int | None
|
Lowest average shares traded per day. None for no volume floor. |
None
|
min_avg_daily_value
|
float | None
|
Lowest average close times volume per day. None for no value floor. |
None
|
lookback_days
|
int
|
Trading days the averages are taken over. |
60
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If lookback_days is not positive. |
required_columns ¶
Volume for either threshold, and the close too for a value one.
Declared from the thresholds actually set, because they read different
things. The declaration matters here: is_eligible treats a missing
VOLUME column as "not liquid enough" and excludes the name, so without
the up-front check a store lacking the column would exclude every
name and the run would fail as "index holds nothing on its base date"
with no mention of volume.
is_eligible ¶
is_eligible(
asset: Asset,
current_date: Timestamp,
market_data_provider: DataFetcher,
context: IndexContext | None = None,
) -> bool
Whether asset's traded volume and value over the lookback qualify.
No session resolution here, and none needed: this reads a window ending at current_date, so a closed day is already spanned by the days around it rather than being the single day everything hangs on.
Errors are not caught. A rule that throws has not said the asset is ineligible, and the two answers must not be spelled the same.
Raises:
| Type | Description |
|---|---|
CalculationError
|
If asset is not an equity, so there is no ticker to read volume against. |
MarketCapRule ¶
Bases: EligibilityRuleBase
Eligibility by market capitalisation, read from a resolved session.
The cap is price times shares outstanding, both read on the same session. A name with no price or no positive share count on that session is excluded (and logged as a warning).
Dates resolve backwards into the data. A weekend, a holiday or any
date inside the data's coverage that carries no bar is read at the last
session on or before it, because that is the universe the index actually
held through the closure. This is the same resolution
:class:MarketCapWeighted performs, through the same primitive, so
selection and weighting cannot resolve a closed day differently.
The bounds are in the index's currency. The cap is converted at the session's rate before it meets either bound; a missing FX pair refuses rather than falling back to the local figure. Outside an index there is no context and so no currency to convert into, and the bounds are then read in the asset's own currency. Inside an index the calculator always supplies one.
Past the last bar it refuses rather than excluding. A rule that cannot evaluate has not found the asset ineligible, it has failed, and the two must not share an answer: "not in the index" is a published fact about a name, while "the data does not reach that date" is a fact about the store.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
min_market_cap
|
float | None
|
Lowest cap admitted, in the index currency. None for no floor. |
None
|
max_market_cap
|
float | None
|
Highest cap admitted, in the index currency. None for no ceiling. |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If min_market_cap is greater than max_market_cap. |
required_columns ¶
A cap is price times shares, so both, whichever bound is set.
prepare ¶
prepare(
candidates: list[Asset],
current_date: Timestamp,
market_data_provider: DataFetcher,
context: IndexContext | None = None,
) -> None
Read the whole candidate set's session in one slice.
Every name this rule is about to be asked about is read on the same session, for the same two columns. Warming that session turns the per-name reads into dictionary lookups and, because the weighting scheme then reads the survivors on the same session, makes the second pricing of every surviving name free.
Raises:
| Type | Description |
|---|---|
CalculationError
|
If current_date lies outside the data's
coverage. That is the same refusal |
is_eligible ¶
is_eligible(
asset: Asset,
current_date: Timestamp,
market_data_provider: DataFetcher,
context: IndexContext | None = None,
) -> bool
Whether asset's market cap at the resolved session clears the bounds.
Raises:
| Type | Description |
|---|---|
CalculationError
|
If asset is not an equity, if current_date lies outside the data's coverage, or if the cap cannot be converted into the index currency, so the rule cannot be evaluated at all. Nothing here turns a failure into an exclusion (see the class docstring). |
MarketCapWeighted ¶
Bases: WeightingSchemeBase
Market capitalisation weighting, optionally free-float adjusted.
Each constituent's weight is its cap (price times shares outstanding, times the free-float factor when use_free_float is set) over the sum of all caps. A name is priced at its last close on or before the rebalance session, and its shares, free float and FX rate are read on that same day.
Every path either weights by real market caps or refuses. There is no equal-weight fallback: an index that comes out equal-weighted because the caps could not be read is not a degraded market-cap index, it is a different index published under the same heading, and nothing downstream looks wrong enough for anyone to ask (the levels are right, the weights sum, the backtest tracks).
Dates resolve backwards into the data. A request for a weekend, a holiday, or any date inside the data's coverage that carries no bar reads the last session on or before it, because that is the composition the index actually held through the closure rather than an approximation of one. Past the last bar it refuses, since there the same read would be a stale print presented as the current one. The bound is the data's own coverage, not a day count, which cannot tell those two apart.
Caps are compared in one currency. A universe spanning currencies is converted into the index currency before the caps are summed, and a missing FX pair refuses. A universe quoted in a single currency needs no conversion at all, since converting every cap by the same rate cannot move a weight. Called outside an index (no context) over several currencies, it refuses, since there is no currency to compare in.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
use_free_float
|
bool
|
Weight by the freely traded portion of each cap rather than the full cap. Requires a FREE_FLOAT column. |
False
|
required_columns ¶
Price and shares, and free float only when this scheme uses it.
use_free_float is the declaration: every free-float read in a run is
behind it, so a scheme that is not float-adjusted never asks the store
for the column and must not be refused for lacking it.
calculate_weights ¶
calculate_weights(
constituents: list[Asset],
current_date: Timestamp,
market_data_provider: DataFetcher,
context: IndexContext | None = None,
) -> dict[Asset, float]
Weights proportional to market cap, or a refusal.
Raises:
| Type | Description |
|---|---|
CalculationError
|
If current_date lies outside the data's coverage, if any constituent is unpriceable, unconvertible or is not an equity, has no positive shares outstanding or (when free-float adjusted) no usable free-float factor, if the universe spans currencies with no context to compare them in, or if the caps sum to nothing. Nothing here falls back to another methodology (see the class docstring). |
WeightingSchemeBase ¶
Bases: ABC
Abstract base class for a weighting scheme.
Weighting schemes determine the proportion of each constituent in an
index. Subclasses implement calculate_weights, and may override
required_columns.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
scheme_name
|
str
|
The scheme's name, used in logs and error messages. |
required |
required_columns ¶
The market-data columns this scheme reads, declared up front.
The same contract as :meth:EligibilityRuleBase.required_columns, and
derived from the scheme's own inputs where they change what it reads:
a scheme's parameters are the declaration, so nothing asks a store
for a column the configured scheme does not use.
calculate_weights
abstractmethod
¶
calculate_weights(
constituents: list[Asset],
current_date: Timestamp,
market_data_provider: DataFetcher,
context: IndexContext | None = None,
) -> dict[Asset, float]
Calculates the weight for each constituent asset.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
constituents
|
list[Asset]
|
A list of assets that are eligible for the index. |
required |
current_date
|
Timestamp
|
The date for which weights are being calculated. |
required |
market_data_provider
|
DataFetcher
|
A DataFetcher instance. |
required |
context
|
IndexContext | None
|
What the index the scheme is running inside reports in and settles. None when it is invoked outside an index. |
None
|
Returns:
| Type | Description |
|---|---|
dict[Asset, float]
|
A dictionary mapping each Asset object to its calculated weight (float). |
dict[Asset, float]
|
The sum of weights should typically be 1.0. |
IndexResult
dataclass
¶
IndexResult(
index_id: str,
index_levels: Series,
divisor_history: Series,
constituent_snapshots: dict[Timestamp, list[str]],
weight_snapshots: dict[Timestamp, dict[str, float]],
cap_reports: dict[Timestamp, CapReport] = dict(),
announcement_dates: dict[Timestamp, Timestamp] = dict(),
daily_weights: DataFrame = empty_daily_weights(),
calendar_coverage: CalendarCoverage | None = None,
_data_fetcher: DataFetcher | None = None,
)
Container holding the output of an index calculation run.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
index_id
|
str
|
Identifier of the calculated index. |
required |
index_levels
|
Series
|
Time series of index levels indexed by
|
required |
divisor_history
|
Series
|
Time series of divisor values indexed by
|
required |
constituent_snapshots
|
dict[Timestamp, list[str]]
|
Mapping of rebalance date -> list of asset_id strings. |
required |
weight_snapshots
|
dict[Timestamp, dict[str, float]]
|
Mapping of rebalance date -> dict of {asset_id: weight}. |
required |
cap_reports
|
dict[Timestamp, CapReport]
|
Mapping of rebalance date -> CapReport, for the rebalances where a weight cap actually bound. Empty for an uncapped index, so its presence is itself the signal that capping occurred. |
dict()
|
announcement_dates
|
dict[Timestamp, Timestamp]
|
Mapping of effective date -> the date that composition was announced. Snapshots are keyed by the effective date, because that is when the weights are in force and what every consumer (drift, attribution, the backtest engine) needs. The announcement is carried alongside rather than instead, since a client showing "rebalance of 18 Sep, effective 22 Sep" needs both. Empty for an index with no lag, where the two always coincide. |
dict()
|
daily_weights
|
DataFrame
|
Long-form panel of what the index held on every
calculation day: |
empty_daily_weights()
|
calendar_coverage
|
CalendarCoverage | None
|
How much of the requested window the trading calendar covered, or None when it covered all of it (the ordinary case). Its presence is the signal that the run's range was narrowed to what the calendar covers. |
None
|
The daily panel is recorded rather than re-derived because the index's daily state is path-dependent. It is not a forward-fill of the rebalance snapshot, and not even "amounts fixed between rebalances, repriced daily": a delisted name is dropped mid-period and the divisor adjusted, a split multiplies the units held on its ex-date, and a total-return index reinvests its cash, all on days that are not rebalances. A path is written down as it happens.
The rebalance snapshots are the record of what a rebalance decided. This panel is the record of what then happened.
capped_assets_on_date ¶
Return the constituents held at the cap at the given rebalance.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
date
|
Timestamp
|
A rebalance date. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
dict |
dict[str, float]
|
|
dict[str, float]
|
that date. Empty when nothing was capped, or when date is not a |
|
dict[str, float]
|
rebalance date. |
with_data ¶
Bind a DataFetcher for asset-level queries. Returns self for chaining.
asset ¶
Return an IndexAssetView for a constituent.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
asset_id
|
str
|
Identifier of the constituent asset. |
required |
Returns:
| Type | Description |
|---|---|
IndexAssetView
|
IndexAssetView |
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If no DataFetcher has been bound via
:meth: |
KeyError
|
If asset_id is not found in any constituent snapshot. |
get_returns ¶
Derive a return series from index levels.
Returns:
| Type | Description |
|---|---|
Series
|
pd.Series: Percentage returns (first entry is dropped). |
get_weights_on_date ¶
Get constituent weights effective on a given date.
Locates the most recent rebalance date on or before date.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
date
|
Timestamp
|
The query date. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
dict |
dict[str, float]
|
Mapping of asset_id to weight. Empty dict if no rebalance |
dict[str, float]
|
has occurred on or before date. |
weights_on ¶
Get the recorded constituent weights as of a given date.
Reads the daily panel rather than the rebalance snapshots, so the
answer includes everything that happened since the last rebalance:
price drift, a deletion, a divisor adjustment. Compare
:meth:get_weights_on_date, which answers the different question of
what the last rebalance decided.
Falls back to the latest recorded date on or before date, which covers a day the holdings could not be valued at all, since such a day records no rows.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
date
|
Timestamp
|
The query date. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
dict |
dict[str, float]
|
Mapping of identifier to weight. Empty when nothing was |
dict[str, float]
|
recorded on or before date, including when no panel was captured |
|
dict[str, float]
|
at all. |
to_dataframe ¶
Flatten index levels and divisor history into a DataFrame.
Returns:
| Type | Description |
|---|---|
DataFrame
|
pd.DataFrame: Columns: |
calculate_derived_index ¶
calculate_derived_index(
definition: OptimisedIndexDefinition,
data_provider: DataFetcher,
start_date: str | None = None,
end_date: str | None = None,
price_column: str = "CLOSE",
parent_result: IndexResult | None = None,
) -> IndexResult
Calculate an optimised index into a standard :class:IndexResult.
Three steps: calculate the parent (or accept a pre-supplied calculation, as the Backtest integration does with its cached one), solve the parent's published weights at every rebalance under the definition's constraints, then chain the solved weights into the derived index's own daily levels.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
definition
|
OptimisedIndexDefinition
|
The derivation to calculate. |
required |
data_provider
|
DataFetcher
|
Data source for the parent calculation, prices and FX. |
required |
start_date
|
str | None
|
First date (YYYY-MM-DD). Defaults to the definition's base date. Ignored when parent_result is supplied, whose own window governs. |
None
|
end_date
|
str | None
|
Last date (YYYY-MM-DD). Required unless parent_result is supplied. |
None
|
price_column
|
str
|
Market-data column read as the price. |
'CLOSE'
|
parent_result
|
IndexResult | None
|
The source's calculation, when the caller already has it. None calculates the source here (recursively, when the source is itself optimised). |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
IndexResult |
IndexResult
|
Daily levels, divisor history, constituent and weight |
IndexResult
|
snapshots at exactly the parent's rebalance dates, and the daily |
|
IndexResult
|
weights panel: a normal index result, data-bound to data_provider. |
Raises:
| Type | Description |
|---|---|
CalculationError
|
If the objective is unknown, the parent produced no rebalance snapshots to solve, or a solve is infeasible (the solver's own message names the binding conflict). |
ValueError
|
If no window end is available to calculate the parent. |
asset_view ¶
IndexAssetView: an asset view extended with index-specific context such as weight history and contribution analysis.
IndexAssetView ¶
IndexAssetView(
asset_id: str,
data_fetcher: DataFetcher,
weight_snapshots: dict[Timestamp, dict[str, float]],
index_levels: Series,
)
Bases: AssetView
AssetView with index weight history and contribution analysis.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
asset_id
|
str
|
The identifier used to look up data in the DataFetcher. |
required |
data_fetcher
|
DataFetcher
|
The data provider instance. |
required |
weight_snapshots
|
dict[Timestamp, dict[str, float]]
|
Mapping of rebalance date -> dict of {asset_id: weight} from the parent IndexResult. |
required |
index_levels
|
Series
|
Index level time series from the parent IndexResult. |
required |
weight_on_date ¶
Get this asset's index weight on a specific date.
Finds the most recent rebalance on or before date and returns
the asset's weight. Returns None if the asset was not a
constituent at that point.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
date
|
Timestamp
|
The query date. |
required |
Returns:
| Type | Description |
|---|---|
float | None
|
float or None |
weight_series ¶
Return a Series of this asset's weight at each rebalance date.
Returns:
| Type | Description |
|---|---|
Series
|
pd.Series: Indexed by rebalance date. Rebalance dates where the |
Series
|
asset was not a constituent are excluded. |
contribution ¶
Calculate this asset's contribution to index returns.
Contribution on day t = weight_{t-1} * asset_return_t.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
start
|
str
|
Start date (YYYY-MM-DD). |
required |
end
|
str
|
End date (YYYY-MM-DD). |
required |
price_column
|
str
|
Column name for return calculation. |
'CLOSE'
|
Returns:
| Type | Description |
|---|---|
Series
|
pd.Series: Contribution series indexed by date. |
cache ¶
A persistent, content-addressed cache of calculated :class:IndexResults.
A result is stored under a fingerprint of the four things the calculation is a pure function of (the definition, the store behind the fetcher, the window and the library version), so there is deliberately no invalidation logic: changed inputs never match, and stale entries age out by size-capped pruning.
The safety rule: cache only what can be keyed completely; anything else
calculates fresh, every time. An incomplete key would mean silently stale
numbers, so anything the key cannot capture (an unregistered rule class, a
parameter that does not serialise, a fetcher with no on-disk store behind it)
makes :func:fingerprint return None, and no key means no cache, at the cost
of a recalculation the caller would have paid anyway.
:func:explain_uncacheable answers why.
Storage is one directory per fingerprint: panels in the data store's own
reproducible gzipped-CSV format, plus a manifest.json keeping the key
parts in the clear so "why didn't this hit?" is answerable by looking. Entries
are staged and renamed into place; on read, anything missing or unparseable is
a miss and the corrupt entry is removed, so the cache never raises into a
calculation. There is no cache schema version: the library version is part of
every key, so a format change rides on the version bump that ships it. The
cache is pruned to 512 MB (least recently used first) on every write.
IndexResultCache ¶
Filesystem cache of IndexResults, keyed by :func:fingerprint.
Reading never raises: a corrupt or half-present entry is a miss, removed on sight. Writing never raises either: a cache write is a convenience, and failing one must not fail the calculation that produced the result.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
root
|
Path | None
|
Directory the cache lives in, created on first write. None
uses :func: |
None
|
entry_path ¶
The directory one key's entry occupies (which may not exist).
get ¶
Read a cached result, or None on a miss.
A hit refreshes the last_used stamp pruning orders evictions by.
The returned result has no DataFetcher bound; a consumer that needs
asset views re-binds via :meth:IndexResult.with_data.
put ¶
Store a result under its fingerprint, then prune to the size cap.
The caller owns the key/result pairing: this module cannot re-derive
the inputs from the result. parts is the :func:key_parts payload,
recorded in the entry's manifest in the clear so a stored entry can
say what it was keyed on; optional, the fingerprint alone serves hits.
An existing entry under key is left as it is, and an invalid key or
a failed write is logged and skipped rather than raised.
default_root ¶
The cache location used when no explicit root is given.
Requires platformdirs, imported inside the function for the same reason
store.default_path does: only the default location is a platform
question, and the core import path must not need an optional package.
fingerprint ¶
fingerprint(
definition: AnyIndexDefinition,
fetcher: DataFetcher,
start_date: str | None,
end_date: str,
) -> str | None
The cache key for one calculation, or None when it cannot be keyed.
A sha256 hexdigest over the canonical JSON dump of the key parts. The
window arguments are the calculator's own (start_date None means the
base date) and are normalised, so two spellings of the same window share
a key. None means uncacheable: the reason is logged at DEBUG and available
from :func:explain_uncacheable.
key_parts ¶
key_parts(
definition: AnyIndexDefinition,
fetcher: DataFetcher,
start_date: str | None,
end_date: str,
) -> dict[str, Any] | None
The four key parts in the clear, or None when uncacheable.
These are what :func:fingerprint hashes and what an entry's manifest
records.
explain_uncacheable ¶
explain_uncacheable(
definition: AnyIndexDefinition,
fetcher: DataFetcher,
start_date: str | None,
end_date: str,
) -> str | None
Why this calculation cannot be cached, or None when it can.
calculation ¶
The index calculator.
Re-exports IndexCalculator, which composes constituent selection, weighting,
market values, corporate actions, deletions and total-return reinvestment, and
the selection result objects that record how a universe narrowed.
IndexCalculator ¶
IndexCalculator(
index_definition: IndexDefinition,
data_provider: DataFetcher,
price_column: str = "CLOSE",
)
Bases: MarketValuesMixin, DeletionMixin, TotalReturnMixin, CorporateActionsMixin
Calculates an index from its definition and a data source.
Stateless between runs: it holds only its configuration (the definition,
the data source, the price column and the index context), and everything
a run produces is returned in its :class:IndexResult. It provides
constituent selection, weighting, index level calculation, and
corporate-action, deletion and distribution adjustments.
Initializes the IndexCalculator.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
index_definition
|
IndexDefinition
|
The IndexDefinition object that specifies the index rules. |
required |
data_provider
|
DataFetcher
|
A DataFetcher instance to access market and asset data. |
required |
price_column
|
str
|
Market-data column read as the constituent price when
computing market values. Defaults to |
'CLOSE'
|
resolve_universe ¶
Resolve the definition's universe identifiers into Asset objects.
The public entry point for universe resolution, for callers outside
the calculation loop (the constituent preview, for one). The reference
data is read once for the whole universe, and each resolved name
becomes an :class:~beacon.asset.equity.Equity built from its own
row. An identifier the reference data does not know is skipped with a
warning.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
date
|
Timestamp
|
Point-in-time date for the reference-data lookup. |
required |
Returns:
| Type | Description |
|---|---|
list[Asset]
|
list[Asset]: Assets for every identifier that resolved, in the |
list[Asset]
|
order the definition names them. |
Raises:
| Type | Description |
|---|---|
CalculationError
|
If the definition names no universe at all. |
select_constituents ¶
Select index constituents from a universe by the eligibility rules.
A thin projection of :meth:select_with_provenance: the survivors, with
the record of which rule removed each excluded name discarded. Callers
wanting that record (the preview waterfall, anything answering "why is
this name missing") should use the fuller method rather than repeating
the walk.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
universe
|
list[Asset]
|
A list of potential Asset objects to consider for inclusion. |
required |
current_date
|
Timestamp
|
The date for which selection is being made. |
required |
Returns:
| Type | Description |
|---|---|
list[Asset]
|
A list of Asset objects that are eligible for the index. |
select_with_provenance ¶
Select constituents, keeping the record of how the universe narrowed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
universe
|
list[Asset]
|
A list of potential Asset objects to consider for inclusion. |
required |
current_date
|
Timestamp
|
The date for which selection is being made. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
SelectionResult |
SelectionResult
|
Survivors, one step per rule, and the position of |
SelectionResult
|
the rule that excluded each removed asset. |
calculate_constituent_weights ¶
calculate_constituent_weights(
constituents: list[Asset], current_date: Timestamp
) -> dict[Asset, float]
Weight the constituents by the index's weighting scheme.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
constituents
|
list[Asset]
|
A list of Asset objects that are part of the index. |
required |
current_date
|
Timestamp
|
The date for which weights are calculated. |
required |
Returns:
| Type | Description |
|---|---|
dict[Asset, float]
|
A dictionary mapping each Asset to its weight, summing to 1. Empty |
dict[Asset, float]
|
(with a warning) when constituents is empty. |
Raises:
| Type | Description |
|---|---|
CalculationError
|
If the scheme refuses (an unpriced constituent, an unknown share count, a market cap of zero), in which case it propagates exactly as the scheme raised it, remedy and all. Also if the scheme's weights do not sum to 1: rescaling them would publish an allocation the scheme did not produce under the scheme's own name. |
UnexpectedCalculationError
|
If the scheme raises anything else. A crash, not a decision, and it carries its own published code so a client does not read it as a refusal. |
cap_weights ¶
Apply the definition's cap, returning the weights and a report.
Capping happens here rather than inside a weighting scheme so that it
composes with every scheme, and it returns its report rather than
storing one so the calculator stays stateless and run() stays
idempotent.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
weights
|
dict[Asset, float]
|
Normalised weights keyed by Asset. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
tuple |
dict[Asset, float]
|
The capped weights and a CapReport. With no cap configured |
CapReport
|
the weights are returned unchanged and the report is empty. |
initialize_divisor ¶
Calculate the index's initial divisor on its base date.
divisor = initial_total_market_value / base_value.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
initial_total_market_value
|
float
|
The sum of (price * shares * fx_rate * free_float_if_applicable) for all base constituents on the base_date, expressed in index currency. |
required |
Returns:
| Type | Description |
|---|---|
float
|
The initial divisor as a float. |
Raises:
| Type | Description |
|---|---|
CalculationError
|
If initial_total_market_value or the definition's base value is not positive. |
adjust_divisor_for_rebalance
staticmethod
¶
adjust_divisor_for_rebalance(
old_divisor: float,
old_market_value: float,
new_market_value: float,
) -> float
Adjust the divisor to maintain index level continuity across a rebalance.
When index composition or weights change, the total market value shifts. To prevent an artificial jump in the index level the divisor is scaled:
new_divisor = old_divisor * (new_market_value / old_market_value)
This guarantees: level_before == level_after.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
old_divisor
|
float
|
The divisor in effect before the rebalance. |
required |
old_market_value
|
float
|
Aggregate market value under the old composition. |
required |
new_market_value
|
float
|
Aggregate market value under the new composition. |
required |
Returns:
| Type | Description |
|---|---|
float
|
The adjusted divisor. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If old_divisor, old_market_value or new_market_value is zero or negative. |
run ¶
Run the full index calculation over a date range.
Iterates the index's own trading sessions (the definition's calendar, not Monday to Friday) from start_date to end_date, handling three day types:
- Base date: resolve universe, select constituents, compute weights, apply the cap, initialise divisor, set level = base_value. Rolled forward to the first session when the base date itself was not one.
- Rebalance date: reinvest the day's distributions (total-return indices), then reconstitute (re-resolve universe, re-select, re-weight, re-cap) and adjust the divisor for continuity. With an announcement lag, the composition is selected and weighted as of the announcement date and applied on the effective date.
- Regular day: drop any holding that stopped being listed, reinvest distributions (total-return indices), and compute the level from the units held.
The method is idempotent: it carries no state between calls. When the
calendar covers only part of the window, the run is calculated over
the covered part and the result's calendar_coverage says so; a
window with no sessions at all (a single weekend, say) gives an empty
result.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
start_date
|
str | None
|
First calculation date (YYYY-MM-DD). Defaults to
|
None
|
end_date
|
str | None
|
Last calculation date (YYYY-MM-DD). Required. |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
An |
IndexResult
|
class: |
IndexResult
|
constituent snapshots, weight snapshots, cap reports, announcement |
|
IndexResult
|
dates and the daily weights panel (one row per constituent per |
|
IndexResult
|
day, recorded as the loop goes, since the state it holds each day |
|
IndexResult
|
is path-dependent and cannot be reconstructed from the rebalance |
|
IndexResult
|
snapshots afterwards). The result is bound to the data source. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If end_date is not provided or precedes the base date. |
CalculationError
|
If the dataset lacks a column the definition reads (checked before any work, rather than discovered at the first read and reported as one company's problem), if the calendar can cover none of the window, if the index holds nothing on its base date, or if a rule, scheme or valuation refuses. |
require_columns ¶
Refuse up front if the dataset cannot support this definition.
Public because the constituent preview runs a definition without
calling run, and deserves the same answer: a preview of a
market-cap index over a store with no share counts should say so,
not fail on the first name it tries to price.
Raises:
| Type | Description |
|---|---|
CalculationError
|
Naming each missing column and what needs it. |
run_daily_calculation ¶
run_daily_calculation(
current_date: Timestamp,
constituents: list[Asset],
weights: dict[Asset, float],
previous_index_level: float,
previous_divisor: float,
) -> tuple[float, float]
Runs a single day's index calculation process.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
current_date
|
Timestamp
|
The date for which to perform calculations. |
required |
constituents
|
list[Asset]
|
Current index constituents. |
required |
weights
|
dict[Asset, float]
|
Current constituent weights. |
required |
previous_index_level
|
float
|
Index level from the previous period. |
required |
previous_divisor
|
float
|
Divisor from the previous period. |
required |
Returns:
| Type | Description |
|---|---|
tuple[float, float]
|
Tuple of (new_index_level, new_divisor). |
SelectionResult
dataclass
¶
SelectionResult(
survivors: list[Asset],
steps: list[SelectionStep],
exclusions: dict[str, int] = dict(),
)
Which assets survived selection, and how each one fared.
Attributes:
| Name | Type | Description |
|---|---|---|
survivors |
list[Asset]
|
Assets that passed every rule, in universe order. |
steps |
list[SelectionStep]
|
One entry per rung, starting with the universe, then the stale-price rung when any name was stale, then one per rule. |
exclusions |
dict[str, int]
|
Identifier to the position of the rule that removed it (STALENESS_POSITION for a name dropped as stale). Each excluded asset appears exactly once: an asset leaves the surviving set the moment it fails, so no later rule ever sees it and no name can be blamed on two rules. That single-owner property is what makes the funnel answer "why is this name missing" rather than only "how many are left". |
rule_steps
property
¶
Every rung after the universe, including any stale-price rung.
excluded_by ¶
The rung that removed an asset.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
asset_id
|
str
|
The identifier to look up. |
required |
Returns:
| Type | Description |
|---|---|
SelectionStep | None
|
SelectionStep or None: The rung, or None if the asset survived or |
SelectionStep | None
|
was never in the universe. |
SelectionStep
dataclass
¶
One rung of the selection funnel.
Attributes:
| Name | Type | Description |
|---|---|---|
position |
int
|
1-based index of the rule, UNIVERSE_POSITION (0) for the starting universe, or STALENESS_POSITION (-1) for the stale-price rung. |
rule_name |
str
|
Type of the rule applied ( |
remaining |
int
|
How many assets survived this rung. |
excluded |
list[str]
|
Identifiers this rung removed, sorted. Empty for the universe rung. |
select_with_provenance ¶
select_with_provenance(
universe: list[Asset],
rules: list[EligibilityRuleBase],
current_date: Timestamp,
data_fetcher: DataFetcher,
context: IndexContext | None = None,
) -> SelectionResult
Narrow a universe to its eligible constituents, recording each step.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
universe
|
list[Asset]
|
Assets to select from. |
required |
rules
|
list[EligibilityRuleBase]
|
Eligibility rules, applied in order. Each rule sees only what survived the ones before it. |
required |
current_date
|
Timestamp
|
The date to evaluate at. |
required |
data_fetcher
|
DataFetcher
|
Data source the rules read from. |
required |
context
|
IndexContext | None
|
What the index settles for its rules: its currency, so a bound stated in it is compared against a converted figure rather than a local one. None outside an index, and then a rule that needs it says so rather than assuming one. |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
SelectionResult |
SelectionResult
|
Survivors, the funnel, and per-asset provenance. |
Raises:
| Type | Description |
|---|---|
Exception
|
Whatever a rule raises, unchanged. A rule that could not run has not excluded anything, so its failure propagates rather than being recorded as an exclusion. |
calculator ¶
The IndexCalculator: constituent selection, weighting, index level calculation, and corporate-action, deletion and distribution adjustments.
IndexCalculator ¶
IndexCalculator(
index_definition: IndexDefinition,
data_provider: DataFetcher,
price_column: str = "CLOSE",
)
Bases: MarketValuesMixin, DeletionMixin, TotalReturnMixin, CorporateActionsMixin
Calculates an index from its definition and a data source.
Stateless between runs: it holds only its configuration (the definition,
the data source, the price column and the index context), and everything
a run produces is returned in its :class:IndexResult. It provides
constituent selection, weighting, index level calculation, and
corporate-action, deletion and distribution adjustments.
Initializes the IndexCalculator.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
index_definition
|
IndexDefinition
|
The IndexDefinition object that specifies the index rules. |
required |
data_provider
|
DataFetcher
|
A DataFetcher instance to access market and asset data. |
required |
price_column
|
str
|
Market-data column read as the constituent price when
computing market values. Defaults to |
'CLOSE'
|
resolve_universe ¶
Resolve the definition's universe identifiers into Asset objects.
The public entry point for universe resolution, for callers outside
the calculation loop (the constituent preview, for one). The reference
data is read once for the whole universe, and each resolved name
becomes an :class:~beacon.asset.equity.Equity built from its own
row. An identifier the reference data does not know is skipped with a
warning.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
date
|
Timestamp
|
Point-in-time date for the reference-data lookup. |
required |
Returns:
| Type | Description |
|---|---|
list[Asset]
|
list[Asset]: Assets for every identifier that resolved, in the |
list[Asset]
|
order the definition names them. |
Raises:
| Type | Description |
|---|---|
CalculationError
|
If the definition names no universe at all. |
select_constituents ¶
Select index constituents from a universe by the eligibility rules.
A thin projection of :meth:select_with_provenance: the survivors, with
the record of which rule removed each excluded name discarded. Callers
wanting that record (the preview waterfall, anything answering "why is
this name missing") should use the fuller method rather than repeating
the walk.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
universe
|
list[Asset]
|
A list of potential Asset objects to consider for inclusion. |
required |
current_date
|
Timestamp
|
The date for which selection is being made. |
required |
Returns:
| Type | Description |
|---|---|
list[Asset]
|
A list of Asset objects that are eligible for the index. |
select_with_provenance ¶
Select constituents, keeping the record of how the universe narrowed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
universe
|
list[Asset]
|
A list of potential Asset objects to consider for inclusion. |
required |
current_date
|
Timestamp
|
The date for which selection is being made. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
SelectionResult |
SelectionResult
|
Survivors, one step per rule, and the position of |
SelectionResult
|
the rule that excluded each removed asset. |
calculate_constituent_weights ¶
calculate_constituent_weights(
constituents: list[Asset], current_date: Timestamp
) -> dict[Asset, float]
Weight the constituents by the index's weighting scheme.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
constituents
|
list[Asset]
|
A list of Asset objects that are part of the index. |
required |
current_date
|
Timestamp
|
The date for which weights are calculated. |
required |
Returns:
| Type | Description |
|---|---|
dict[Asset, float]
|
A dictionary mapping each Asset to its weight, summing to 1. Empty |
dict[Asset, float]
|
(with a warning) when constituents is empty. |
Raises:
| Type | Description |
|---|---|
CalculationError
|
If the scheme refuses (an unpriced constituent, an unknown share count, a market cap of zero), in which case it propagates exactly as the scheme raised it, remedy and all. Also if the scheme's weights do not sum to 1: rescaling them would publish an allocation the scheme did not produce under the scheme's own name. |
UnexpectedCalculationError
|
If the scheme raises anything else. A crash, not a decision, and it carries its own published code so a client does not read it as a refusal. |
cap_weights ¶
Apply the definition's cap, returning the weights and a report.
Capping happens here rather than inside a weighting scheme so that it
composes with every scheme, and it returns its report rather than
storing one so the calculator stays stateless and run() stays
idempotent.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
weights
|
dict[Asset, float]
|
Normalised weights keyed by Asset. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
tuple |
dict[Asset, float]
|
The capped weights and a CapReport. With no cap configured |
CapReport
|
the weights are returned unchanged and the report is empty. |
initialize_divisor ¶
Calculate the index's initial divisor on its base date.
divisor = initial_total_market_value / base_value.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
initial_total_market_value
|
float
|
The sum of (price * shares * fx_rate * free_float_if_applicable) for all base constituents on the base_date, expressed in index currency. |
required |
Returns:
| Type | Description |
|---|---|
float
|
The initial divisor as a float. |
Raises:
| Type | Description |
|---|---|
CalculationError
|
If initial_total_market_value or the definition's base value is not positive. |
adjust_divisor_for_rebalance
staticmethod
¶
adjust_divisor_for_rebalance(
old_divisor: float,
old_market_value: float,
new_market_value: float,
) -> float
Adjust the divisor to maintain index level continuity across a rebalance.
When index composition or weights change, the total market value shifts. To prevent an artificial jump in the index level the divisor is scaled:
new_divisor = old_divisor * (new_market_value / old_market_value)
This guarantees: level_before == level_after.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
old_divisor
|
float
|
The divisor in effect before the rebalance. |
required |
old_market_value
|
float
|
Aggregate market value under the old composition. |
required |
new_market_value
|
float
|
Aggregate market value under the new composition. |
required |
Returns:
| Type | Description |
|---|---|
float
|
The adjusted divisor. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If old_divisor, old_market_value or new_market_value is zero or negative. |
run ¶
Run the full index calculation over a date range.
Iterates the index's own trading sessions (the definition's calendar, not Monday to Friday) from start_date to end_date, handling three day types:
- Base date: resolve universe, select constituents, compute weights, apply the cap, initialise divisor, set level = base_value. Rolled forward to the first session when the base date itself was not one.
- Rebalance date: reinvest the day's distributions (total-return indices), then reconstitute (re-resolve universe, re-select, re-weight, re-cap) and adjust the divisor for continuity. With an announcement lag, the composition is selected and weighted as of the announcement date and applied on the effective date.
- Regular day: drop any holding that stopped being listed, reinvest distributions (total-return indices), and compute the level from the units held.
The method is idempotent: it carries no state between calls. When the
calendar covers only part of the window, the run is calculated over
the covered part and the result's calendar_coverage says so; a
window with no sessions at all (a single weekend, say) gives an empty
result.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
start_date
|
str | None
|
First calculation date (YYYY-MM-DD). Defaults to
|
None
|
end_date
|
str | None
|
Last calculation date (YYYY-MM-DD). Required. |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
An |
IndexResult
|
class: |
IndexResult
|
constituent snapshots, weight snapshots, cap reports, announcement |
|
IndexResult
|
dates and the daily weights panel (one row per constituent per |
|
IndexResult
|
day, recorded as the loop goes, since the state it holds each day |
|
IndexResult
|
is path-dependent and cannot be reconstructed from the rebalance |
|
IndexResult
|
snapshots afterwards). The result is bound to the data source. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If end_date is not provided or precedes the base date. |
CalculationError
|
If the dataset lacks a column the definition reads (checked before any work, rather than discovered at the first read and reported as one company's problem), if the calendar can cover none of the window, if the index holds nothing on its base date, or if a rule, scheme or valuation refuses. |
require_columns ¶
Refuse up front if the dataset cannot support this definition.
Public because the constituent preview runs a definition without
calling run, and deserves the same answer: a preview of a
market-cap index over a store with no share counts should say so,
not fail on the first name it tries to price.
Raises:
| Type | Description |
|---|---|
CalculationError
|
Naming each missing column and what needs it. |
run_daily_calculation ¶
run_daily_calculation(
current_date: Timestamp,
constituents: list[Asset],
weights: dict[Asset, float],
previous_index_level: float,
previous_divisor: float,
) -> tuple[float, float]
Runs a single day's index calculation process.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
current_date
|
Timestamp
|
The date for which to perform calculations. |
required |
constituents
|
list[Asset]
|
Current index constituents. |
required |
weights
|
dict[Asset, float]
|
Current constituent weights. |
required |
previous_index_level
|
float
|
Index level from the previous period. |
required |
previous_divisor
|
float
|
Divisor from the previous period. |
required |
Returns:
| Type | Description |
|---|---|
tuple[float, float]
|
Tuple of (new_index_level, new_divisor). |
weight_rows ¶
weight_rows(
date: Timestamp,
units: dict[Asset, float],
values: dict[Asset, float],
) -> list[dict[str, object]]
One record per constituent for one day: what was held, and its share.
The weights are realised shares of the day's aggregate (value over total), so they drift with prices between rebalances, and they renormalise the moment a name is deleted. That is why the panel is recorded as the calculation runs rather than derived later from the rebalance snapshot: the two only agree on the rebalance date itself.
Plain dicts, appended during the run and converted once at the end with
:func:~beacon.index.result.daily_weights_frame.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
date
|
Timestamp
|
The calculation day. |
required |
units
|
dict[Asset, float]
|
What the index holds after the day's events. |
required |
values
|
dict[Asset, float]
|
Those holdings valued for date, from
:meth: |
required |
Returns:
| Name | Type | Description |
|---|---|---|
list |
list[dict[str, object]]
|
Records keyed by :data: |
list[dict[str, object]]
|
A day whose holdings are worth nothing at all records nothing, because |
|
list[dict[str, object]]
|
it has no weights to record: the level is carried forward on such a |
|
list[dict[str, object]]
|
day, and a row of zeros would read as "held nothing" rather than |
|
list[dict[str, object]]
|
"could not be valued". |
corporate_actions ¶
CorporateActionsMixin: adjusting the index divisor in response to corporate actions.
CorporateActionsMixin ¶
Corporate-action logic, mixed into IndexCalculator: share-count changes to the units held, and divisor adjustment for a special dividend.
ratio_schedule ¶
Every split and stock dividend in the data, by ex-date and name.
Returns:
| Name | Type | Description |
|---|---|---|
dict |
dict[Timestamp, dict[str, float]]
|
ex-date -> {identifier: share-count multiplier}. Empty when |
dict[Timestamp, dict[str, float]]
|
the data holds no action history. |
apply_ratios
staticmethod
¶
apply_ratios(
units: dict[Asset, float],
schedule: dict[Timestamp, dict[str, float]],
after: Timestamp,
through: Timestamp,
) -> dict[Asset, float]
The units held, after any split or stock dividend since after.
On the ex-date the stored close falls by the ratio, so the units rise by it and the index's value, level and divisor are unchanged.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
units
|
dict[Asset, float]
|
What the index holds. Not mutated. |
required |
schedule
|
dict[Timestamp, dict[str, float]]
|
Output of :meth: |
required |
after
|
Timestamp
|
The last day already calculated (excluded). |
required |
through
|
Timestamp
|
Today (included). |
required |
Returns:
| Name | Type | Description |
|---|---|---|
dict |
dict[Asset, float]
|
The units, scaled where a ratio applied. The same mapping |
dict[Asset, float]
|
when none did. |
handle_corporate_action ¶
handle_corporate_action(
action: dict[str, Any],
constituents: list[Asset],
current_total_market_value_before_ca: float,
current_divisor_before_ca: float,
) -> float
Adjust the index divisor for a corporate action to maintain continuity.
Supports SPECIAL_DIVIDEND. Other recognised types
(RIGHTS_ISSUE, SPIN_OFF, STOCK_DIVIDEND, MERGER) are
not implemented and are refused.
Returning the divisor unchanged is this method's answer only for an action that genuinely has no effect on the index: one affecting a name the index does not hold, or one whose adjustment rounds to nothing. A malformed action, an unimplemented type or an unknown type is refused instead, because an unadjustable action and a harmless one must not be spelled the same: the second is safe to publish and the first leaves the level wrong from that day onward.
For a special dividend the market-value reduction is::
reduction = dividend_per_share * shares_outstanding * ff * fx
where ff is the free-float factor (only when the weighting scheme
is free-float adjusted) and fx converts the name's currency into
the index currency on the ex-date under the dataset's FX policy. The
new divisor is::
new_divisor = old_divisor * (mv_after / mv_before)
where mv_after = mv_before - reduction.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
action
|
dict[str, Any]
|
Dictionary with keys |
required |
constituents
|
list[Asset]
|
Current index constituents. |
required |
current_total_market_value_before_ca
|
float
|
Aggregate market value of all constituents just before the action takes effect. |
required |
current_divisor_before_ca
|
float
|
Divisor in effect before this action. |
required |
Returns:
| Type | Description |
|---|---|
float
|
The (possibly adjusted) divisor. |
Raises:
| Type | Description |
|---|---|
CalculationError
|
If the action is malformed (no ex_date, asset or value), if its type is unknown or recognised but not implemented, if the affected asset is a constituent and is not an equity, or if a special dividend cannot be sized (no shares outstanding, no usable free-float factor, no FX rate) or would leave the index worth nothing. |
deletions ¶
Removing a constituent that stopped being one part-way through a period.
An index reconstitutes on its rebalance dates, and between them it holds fixed units. That works until a constituent is acquired, fails, or is otherwise delisted, because from that day on there is no price, and the holding cannot be valued at all.
Why a deletion is needed¶
A holding with no price is valued at 0.0, which is the right answer to "what is this worth today" and the wrong basis for an index level. A name that was 4% of the index would simply stop contributing, so the level would fall 4% on the day it delists and never recover it: a loss that no holder experienced, since in reality the position was sold, at a price, and the proceeds stayed in the fund.
What a deletion is¶
The same divisor adjustment a rebalance uses. Value the book on the last day the leaver had a price, once including it and once without:
divisor ← divisor × (aggregate without) / (aggregate with)
The level is then identical across the change, which is the entire purpose of a divisor. Holdings of the survivors are untouched, so their weights renormalise upward in proportion, which is exactly what reinvesting the proceeds pro rata across the remainder would have done.
A holding is removed on the first calculation day after its last listed date, including a rebalance day, where it leaves before the outgoing book is valued. If every holding would be removed, the holdings are kept (and an error is logged) rather than emptying the index. If the book cannot be valued on the last day, the leavers are removed without a divisor adjustment and the level steps (logged as a warning).
Why the reference data decides, not the price¶
A missing price and a delisting are different things. A gap in a feed is a
data-quality problem, and carrying the last level forward (which
level_from_units does) is the right response. A name whose reference record
has ended is gone, and holding it forever is wrong. Reading the reference
data's DATE_TO, rather than guessing from absent prices, keeps those two
apart.
DeletionMixin ¶
Intra-period constituent deletion, mixed into IndexCalculator.
aggregate_value ¶
Provided by MarketValuesMixin; declared so this one type-checks.
delisting_schedule ¶
The last date each identifier is listed, for those that end.
Built once per run rather than queried per holding per day: a five-thousand-name index over ten years would otherwise make twelve million reference lookups to find a few hundred deletions.
Returns:
| Name | Type | Description |
|---|---|---|
dict |
dict[str, Timestamp]
|
identifier -> last listed date. Names still listed at the |
dict[str, Timestamp]
|
end of their record are absent, so an empty mapping means nothing |
|
dict[str, Timestamp]
|
ever leaves and the daily check costs one |
apply_deletions ¶
apply_deletions(
units: dict[Asset, float],
divisor: float,
date: Timestamp,
schedule: dict[str, Timestamp],
valuation_date: Timestamp,
) -> tuple[dict[Asset, float], float, list[str]]
Drop any holding whose listing ended, keeping the level continuous.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
units
|
dict[Asset, float]
|
What the index holds. Not mutated. |
required |
divisor
|
float
|
The divisor in force. |
required |
date
|
Timestamp
|
Today. |
required |
schedule
|
dict[str, Timestamp]
|
Output of :meth: |
required |
valuation_date
|
Timestamp
|
The last date the leavers still had prices, normally the previous trading day. Both aggregates are taken here, so the ratio is a like-for-like comparison rather than one that mixes today's prices with yesterday's. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
tuple |
dict[Asset, float]
|
The surviving holdings, the adjusted divisor, and the |
float
|
identifiers removed. All three are unchanged when nothing left. |
market_values ¶
MarketValuesMixin: constituent market values, holding values and the index level.
MarketValuesMixin ¶
Market-value and index-level calculation logic, mixed into IndexCalculator.
rate_on ¶
An FX rate on a date, through :meth:DataFetcher.fx_rate_on.
The calculator's name for the library's one FX lookup, so the levels, the weights and the reference display all convert currency the same way.
Returns:
| Type | Description |
|---|---|
float | None
|
float | None: The rate as of |
float | None
|
under the dataset's FX policy, or None when the pair is unknown, |
float | None
|
which callers treat as "cannot convert" rather than as a rate of |
float | None
|
one. |
asset_unit_value ¶
Value of one unit of asset in the index currency: price times FX.
Distinct from a constituent's market value, which multiplies by shares outstanding and free float. Those belong to the weighting of the index; this is what one unit is worth, which is what the index's holdings are valued at day to day.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
asset
|
Asset
|
The constituent. Must be an Equity. |
required |
current_date
|
Timestamp
|
Valuation date. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
float | None
|
float | None: The price in index currency, or None when the name |
|
float | None
|
could not be priced on current_date. None rather than 0.0, |
|
float | None
|
because "could not be priced" and "priced at zero" are different |
|
answers |
float | None
|
:meth: |
float | None
|
meth: |
Raises:
| Type | Description |
|---|---|
CalculationError
|
If asset is not an equity, or if its currency cannot be converted into the index's. Valuing either at 0.0 would make a constituent the index still holds contribute nothing, which is a silent restatement of the index rather than a missing price. |
Errors from the data source are not caught: a fetcher that raised and a name that is genuinely unvaluable must not look the same.
index_units ¶
index_units(
weights: dict[Asset, float],
aggregate: float,
current_date: Timestamp,
) -> dict[Asset, float]
Units of each constituent the index holds to realise weights.
The index holds a fixed number of units of each constituent between rebalances, which is what makes weights drift with relative performance rather than being silently reset every day.
Units are set so that unit_value * units is weight of
aggregate, which makes the weights exactly right on the rebalance
date and lets them move from there.
For a market-capitalisation weighting this reduces to shares outstanding (the weight is itself the share of aggregate market value), so that methodology gives the same levels as holding shares outstanding directly.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
weights
|
dict[Asset, float]
|
Target weight per constituent, summing to 1. |
required |
aggregate
|
float
|
Total value the index represents on this date. |
required |
current_date
|
Timestamp
|
Rebalance date. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
dict |
dict[Asset, float]
|
Units per constituent. A constituent priced at exactly zero |
dict[Asset, float]
|
gets zero units rather than an infinite position, and so does one |
|
dict[Asset, float]
|
carried at a target weight of zero that could not be priced. |
Raises:
| Type | Description |
|---|---|
CalculationError
|
If a constituent the index allocates weight to could not be priced at all, or was priced below zero. A name genuinely quoted at zero is a fact about a market; a missing price is the absence of one. Holding zero units of a name with a target weight would leave the index short by that whole weight and publish the shortfall as the index's own level. The optimised (chained) path makes the same refusal. |
holding_values ¶
What each holding is worth today: units times unit value.
Separate from :meth:aggregate_value because the daily weights panel
needs the parts as well as the total, and a part costs a market-data
lookup: computing them twice would double the lookups a run makes,
which is its dominant cost.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
units
|
dict[Asset, float]
|
What the index holds, asset to unit count. |
required |
current_date
|
Timestamp
|
Valuation date. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
dict |
dict[Asset, float]
|
Value per holding in the index currency. A name with no |
dict[Asset, float]
|
price today is worth 0.0 (with a warning) and still appears, |
|
dict[Asset, float]
|
because it is still held: a feed gap is a data-quality problem, |
|
dict[Asset, float]
|
and carrying the level forward (which :meth: |
|
dict[Asset, float]
|
does) is the right response to one. |
Raises:
| Type | Description |
|---|---|
CalculationError
|
If a holding is not an equity, or a priced holding's currency cannot be converted into the index's. |
aggregate_value ¶
Total value of the index's holdings: units times unit value.
level_from_units ¶
level_from_units(
units: dict[Asset, float],
divisor: float,
current_date: Timestamp,
previous_index_level: float,
values: dict[Asset, float] | None = None,
) -> float
Index level on an ordinary day: holdings value over the divisor.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
units
|
dict[Asset, float]
|
What the index holds, fixed since the last rebalance. |
required |
divisor
|
float
|
Current divisor. |
required |
current_date
|
Timestamp
|
Valuation date. |
required |
previous_index_level
|
float
|
Carried forward when the index cannot be valued today, so a missing price shows as a flat day rather than a collapse to zero. |
required |
values
|
dict[Asset, float] | None
|
Holdings already valued for current_date, from
:meth: |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
float |
float
|
The index level. |
Raises:
| Type | Description |
|---|---|
CalculationError
|
If the divisor is not positive. |
The previous level is carried forward on a day on which the data has nothing to say: when there are no holdings (a rebalance that selected no constituents), or when the holdings are worth nothing (every holding unpriced on one date).
calculate_index_level ¶
calculate_index_level(
current_date: Timestamp,
constituents: list[Asset],
weights: dict[Asset, float],
divisor: float,
previous_index_level: float,
) -> tuple[float, float]
Calculate the index level from constituent market values.
A Laspeyres-type formula: the sum of the constituents' current market
values (price times shares, times free float when the scheme is
float-adjusted, converted into the index currency) over the divisor.
run does not use this; it values the units the index holds, through
:meth:level_from_units.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
current_date
|
Timestamp
|
The date for which to calculate the index level. |
required |
constituents
|
list[Asset]
|
Current index constituents. |
required |
weights
|
dict[Asset, float]
|
Current constituent weights. |
required |
divisor
|
float
|
The current index divisor. |
required |
previous_index_level
|
float
|
The index level from the previous calculation period. |
required |
Returns:
| Type | Description |
|---|---|
float
|
A tuple of (new_index_level, divisor). With no constituents the |
float
|
previous level is returned. |
Raises:
| Type | Description |
|---|---|
CalculationError
|
If the divisor is not positive, a constituent is not an equity, or a currency cannot be converted. |
selection ¶
Constituent selection, and the record of how it happened.
One function, select_with_provenance, answers "which assets are eligible",
and it answers it by walking the rules in order and narrowing the universe a
rung at a time, keeping a note of which rule removed each name as it goes.
Before any rule runs, names with no trade within the data source's
max_price_staleness_days are dropped at their own rung
(STALENESS_POSITION, named StalePrice), so no rule evaluates a stale close.
Why provenance is the general form¶
Survivors fall out of the provenance for free; provenance cannot be recovered from a list of survivors. So the calculator and the preview waterfall both use this one walk, and a preview cannot disagree with the run it is previewing.
The rule-outer loop also leaves room for a rule that ranks (the largest
hundred by market capitalisation, at most ten per sector), which needs to
see the set it is choosing from. is_eligible is a per-asset predicate, so no
rule ranks yet, but this loop structure can accommodate one where an
asset-outer loop could not, because it never has a set in hand.
Rules are identified by position, not by name¶
A rule object carries a rule_name, which is its type ("MarketCapRule"),
and an index definition may hold several of the same type. Stable per-rule
identifiers exist only in the server's stored document, which is the server's
concern and not this layer's. So provenance here is keyed by position in the
rule list, and a caller that has its own identifiers maps position to them.
SelectionStep
dataclass
¶
One rung of the selection funnel.
Attributes:
| Name | Type | Description |
|---|---|---|
position |
int
|
1-based index of the rule, UNIVERSE_POSITION (0) for the starting universe, or STALENESS_POSITION (-1) for the stale-price rung. |
rule_name |
str
|
Type of the rule applied ( |
remaining |
int
|
How many assets survived this rung. |
excluded |
list[str]
|
Identifiers this rung removed, sorted. Empty for the universe rung. |
SelectionResult
dataclass
¶
SelectionResult(
survivors: list[Asset],
steps: list[SelectionStep],
exclusions: dict[str, int] = dict(),
)
Which assets survived selection, and how each one fared.
Attributes:
| Name | Type | Description |
|---|---|---|
survivors |
list[Asset]
|
Assets that passed every rule, in universe order. |
steps |
list[SelectionStep]
|
One entry per rung, starting with the universe, then the stale-price rung when any name was stale, then one per rule. |
exclusions |
dict[str, int]
|
Identifier to the position of the rule that removed it (STALENESS_POSITION for a name dropped as stale). Each excluded asset appears exactly once: an asset leaves the surviving set the moment it fails, so no later rule ever sees it and no name can be blamed on two rules. That single-owner property is what makes the funnel answer "why is this name missing" rather than only "how many are left". |
rule_steps
property
¶
Every rung after the universe, including any stale-price rung.
excluded_by ¶
The rung that removed an asset.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
asset_id
|
str
|
The identifier to look up. |
required |
Returns:
| Type | Description |
|---|---|
SelectionStep | None
|
SelectionStep or None: The rung, or None if the asset survived or |
SelectionStep | None
|
was never in the universe. |
select_with_provenance ¶
select_with_provenance(
universe: list[Asset],
rules: list[EligibilityRuleBase],
current_date: Timestamp,
data_fetcher: DataFetcher,
context: IndexContext | None = None,
) -> SelectionResult
Narrow a universe to its eligible constituents, recording each step.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
universe
|
list[Asset]
|
Assets to select from. |
required |
rules
|
list[EligibilityRuleBase]
|
Eligibility rules, applied in order. Each rule sees only what survived the ones before it. |
required |
current_date
|
Timestamp
|
The date to evaluate at. |
required |
data_fetcher
|
DataFetcher
|
Data source the rules read from. |
required |
context
|
IndexContext | None
|
What the index settles for its rules: its currency, so a bound stated in it is compared against a converted figure rather than a local one. None outside an index, and then a rule that needs it says so rather than assuming one. |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
SelectionResult |
SelectionResult
|
Survivors, the funnel, and per-asset provenance. |
Raises:
| Type | Description |
|---|---|
Exception
|
Whatever a rule raises, unchanged. A rule that could not run has not excluded anything, so its failure propagates rather than being recorded as an exclusion. |
total_return ¶
Total-return and net-total-return accumulation.
On an ex-dividend date a constituent's price drops, the aggregate drops with
it, and a price index (PRICE, the default return type) falls by the whole
distribution. That is what a price index is supposed to do, and it is why a
price index understates what an investor actually earned. TOTAL_RETURN and
NET_TOTAL_RETURN indices reinvest cash distributions from the data source's
corporate-action history.
The construction: a divisor adjustment, not a purchase¶
The tempting implementation is to buy more units of the paying constituent with its own dividend. That is wrong twice over: it silently re-weights the index towards whatever paid, and it makes the composition depend on the return type, so a price and a total-return version of one index would hold different things.
Index providers do it with the divisor instead. On an ex-date, with aggregate
holdings value A and total cash received D:
divisor_new = divisor_old x A / (A + D)
The level that day is then A / divisor_new == (A + D) / divisor_old (the
drop is exactly offset), and because the divisor is permanently smaller, every
later level is scaled up in the same proportion. That is reinvestment across
the index, and it leaves the units untouched. Distributions paid in another
currency are converted into the index currency first; a missing FX pair
refuses.
Only cash actions, and only once¶
A split is a ratio action: it changes the share count and the price together
and distributes nothing. Reinvesting a split ratio as though it were cash would
inflate the index by a factor of two. A rights issue, spin-off or merger is
structural and carries no directly aggregable value at all. So the filter is
kind == "cash", using the classification the data layer publishes rather
than a list of type strings kept here. A name paying two cash distributions on
one ex-date receives both.
The price path must already contain the drop for this to be right. It does for a real feed, which quotes prices unadjusted, and for Beacon's synthetic data, whose generator applies the ex-date drop when it builds the close. Adding the cash back to an aggregate that never fell would double-count the distribution.
Net return¶
The same, with a flat withholding rate applied to the cash: D x (1 - rate).
A flat index-level rate rather than a per-country table, because a table keyed
off reference data is only as good as the country field behind it, and an
unpopulated table produces a number that looks precise and is not. The rate is
a property of the index and stated on it.
TotalReturnMixin ¶
Dividend reinvestment, mixed into IndexCalculator.
rate_on ¶
Provided by MarketValuesMixin; declared so this one type-checks.
cash_distribution_schedule ¶
Every cash distribution the data source holds, by date and name.
Built once per run rather than queried per constituent per day: a five-hundred-name index over five years would otherwise make more than half a million lookups to find a few thousand dividends.
Returns:
| Name | Type | Description |
|---|---|---|
dict |
dict[Timestamp, dict[str, float]]
|
ex-date -> {identifier: cash per share}. Empty when the data |
dict[Timestamp, dict[str, float]]
|
source holds no action history. |
distribution_received
staticmethod
¶
distribution_received(
units: dict[Asset, float],
per_share: dict[str, float],
withholding: float = 0.0,
rates: dict[str, float] | None = None,
) -> float
Cash the index's holdings receive on one date, in index currency.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
units
|
dict[Asset, float]
|
What the index holds, asset to unit count. |
required |
per_share
|
dict[str, float]
|
Cash per share by identifier, for this date, quoted in the paying company's own currency. |
required |
withholding
|
float
|
Fraction withheld; 0.0 for a gross index. |
0.0
|
rates
|
dict[str, float] | None
|
Identifier to FX rate into the index currency, as returned
by :meth: |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
float |
float
|
Total cash, net of withholding. Zero when nothing paid. |
distribution_rates ¶
distribution_rates(
per_share: dict[str, float],
units: dict[Asset, float],
date: Timestamp,
index_currency: str,
) -> dict[str, float]
FX rates into the index currency, for the names paying on a date.
Dividends are quoted in the paying company's currency while the aggregate they are reinvested into is in the index's, so the two cannot be combined until one is converted.
Only names the index holds that actually paid are looked up, so a quiet day costs nothing. A name already quoted in the index currency gets no entry (it converts at 1.0).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
per_share
|
dict[str, float]
|
Cash per share by identifier, for this date. |
required |
units
|
dict[Asset, float]
|
What the index holds, asset to unit count. |
required |
date
|
Timestamp
|
The ex-date. |
required |
index_currency
|
str
|
The currency the index reports in. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
dict |
dict[str, float]
|
Identifier to FX rate into the index currency. |
Raises:
| Type | Description |
|---|---|
CalculationError
|
If a paying name's pair is unknown. A level cannot publish "unknown" for one constituent and stay a level, so the calculation refuses rather than reinvest the local number as though it were in the index currency. |
reinvest
staticmethod
¶
Shrink the divisor so a distribution is reinvested across the index.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
divisor
|
float
|
The divisor in force. |
required |
aggregate
|
float
|
Holdings value on the ex-date, after the price drop. |
required |
distribution
|
float
|
Cash received, net of any withholding. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
float |
float
|
The new divisor. Unchanged when nothing was distributed, or |
float
|
when the aggregate or divisor is non-positive: an index with no |
|
float
|
value cannot reinvest into itself, and scaling by zero would |
|
float
|
destroy the divisor rather than adjust it. |
withholding_for ¶
The fraction of each distribution withheld.
Zero for a gross total-return index however the rate is set, so a definition carrying a rate it does not use cannot quietly apply it.
capping ¶
Weight capping.
Capping limits how much of an index any single constituent may represent, for UCITS 5/10/40, concentration control or investability. It is orthogonal to how the base weights were derived, so it lives here rather than inside any one weighting scheme, and composes with all of them.
Capping is iterative, not a single pass: reducing a breaching name to the cap redistributes its excess across the others, which can push a previously compliant name above the cap.
CapReport
dataclass
¶
CapReport(
cap: float | None = None,
capped: dict[str, float] = dict(),
redistributed: float = 0.0,
passes: int = 0,
uncapped_weights: dict[str, float] = dict(),
)
What capping did, for the layers that need to show it.
Attributes:
| Name | Type | Description |
|---|---|---|
cap |
float | None
|
The maximum weight applied, or None when no cap was requested. |
capped |
dict[str, float]
|
Identifiers held at the cap, mapped to the weight each would have had uncapped. The difference is what was redistributed. |
redistributed |
float
|
Total weight moved off capped names and onto the rest. |
passes |
int
|
Iterations the loop took to settle. 0 means nothing breached. |
uncapped_weights |
dict[str, float]
|
The full weight vector before capping. Kept so the
counterfactual (what the index would have returned uncapped) can be
computed later. Reconstructing it from |
minimum_feasible_cap ¶
The smallest cap that can still distribute a full unit of weight.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
count
|
int
|
Number of constituents. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
float |
float
|
|
float
|
every name would sit at the cap and the total would still fall short |
|
float
|
of 1.0. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If count is not positive. |
apply_cap ¶
Cap constituent weights, redistributing the excess pro rata.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
weights
|
dict[str, float]
|
Base weights, expected to sum to 1.0. Keys are identifiers. |
required |
cap
|
float | None
|
Maximum weight for any one constituent, or None for no capping. A cap of 1.0 is a no-op, as is any cap at or above the largest weight. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
tuple |
dict[str, float]
|
The capped weights (summing to 1.0) and a CapReport describing |
CapReport
|
what happened. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If cap is not in (0, 1]. |
CalculationError
|
If the cap is infeasible for this many constituents, or if the iteration fails to settle within MAX_PASSES. |
chaining ¶
Weight-rebalanced level chaining, over identifiers.
The arithmetic that turns a schedule of target weights into a daily level path: units are fixed between rebalances so weights drift with relative performance, each rebalance rebuilds the units at the value the old holdings reached (which keeps the level continuous across it), and prices are converted into the index currency with as-of FX rates, exactly as the calculator and the engine convert theirs. The divisor is 1.0 from the first rebalance on, because the aggregate this represents is its own portfolio value and there is no market-value scale for a divisor to absorb.
Nothing here knows what a derived index is: chain_levels takes an index's
identity, a parent calculation and a solved schedule, and serves any caller
with weights to chain. It reimplements IndexCalculator's arithmetic over
identifiers rather than reusing its mixins, which are built around Asset
objects and per-day price lookups; the economics are the same.
chain_levels ¶
chain_levels(
index_id: str,
base_value: float,
currency: str,
parent: IndexResult,
solved: dict[Timestamp, dict[str, float]],
data_provider: DataFetcher,
price_column: str,
) -> IndexResult
Chain the solved weights into daily levels on the parent's calendar.
The weight-rebalanced arithmetic the calculator applies, restated over identifiers: units are fixed between rebalances, each rebalance rebuilds them at the value the old holdings reached (which keeps the level continuous), and the path starts at base_value on the first rebalance. The divisor is 1.0 from then on (0.0 on any earlier day): the aggregate this index represents is its own portfolio value, so there is no market-value scale for a divisor to absorb.
A day on which the holdings cannot be valued at all carries the level forward and records no weights, matching the calculator's behaviour.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
index_id
|
str
|
Identifier of the resulting index. |
required |
base_value
|
float
|
The level on the first rebalance. |
required |
currency
|
str
|
The index currency; prices are converted into it. |
required |
parent
|
IndexResult
|
The calculation whose days the path follows. |
required |
solved
|
dict[Timestamp, dict[str, float]]
|
Target weights by rebalance date. |
required |
data_provider
|
DataFetcher
|
Source of prices, reference data and FX rates. |
required |
price_column
|
str
|
The market-data column holdings are valued with. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
IndexResult |
IndexResult
|
Levels, divisors, rebalance snapshots and the daily |
IndexResult
|
weights panel. It carries no cap reports or announcement dates. |
Raises:
| Type | Description |
|---|---|
CalculationError
|
If a name the schedule allocates to (at a non-zero weight) has no prices in the window, is quoted in a currency with no rate into the index currency, or has no usable price on a rebalance day. A name carried at a weight of zero is never held, so missing data for it is only logged. |
constructor ¶
IndexDefinition: the static rules of an index.
IndexDefinition ¶
IndexDefinition(
index_id: str,
index_name: str,
base_date: str,
base_value: float,
currency: str,
eligibility_rules: list[EligibilityRuleBase],
weighting_scheme: WeightingSchemeBase,
rebalancing_frequency: str,
calendar: str,
description: str | None = None,
universe_identifiers: list[str] | None = None,
max_constituent_weight: float | None = None,
rebalance_day_rule: str = DEFAULT_DAY_RULE,
return_type: str = PRICE,
withholding_tax_rate: float = 0.0,
effective_lag_sessions: int = 0,
)
Defines the static characteristics and rules for constructing an index.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
index_id
|
str
|
A unique identifier for the index. |
required |
index_name
|
str
|
The common name of the index. |
required |
base_date
|
str
|
The date from which the index calculation begins (YYYY-MM-DD). |
required |
base_value
|
float
|
The initial value of the index on its base_date. Must be positive. |
required |
currency
|
str
|
The currency of the index (stored upper-cased). |
required |
eligibility_rules
|
list[EligibilityRuleBase]
|
A list of EligibilityRuleBase objects that define criteria for constituent selection. An empty list is allowed but logged as a warning. |
required |
weighting_scheme
|
WeightingSchemeBase
|
A WeightingSchemeBase object that defines how constituents are weighted. |
required |
rebalancing_frequency
|
str
|
How often the index is rebalanced:
|
required |
calendar
|
str
|
Exchange MIC backing trading-day arithmetic, e.g.
|
required |
description
|
str | None
|
Optional textual description of the index. |
None
|
universe_identifiers
|
list[str] | None
|
Optional list of string identifiers (e.g. tickers, ISINs) defining the asset universe from which constituents are selected. When given, it must not be empty. An index calculated with none is refused. |
None
|
max_constituent_weight
|
float | None
|
Optional cap on any single constituent's weight, as a fraction (0.1 is 10%), in (0, 1]. Applied after the weighting scheme and iterated until no constituent breaches it. None means uncapped. |
None
|
rebalance_day_rule
|
str
|
Which day of a scheduled month the rebalance
falls on: |
DEFAULT_DAY_RULE
|
return_type
|
str
|
|
PRICE
|
withholding_tax_rate
|
float
|
Fraction of each distribution withheld, for a net index, in [0, 1). Ignored unless the return type is NET_TOTAL_RETURN, so a definition carrying a rate it does not use cannot quietly apply it. |
0.0
|
effective_lag_sessions
|
int
|
Sessions between a composition being announced and its weights taking effect. Zero (the default) is same-day. Must not be negative. |
0
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If a required argument is empty or out of range, or the day rule or return type is not supported. |
get_rebalance_dates ¶
Every rebalance date within [start_date, end_date].
Follows the index's rebalancing frequency, day rule and calendar (see
beacon.index.schedule). The calendar is always a real exchange
calendar, so a date this returns is always a date the exchange has a
session for. The cadence is anchored on the first scheduled date in
the range.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
start_date
|
str
|
Start of the range (YYYY-MM-DD), inclusive. |
required |
end_date
|
str
|
End of the range (YYYY-MM-DD), inclusive. |
required |
Returns:
| Type | Description |
|---|---|
list[Timestamp]
|
A chronologically sorted list of rebalance dates, each a session on |
list[Timestamp]
|
the index's calendar. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the rebalancing frequency is unsupported. |
next_rebalance ¶
The first rebalance strictly after a date.
Anchored on the base date, as a calculation run from the base date is, so the answer names a day the index would genuinely rebalance on.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
as_of
|
str
|
The date being asked from, YYYY-MM-DD. |
required |
Returns:
| Type | Description |
|---|---|
Timestamp | None
|
The date, or None if none falls within the lookahead window. |
context ¶
What a methodology rule knows about the index it is running inside.
IndexContext
dataclass
¶
The index's own settings, handed to its rules and weighting scheme.
The calculator passes one to every eligibility rule and weighting scheme
as their context argument, so a rule can see what it needs about the
index it runs inside. For example, a market-cap weighting needs the index
currency to compare a yen cap with a dollar one.
Attributes:
| Name | Type | Description |
|---|---|---|
currency |
str
|
The currency the index reports in, upper-cased. Money amounts in a methodology (a market-cap bound, the caps a weighting compares) are denominated in it. |
derived ¶
Optimised indices: an index derived from an index you already built.
Create an index, then optimise it (an objective function and constraints), and that creates a new index. The derivation stores exactly three things, the source index, the objective and the constraints, plus the usual identity attributes. No weights are stored anywhere: definitions are rules, weights are calculated, and calculations are cached.
Two consequences shape this module:
- Rebalancing follows the parent. The child solves exactly at the parent's published snapshots; a frequency of its own would have no parent weights at the extra dates. The derived definition therefore declares no schedule.
- The calculation is a normal IndexResult. Solve the parent's weights at each rebalance, then chain the solved weights into daily levels on the parent's calendar, so the level path equals compounding the solved-weight portfolio's returns from the parent's price data.
:class:OptimisedIndexDefinition is a sibling of
:class:~beacon.index.constructor.IndexDefinition rather than a subclass: a
subclass would have to invent eligibility rules, a weighting scheme and a
rebalancing frequency it does not have, and the calculator must never receive
one by accident. The level chaining lives in beacon.index.chaining.
This module imports on the core install. scipy (the optimise extra) is
needed only when a solve actually runs.
OptimisedIndexDefinition ¶
OptimisedIndexDefinition(
index_id: str,
index_name: str,
source: AnyIndexDefinition,
objective: str = MIN_TRACKING_ERROR,
constraints: Sequence[Constraint] = (),
base_date: str | None = None,
base_value: float | None = None,
currency: str | None = None,
description: str | None = None,
risk_model: RiskModel | None = None,
)
An optimised index: a derivation on a source index, plus identity.
The source stays first-class (referenced, never copied), so editing the parent changes its optimised children at their next calculation, which is what "optimise the index I built" means. Chained optimisation (a source that is itself optimised) falls out of the recursion for free.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
index_id
|
str
|
A unique identifier for the derived index. |
required |
index_name
|
str
|
The common name of the derived index. |
required |
source
|
AnyIndexDefinition
|
The parent: a plain :class: |
required |
objective
|
str
|
What to minimise. Only |
MIN_TRACKING_ERROR
|
constraints
|
Sequence[Constraint]
|
What the solved weights must satisfy, as
:class: |
()
|
base_date
|
str | None
|
First calculation date (YYYY-MM-DD). None inherits the source's, which is the usual case: the child lives on the parent's calendar. |
None
|
base_value
|
float | None
|
The level the chained path starts at. None inherits the source's. |
None
|
currency
|
str | None
|
The derived index's currency. None inherits the source's. |
None
|
description
|
str | None
|
Optional textual description. |
None
|
risk_model
|
RiskModel | None
|
Reserved: carried but unused, mirroring
:class: |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If index_id or index_name is empty, source is None, or base_value is given and not positive. |
base_date
property
¶
The first calculation date: own when given, else the source's.
calendar
property
¶
The trading calendar, which is always the source's.
No override, unlike the currency or the base date: the derivation reallocates on exactly the parent's rebalance dates, so a calendar of its own could only disagree with the days it actually has weights for.
universe_identifiers
property
¶
The investable universe, which is always the source's.
The derivation holds no universe of its own (it reallocates over exactly the names the parent published), so the answer resolves through the chain to the root definition's.
from_config
classmethod
¶
from_config(
index_id: str,
index_name: str,
source: AnyIndexDefinition,
config: OptimisationConfig,
) -> OptimisedIndexDefinition
The derivation an :class:OptimisationConfig describes.
One vocabulary for ad-hoc and stored runs: the config is the stored
derivation minus the source, so an ad-hoc Backtest.run builds an
ephemeral definition through here and calculates it exactly as a
stored one would be. The config's objective, constraints and risk
model are carried over; base date, base value and currency inherit
the source's.
calculate_derived_index ¶
calculate_derived_index(
definition: OptimisedIndexDefinition,
data_provider: DataFetcher,
start_date: str | None = None,
end_date: str | None = None,
price_column: str = "CLOSE",
parent_result: IndexResult | None = None,
) -> IndexResult
Calculate an optimised index into a standard :class:IndexResult.
Three steps: calculate the parent (or accept a pre-supplied calculation, as the Backtest integration does with its cached one), solve the parent's published weights at every rebalance under the definition's constraints, then chain the solved weights into the derived index's own daily levels.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
definition
|
OptimisedIndexDefinition
|
The derivation to calculate. |
required |
data_provider
|
DataFetcher
|
Data source for the parent calculation, prices and FX. |
required |
start_date
|
str | None
|
First date (YYYY-MM-DD). Defaults to the definition's base date. Ignored when parent_result is supplied, whose own window governs. |
None
|
end_date
|
str | None
|
Last date (YYYY-MM-DD). Required unless parent_result is supplied. |
None
|
price_column
|
str
|
Market-data column read as the price. |
'CLOSE'
|
parent_result
|
IndexResult | None
|
The source's calculation, when the caller already has it. None calculates the source here (recursively, when the source is itself optimised). |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
IndexResult |
IndexResult
|
Daily levels, divisor history, constituent and weight |
IndexResult
|
snapshots at exactly the parent's rebalance dates, and the daily |
|
IndexResult
|
weights panel: a normal index result, data-bound to data_provider. |
Raises:
| Type | Description |
|---|---|
CalculationError
|
If the objective is unknown, the parent produced no rebalance snapshots to solve, or a solve is infeasible (the solver's own message names the binding conflict). |
ValueError
|
If no window end is available to calculate the parent. |
check_objective ¶
Refuse an objective this module cannot solve, naming the accepted set.
Checked before anything expensive happens (a full parent calculation, in the usual case), so a typo in a stored derivation fails in the time it takes to read the document rather than after a minute of arithmetic.
Raises:
| Type | Description |
|---|---|
CalculationError
|
If the objective is not one of :data: |
calculate_source ¶
calculate_source(
source: AnyIndexDefinition,
data_provider: DataFetcher,
start_date: str | None = None,
end_date: str | None = None,
price_column: str = "CLOSE",
) -> IndexResult
The parent's calculation, recursive when the parent is itself derived.
The parent's published weights are what a derivation is defined against, so anything reasoning about a derivation (the derived calculation, the server's preview) needs them, and needs them from one place. A chain resolves here rather than at each caller.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
AnyIndexDefinition
|
The parent definition: rule-driven, or another derivation. |
required |
data_provider
|
DataFetcher
|
Data source for prices, reference data and FX. |
required |
start_date
|
str | None
|
First date (YYYY-MM-DD). None uses the source's base date. |
None
|
end_date
|
str | None
|
Last date (YYYY-MM-DD). Required by the calculator. |
None
|
price_column
|
str
|
Market-data column read as the price. |
'CLOSE'
|
Returns:
| Name | Type | Description |
|---|---|---|
IndexResult |
IndexResult
|
The source's own calculation over that window. |
solve_snapshot ¶
solve_snapshot(
definition: OptimisedIndexDefinition,
source_weights: dict[str, float],
) -> OptimisationResult
Solve one of the parent's snapshots under the derivation's constraints.
The single solve of this module, so the derived calculation and any
caller asking "what would this derivation do at that date" (the server's
preview) cannot disagree about what the answer is. The whole
:class:~beacon.optimise.result.OptimisationResult is returned rather than
only its weights, because which constraints bound and how much room the
rest had left is the interesting half of the answer, and re-deriving it
from the weights afterwards would be a second implementation of the rules.
scipy is required inside the solve. An infeasible constraint set raises there with a message naming the binding conflict, and nothing is caught here, so the failure is loud.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
definition
|
OptimisedIndexDefinition
|
The derivation supplying the objective and constraints. |
required |
source_weights
|
dict[str, float]
|
The parent's published weights at one rebalance. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
OptimisationResult |
OptimisationResult
|
Solved weights, binding constraints, every |
OptimisationResult
|
constraint's slack, and the solver's diagnostics. |
Raises:
| Type | Description |
|---|---|
CalculationError
|
If the objective is unknown, or the solve is infeasible or fails to converge. |
expression_rules ¶
The rule an expression compiles into.
rule = ExpressionRule.from_expression(
(data.market.market_cap > 1e9)
& (data.features.fundamentals.pe_ratio < 20))
IndexDefinition(..., eligibility_rules=[rule])
A rule type beside the others¶
ExpressionRule sits beside MarketCapRule, LiquidityRule, FeatureRule
and the rest, and stores its tree in params, so a definition written
in Python and one built in the client are the same document and neither
has to know which produced it:
{"id": "r1", "type": "ExpressionRule",
"params": {"expression": {"node": "all", "operands": [...]}}}
An expression that could not serialise could never reach a saved definition, which is most of what a rule is for.
Evaluated at the rebalance date¶
Every read goes through the point-in-time path (beacon.expressions.resolve),
so a value published after the rebalance date is invisible. Reading the latest
value instead would make the backtest look better and be wrong.
Missing coverage is a stated behaviour¶
A name with no value for a field is excluded by default, matching
FeatureRule so the two do not disagree about the same situation.
The alternative (including it) means a screen for "revenue above a billion" silently admits every company the dataset has never heard of, which is the opposite of what the screen says. Excluding can be wrong too, so it is a parameter; the default is the one whose failure is visible, since an index that comes out too small prompts a question where one quietly full of uncovered names does not.
ExpressionRule ¶
ExpressionRule(
expression: dict[str, Any],
on_missing: str = EXCLUDE,
max_age_days: int | None = MAX_AGE_DAYS,
)
Bases: EligibilityRuleBase
Select instruments that satisfy an expression.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
expression
|
dict[str, Any]
|
The serialised expression tree ( |
required |
on_missing
|
str
|
|
EXCLUDE
|
max_age_days
|
int | None
|
How old a feature value may be and still count. None means no limit. |
MAX_AGE_DAYS
|
Raises:
| Type | Description |
|---|---|
InvalidRuleError
|
If on_missing is not recognised, or expression is not a valid tree. |
required_columns ¶
The market columns the expression reads, derived from its tree.
An expression's needs are whatever it references, so they come from
the fields in the tree. A derived field is expanded into what it is
computed from: a screen on market_cap needs CLOSE and
SHARES_OUTSTANDING, not a column called MARKET_CAP that no store has.
Reference, action and feature fields read other tables and add
nothing here.
from_expression
classmethod
¶
from_expression(
expression: Expression,
on_missing: str = EXCLUDE,
max_age_days: int | None = MAX_AGE_DAYS,
) -> ExpressionRule
Build from a live expression rather than from its serialised form.
What a user writing Python calls. The stored params are identical
either way, which is the point: one representation, two front doors.
is_eligible ¶
is_eligible(
asset: Asset,
current_date: Timestamp,
market_data_provider: DataFetcher,
context: IndexContext | None = None,
) -> bool
Whether the asset passes, as of current_date.
The date is the rebalance date and is passed straight through to the point-in-time reads. A value published after it is invisible.
feature_rules ¶
Eligibility rules that screen on features.
A feature is any per-instrument datapoint that is not price, reference or
action data (beacon.data.features), so this is the rule that lets an index
select on a fundamental, an alternative dataset, or a value somebody derived
and imported, without a new rule class per datapoint.
Resolved at the rebalance date, through the point-in-time accessor¶
An index rebalancing on 1 April screens on what was published by 1 April.
Q1 revenue announced in mid-May is invisible, however completely the quarter
had ended. The rule reads through DataFetcher.fetch_feature, which enforces
this; reading the table directly would put look-ahead back in, and the
resulting backtest would look better and be wrong. A value older than
max_age_days at the rebalance counts as missing.
Missing coverage is a decision, not an accident¶
A name with no value for the field is excluded by default.
The alternative (including it) means a screen for "revenue above a billion"
silently admits every company the dataset has never heard of, which is the
opposite of what the screen says. Excluding can be wrong too: a universe with
patchy coverage shrinks to the names the vendor happened to cover. So the
behaviour is a parameter, on_missing, and the default is the one whose
failure is visible: an index that comes out too small prompts a question,
where one quietly containing uncovered names does not.
This is deliberately not the same as a name whose value is legitimately
zero. Zero fails a > 0 test honestly; missing has no value to compare at
all.
FeatureRule ¶
FeatureRule(
field: str,
comparison: str = "gt",
threshold: float = 0.0,
feature_type: str | None = None,
on_missing: str = EXCLUDE,
max_age_days: int | None = MAX_AGE_DAYS,
)
Bases: EligibilityRuleBase
Select instruments whose feature value passes a threshold.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
field
|
str
|
The feature to read, e.g. |
required |
comparison
|
str
|
How the value is tested against threshold: |
'gt'
|
threshold
|
float
|
The value compared against. |
0.0
|
feature_type
|
str | None
|
Which feature dataset ( |
None
|
on_missing
|
str
|
|
EXCLUDE
|
max_age_days
|
int | None
|
How old a value may be and still count. None means no limit. |
MAX_AGE_DAYS
|
Raises:
| Type | Description |
|---|---|
InvalidRuleError
|
If comparison or on_missing is not recognised, or field is empty. |
required_columns ¶
No market columns: a feature is read from the features table.
Stated rather than inherited, so the absence is visibly a decision. Whether the named feature exists is a real question with the same shape as a missing column, but it is asked of a different table, and the up-front column check covers market data only.
is_eligible ¶
is_eligible(
asset: Asset,
current_date: Timestamp,
market_data_provider: DataFetcher,
context: IndexContext | None = None,
) -> bool
Whether the asset passes, as of current_date.
The date is the rebalance date, and it is passed straight through to the point-in-time accessor. A value published after it is invisible.
methodology ¶
Index methodology: eligibility rules and weighting schemes.
EligibilityRuleBase and WeightingSchemeBase are the interfaces a rule or
scheme implements. MarketCapRule and LiquidityRule screen constituents;
MarketCapWeighted and EqualWeighted weight them.
EligibilityRuleBase ¶
Bases: ABC
Abstract base class for an eligibility rule.
Eligibility rules determine whether an asset can be part of an index.
Subclasses implement is_eligible, and may override required_columns
and prepare.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
rule_name
|
str
|
The rule's name, used in logs and error messages. |
required |
required_columns ¶
The market-data columns this rule reads, declared up front.
Checked against the dataset before a run does any work, so a store with no SHARES_OUTSTANDING column is refused at the start as "this rule needs SHARES_OUTSTANDING and the dataset has none", rather than at the first rebalance as "N0 has no SHARES_OUTSTANDING on 2024-01-02", which is true, and sends a reader to inspect one company whose data is fine.
Empty by default rather than abstract, so a rule written outside this package keeps working. It is then simply not checked up front, and fails at the first read instead. Every rule shipped here declares its own.
prepare ¶
prepare(
candidates: list[Asset],
current_date: Timestamp,
market_data_provider: DataFetcher,
context: IndexContext | None = None,
) -> None
Read in one go whatever this rule is about to read per name.
Called once with the whole candidate set before is_eligible is asked
about any of them. It decides nothing and returns nothing: a rule must
give exactly the same answers whether or not it prepared, because this
is a hint about how to read rather than about what is eligible.
Doing nothing is therefore the right default, and it is the base
implementation.
It exists because is_eligible is a predicate over one asset, so a
rule reading market data reads it a name at a time, and each read
slices a frame whose size is the whole store: the cost of one lookup
grows with the universe around it rather than with the row it wants.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
candidates
|
list[Asset]
|
Everything that reached this rung, in order. A rule that ranks rather than screens would want this set too; that is not what this is for, but it is the same set. |
required |
current_date
|
Timestamp
|
The date selection is being made at. |
required |
market_data_provider
|
DataFetcher
|
The data source the reads will go to. |
required |
context
|
IndexContext | None
|
What the index settles for its rules, as for
:meth: |
None
|
is_eligible
abstractmethod
¶
is_eligible(
asset: Asset,
current_date: Timestamp,
market_data_provider: DataFetcher,
context: IndexContext | None = None,
) -> bool
Checks if a given asset is eligible based on this rule.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
asset
|
Asset
|
The asset to check. |
required |
current_date
|
Timestamp
|
The date on which eligibility is being assessed. |
required |
market_data_provider
|
DataFetcher
|
A DataFetcher instance to get necessary market data (e.g., market cap, trading volume). |
required |
context
|
IndexContext | None
|
What the index the rule is running inside reports in and settles. None when the rule is evaluated outside an index, in which case nothing here may assume a currency it was not told. |
None
|
Returns:
| Type | Description |
|---|---|
bool
|
True if the asset is eligible, False otherwise. |
MarketCapRule ¶
Bases: EligibilityRuleBase
Eligibility by market capitalisation, read from a resolved session.
The cap is price times shares outstanding, both read on the same session. A name with no price or no positive share count on that session is excluded (and logged as a warning).
Dates resolve backwards into the data. A weekend, a holiday or any
date inside the data's coverage that carries no bar is read at the last
session on or before it, because that is the universe the index actually
held through the closure. This is the same resolution
:class:MarketCapWeighted performs, through the same primitive, so
selection and weighting cannot resolve a closed day differently.
The bounds are in the index's currency. The cap is converted at the session's rate before it meets either bound; a missing FX pair refuses rather than falling back to the local figure. Outside an index there is no context and so no currency to convert into, and the bounds are then read in the asset's own currency. Inside an index the calculator always supplies one.
Past the last bar it refuses rather than excluding. A rule that cannot evaluate has not found the asset ineligible, it has failed, and the two must not share an answer: "not in the index" is a published fact about a name, while "the data does not reach that date" is a fact about the store.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
min_market_cap
|
float | None
|
Lowest cap admitted, in the index currency. None for no floor. |
None
|
max_market_cap
|
float | None
|
Highest cap admitted, in the index currency. None for no ceiling. |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If min_market_cap is greater than max_market_cap. |
required_columns ¶
A cap is price times shares, so both, whichever bound is set.
prepare ¶
prepare(
candidates: list[Asset],
current_date: Timestamp,
market_data_provider: DataFetcher,
context: IndexContext | None = None,
) -> None
Read the whole candidate set's session in one slice.
Every name this rule is about to be asked about is read on the same session, for the same two columns. Warming that session turns the per-name reads into dictionary lookups and, because the weighting scheme then reads the survivors on the same session, makes the second pricing of every surviving name free.
Raises:
| Type | Description |
|---|---|
CalculationError
|
If current_date lies outside the data's
coverage. That is the same refusal |
is_eligible ¶
is_eligible(
asset: Asset,
current_date: Timestamp,
market_data_provider: DataFetcher,
context: IndexContext | None = None,
) -> bool
Whether asset's market cap at the resolved session clears the bounds.
Raises:
| Type | Description |
|---|---|
CalculationError
|
If asset is not an equity, if current_date lies outside the data's coverage, or if the cap cannot be converted into the index currency, so the rule cannot be evaluated at all. Nothing here turns a failure into an exclusion (see the class docstring). |
LiquidityRule ¶
LiquidityRule(
min_avg_daily_volume: int | None = None,
min_avg_daily_value: float | None = None,
lookback_days: int = 60,
)
Bases: EligibilityRuleBase
Eligibility by trading liquidity: average daily volume or value.
The averages are taken over the last lookback_days rows of market data on or before the date asked about. A name with fewer than 80% of that many rows, or with the needed column missing or empty, is excluded (and logged as a warning). Values are in the currency the name trades in, not converted.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
min_avg_daily_volume
|
int | None
|
Lowest average shares traded per day. None for no volume floor. |
None
|
min_avg_daily_value
|
float | None
|
Lowest average close times volume per day. None for no value floor. |
None
|
lookback_days
|
int
|
Trading days the averages are taken over. |
60
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If lookback_days is not positive. |
required_columns ¶
Volume for either threshold, and the close too for a value one.
Declared from the thresholds actually set, because they read different
things. The declaration matters here: is_eligible treats a missing
VOLUME column as "not liquid enough" and excludes the name, so without
the up-front check a store lacking the column would exclude every
name and the run would fail as "index holds nothing on its base date"
with no mention of volume.
is_eligible ¶
is_eligible(
asset: Asset,
current_date: Timestamp,
market_data_provider: DataFetcher,
context: IndexContext | None = None,
) -> bool
Whether asset's traded volume and value over the lookback qualify.
No session resolution here, and none needed: this reads a window ending at current_date, so a closed day is already spanned by the days around it rather than being the single day everything hangs on.
Errors are not caught. A rule that throws has not said the asset is ineligible, and the two answers must not be spelled the same.
Raises:
| Type | Description |
|---|---|
CalculationError
|
If asset is not an equity, so there is no ticker to read volume against. |
WeightingSchemeBase ¶
Bases: ABC
Abstract base class for a weighting scheme.
Weighting schemes determine the proportion of each constituent in an
index. Subclasses implement calculate_weights, and may override
required_columns.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
scheme_name
|
str
|
The scheme's name, used in logs and error messages. |
required |
required_columns ¶
The market-data columns this scheme reads, declared up front.
The same contract as :meth:EligibilityRuleBase.required_columns, and
derived from the scheme's own inputs where they change what it reads:
a scheme's parameters are the declaration, so nothing asks a store
for a column the configured scheme does not use.
calculate_weights
abstractmethod
¶
calculate_weights(
constituents: list[Asset],
current_date: Timestamp,
market_data_provider: DataFetcher,
context: IndexContext | None = None,
) -> dict[Asset, float]
Calculates the weight for each constituent asset.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
constituents
|
list[Asset]
|
A list of assets that are eligible for the index. |
required |
current_date
|
Timestamp
|
The date for which weights are being calculated. |
required |
market_data_provider
|
DataFetcher
|
A DataFetcher instance. |
required |
context
|
IndexContext | None
|
What the index the scheme is running inside reports in and settles. None when it is invoked outside an index. |
None
|
Returns:
| Type | Description |
|---|---|
dict[Asset, float]
|
A dictionary mapping each Asset object to its calculated weight (float). |
dict[Asset, float]
|
The sum of weights should typically be 1.0. |
MarketCapWeighted ¶
Bases: WeightingSchemeBase
Market capitalisation weighting, optionally free-float adjusted.
Each constituent's weight is its cap (price times shares outstanding, times the free-float factor when use_free_float is set) over the sum of all caps. A name is priced at its last close on or before the rebalance session, and its shares, free float and FX rate are read on that same day.
Every path either weights by real market caps or refuses. There is no equal-weight fallback: an index that comes out equal-weighted because the caps could not be read is not a degraded market-cap index, it is a different index published under the same heading, and nothing downstream looks wrong enough for anyone to ask (the levels are right, the weights sum, the backtest tracks).
Dates resolve backwards into the data. A request for a weekend, a holiday, or any date inside the data's coverage that carries no bar reads the last session on or before it, because that is the composition the index actually held through the closure rather than an approximation of one. Past the last bar it refuses, since there the same read would be a stale print presented as the current one. The bound is the data's own coverage, not a day count, which cannot tell those two apart.
Caps are compared in one currency. A universe spanning currencies is converted into the index currency before the caps are summed, and a missing FX pair refuses. A universe quoted in a single currency needs no conversion at all, since converting every cap by the same rate cannot move a weight. Called outside an index (no context) over several currencies, it refuses, since there is no currency to compare in.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
use_free_float
|
bool
|
Weight by the freely traded portion of each cap rather than the full cap. Requires a FREE_FLOAT column. |
False
|
required_columns ¶
Price and shares, and free float only when this scheme uses it.
use_free_float is the declaration: every free-float read in a run is
behind it, so a scheme that is not float-adjusted never asks the store
for the column and must not be refused for lacking it.
calculate_weights ¶
calculate_weights(
constituents: list[Asset],
current_date: Timestamp,
market_data_provider: DataFetcher,
context: IndexContext | None = None,
) -> dict[Asset, float]
Weights proportional to market cap, or a refusal.
Raises:
| Type | Description |
|---|---|
CalculationError
|
If current_date lies outside the data's coverage, if any constituent is unpriceable, unconvertible or is not an equity, has no positive shares outstanding or (when free-float adjusted) no usable free-float factor, if the universe spans currencies with no context to compare them in, or if the caps sum to nothing. Nothing here falls back to another methodology (see the class docstring). |
EqualWeighted ¶
Bases: WeightingSchemeBase
Equal weighting: every constituent gets 1 / n.
Reads no market data. An empty constituent list gets empty weights.
required_columns ¶
Nothing: equal weights are decided without reading the market.
Stated rather than inherited, so the absence is a decision a reader can see. The index still needs a price column to value its holdings daily, but that is the calculator's requirement, not this scheme's.
requirements ¶
Whether a dataset has the columns a definition needs, asked once up front.
Every rule and weighting scheme declares the market columns it reads
(required_columns), and require_columns checks the union, plus the price
column the calculation values holdings with, against the dataset before a
run does any work. A missing column is refused with a CalculationError
that names each column, what needs it, and what the dataset has instead.
Without this check, a run would get as far as the first read that needed the column and then fail in terms of one company on one day (for example, "N0 has no positive SHARES_OUTSTANDING on 2024-01-02"), sending the reader to inspect that company's data when the dataset has no share-count column at all. A missing VOLUME column is worse: every name fails a liquidity screen, and the run fails as "index holds nothing on its base date" with no mention of volume. A column cannot appear or vanish halfway through a run, so asking once is enough.
When the data provider cannot say which columns it has (it has no
market_columns list), the check is skipped and the run fails, if it fails,
at the first read.
required_by ¶
Every market column a definition reads, and what reads it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
definition
|
IndexDefinition
|
The index definition to check. |
required |
price_column
|
str
|
The column the calculation values holdings with every day. It is a requirement of the calculation itself, separate from any rule or scheme. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
dict |
dict[str, list[str]]
|
column -> the parts of the definition that need it, in the order |
dict[str, list[str]]
|
they appear. The who is what makes the refusal actionable: "needs |
|
dict[str, list[str]]
|
SHARES_OUTSTANDING" says what to load, and "for MarketCapWeighted" says |
|
dict[str, list[str]]
|
what to change instead if loading it is not an option. |
require_columns ¶
Refuse a definition the dataset cannot support, before any work is done.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
definition
|
IndexDefinition
|
The index definition about to run. |
required |
fetcher
|
DataFetcher
|
The data it would run on. |
required |
price_column
|
str
|
The calculation's daily valuation column. |
required |
Raises:
| Type | Description |
|---|---|
CalculationError
|
If any column the definition reads is absent from the dataset's market data, naming each one, what needs it, and what the dataset has instead. |
require_price_column ¶
The one requirement a run with no definition still has: a price.
Used by the backtest engine, which may be driven by a raw weight schedule with no definition behind it, and so has nothing to declare beyond the column it marks positions at. Skipped when the provider cannot list its columns.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
fetcher
|
DataFetcher
|
The data the run would use. |
required |
price_column
|
str
|
The column positions are priced from. |
required |
who
|
str
|
What needs the column, for the error message. |
required |
Raises:
| Type | Description |
|---|---|
CalculationError
|
If the dataset has no such column. |
result ¶
IndexResult: the output of an index calculation run.
IndexResult
dataclass
¶
IndexResult(
index_id: str,
index_levels: Series,
divisor_history: Series,
constituent_snapshots: dict[Timestamp, list[str]],
weight_snapshots: dict[Timestamp, dict[str, float]],
cap_reports: dict[Timestamp, CapReport] = dict(),
announcement_dates: dict[Timestamp, Timestamp] = dict(),
daily_weights: DataFrame = empty_daily_weights(),
calendar_coverage: CalendarCoverage | None = None,
_data_fetcher: DataFetcher | None = None,
)
Container holding the output of an index calculation run.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
index_id
|
str
|
Identifier of the calculated index. |
required |
index_levels
|
Series
|
Time series of index levels indexed by
|
required |
divisor_history
|
Series
|
Time series of divisor values indexed by
|
required |
constituent_snapshots
|
dict[Timestamp, list[str]]
|
Mapping of rebalance date -> list of asset_id strings. |
required |
weight_snapshots
|
dict[Timestamp, dict[str, float]]
|
Mapping of rebalance date -> dict of {asset_id: weight}. |
required |
cap_reports
|
dict[Timestamp, CapReport]
|
Mapping of rebalance date -> CapReport, for the rebalances where a weight cap actually bound. Empty for an uncapped index, so its presence is itself the signal that capping occurred. |
dict()
|
announcement_dates
|
dict[Timestamp, Timestamp]
|
Mapping of effective date -> the date that composition was announced. Snapshots are keyed by the effective date, because that is when the weights are in force and what every consumer (drift, attribution, the backtest engine) needs. The announcement is carried alongside rather than instead, since a client showing "rebalance of 18 Sep, effective 22 Sep" needs both. Empty for an index with no lag, where the two always coincide. |
dict()
|
daily_weights
|
DataFrame
|
Long-form panel of what the index held on every
calculation day: |
empty_daily_weights()
|
calendar_coverage
|
CalendarCoverage | None
|
How much of the requested window the trading calendar covered, or None when it covered all of it (the ordinary case). Its presence is the signal that the run's range was narrowed to what the calendar covers. |
None
|
The daily panel is recorded rather than re-derived because the index's daily state is path-dependent. It is not a forward-fill of the rebalance snapshot, and not even "amounts fixed between rebalances, repriced daily": a delisted name is dropped mid-period and the divisor adjusted, a split multiplies the units held on its ex-date, and a total-return index reinvests its cash, all on days that are not rebalances. A path is written down as it happens.
The rebalance snapshots are the record of what a rebalance decided. This panel is the record of what then happened.
capped_assets_on_date ¶
Return the constituents held at the cap at the given rebalance.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
date
|
Timestamp
|
A rebalance date. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
dict |
dict[str, float]
|
|
dict[str, float]
|
that date. Empty when nothing was capped, or when date is not a |
|
dict[str, float]
|
rebalance date. |
with_data ¶
Bind a DataFetcher for asset-level queries. Returns self for chaining.
asset ¶
Return an IndexAssetView for a constituent.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
asset_id
|
str
|
Identifier of the constituent asset. |
required |
Returns:
| Type | Description |
|---|---|
IndexAssetView
|
IndexAssetView |
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If no DataFetcher has been bound via
:meth: |
KeyError
|
If asset_id is not found in any constituent snapshot. |
get_returns ¶
Derive a return series from index levels.
Returns:
| Type | Description |
|---|---|
Series
|
pd.Series: Percentage returns (first entry is dropped). |
get_weights_on_date ¶
Get constituent weights effective on a given date.
Locates the most recent rebalance date on or before date.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
date
|
Timestamp
|
The query date. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
dict |
dict[str, float]
|
Mapping of asset_id to weight. Empty dict if no rebalance |
dict[str, float]
|
has occurred on or before date. |
weights_on ¶
Get the recorded constituent weights as of a given date.
Reads the daily panel rather than the rebalance snapshots, so the
answer includes everything that happened since the last rebalance:
price drift, a deletion, a divisor adjustment. Compare
:meth:get_weights_on_date, which answers the different question of
what the last rebalance decided.
Falls back to the latest recorded date on or before date, which covers a day the holdings could not be valued at all, since such a day records no rows.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
date
|
Timestamp
|
The query date. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
dict |
dict[str, float]
|
Mapping of identifier to weight. Empty when nothing was |
dict[str, float]
|
recorded on or before date, including when no panel was captured |
|
dict[str, float]
|
at all. |
to_dataframe ¶
Flatten index levels and divisor history into a DataFrame.
Returns:
| Type | Description |
|---|---|
DataFrame
|
pd.DataFrame: Columns: |
daily_weights_frame ¶
Build the daily weights panel from records collected during a run.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
records
|
list[dict[str, object]]
|
One dict per constituent per calculation day, with the keys
in :data: |
required |
Returns:
| Type | Description |
|---|---|
DataFrame
|
pd.DataFrame: Long-form |
DataFrame
|
in the storage dtypes. Empty records give an empty frame that still |
DataFrame
|
carries the columns, so a consumer can slice it without checking. |
empty_daily_weights ¶
The panel an :class:IndexResult carries when nothing recorded one.
schedule ¶
When an index rebalances, and which days it has a level on.
Two things, kept apart¶
A calendar says which days exist. A day rule says which of those days within a month is the one. Every schedule here is the product of the two, so "third Friday, on the New York calendar" (the S&P and FTSE convention) needs no special case: the third Friday is found, and if it is not a session it rolls back to the one before.
Frequencies are MONTHLY, QUARTERLY, SEMI-ANNUAL and ANNUAL; day rules
are FIRST_BUSINESS_DAY (the default), LAST_BUSINESS_DAY and
THIRD_FRIDAY. The cadence is anchored on the first scheduled date in the
range, not on the calendar year: a quarterly index starting in February
rebalances in February, May, August and November.
Rolling back, not forward¶
A rebalance landing on a holiday moves to the previous session. Forward would push it into the next month at a month end, which is the one case where the choice is visible, and the convention every index provider follows is back. Good Friday is the case that makes this concrete: the third Friday of April 2025 is the 18th, which is not a session on any US exchange, so the rebalance falls on Thursday the 17th.
The calendar is required¶
Every index schedules against a real exchange calendar, named by its MIC (for
example "XNYS"), from the exchange_calendars package. An IndexDefinition
always carries one, and stored documents without one are migrated to
DEFAULT_CALENDAR (XNYS). A date this module schedules is therefore always a
date the exchange has a session for.
sessions, rebalance_dates and next_rebalance take calendar as a
required argument. Passing None asks for plain Monday-to-Friday business days
(pd.bdate_range, holidays included), which a library caller may want, but
must ask for in writing rather than get by omission.
Calendar bounds¶
A calendar covers a limited span. Its history is widened back to the requested
start where the package allows, falling back to the earliest start it will
build. The far end stops at the calendar's last published session. sessions
clamps to those bounds; calendar_coverage reports how much of a window was
covered, so an index calculation can refuse a window with no cover at all and
report one with partial cover.
CalendarCoverage
dataclass
¶
CalendarCoverage(
calendar: str | None,
requested_start: Timestamp,
requested_end: Timestamp,
covered_start: Timestamp | None,
covered_end: Timestamp | None,
calendar_start: Timestamp | None = None,
calendar_end: Timestamp | None = None,
)
What a calendar could offer of the window it was asked for.
Publishes the requested window beside the covered one, because a covered range alone cannot say what was asked for.
is_partial says whether the cover was narrowed, so a client never has to
compare two dates to find out. trimmed_start and trimmed_end say
which end moved, because the two have different causes and different
remedies: the near end is a calendar whose history does not reach (change
the calendar or the base date), the far end is one whose published sessions
stop (wait, or ask for less).
Attributes:
| Name | Type | Description |
|---|---|---|
calendar |
str | None
|
The MIC asked for, or None for plain business days. |
requested_start |
Timestamp
|
The window's first date, as asked for. |
requested_end |
Timestamp
|
The window's last date, as asked for. |
covered_start |
Timestamp | None
|
First date the calendar can speak for, or None when it can speak for none of the window. |
covered_end |
Timestamp | None
|
Last such date, or None in the same case. |
calendar_start |
Timestamp | None
|
The calendar's first session as resolved for this request (its history is widened back on demand, so the same MIC can answer differently depending on how far back it was asked to reach). None when no calendar was named. |
calendar_end |
Timestamp | None
|
The calendar's last published session, or None when no calendar was named. |
starts_before_calendar
property
¶
Whether the window opens before anything the calendar knows.
Which end failed decides the remedy, so a refusal has to know: too early is a calendar whose history does not reach, too late is one whose published sessions stop, and the two are acted on differently.
trimmed_start
property
¶
Whether the calendar's history does not reach the requested start.
trimmed_end
property
¶
Whether the calendar's sessions stop before the requested end.
is_partial
property
¶
Whether either end was narrowed. False when the cover is complete.
describe_window ¶
The requested window beside the covered one, for a log line.
calendar_coverage ¶
How much of a window a calendar can actually speak for.
A pure query: it refuses nothing and logs nothing, so the caller decides
what a narrowing means. :func:sessions answers with what it has (and
is_session depends on that, since a date outside a calendar's bounds is a
date it cannot call open), while an index calculation asks this first and
refuses on an empty answer. An inverted window (start after end) is
reported as covering nothing.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
start
|
Timestamp
|
First date of the window, inclusive. |
required |
end
|
Timestamp
|
Last date, inclusive. |
required |
calendar
|
str | None
|
Exchange MIC, or None for plain business days, which have no bounds and therefore always cover the window in full. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
CalendarCoverage |
CalendarCoverage
|
The requested window beside the covered one. |
earliest_available
cached
¶
The earliest session the package will build for a calendar.
Found by bisection rather than read off an attribute, because there is no
attribute: get_calendar(code) returns twenty years of history whatever
the underlying data supports, and the true floor only shows up as the
earliest start it will accept. Each calendar's floor differs and none is
the default bound: XTKS and XBOM reach back to 1997, XHKG to 1960, while
all three report a first session of 2006 until asked for more.
Worth the handful of calendar builds because it is only ever called to explain a refusal, and the alternative is a message that understates the reach: quoting the default bound would tell someone asking for a 1950 Hong Kong index that the calendar starts in 2006, which is both false and the wrong thing to act on.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
calendar
|
str
|
Exchange MIC. |
required |
Returns:
| Type | Description |
|---|---|
Timestamp
|
pd.Timestamp: First session of the widest calendar obtainable. |
describe_bounds ¶
Why a calendar could not cover a window, in terms of the end that failed.
Only the failing end is searched for. A window that opens before the calendar's history needs the true floor, and finding it costs a handful of calendar builds; a window that opens after the calendar's last published session needs no search at all, because that bound is already known.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
coverage
|
CalendarCoverage
|
A coverage with a named calendar (not None). |
required |
Returns:
| Name | Type | Description |
|---|---|---|
str |
str
|
"XTKS (Tokyo Stock Exchange) has no sessions before ..." when |
str
|
the window opens too early, otherwise "... has no published sessions |
|
str
|
after ..." with the calendar's last session. |
sessions ¶
The days an index has a level on, over a range.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
start
|
Timestamp
|
First date, inclusive. |
required |
end
|
Timestamp
|
Last date, inclusive. |
required |
calendar
|
str | None
|
Exchange MIC, e.g. |
required |
Returns:
| Type | Description |
|---|---|
DatetimeIndex
|
pd.DatetimeIndex: Sessions in ascending order, clamped to the |
DatetimeIndex
|
calendar's bounds (empty when the range lies wholly outside them). |
is_session ¶
Whether the market was open on date.
The single-day face of :func:sessions, and the question that tells a
holiday apart from a hole in the data: a day with no bar that the calendar
says was closed is a market that was shut, while a day with no bar that
the calendar says was open is data that is missing something.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
date
|
Timestamp
|
The date asked about. |
required |
calendar
|
str | None
|
Exchange MIC. None asks about Monday to Friday, holidays
included (the same explicit request :func: |
required |
Returns:
| Name | Type | Description |
|---|---|---|
bool |
bool
|
True when date is a trading session on calendar. A date past |
bool
|
the calendar's published bounds answers False, since a day the |
|
bool
|
calendar cannot speak for is not a day it says was open. |
known_calendars ¶
Every MIC this installation can schedule against, sorted.
Read from the package rather than listed here, so the set a client is told about is the set the calculation actually accepts. A hand-kept copy of a hundred-odd MICs would be wrong the first time the package gained one.
calendar_region ¶
A calendar's region and IANA timezone, e.g. ("Europe", "Europe/Oslo").
Derived from the calendar's own timezone rather than from a table, for the same reason the code list is: a mapping kept here would be one release behind the package the schedule actually runs on. Every calendar has a timezone and splits into a region (Europe, America, Asia, Australia, Atlantic, Africa, Pacific); the few on bare UTC report "UTC" as their own region rather than being forced into a continent they do not have.
is_known_calendar ¶
Whether a MIC names a calendar this installation can use.
day_in_month ¶
day_in_month(
year: int,
month: int,
day_rule: str,
available: DatetimeIndex,
) -> pd.Timestamp | None
The scheduled day within one month, or None if it has no sessions.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
year
|
int
|
Calendar year. |
required |
month
|
int
|
Calendar month, 1-12. |
required |
day_rule
|
str
|
One of DAY_RULES. |
required |
available
|
DatetimeIndex
|
Sessions covering at least this month. |
required |
Returns:
| Type | Description |
|---|---|
Timestamp | None
|
The date, or None when the month holds no session at all. |
rebalance_dates ¶
rebalance_dates(
frequency: str,
start: str | Timestamp,
end: str | Timestamp,
calendar: str | None,
day_rule: str = DEFAULT_DAY_RULE,
) -> list[pd.Timestamp]
Every rebalance date in a range.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
frequency
|
str
|
One of FREQUENCIES. |
required |
start
|
str | Timestamp
|
First date of the range, inclusive. |
required |
end
|
str | Timestamp
|
Last date, inclusive. |
required |
calendar
|
str | None
|
Exchange MIC. Required; None asks explicitly for business days, which no stored index does. |
required |
day_rule
|
str
|
Which day within a scheduled month. |
DEFAULT_DAY_RULE
|
Returns:
| Name | Type | Description |
|---|---|---|
list |
list[Timestamp]
|
Dates in ascending order, empty when the range holds none. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the frequency or day rule is unknown. |
effective_date ¶
effective_date(
announced: Timestamp,
lag_sessions: int,
available: DatetimeIndex,
) -> pd.Timestamp
The date an announced rebalance takes effect.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
announced
|
Timestamp
|
When the composition was published. |
required |
lag_sessions
|
int
|
Sessions to wait. Zero means same-day. |
required |
available
|
DatetimeIndex
|
Sessions covering the announcement and the lag. |
required |
Returns:
| Type | Description |
|---|---|
Timestamp
|
The effective date. The announcement itself when the lag is zero, or |
Timestamp
|
when the panel holds too few sessions after it (logged as a warning): |
Timestamp
|
an index whose data ends mid-lag applies its last rebalance rather |
Timestamp
|
than dropping it. |
next_rebalance ¶
next_rebalance(
frequency: str,
base_date: str | Timestamp,
as_of: str | Timestamp,
calendar: str | None,
day_rule: str = DEFAULT_DAY_RULE,
) -> pd.Timestamp | None
The first rebalance strictly after a date.
Anchored on the base date, because that is what the calculator anchors on: a "next rebalance" computed from any other origin could name a date the index would never actually rebalance on.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
frequency
|
str
|
One of FREQUENCIES. |
required |
base_date
|
str | Timestamp
|
The index's base date, which anchors the cadence. |
required |
as_of
|
str | Timestamp
|
The date being asked from. |
required |
calendar
|
str | None
|
Exchange MIC. Required; None asks explicitly for business days. |
required |
day_rule
|
str
|
Which day within a scheduled month. |
DEFAULT_DAY_RULE
|
Returns:
| Type | Description |
|---|---|
Timestamp | None
|
The next date, or None if none falls within the lookahead. |