beacon.index¶
Index construction and calculation: IndexDefinition captures the static
rules, methodology provides eligibility rules and weighting schemes, and
IndexCalculator runs the day-by-day calculation. See
Methodology for the narrative version.
index ¶
The init.py for the 'index' module.
This module is core for defining index methodologies, selecting constituents, calculating weights, and computing index levels.
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 |
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/index/asset_view.py
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 |
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/index/asset_view.py
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. |
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/index/asset_view.py
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. |
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/index/asset_view.py
IndexCalculator ¶
IndexCalculator(
index_definition: IndexDefinition,
data_provider: DataFetcher,
price_column: str = "CLOSE",
)
Bases: MarketValuesMixin, DeletionMixin, TotalReturnMixin, CorporateActionsMixin
Stateless index calculator. Accepts an IndexDefinition and DataFetcher, and provides methods for constituent selection, weighting, index level calculation, and corporate action adjustments. All state is passed through method parameters and return values.
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'
|
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/index/calculation/calculator.py
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. Delegates to the internal implementation, so anything that stubs that also governs this.
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. |
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/index/calculation/calculator.py
select_constituents ¶
Selects index constituents from a given universe based on 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, which is what BN-102 existed to stop.
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. |
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/index/calculation/calculator.py
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. |
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/index/calculation/calculator.py
calculate_constituent_weights ¶
calculate_constituent_weights(
constituents: list[Asset], current_date: Timestamp
) -> dict[Asset, float]
Calculates the weights for the given constituents based on 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 float weight. Sum of weights should be 1.0. |
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 (BN-196). Also if its weights do not sum to 1: that used to be silently renormalised with a warning, and a scheme's own output rescaled is the scheme not being applied, which is the BN-179 argument exactly (BN-184). |
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 (BN-194). |
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/index/calculation/calculator.py
309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 | |
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. |
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/index/calculation/calculator.py
initialize_divisor ¶
Calculates the initial divisor for the index on its base_date. Divisor = Initial Total Market Value / Base Index 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. |
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/index/calculation/calculator.py
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. |
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/index/calculation/calculator.py
run ¶
Run the full index calculation over a date range.
Iterates the index's own trading sessions from start_date to end_date — the definition's calendar, not Monday to Friday (BN-186) — handling three day types:
- Base date – resolve universe, select constituents, compute weights, initialise divisor, set level = base_value. Rolled forward to the first session when the base date itself was not one.
- Rebalance date – reconstitute (re-resolve universe, re-select, re-weight) and adjust divisor for continuity.
- Regular day – compute index level using current constituents and weights.
The method is idempotent: it carries no state between calls.
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, and the daily weights |
|
IndexResult
|
panel — one row per constituent per day, recorded as the loop |
|
IndexResult
|
goes, since the state it holds each day is path-dependent and |
|
IndexResult
|
cannot be reconstructed from the rebalance snapshots afterwards. |
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 (BN-217). |
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/index/calculation/calculator.py
529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 | |
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. |
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/index/calculation/calculator.py
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). |
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/index/calculation/calculator.py
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 a financial index.
Initializes an IndexDefinition.
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. |
required |
currency
|
str
|
The currency of the index. |
required |
eligibility_rules
|
list[EligibilityRuleBase]
|
A list of EligibilityRuleBase objects that define criteria for constituent selection. |
required |
weighting_scheme
|
WeightingSchemeBase
|
A WeightingSchemeBase object that defines how constituents are weighted. |
required |
rebalancing_frequency
|
str
|
A string indicating how often the index is rebalanced (e.g., 'QUARTERLY', 'MONTHLY', 'SEMI-ANNUAL', 'ANNUAL'). More complex schedules (e.g. "Third Friday of March, June...") would require a more sophisticated scheduler. |
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. |
None
|
max_constituent_weight
|
float | None
|
Optional cap on any single constituent's weight, as a fraction (0.1 is 10%). 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. Defaults to the first business day, which is what every index defined before BN-121 used. |
DEFAULT_DAY_RULE
|
return_type
|
str
|
PRICE, TOTAL_RETURN or NET_TOTAL_RETURN. PRICE is the default and the behaviour of every index defined before BN-125; the other two reinvest cash distributions across the index. |
PRICE
|
withholding_tax_rate
|
float
|
Fraction of each distribution withheld, for a net index. 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 is same-day, which is what every index did before BN-126. |
0
|
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/index/constructor.py
25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 | |
get_rebalance_dates ¶
Return all rebalance dates within [start_date, end_date] based on the index's rebalancing frequency, day rule and calendar.
Delegates to beacon.index.schedule, which replaced the first-business-
day-of-month assumption this method used to hard-code. Since BN-180 the
calendar is always a real one, so a date this returns is always a date
the exchange has a session for — an index that named none used to
schedule 1 January and 25 December.
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 business-day-adjusted rebalance dates. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the rebalancing frequency is unsupported. |
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/index/constructor.py
next_rebalance ¶
The first rebalance strictly after a date.
Anchored on the base date, like every other date this class produces, 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. |
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/index/constructor.py
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
|
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/index/derived.py
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 (owner decision): 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.
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/index/derived.py
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.
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/index/expression_rules.py
required_columns ¶
The market columns the expression reads, derived from its tree.
An expression's needs are whatever it references, so they come from
fields_in rather than being written out -- and a derived field is
expanded into what it is computed from, because a screen on
market_cap needs CLOSE and SHARES_OUTSTANDING, not a column called
MARKET_CAP that no store has (BN-217). Reference and feature fields
read other tables and add nothing here.
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/index/expression_rules.py
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.
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/index/expression_rules.py
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.
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/index/expression_rules.py
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.
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/index/feature_rules.py
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 this check deliberately covers market data only (BN-217).
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/index/feature_rules.py
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.
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/index/feature_rules.py
EligibilityRuleBase ¶
Bases: ABC
Abstract base class for an eligibility rule. Eligibility rules determine if an asset can be part of an index.
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/index/methodology.py
required_columns ¶
The market-data columns this rule reads, declared up front (BN-217).
Checked against the dataset before a run does any work, so a store with no SHARES_OUTSTANDING column is refused on day zero as "this rule needs SHARES_OUTSTANDING and the dataset has none" -- rather than on 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 where it always did -- at the first read -- with the message it always had. Every rule shipped here declares its own.
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/index/methodology.py
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 that
did no preparation must give exactly the answers it gives now, 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 (BN-190).
It exists because the per-name shape is what made a universe expensive.
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 growing 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
|
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/index/methodology.py
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. |
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/index/methodology.py
EqualWeighted ¶
Bases: WeightingSchemeBase
Equal weighting scheme.
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/index/methodology.py
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.
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/index/methodology.py
LiquidityRule ¶
LiquidityRule(
min_avg_daily_volume: int | None = None,
min_avg_daily_value: float | None = None,
lookback_days: int = 60,
)
Bases: EligibilityRuleBase
Eligibility rule based on trading liquidity (e.g., average daily volume or value).
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/index/methodology.py
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. And declared at all because of what a missing VOLUME column
used to do here: is_eligible treats it as "not liquid enough" and
excludes the name, so a store without the column excluded every
name, and the run failed as "index holds nothing on its base date"
with no mention of volume anywhere. A reader loosened the threshold.
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/index/methodology.py
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 (BN-182). 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 (BN-185). |
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/index/methodology.py
410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 | |
MarketCapRule ¶
Bases: EligibilityRuleBase
Eligibility by market capitalisation, read from a resolved session.
Dates resolve backwards into the data (BN-182). 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. Reading the exact date instead excluded every name on a closed day: the universe emptied, the weighting was handed nothing, and an index of no constituents computed a coherent level of zero.
That is the same resolution :class:MarketCapWeighted performs, through
the same primitive and by design. Selection running on one calendar and
weighting on another is two methodologies under one heading.
The bounds are in the index's currency, and now the arithmetic is too (BN-188). The published help text has always said so while the code compared a name's local number against the bound, so a 5bn floor admitted a name whose yen cap read 5.2bn and excluded a genuinely larger one quoted in a strong currency. The cap is converted at the session's rate before it meets either bound; a missing 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. That is the only honest answer to "over five billion of what?" when nobody has said — and it is not a fallback inside an index, where 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.
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/index/methodology.py
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 (BN-190).
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 below 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 |
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/index/methodology.py
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. |
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/index/methodology.py
MarketCapWeighted ¶
Bases: WeightingSchemeBase
Market capitalization weighting, optionally free-float adjusted.
Every path either weights by real market caps or refuses (BN-179). 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 (BN-188). This weighted
price x shares in whatever money the name traded in, so a yen name
entered the sum as though a thousand billion yen were a thousand billion
dollars — a fifteen-fold error on its own weight, and a wrong weight on
every other constituent with it. A universe spanning currencies is
converted into the index's before the caps are summed, and a missing pair
refuses; see :meth:_target_currency for why a universe quoted in one
currency needs no conversion at all.
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/index/methodology.py
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.
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/index/methodology.py
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, or if the caps sum to nothing. Nothing here falls back to another methodology — see the class docstring. |
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/index/methodology.py
WeightingSchemeBase ¶
Bases: ABC
Abstract base class for a weighting scheme. Weighting schemes determine the proportion of each constituent in an index.
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/index/methodology.py
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 (BN-217).
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/index/methodology.py
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. |
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/index/methodology.py
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()
|
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":
:class:~beacon.index.calculation.deletions.DeletionMixin drops a delisted
name mid-period and adjusts the divisor, and
:class:~beacon.index.calculation.corporate_actions.CorporateActionsMixin
adjusts it on ex-dates. Both change what is held and what each name weighs
on a day that is not a rebalance. A path is written down as it happens.
The rebalance snapshots stay what they always were: 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. |
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/index/result.py
with_data ¶
Bind a DataFetcher for asset-level queries. Returns self for chaining.
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/index/result.py
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. |
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/index/result.py
get_returns ¶
Derive a return series from index levels.
Returns:
| Type | Description |
|---|---|
Series
|
pd.Series: Percentage returns (first entry is dropped). |
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/index/result.py
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. |
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/index/result.py
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. |
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/index/result.py
to_dataframe ¶
Flatten index levels and divisor history into a DataFrame.
Returns:
| Type | Description |
|---|---|
DataFrame
|
pd.DataFrame: Columns: |
Source code in build/cache/py-beacon-2c9c3936c65abdb6b8403c50a023f58355be30ee/src/beacon/index/result.py
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.
The three-step workflow of the design record: calculate the parent (or accept a pre-supplied calculation — the Backtest integration passes 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. |