Backtest¶
Simulating a portfolio that trades to an index's target weights: the Backtest front door, BacktestEngine, and BacktestResult. See Backtest.
backtest ¶
Backtesting: simulate trading a portfolio towards a schedule of target weights.
Backtest is the high-level entry point: construct it with capital and cost
assumptions, then run it with an index definition and a date range. It
calculates the index (reusing a cached calculation when it can) and simulates
a portfolio tracking it. BacktestEngine is the engine underneath, which
trades towards each rebalance's target weights from an IndexResult (sells
before buys, costs in basis points of notional) and returns a
BacktestResult holding the portfolio's books, unfilled orders, price gaps
and tracking against the target index. BacktestModifier subclasses such as
DriftThresholdModifier can skip a rebalance or adjust its trades, and
BacktestAssetView gives one asset's story through a backtest.
An Implementation says how a strategy is carried out at a fund's size: its
screens (MarketCapScreen, LiquidityScreen, MinimumPriceScreen,
ListingAgeScreen, ExclusionScreen, ExpressionScreen) decide which names
may be held at each rebalance, its caps (OwnershipCap, LiquidityCap,
WeightCap) and MinimumPosition how much of each, and its redistribution
rule where the weight of the rest goes. Each rebalance's stages are recorded as a RebalanceStep.
PriceGap
dataclass
¶
A day a held name should have traded on and had no bar.
A day the index's calendar says was closed is a market that was shut: the previous session's price is what the holding was genuinely worth through it, so nothing is missing and nothing is recorded. A day the calendar says was open with no bar is data that is missing something. The holding is valued at its last close instead, and the day is recorded here, because that mark is not what the day's market said.
Carrying the price forward beats the alternatives: valuing the holding at zero drops the level by the name's weight for the day, and refusing would stop a run over five hundred names because one had one bad day.
Attributes:
| Name | Type | Description |
|---|---|---|
date |
Timestamp
|
The day whose bar was missing. |
asset_id |
str
|
The name with no bar. |
priced_from |
Timestamp
|
The session the carried price came from, always earlier than date. |
TradeInstruction
dataclass
¶
TradeInstruction(
asset_id: str,
side: str,
quantity: float,
price: float,
cost: float,
impact: float = 0.0,
reason: str | None = None,
tags: tuple[str, ...] = (),
)
A single trade for a portfolio to record.
Produced by whatever decides trades (the backtest engine sizes, prices
and costs an order) and consumed by :meth:Portfolio.apply, which does
the accounting.
Attributes:
| Name | Type | Description |
|---|---|---|
asset_id |
str
|
Asset identifier. |
side |
str
|
|
quantity |
float
|
Number of units to trade. |
price |
float
|
Execution price per unit. |
cost |
float
|
Transaction cost in currency terms. |
impact |
float
|
Of cost, the part that was market impact; the rest is the fixed cost. |
reason |
str | None
|
Why it was made; the portfolio's current reason when None
(see |
tags |
tuple[str, ...]
|
Tags it carries beyond its reason's. |
BacktestAssetView ¶
BacktestAssetView(
asset_id: str,
data_fetcher: DataFetcher,
portfolio: Portfolio,
index_book: object | None = None,
)
Bases: AssetView
AssetView with backtest context for a specific asset.
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 |
portfolio
|
Portfolio
|
The run's books: positions, weights, transactions. |
required |
index_book
|
object | None
|
The tracked index's book, when the run tracked one.
Target weights come from its |
None
|
trades ¶
This asset's transactions.
Returns:
| Type | Description |
|---|---|
DataFrame
|
pd.DataFrame: DataFrame with columns: date, type, quantity, price, |
DataFrame
|
cost. Empty DataFrame if no trades exist for this asset. |
total_cost ¶
Sum of all transaction costs for this asset.
Returns:
| Name | Type | Description |
|---|---|---|
float |
float
|
Total transaction costs incurred for this asset. |
holding_periods ¶
Continuous periods when this asset was held.
Read from the positions panel (the record of quantities) rather than inferred from weights. Quantity above zero is the fact of holding; a weight of 0.0000 is a rounding statement about size.
Returns:
| Type | Description |
|---|---|
list[dict[str, Timestamp]]
|
list of dict: Each dict has |
list[dict[str, Timestamp]]
|
Timestamps. An open position at the end of the run has |
list[dict[str, Timestamp]]
|
set to the last recorded date. |
weight_series ¶
Time series of this asset's portfolio weight.
Returns:
| Type | Description |
|---|---|
Series
|
pd.Series: Weight at each date where the asset was held. Dates |
Series
|
where the asset had zero or no weight are excluded. |
target_weight_series ¶
Time series of this asset's target index weight.
Read from the rebalance snapshots (what each rebalance decided) rather than the index's daily panel: the target the portfolio traded to is the snapshot, and the daily drift between rebalances is the index's business, not the portfolio's instruction.
Returns:
| Type | Description |
|---|---|
Series
|
pd.Series: Target weight at each rebalance date. Rebalance dates |
Series
|
where the asset was not a constituent are excluded. Empty |
Series
|
Series if the run tracked no index. |
slippage_vs_target ¶
Difference between actual and target weights over time.
For each date the asset was held, finds the applicable target weight (most recent rebalance on or before that date) and computes actual - target.
Returns:
| Type | Description |
|---|---|
Series
|
pd.Series: Slippage series indexed by date. Positive values mean |
Series
|
the asset is overweight vs target. Empty Series if the run |
Series
|
tracked no index. |
weight_on_date ¶
This asset's portfolio weight on a specific date.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
date
|
Timestamp
|
The query date. |
required |
Returns:
| Type | Description |
|---|---|
float | None
|
float or None: The weight, or None if the asset was not held on |
float | None
|
that date. |
CapacityCap ¶
Bases: ABC
The largest value a position may have on a date.
LiquidityCap ¶
LiquidityCap(
days: float,
participation: float = 0.2,
lookback_days: int = DEFAULT_LIQUIDITY_DAYS,
)
Bases: CapacityCap
At most what could be sold over a number of days.
The position's value is limited to days times the name's average daily traded value times participation, the share of each day's trading the fund could be.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
days
|
float
|
Days to liquidate. |
required |
participation
|
float
|
The share of a day's traded value, as a decimal. |
0.2
|
lookback_days
|
int
|
Trading days in the average. |
DEFAULT_LIQUIDITY_DAYS
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If days or participation is not positive. |
MinimumPosition ¶
Positions too small to keep, by value or by weight.
A position below either is dropped and its weight redistributed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
float | None
|
The smallest position value, in the book's currency. |
None
|
weight
|
float | None
|
The smallest weight, as a decimal. |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If neither is given. |
too_small ¶
Whether a position of weight in a book of book_value is.
OwnershipCap ¶
Bases: CapacityCap
At most a share of a name's free-float market cap, or of its whole market cap.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
max_share
|
float
|
The share, as a decimal: 0.05 holds at most 5% of the free float. |
required |
free_float
|
bool
|
Measure against the free-float market cap (the default) or, when False, against every share outstanding. |
True
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If max_share is not above 0 and at most 1. |
WeightCap ¶
Bases: CapacityCap
At most a share of the book, whatever the name.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
max_weight
|
float
|
The share, as a decimal. |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If max_weight is not above 0 and at most 1. |
ExecutionLimit ¶
ExecutionLimit(
participation: float | None = None,
days: int | None = None,
lookback_days: int = DEFAULT_LIQUIDITY_DAYS,
)
How much of an order can trade in one day.
Under a participation limit, a name with a volume of 0 on the day trades
nothing. A blank volume is replaced by the last one reported, if no older
than the run's volume_backfill_days, and otherwise by the name's
average daily volume over lookback_days. A name with no volume at all
is not limited by participation, and the run logs a warning.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
participation
|
float | None
|
At most this share of the day's volume, as a decimal. |
None
|
days
|
int | None
|
Spread each order evenly over this many trading days. |
None
|
lookback_days
|
int
|
Trading days in the average daily volume that stands in for a blank one. |
DEFAULT_LIQUIDITY_DAYS
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If neither is given, or either is out of range. |
volume ¶
The shares asset_id is taken to trade on date: the day's volume, else the last reported within backfill_days calendar days, else the average daily volume. None when it has no volume at all.
allowed ¶
How many shares of order may trade on a day the name trades volume shares; None when its volume is unknown.
MarketImpact ¶
The cost of a trade's size, by the square-root law.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
coefficient
|
float
|
Scales the impact; around 1 is typical. |
1.0
|
lookback_days
|
int
|
Trading days in the volatility and the average traded value. |
DEFAULT_LIQUIDITY_DAYS
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If coefficient is negative. |
rate ¶
The impact of trading trade_value of asset_id, as a fraction of the value. 0 when the name's volatility or traded value is unknown.
Deal
dataclass
¶
One day's flow, priced.
Attributes:
| Name | Type | Description |
|---|---|---|
units |
float
|
Units created (positive) or cancelled (negative). |
cash |
float
|
Cash into the fund (positive) or out of it (negative). |
price |
float
|
The price per unit the units were dealt at. |
adjustment |
float
|
What the dealing investors paid into the fund, through the price or a levy, to cover the trading their flow causes. |
DilutionLevy ¶
Bases: Pricing
A charge into the fund on deals larger than a threshold.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
rate_bps
|
float | None
|
The levy, in basis points of the deal. None estimates it from the cost of trading the flow. |
None
|
threshold
|
float
|
The deal, as a share of the fund, above which it is charged. 0 (the default) charges every deal. |
0.0
|
on
|
str
|
Which deals: |
BOTH
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If rate_bps or threshold is negative, or on is not one of the three. |
DualPricing ¶
Bases: Pricing
Buyers pay an offer price above NAV, sellers receive a bid price below it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
offer_bps
|
float | None
|
The offer price's premium over NAV, in basis points. None estimates it from the cost of buying the flow. |
None
|
bid_bps
|
float | None
|
The bid price's discount to NAV, in basis points. None estimates it from the cost of selling. |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If either is negative. |
Pricing ¶
Bases: ABC
How a fund prices its dealing.
estimated
property
¶
Whether the method needs the run's estimate of the flow's trading cost.
deal
abstractmethod
¶
Price a flow.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
amount
|
float
|
The flow at NAV, in the book's currency: positive for money paid in, negative for the value of units redeemed. |
required |
nav_per_unit
|
float
|
Today's NAV per unit, before the flow. |
required |
net_assets
|
float
|
The fund's net assets before the flow. |
required |
cost_rate
|
float
|
The estimated cost of trading the flow, as a share of it, for a method that estimates. |
required |
SwingPricing ¶
Bases: Pricing
A single price that swings with the day's net flow.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
factor_bps
|
float | None
|
How far the price swings, in basis points. None estimates it from the cost of trading the flow. |
None
|
threshold
|
float
|
The net flow, as a share of the fund, above which the price swings. 0 (the default) swings on every flow: full swing. |
0.0
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If either is negative. |
BacktestEngine ¶
BacktestEngine(
start_date: str,
end_date: str,
initial_capital: float | Mapping[str, float],
data_provider: DataFetcher,
index_result: IndexResult,
price_column: str = "CLOSE",
currency: str | None = None,
modelling_assumptions: ModellingAssumptions
| None = None,
transaction_cost_bps: float = 0.0,
dividends: str = REINVEST,
implementation: Implementation | None = None,
modifiers: list[BacktestModifier] | None = None,
benchmark: IndexResult | Series | None = None,
target_index: IndexResult | None = None,
calendar: str | None = None,
flows: Flows
| list[Flows]
| Mapping[str, Flows | list[Flows]]
| None = None,
vehicle: Vehicle | None = None,
schedule: dict[Timestamp, dict[str, float]]
| None = None,
swap: SwapTerms | None = None,
futures: IndexFutures | None = None,
share_classes: Sequence[ShareClass] | None = None,
reinvest_into: str = PAYER,
rebalance_tags: tuple[str, ...] = (),
)
Bases: PricingMixin, ShareClassesMixin, DividendsMixin, ExecutionMixin, AccountingMixin, CashBandMixin, ListingsMixin, SwapMixin, FuturesMixin, HedgingMixin
Simulates portfolio execution against a target weight schedule.
The engine consumes target weights from an IndexResult and simulates
trading over a date range using prices from a DataFetcher. On each
rebalance date it sells before it buys, charges costs in basis points of
notional, and sizes a buy down to the cash available rather than dropping
it. Holdings whose listing has ended are settled into cash at their last
price, without cost.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
start_date
|
str
|
The start date of the backtest (YYYY-MM-DD). |
required |
end_date
|
str
|
The end date of the backtest (YYYY-MM-DD). |
required |
initial_capital
|
float | Mapping[str, float]
|
The starting capital for the backtest. With share
classes, each class's by name, such as |
required |
data_provider
|
DataFetcher
|
Data source for market prices. |
required |
index_result
|
IndexResult
|
The IndexResult whose weight_snapshots provide the rebalance schedule and target weights. |
required |
price_column
|
str
|
Column name to read from market data. Defaults to
|
'CLOSE'
|
currency
|
str | None
|
The portfolio's currency. Prices quoted in another currency are converted into it with the FX rate on or before each day. None, the default, uses the index's currency, or USD when the index does not record one. Pass another to keep the book in a different currency from the index, such as a dollar investor tracking a euro index. |
None
|
modelling_assumptions
|
ModellingAssumptions | None
|
What the run takes as given about markets and data, laid over the process-wide default. Data is read under its data treatment, cash earns its cash rate, and the result records them resolved. When index_result was calculated under a different data treatment, the run goes ahead and logs a warning naming each setting that differs. |
None
|
transaction_cost_bps
|
float
|
Transaction cost in basis points applied to each trade's notional value. Defaults to 0 (no cost). |
0.0
|
dividends
|
str
|
What happens to cash distributions the holdings are
paid: |
REINVEST
|
reinvest_into
|
str
|
Where reinvested dividends go: |
PAYER
|
implementation
|
Implementation | None
|
The screens and redistribution rule applied at each
rebalance (see :class: |
None
|
flows
|
Flows | list[Flows] | Mapping[str, Flows | list[Flows]] | None
|
Money arriving and leaving: one scenario or several, added
together (see :mod: |
None
|
vehicle
|
Vehicle | None
|
The fund's fee and unit price at launch (see
:class: |
None
|
schedule
|
dict[Timestamp, dict[str, float]] | None
|
The weights to trade to at each rebalance, when they
differ from the index's own: a replication of it (see
:mod: |
None
|
swap
|
SwapTerms | None
|
For a synthetic fund, the total return swap that exchanges the
schedule's basket's return for the index's (see
:class: |
None
|
futures
|
IndexFutures | None
|
For a futures-based fund, the index futures that carry its
whole exposure while its assets stay in cash (see
:class: |
None
|
share_classes
|
Sequence[ShareClass] | None
|
The fund's share classes, sharing one book, each with
its own fee, flows and distribution policy (see
:mod: |
None
|
modifiers
|
list[BacktestModifier] | None
|
Optional hooks that can skip rebalances or adjust trades. |
None
|
benchmark
|
IndexResult | Series | None
|
The benchmark of record, stored on the result so every reader quotes excess return against the same comparator. |
None
|
target_index
|
IndexResult | None
|
The calculated index the traded schedule was derived
from, when it differs from the schedule itself (the
derived-index shape): index_result is an optimised
calculation and this is its parent, and they land in
|
None
|
calendar
|
str | None
|
The exchange MIC the traded index schedules on, which every index definition carries. It decides which days the run steps onto and how a missing bar on one of them is read: on a day the calendar says was closed the market was shut and the previous session's price is what the position was worth, while on a day it says was open the data is missing something and the carried price is recorded as a gap. With None the run steps onto Monday to Friday, and a missing bar is judged against the data's own sessions (a day the store has bars for is treated as open), which is all a caller assembling an engine by hand can offer. |
None
|
rebalance_tags
|
tuple[str, ...]
|
Tags every rebalance trade carries beyond its
reason, as |
()
|
run ¶
Execute the backtest and return a :class:BacktestResult.
Calling run() again on the same engine starts a fresh run.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
progress
|
Callable[[int, int, Timestamp], None] | None
|
Called about a hundred times a run, and on the last day, with the trading day reached, the days in the run and its date. |
None
|
Returns:
| Type | Description |
|---|---|
BacktestResult
|
BacktestResult |
Raises:
| Type | Description |
|---|---|
CalculationError
|
If the dataset has no column to price positions from, checked before the first trade rather than discovered at it. Also raised when a position cannot be priced: a date outside the market data's coverage, or a holding with no FX rate into the book's currency on or before the day. |
EtfMarket ¶
EtfMarket(
spread_floor_bps: float = 2.0,
basket_sensitivity: float = 1.0,
volatility_sensitivity: float = 0.05,
tick: float = 0.01,
persistence: float = 0.5,
flow_sensitivity: float = 0.1,
return_sensitivity: float = 0.05,
noise_bps: float = 2.0,
volatility_days: int = 21,
seed: int = 0,
)
How an ETF's shares are quoted on the exchange.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
spread_floor_bps
|
float
|
The narrowest spread a market maker quotes, in basis points. |
2.0
|
basket_sensitivity
|
float
|
How much of the basket's trading cost the spread passes on: 1 adds the whole cost. |
1.0
|
volatility_sensitivity
|
float
|
How much the spread widens per unit of the NAV's daily volatility: 0.05 adds 5 basis points at 1% a day. |
0.05
|
tick
|
float
|
The smallest price step, in the book's currency. |
0.01
|
persistence
|
float
|
How much of yesterday's premium remains today, from 0 up to 1: 0.5 halves it each day. |
0.5
|
flow_sensitivity
|
float
|
The premium per unit of the day's net creations as a share of the fund: 0.1 turns 1% of creations into a 10 basis point premium. |
0.1
|
return_sensitivity
|
float
|
The premium per unit of the NAV's daily return, so the price leads NAV on large moves: 0.05 turns a 4% fall into a 20 basis point discount. |
0.05
|
noise_bps
|
float
|
The standard deviation of the premium's daily wander, in basis points. |
2.0
|
volatility_days
|
int
|
Simulated days the volatility is measured over. |
21
|
seed
|
int
|
The random seed, so a run repeats exactly. |
0
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If a setting is out of range. |
EtfVehicle ¶
EtfVehicle(
creation_unit: int = DEFAULT_CREATION_UNIT,
in_kind: bool = False,
ap_fee: float = 500.0,
creation_fee_bps: float | None = None,
market: EtfMarket | None = None,
**settings: Any,
)
Bases: Vehicle
A vehicle whose shares are created in units and traded on an exchange.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
creation_unit
|
int
|
Shares in a creation unit. |
DEFAULT_CREATION_UNIT
|
in_kind
|
bool
|
Create and redeem by delivering the holdings, so the fund does not trade, rather than in cash. |
False
|
ap_fee
|
float
|
What an AP pays per creation or redemption, in the book's currency. It covers the fund's administration, so it is not paid into the fund, but it is part of the cost of arbitrage. |
500.0
|
creation_fee_bps
|
float | None
|
For cash creations, the variable fee charged to the AP and paid into the fund, in basis points. None estimates it from the cost of trading the basket. |
None
|
market
|
EtfMarket | None
|
How the shares are quoted; |
None
|
**settings
|
Any
|
Any other |
{}
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If creation_unit is below 1 or ap_fee is negative. |
creation_units ¶
How many whole creation units amount deals, toward zero.
Quote
dataclass
¶
Quote(
date: Timestamp,
nav_per_unit: float,
premium: float,
spread: float,
market_price: float,
bid: float,
ask: float,
create_cost: float,
redeem_cost: float,
)
One day's quote for an ETF's shares.
Attributes:
| Name | Type | Description |
|---|---|---|
date |
Timestamp
|
The day. |
nav_per_unit |
float
|
NAV per share at the close. |
premium |
float
|
The market price over NAV, less one; negative for a discount. |
spread |
float
|
The full bid-ask spread, as a share of the market price. |
market_price |
float
|
The mid price on the exchange. |
bid |
float
|
What a seller receives. |
ask |
float
|
What a buyer pays. |
create_cost |
float
|
The arbitrage band's upper edge, as a share of NAV. |
redeem_cost |
float
|
Its lower edge, as a share of NAV. |
DatedFlows ¶
Bases: Flows
Amounts on given dates. One dated on a day the run does not simulate arrives on the next simulated day.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
amounts
|
Mapping[str | Timestamp, float]
|
Date (YYYY-MM-DD or a timestamp) to amount, positive in and negative out. |
required |
FlowContext
dataclass
¶
What a scenario is told on each simulated day.
Attributes:
| Name | Type | Description |
|---|---|---|
previous |
Timestamp
|
The last simulated day, or the eve of the run on the first. |
date |
Timestamp
|
Today. |
aum |
float
|
The fund's net assets before today's flows, in the book's currency. |
nav_per_unit |
Series
|
NAV per unit on each simulated day so far, today's before its flows included. |
FlowRecord
dataclass
¶
FlowRecord(
date: Timestamp,
amount: float,
nav_per_unit: float,
units: float,
dealing_price: float,
adjustment: float = 0.0,
creation_units: float | None = None,
)
One day's flow, as the run dealt it.
Attributes:
| Name | Type | Description |
|---|---|---|
date |
Timestamp
|
The day it was dealt. |
amount |
float
|
What was paid in (positive) or out (negative), in the book's currency. A redemption larger than the fund is cut to the fund. |
nav_per_unit |
float
|
The day's NAV per unit, before the flow. |
units |
float
|
Units created (positive) or cancelled (negative). |
dealing_price |
float
|
The price per unit they were dealt at: NAV per unit, or NAV moved by the vehicle's pricing method. |
adjustment |
float
|
What the dealing investors paid into the fund, through the price or a levy, toward the trading their flow caused. 0 under single pricing. |
creation_units |
float | None
|
For an ETF, the creation units created (positive) or redeemed (negative); None for any other vehicle. |
Flows ¶
Bases: ABC
A flow scenario: how much arrives or leaves on each simulated day.
PerformanceChasingFlows ¶
PerformanceChasingFlows(
sensitivity: float = 0.1,
base: float = 0.0,
lookback_days: int = 63,
frequency: str = MONTHLY,
)
Bases: _Periodic
Flows that follow the fund's trailing return, as investors chase performance.
Each flow day's share of the assets is base + sensitivity x trailing
return, the trailing return being the NAV per unit's change over the
last lookback_days simulated days. No flow until the run has that much
history.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
sensitivity
|
float
|
The share of the assets that flows per unit of trailing return: 0.1 turns a 10% trailing return into a 1% inflow. |
0.1
|
base
|
float
|
A share of the assets that flows each flow day whatever the return. |
0.0
|
lookback_days
|
int
|
Simulated days the trailing return covers. |
63
|
frequency
|
str
|
How often money moves ( |
MONTHLY
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If lookback_days is below 1. |
PeriodicFlows ¶
PeriodicFlows(
amount: float | None = None,
fraction: float | None = None,
frequency: str = MONTHLY,
)
Bases: _Periodic
A fixed amount, or a fixed share of the fund's assets, each period.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
amount
|
float | None
|
In the book's currency, positive in and negative out. |
None
|
fraction
|
float | None
|
A share of the fund's assets, such as 0.01 for 1% in or -0.01 for 1% out. |
None
|
frequency
|
str
|
|
MONTHLY
|
Raises:
| Type | Description |
|---|---|
ValueError
|
Unless exactly one of amount and fraction is given. |
RandomFlows ¶
Bases: _Periodic
Flows drawn at random as a share of the fund's assets, seeded so a run repeats exactly.
Each flow day's share is normal, with the annual drift and volatility scaled to the frequency: a mean of drift / n and a standard deviation of volatility / sqrt(n), for n flow days a year.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
drift
|
float
|
The expected net flow a year, as a share of the assets. |
0.0
|
volatility
|
float
|
Its annual standard deviation. |
0.1
|
frequency
|
str
|
How often money moves ( |
DAILY
|
seed
|
int
|
The random seed. |
0
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If volatility is negative. |
FuturesRoll
dataclass
¶
FuturesRoll(
date: Timestamp,
expiry: Timestamp,
notional: float,
cost: float,
dividend_yield: float,
contracts: float = 0.0,
price: float = 0.0,
multiplier: float = 1.0,
)
One roll of a fund's index futures to the next contract.
Attributes:
| Name | Type | Description |
|---|---|---|
date |
Timestamp
|
The day it rolled. |
expiry |
Timestamp
|
The expiry of the contract rolled into. |
notional |
float
|
The exposure carried into the new contract. |
cost |
float
|
What the roll cost, paid from cash. |
dividend_yield |
float
|
The dividend yield the new contract is priced with. |
contracts |
float
|
How many contracts of the new expiry were held; the contracts times the price times the multiplier is the notional. |
price |
float
|
The new contract's price, at fair value. |
multiplier |
float
|
The value of one index point per contract. |
Implementation ¶
Implementation(
screens: Iterable[Screen] = (),
caps: Iterable[CapacityCap] = (),
minimum_position: MinimumPosition | None = None,
impact: MarketImpact | None = None,
execution: ExecutionLimit | None = None,
redistribution: str = PRO_RATA,
cash_buffer: float = 0.0,
invest_flows: str = TARGET,
equitise: IndexFutures | None = None,
cash_band: tuple[float, float] | None = None,
)
A backtest's screens, capacity limits and redistribution rule.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
screens
|
Iterable[Screen]
|
Applied in order at each rebalance; a name must pass all. |
()
|
caps
|
Iterable[CapacityCap]
|
Capacity caps; a name is held at no more than the smallest. |
()
|
minimum_position
|
MinimumPosition | None
|
Positions too small to keep, or None to keep all. |
None
|
impact
|
MarketImpact | None
|
Market impact charged on every trade, beside the fixed cost, or None for none. |
None
|
execution
|
ExecutionLimit | None
|
How much of an order can trade in a day, or None to trade every order in full on the rebalance day. |
None
|
redistribution
|
str
|
Where removed or capped weight goes: |
PRO_RATA
|
cash_buffer
|
float
|
The share of the book kept in cash, such as 0.02 for 2%. Outflows are paid from cash first. |
0.0
|
cash_band
|
tuple[float, float] | None
|
The lowest and highest share of the book the cash may be before it is brought back to cash_buffer, checked every day; None (the default) leaves it to the next rebalance. |
None
|
invest_flows
|
str
|
What money arriving between rebalances buys: the last
rebalance's weights ( |
TARGET
|
equitise
|
IndexFutures | None
|
Index futures held on the cash the fund keeps uninvested,
so its buffer earns the index's return, or None to leave the cash
as cash. See :mod: |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If redistribution or invest_flows is not one of its choices, cash_buffer is not from 0 up to 1, or cash_band does not hold it. |
RebalanceStep
dataclass
¶
RebalanceStep(
date: Timestamp,
target: dict[str, float],
removed: dict[str, str] = dict(),
capped: dict[str, float] = dict(),
weights: dict[str, float] = dict(),
cash_weight: float = 0.0,
diversified: dict[str, float] = dict(),
screened: dict[str, float] = dict(),
)
What each stage of one rebalance did.
Attributes:
| Name | Type | Description |
|---|---|---|
date |
Timestamp
|
The rebalance date. |
target |
dict[str, float]
|
The strategy's weights, before any stage. |
removed |
dict[str, str]
|
Each name taken out, and the screen that took it out
( |
capped |
dict[str, float]
|
Each name cut to its capacity, and the weight it was cut to. |
screened |
dict[str, float]
|
The weights after the screens and redistribution, before the cash buffer and the caps. |
weights |
dict[str, float]
|
What the trades aimed at, after redistribution. |
cash_weight |
float
|
The share of the book the weights leave in cash. |
Act1940Limits ¶
DiversificationLimit ¶
UcitsLimits ¶
Bases: DiversificationLimit
The UCITS diversification rules.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
index_tracking
|
bool
|
Apply the rules for a fund replicating an index (20% in one issuer, 35% in the largest) rather than the standard 5/10/40. |
True
|
Backtest ¶
Backtest(
initial_capital: float | Mapping[str, float],
transaction_cost_bps: float = 0.0,
price_column: str = "CLOSE",
currency: str | None = None,
modelling_assumptions: ModellingAssumptions
| None = None,
dividends: str = REINVEST,
implementation: Implementation | None = None,
modifiers: list[BacktestModifier] | None = None,
benchmark: IndexResult | Series | None = None,
data_provider: DataFetcher | None = None,
cache: IndexResultCache | Literal[False] | None = None,
flows: Flows
| list[Flows]
| Mapping[str, Flows | list[Flows]]
| None = None,
vehicle: Vehicle | None = None,
share_classes: Sequence[ShareClass] | None = None,
reinvest_into: str = PAYER,
)
One-call backtests: assumptions on the object, the index per run.
The constructor mirrors :class:BacktestEngine's parameters (what stays
fixed across runs) and :meth:run takes the definition and window, so a
parameter sweep is one object per assumption set over one shared (cached)
calculation::
bt = Backtest(initial_capital=1_000_000, transaction_cost_bps=5.0)
result = bt.run(definition, start="2023-01-03", end="2023-12-29")
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
initial_capital
|
float | Mapping[str, float]
|
The starting capital for each run. With share classes, each class's by name. |
required |
transaction_cost_bps
|
float
|
Transaction cost in basis points applied to each trade's notional value. Defaults to 0 (no cost). |
0.0
|
price_column
|
str
|
Market-data column both the calculator and the engine
read. Defaults to |
'CLOSE'
|
currency
|
str | None
|
The simulated book's currency. None, the default, uses the index's currency. Pass another to keep the book in a different currency from the index. |
None
|
modelling_assumptions
|
ModellingAssumptions | None
|
What the run takes as given about markets and
data, laid over the process-wide default (see
:func: |
None
|
dividends
|
str
|
What happens to cash distributions: |
REINVEST
|
reinvest_into
|
str
|
Where reinvested dividends go: |
PAYER
|
implementation
|
Implementation | None
|
The screens and redistribution rule applied at each rebalance. None screens nothing beyond stale prices. |
None
|
share_classes
|
Sequence[ShareClass] | None
|
The fund's share classes, sharing one book, each with
its own fee, flows and distribution policy (see
:mod: |
None
|
flows
|
Flows | list[Flows] | Mapping[str, Flows | list[Flows]] | None
|
Money arriving and leaving: one scenario or several, added
together (see :mod: |
None
|
vehicle
|
Vehicle | None
|
The fund's fee and unit price at launch. None charges no fee. |
None
|
modifiers
|
list[BacktestModifier] | None
|
Optional hooks that can skip rebalances or adjust trades. |
None
|
benchmark
|
IndexResult | Series | None
|
The benchmark of record, stored on every result this object produces. |
None
|
data_provider
|
DataFetcher | None
|
Data source for both the calculation and the
simulation. None resolves the process's ambient source
(:func: |
None
|
cache
|
IndexResultCache | Literal[False] | None
|
Where calculated IndexResults are kept between runs. None
uses the default location when |
None
|
run ¶
run(
definition: AnyIndexDefinition
| IndexTracking
| ActiveStrategy,
start: str | None = None,
end: str | None = None,
optimised: bool = False,
optimisation_config: OptimisationConfig | None = None,
progress: Progress | None = None,
analysis: AnalysisRequest | None = None,
) -> BacktestResult
Calculate (or reuse) the index, then simulate tracking it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
definition
|
AnyIndexDefinition | IndexTracking | ActiveStrategy
|
The index to calculate and track: a plain
:class: |
required |
start
|
str | None
|
First date (YYYY-MM-DD). Defaults to the definition's base date. |
None
|
end
|
str | None
|
Last date (YYYY-MM-DD). Required. |
None
|
optimised
|
bool
|
Ad-hoc optimisation of definition: build an ephemeral derived index over it from optimisation_config (same solve, same chained levels as a stored one) and trade that. Requires the config; needs scipy only on this path. |
False
|
optimisation_config
|
OptimisationConfig | None
|
What the ad-hoc derivation is asked to do
(objective, constraints, reserved risk model). Only
meaningful, and only allowed, with |
None
|
progress
|
Progress | None
|
Called with the share of the run done, from 0 to 1, and what is happening: the index's calculation, the strategy's weights, then each trading day simulated, by date. |
None
|
analysis
|
AnalysisRequest | None
|
What to analyse with the run: its standard portfolio, risk and attribution figures are worked out before it returns, as well as its performance, and the result keeps the request and how long each part took. Without it every part is still built on first read, with default settings. |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
BacktestResult |
BacktestResult
|
The engine's result: portfolio kept whole, books |
BacktestResult
|
filled, data bound to the run's own source. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If end is not provided, or optimised and optimisation_config contradict each other (a flag with no config, or a config with no flag). |
CalculationError
|
If a calculation comes back empty (no level was computed, every level is zero, or the index never held a constituent), the objective is unknown, or an optimisation is infeasible. |
DataSourceError
|
If no data source is bound and the process has no ambient one. |
calculate ¶
calculate(
definition: AnyIndexDefinition,
start: str | None = None,
end: str | None = None,
) -> IndexResult
Calculate (or reuse) an index alone, as a run would calculate it.
The same calculation run starts from, with this backtest's data and
modelling assumptions, through the same cache: an index calculated
here is reused by a run over the same dates, and the other way round.
No portfolio is simulated.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
definition
|
AnyIndexDefinition
|
The index: a plain :class: |
required |
start
|
str | None
|
First date (YYYY-MM-DD). Defaults to the definition's base date. |
None
|
end
|
str | None
|
Last date (YYYY-MM-DD). Required. |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
IndexResult |
IndexResult
|
Its levels, divisors and the weights at each |
IndexResult
|
rebalance. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If end is not provided. |
CalculationError
|
If the calculation comes back empty. |
Preset
dataclass
¶
A preset by name, for listing.
Attributes:
| Name | Type | Description |
|---|---|---|
key |
str
|
The name |
name |
str
|
What the structure is called. |
build |
Callable[..., Vehicle]
|
Makes the vehicle; takes any |
BacktestResult
dataclass
¶
BacktestResult(
portfolio: Portfolio,
index: IndexBooks = IndexBooks(),
benchmark: Book | None = None,
unfilled: list[UnfilledOrder] = list(),
price_gaps: list[PriceGap] = list(),
rebalance_pricing: list[RebalancePricing] = list(),
rebalance_steps: list[RebalanceStep] = list(),
currency: str = "USD",
modelling_assumptions: ModellingAssumptions
| None = None,
flows: list[FlowRecord] = list(),
units: Series = (lambda: pd.Series(dtype=float))(),
fees_payable: Series = (
lambda: pd.Series(dtype=float)
)(),
launch_price: float = 1.0,
market: DataFrame = pd.DataFrame(),
replication: list[ReplicationStep] = list(),
active: list[ActiveStep] = list(),
swap_value: Series = (lambda: pd.Series(dtype=float))(),
swap_resets: list[SwapReset] = list(),
futures_notional: Series = (
lambda: pd.Series(dtype=float)
)(),
futures_rolls: list[FuturesRoll] = list(),
cash_band_events: DataFrame = pd.DataFrame(),
futures_trades: list[FuturesTrade] = list(),
futures_days: DataFrame = pd.DataFrame(),
hedge_value: Series = (
lambda: pd.Series(dtype=float)
)(),
dividends_receivable: Series = (
lambda: pd.Series(dtype=float)
)(),
dividends_entitled: DataFrame = (
lambda: pd.DataFrame(
columns=["GROSS", "WITHHELD"], dtype=float
)
)(),
share_classes: dict[str, ShareClassBook] = dict(),
analysis_request: Any = None,
analysis_timings: dict[str, float] = dict(),
analysis_block: dict[str, Any] | None = None,
_data_fetcher: DataFetcher | None = None,
)
Bases: MetricsMixin, AnalyticsMixin
The record of one backtest run.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
portfolio
|
Portfolio
|
The books (positions, weights, cash, NAV, transactions), kept whole and frozen by the engine on completion. |
required |
index
|
IndexBooks
|
The run's calculated indices, as an :class: |
IndexBooks()
|
benchmark
|
Book | None
|
The benchmark of record, when one was given to the engine. |
None
|
unfilled
|
list[UnfilledOrder]
|
Buys the simulation could not execute in full. Empty for a run where every rebalance leg filled, so a non-empty list is itself the signal that the portfolio drifted off target for a reason other than price movement. |
list()
|
price_gaps
|
list[PriceGap]
|
Days a name had no bar on a session its calendar says was open, and was therefore marked at a carried-forward price. Empty for a run with complete data. A market holiday is not a gap, since nothing is missing on a day nothing traded. |
list()
|
rebalance_pricing
|
list[RebalancePricing]
|
What each rebalance priced from, in date order.
|
list()
|
rebalance_steps
|
list[RebalanceStep]
|
What the screening and redistribution stages did at each rebalance: the target, the names removed and by what, and the weights traded to. |
list()
|
currency
|
str
|
The book's currency, which the NAV, costs and holdings' values are in. |
'USD'
|
modelling_assumptions
|
ModellingAssumptions | None
|
What the run assumed, every field resolved. None for a result built by hand, which the summary then measures with a zero risk-free rate over 252 periods a year. |
None
|
flows
|
list[FlowRecord]
|
Each day's flow as dealt: the amount, and the units created or cancelled at that day's NAV per unit. Empty without flows. |
list()
|
units
|
Series
|
Units outstanding at the end of each day, from the eve of the first trading day. |
(lambda: Series(dtype=float))()
|
fees_payable
|
Series
|
The management fee owed and not yet paid at the end of each day, which the NAV is net of. |
(lambda: Series(dtype=float))()
|
launch_price
|
float
|
NAV per unit at launch. |
1.0
|
market
|
DataFrame
|
For an ETF, each day's quote on the exchange: NAV per share, premium, spread, market price, bid and ask, and the arbitrage band. Empty for any other vehicle. |
DataFrame()
|
replication
|
list[ReplicationStep]
|
For an index held through |
list()
|
active
|
list[ActiveStep]
|
For an |
list()
|
swap_value
|
Series
|
For a synthetic fund, its swap's value at the end of each day: owed to the fund when positive, by it when negative. The NAV includes it. Empty for any other fund. |
(lambda: Series(dtype=float))()
|
swap_resets
|
list[SwapReset]
|
For a synthetic fund, each reset of its swap: the day, the value settled, why, and the notional it was reset to. |
list()
|
futures_notional
|
Series
|
For a fund holding index futures (futures-based, or equitising its cash buffer), the exposure they carried at the end of each day. Their profit and loss is already in the cash. Empty for any other fund. |
(lambda: Series(dtype=float))()
|
futures_rolls
|
list[FuturesRoll]
|
Each roll of those futures to the next contract: the day, the new expiry, the notional, the cost and the dividend yield it was priced with. |
list()
|
futures_trades
|
list[FuturesTrade]
|
Each trade of those futures: opened, rolled or resized, the contracts before, after and traded, the price, the notional traded and the cost. |
list()
|
futures_days
|
DataFrame
|
Each day the futures were held: the contracts, their price, their fair value, the level of the index they are on and of the fund's own, the assumed mispricing and their expiry. Empty for any other fund. |
DataFrame()
|
cash_band_events
|
DataFrame
|
For a fund with a cash band, each time its cash left the band and was brought back, by date: whether it was a raise or a spend-down, the cash's share of the fund before and after, the target and the band, the value traded, its cost and the number of trades. Empty otherwise. |
DataFrame()
|
hedge_value
|
Series
|
For a fund with currency-hedged share classes, the value of their open forwards at the end of each day, in the book's currency. The NAV includes it. Empty otherwise. |
(lambda: Series(dtype=float))()
|
dividends_receivable
|
Series
|
The dividends the book is owed at the end of each day: gone ex and not yet paid, at the day's exchange rates and net of withholding tax. The NAV includes it, as a fund's accounts do; nothing is bought with it until it is paid. |
(lambda: Series(dtype=float))()
|
dividends_entitled
|
DataFrame
|
What the book became owed on each day a holding
went ex: |
(lambda: DataFrame(columns=['GROSS', 'WITHHELD'], dtype=float))()
|
share_classes
|
dict[str, ShareClassBook]
|
For a fund run with share classes, each class's book by name: its net assets, units, NAV per unit, fees, distributions and flows. The result's own NAV and metrics are the whole fund's. Empty otherwise. |
dict()
|
trading_nav
property
¶
NAV over the simulated days, with the day-zero row excluded.
The portfolio's own nav opens with initial capital on the eve of
the first trading day, the record of what the run started with.
Every metric derives from this series instead: the eve row is a
starting fact, not a day the simulation traded. Net of the management
fee owed and not yet paid, when the run had a vehicle charging one.
counterparty_exposure
property
¶
For a synthetic fund, what it would lose if its swap counterparty failed, as a share of NAV at the end of each day: the swap's value when owed to the fund, otherwise zero. Empty for any other fund.
futures_exposure
property
¶
For a fund holding index futures, the exposure they carried as a share of NAV at the end of each day. Empty for any other fund.
total_unfilled_value
property
¶
Total notional that went unfilled across the run.
with_data ¶
Bind a DataFetcher for asset-level queries. Returns self for chaining.
asset ¶
Return a BacktestAssetView for an asset the run ever held.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
asset_id
|
str
|
Identifier of the asset. |
required |
Returns:
| Type | Description |
|---|---|
BacktestAssetView
|
BacktestAssetView |
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If no DataFetcher has been bound via
:meth: |
KeyError
|
If the run's books never held asset_id. Membership is judged from the positions panel (the record of holdings) rather than from a weight column, so a position too small to round to a visible weight still counts as held. |
against ¶
Compare this run's NAV against any comparator, after the fact.
The run-time benchmark is a fact about the run; this is a question asked later, so it computes and returns, and stores nothing. Ask against ten comparators and the result is byte-for-byte what it was.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
other
|
Comparable
|
Another result, a book, an index result, or a bare level series. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
RelativeMetrics |
RelativeMetrics
|
Excess return, tracking error, beta and |
RelativeMetrics
|
correlation over the common window, as |
|
RelativeMetrics
|
computes them. |
RebalancePricing
dataclass
¶
What one rebalance priced from.
A rebalance scheduled on a day the market was shut still trades: it prices
from the session in force through the closure. Recording both dates lets a
reader tell that case apart from an ordinary rebalance. date and
priced_from are equal for the ordinary rebalance, which is what makes an
unequal pair worth reading.
Attributes:
| Name | Type | Description |
|---|---|---|
date |
Timestamp
|
The rebalance date from the weight schedule. |
priced_from |
Timestamp
|
The session its prices were read from. |
UnfilledOrder
dataclass
¶
UnfilledOrder(
date: Timestamp,
asset_id: str,
requested_quantity: float,
filled_quantity: float,
price: float,
shortfall_value: float,
reason: str = CASH_SHORT,
)
An order the simulation could not execute in full.
Recorded on the result rather than only logged: a partially filled rebalance leaves the portfolio off its target weights, and a caller comparing tracking error against expectations needs to know that happened rather than reading it as a modelling result.
Attributes:
| Name | Type | Description |
|---|---|---|
date |
Timestamp
|
The rebalance date. |
asset_id |
str
|
Asset that could not be fully traded. |
requested_quantity |
float
|
Quantity the rebalance asked for; 0.0 when the name could not be priced to size an order. |
filled_quantity |
float
|
Quantity actually traded; 0.0 when nothing was. |
price |
float
|
Execution price used; NaN when there was none. |
shortfall_value |
float
|
Notional value that went unfilled; for a name with no price, the value the rebalance aimed to hold. NaN for an order an execution limit left unfinished. |
reason |
str
|
Why: |
BacktestModifier ¶
Bases: ABC
Abstract base class for modifiers that alter rebalance behaviour.
A modifier can veto a rebalance entirely via :meth:should_skip_rebalance
or adjust the trade list via :meth:adjust_trades.
should_skip_rebalance
abstractmethod
¶
should_skip_rebalance(
date: Timestamp,
portfolio: Portfolio,
target_weights: dict[str, float],
) -> bool
Return True to skip the scheduled rebalance on date.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
date
|
Timestamp
|
The rebalance date. |
required |
portfolio
|
Portfolio
|
Current portfolio state (prices already updated). |
required |
target_weights
|
dict[str, float]
|
Target weights for this rebalance. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
bool |
adjust_trades
abstractmethod
¶
adjust_trades(
trades: list[TradeInstruction],
date: Timestamp,
portfolio: Portfolio,
) -> list[TradeInstruction]
Optionally modify the trade list before execution.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
trades
|
list[TradeInstruction]
|
The trades the engine generated for this rebalance, sells first, then buys. |
required |
date
|
Timestamp
|
The rebalance date. |
required |
portfolio
|
Portfolio
|
Current portfolio state. |
required |
Returns:
| Type | Description |
|---|---|
list[TradeInstruction]
|
list of TradeInstruction: The (possibly modified) trade list. |
DriftThresholdModifier ¶
Bases: BacktestModifier
Only rebalance when max weight drift exceeds a threshold.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
threshold
|
float
|
Maximum tolerable absolute drift between current and target weights. If every asset's drift is within this threshold the rebalance is skipped. For example, 0.05 means 5%. |
required |
adjust_trades ¶
adjust_trades(
trades: list[TradeInstruction],
date: Timestamp,
portfolio: Portfolio,
) -> list[TradeInstruction]
Pass-through: no trade adjustment.
ExclusionScreen ¶
ExclusionScreen(
identifiers: Iterable[str] = (),
sectors: Iterable[str] = (),
regions: Iterable[str] = (),
)
Bases: Screen
Names, sectors or regions a backtest may not hold.
Sector and region are read from the reference record in force on the rebalance date, so a name that moved sector is judged by the sector it was in then.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
identifiers
|
Iterable[str]
|
Names excluded outright. |
()
|
sectors
|
Iterable[str]
|
|
()
|
regions
|
Iterable[str]
|
|
()
|
ExpressionScreen ¶
Bases: Screen
Names that pass an expression, resolved at each rebalance.
Money fields in the expression, such as data.market.market_cap, are in
the book's currency.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
expression
|
Expression
|
The screen. |
required |
on_missing
|
bool
|
Whether a name with no value for a field passes. |
False
|
prepare ¶
Refuse a field the data does not have, before the run starts.
Raises:
| Type | Description |
|---|---|
ExpressionError
|
Naming each field that does not resolve. |
LiquidityScreen ¶
LiquidityScreen(
min_traded_value: float | None = None,
min_volume: float | None = None,
exit: float | None = None,
lookback_days: int = DEFAULT_LIQUIDITY_DAYS,
on_missing: bool = False,
)
Bases: ThresholdScreen
A minimum average traded value (book currency) or volume (shares).
Exactly one of min_traded_value and min_volume is given.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
min_traded_value
|
float | None
|
The entry level for the average of close times volume, converted into the book's currency day by day. |
None
|
min_volume
|
float | None
|
The entry level for the average share volume. |
None
|
exit
|
float | None
|
The exit level for a held name, on the same measure. |
None
|
lookback_days
|
int
|
How many of the name's most recent trading days are averaged. |
DEFAULT_LIQUIDITY_DAYS
|
on_missing
|
bool
|
Whether a name with no volume passes. |
False
|
Raises:
| Type | Description |
|---|---|
ValueError
|
Unless exactly one minimum is given. |
ListingAgeScreen ¶
Bases: Screen
A minimum time since a name's first price in the data.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
min_days
|
int
|
Calendar days since the first price. |
required |
MarketCapScreen ¶
MarketCapScreen(
min_cap: float,
exit: float | None = None,
float_adjusted: bool = False,
on_missing: bool = False,
)
Bases: ThresholdScreen
A minimum market cap, in the book's currency.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
min_cap
|
float
|
The entry level. |
required |
exit
|
float | None
|
The exit level for a held name. None uses min_cap. |
None
|
float_adjusted
|
bool
|
Measure the free-float market cap instead. |
False
|
on_missing
|
bool
|
Whether a name with no market cap passes. |
False
|
MinimumPriceScreen ¶
Bases: ThresholdScreen
A minimum price, in the book's currency.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
min_price
|
float
|
The entry level. |
required |
exit
|
float | None
|
The exit level for a held name. None uses min_price. |
None
|
on_missing
|
bool
|
Whether a name with no recent price passes. |
False
|
Screen ¶
Bases: ABC
Decides whether a name may be held on a rebalance date.
prepare ¶
Check the screen against the data before the run starts.
admits
abstractmethod
¶
Whether asset_id may be held on date.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
asset_id
|
str
|
The name. |
required |
date
|
Timestamp
|
The rebalance date. |
required |
held
|
bool
|
Whether the book holds it now, which decides whether a buffered screen applies its entry or its exit level. |
required |
context
|
ScreenContext
|
The run's data and book currency. |
required |
ScreenContext
dataclass
¶
What a screen is told about the run it screens for.
Attributes:
| Name | Type | Description |
|---|---|---|
fetcher |
DataFetcher
|
The data the run reads. |
currency |
str
|
The book's currency, which money thresholds are in. |
ThresholdScreen ¶
Bases: Screen
A minimum on a value, with an optional lower exit level.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entry
|
float
|
The level a name not held must reach. |
required |
exit
|
float | None
|
The level a held name must stay at or above. None uses entry. Must not be above entry. |
None
|
on_missing
|
bool
|
Whether a name with no value passes. |
False
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If exit is above entry. |
value
abstractmethod
¶
The value compared against the levels, or None when unknown.
ShareClass
dataclass
¶
ShareClass(
name: str,
management_fee_bps: float | None = None,
distribution: str = ACCUMULATING,
currency: str | None = None,
hedged: bool = False,
hedge_ratio: float = 1.0,
hedge_band: float | None = 0.03,
hedge_cost_bps: float = 0.0,
)
One share class of a fund.
Attributes:
| Name | Type | Description |
|---|---|---|
name |
str
|
What the class is called, such as |
management_fee_bps |
float | None
|
The class's annual management fee in basis points. None uses the vehicle's. |
distribution |
str
|
|
currency |
str | None
|
The class's currency, such as |
hedged |
bool
|
Whether it hedges its share of the assets into its currency with a monthly forward. |
hedge_ratio |
float
|
The share of its net assets it hedges, 1.0 for all. |
hedge_band |
float | None
|
How far the hedge ratio may drift between monthly rolls, such as 0.03 for 97% to 103%, before the forward is resized. None resizes only at the rolls. |
hedge_cost_bps |
float
|
What trading its forwards costs, in basis points of the notional each trade deals. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the distribution is not one of the two, the fee is negative, the currency is not a three-letter code, a hedge has no currency, or the hedge ratio, band or cost is out of range. |
ShareClassBook
dataclass
¶
ShareClassBook(
name: str,
distribution: str,
management_fee_bps: float,
net_assets: Series,
units: Series,
nav_per_unit: Series,
fees: Series,
distributions: Series,
distributions_per_unit: Series,
flows: list[FlowRecord] = list(),
currency: str = "",
hedged: bool = False,
book_net_assets: Series = (
lambda: pd.Series(dtype=float)
)(),
hedge_value: Series = (
lambda: pd.Series(dtype=float)
)(),
hedges: list[HedgeTrade] = list(),
)
One share class's record of a run.
Attributes:
| Name | Type | Description |
|---|---|---|
name |
str
|
The class's name. |
distribution |
str
|
|
management_fee_bps |
float
|
The fee it was charged, in basis points a year. |
net_assets |
Series
|
Its net assets at the end of each day, from the eve of the first. |
units |
Series
|
Its units outstanding at the end of each day. |
nav_per_unit |
Series
|
Its NAV per unit at the end of each day. |
fees |
Series
|
The fee it accrued each day. |
distributions |
Series
|
What it paid out each day, in total. |
distributions_per_unit |
Series
|
What it paid out each day, per unit. |
flows |
list[FlowRecord]
|
Its own flows as dealt. |
currency |
str
|
The currency its NAV, flows, fees and distributions are in. |
hedged |
bool
|
Whether it hedged into that currency. |
book_net_assets |
Series
|
Its net assets in the book's currency, which add up to the fund's across the classes. |
hedge_value |
Series
|
Its forward's value at the end of each day, in its currency. Empty for an unhedged class. |
hedges |
list[HedgeTrade]
|
Each trade of its hedge: opened, rolled or resized. |
get_returns ¶
The class's daily returns per unit: its NAV per unit from one day to the next, with what it paid out added back, so a distributing class's returns are total returns. The first runs from the launch price to the first close.
total_return ¶
The class's total return over the run, distributions reinvested.
SwapReset
dataclass
¶
One reset of a synthetic fund's swap.
Attributes:
| Name | Type | Description |
|---|---|---|
date |
Timestamp
|
The day it was reset. |
settled |
float
|
The swap's value paid in cash: received by the fund when positive, paid by it when negative. 0 when the swap opened. |
reason |
str
|
Why: |
notional |
float
|
The notional it was reset to: the fund's net assets. |
Vehicle ¶
Vehicle(
management_fee_bps: float = 0.0,
launch_price: float = DEFAULT_LAUNCH_PRICE,
pricing: Pricing | None = None,
limits: Iterable[DiversificationLimit] = (),
name: str = "Vehicle",
domicile: str | None = None,
withholding_rates: Mapping[str, float] | None = None,
)
How a fund holds its money.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
management_fee_bps
|
float
|
The annual management fee in basis points, such as 20 for 0.20%. Accrued daily on net assets, ACT/365. |
0.0
|
launch_price
|
float
|
NAV per unit when the fund launches, which sets how many units the initial capital buys. |
DEFAULT_LAUNCH_PRICE
|
pricing
|
Pricing | None
|
How the fund prices dealing (see |
None
|
limits
|
Iterable[DiversificationLimit]
|
The structure's diversification limits (see
|
()
|
name
|
str
|
What the vehicle is called, such as a preset's name. |
'Vehicle'
|
domicile
|
str | None
|
Where the fund is domiciled, as a two-letter country code
such as |
None
|
withholding_rates
|
Mapping[str, float] | None
|
The fund's own withholding rates by the dividend's
source country, such as |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If the fee is negative, the launch price not positive, the domicile not a two-letter code, or a withholding rate not a share from 0 up to 1. |
daily_fee ¶
The fee net_assets accrue over days calendar days.
irish_icav ¶
An Irish ICAV under UCITS: an anti-dilution levy on deals above 2% of the fund, and the UCITS limits.
luxembourg_sicav ¶
A Luxembourg SICAV under UCITS: a partial swing above 2% of the fund, and the UCITS limits.
preset ¶
The vehicle for a preset, by name, with any settings changed.
Raises:
| Type | Description |
|---|---|
KeyError
|
If there is no preset by that name; the message lists them. |
ucits_etf ¶
A UCITS ETF: cash creations, a creation fee estimated from the basket's trading cost, and the UCITS limits.
uk_oeic ¶
A UK open-ended investment company: daily dealing, a full swinging single price, and the UCITS limits.
us_etf ¶
A US ETF: in-kind creations and the 1940 Act's test for a diversified fund.
us_mutual_fund ¶
A US mutual fund: single pricing, an optional redemption fee, and the 1940 Act's test for a diversified fund.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
redemption_fee_bps
|
float
|
A fee on redemptions, paid into the fund, in basis points. Charged on every redemption, since a run does not track how long each investor held. |
0.0
|
**settings
|
Any
|
Any other |
{}
|
accounting ¶
A backtest's fund accounting: units, flows, the management fee and interest.
Units¶
The initial capital buys units at the vehicle's launch price (1.0 without a vehicle). A flow creates or cancels units at that day's NAV per unit, before anything is traded for it, so a flow changes the fund's size and not its NAV per unit; performance is measured per unit. Without flows the units never change and every figure is what it would be without them.
A day's flows¶
Flows are dealt after the day's prices are marked:
- In: the cash arrives and units are created. On a rebalance day the
rebalance invests it; on any other day it is invested at once, as the
implementation's
invest_flowssays, keeping its cash buffer topped up first. Under an execution limit the buying is worked over the following days like any other order. - Out: paid from cash first, then by selling every holding pro rata, and units are cancelled. A redemption is paid the day it is dealt, so the sales that fund it are not held back by an execution limit. One larger than the fund is cut to the fund.
Pricing¶
The vehicle's pricing method (see beacon.backtest.dealing) sets the price
units are dealt at, or adds a levy, so the dealing investors can pay for the
trading their flow causes. Under single pricing, the default, the fund bears
it. A method left to estimate its rate is given the cost of trading the whole
flow: the fixed cost plus market impact, across the names the flow would
buy or sell.
Exchange-traded funds¶
With an EtfVehicle the flows are the APs' net demand, rounded toward zero
to whole creation units each day, with the rest carried to the next. An
in-kind creation or redemption moves the holdings themselves, pro rata and
without cost; a cash one is invested or raised like any flow, and its
creation fee is paid into the fund. After each close the shares are quoted
on the exchange (see beacon.backtest.etf).
The management fee¶
The vehicle's fee accrues every calendar day on the fund's net assets (ACT/365) and is owed as a liability: the NAV is net of it, a rebalance invests only what is not owed, and it is paid from cash as soon as there is cash to pay it.
analytics_access ¶
A backtest result's analytics, each built on first read and kept: its performance, portfolio, risk and attribution, with the settings of the analysis the run was asked for, or the defaults.
AnalyticsMixin ¶
The result's analytics, mixed into BacktestResult.
analytics
property
¶
The run's performance (beacon.analysis.performance.PerformanceAnalytics).
portfolio_analytics
property
¶
The run's portfolio figures (beacon.analysis.performance.portfolio
.PortfolioAnalytics).
risk
property
¶
The run's risk (beacon.analysis.risk.analytics.RiskAnalytics),
with the window, risk model and liquidity choices of the analysis the
run was asked for, or the defaults.
Raises:
| Type | Description |
|---|---|
ValueError
|
If the run kept no data source to measure from. |
attribution
property
¶
The run's attribution (beacon.analysis.attribution.Attribution).
asset_view ¶
BacktestAssetView: one asset's story inside a backtest, read from the books.
The view reads the run's Portfolio (positions, transactions) and the tracked index's book directly: its trades and costs, when it was held, its weight over time, and how that weight compared with the index's target.
BacktestAssetView ¶
BacktestAssetView(
asset_id: str,
data_fetcher: DataFetcher,
portfolio: Portfolio,
index_book: object | None = None,
)
Bases: AssetView
AssetView with backtest context for a specific asset.
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 |
portfolio
|
Portfolio
|
The run's books: positions, weights, transactions. |
required |
index_book
|
object | None
|
The tracked index's book, when the run tracked one.
Target weights come from its |
None
|
trades ¶
This asset's transactions.
Returns:
| Type | Description |
|---|---|
DataFrame
|
pd.DataFrame: DataFrame with columns: date, type, quantity, price, |
DataFrame
|
cost. Empty DataFrame if no trades exist for this asset. |
total_cost ¶
Sum of all transaction costs for this asset.
Returns:
| Name | Type | Description |
|---|---|---|
float |
float
|
Total transaction costs incurred for this asset. |
holding_periods ¶
Continuous periods when this asset was held.
Read from the positions panel (the record of quantities) rather than inferred from weights. Quantity above zero is the fact of holding; a weight of 0.0000 is a rounding statement about size.
Returns:
| Type | Description |
|---|---|
list[dict[str, Timestamp]]
|
list of dict: Each dict has |
list[dict[str, Timestamp]]
|
Timestamps. An open position at the end of the run has |
list[dict[str, Timestamp]]
|
set to the last recorded date. |
weight_series ¶
Time series of this asset's portfolio weight.
Returns:
| Type | Description |
|---|---|
Series
|
pd.Series: Weight at each date where the asset was held. Dates |
Series
|
where the asset had zero or no weight are excluded. |
target_weight_series ¶
Time series of this asset's target index weight.
Read from the rebalance snapshots (what each rebalance decided) rather than the index's daily panel: the target the portfolio traded to is the snapshot, and the daily drift between rebalances is the index's business, not the portfolio's instruction.
Returns:
| Type | Description |
|---|---|
Series
|
pd.Series: Target weight at each rebalance date. Rebalance dates |
Series
|
where the asset was not a constituent are excluded. Empty |
Series
|
Series if the run tracked no index. |
slippage_vs_target ¶
Difference between actual and target weights over time.
For each date the asset was held, finds the applicable target weight (most recent rebalance on or before that date) and computes actual - target.
Returns:
| Type | Description |
|---|---|
Series
|
pd.Series: Slippage series indexed by date. Positive values mean |
Series
|
the asset is overweight vs target. Empty Series if the run |
Series
|
tracked no index. |
weight_on_date ¶
This asset's portfolio weight on a specific date.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
date
|
Timestamp
|
The query date. |
required |
Returns:
| Type | Description |
|---|---|
float | None
|
float or None: The weight, or None if the asset was not held on |
float | None
|
that date. |
books ¶
A book: one series of levels and weights, compared the same way whatever it is. A backtest's result holds one for the portfolio's target index, one for an optimised index when there is one, and one for its benchmark.
Book ¶
One comparator's daily record: levels, weights, returns.
The uniform surface every comparator answers through, so
result.index.target.weights and result.benchmark.levels are the
same spelling on every book. A book built from an IndexResult keeps it as
source, because the snapshots it holds (what each rebalance decided)
are a different fact from the daily panel (what happened between).
A benchmark supplied as a bare level series has no weights; its
weights frame is empty rather than invented.
from_index
classmethod
¶
A book over an index result.
The result's daily weights panel becomes the wide weights frame (dates by identifiers). A result with no daily weights panel yields an empty frame rather than a derived one: weights are recorded, not reconstructed.
from_levels
classmethod
¶
A book over a bare level series, such as a benchmark from raw data.
IndexBooks
dataclass
¶
The run's calculated indices, one home each.
The container is always present; its books are what can be None.
That distinction reads as safe and is not: a guard written against
result.index rather than result.index.target never fires.
Attributes:
| Name | Type | Description |
|---|---|---|
target |
Book | None
|
The index being aimed at, pre-optimisation. Always filled on an engine-produced result, since the engine's schedule is always an IndexResult. None only on a result built by hand from an empty container. |
optimised |
Book | None
|
The solved index's own calculation, filled when the run tracked a derived (optimised) index. None on plain passive runs. |
tracked
property
¶
The book the engine traded toward: optimised when solved, else target.
index_books ¶
A run's calculated indices, one book each.
Given index_result alone, the run traded a plain calculation: it is the
target book. Given target_index as well, the schedule the engine
traded is an optimised calculation and target_index is the parent it was
solved from, so they are the target and optimised books.
benchmark_book ¶
The benchmark of record as a book, whichever form it was given in.
breakdown ¶
A run's money, day by day: what it cost, and where its cash went.
costs = daily_costs(result)
cash = daily_cash(result)
Each table has a row for every day of the run's NAV and a column for each kind, in the book's currency, and is 0 on a day nothing of that kind happened. Summed over the run, each column is the run's total of its kind.
daily_costs ¶
What the run cost, by day and kind.
Columns:
fixed: trading costs at the run's fixed rate, on every trade that paid them.impact: the market impact trades paid, when the run models it.futures_rolls: what rolling index futures to the next contract cost: the roll cost and the cost per contract of both legs.futures_trading: what trading them between rolls cost: opening the position and each resize.creation_fees: what investors dealing in or out paid into the fund toward the trading their flow caused, through the dealing price or a levy (an ETF's creation fee among them). Received by the fund, so it offsets the costs above rather than adding to them.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
result
|
BacktestResult
|
The finished run. |
required |
Returns:
| Type | Description |
|---|---|
DataFrame
|
pd.DataFrame: Dates of the NAV by kind, in the book's currency, 0 |
DataFrame
|
where nothing of a kind happened. |
trades ¶
Every trade the run made, in the order it made them.
Returns:
| Type | Description |
|---|---|
DataFrame
|
pd.DataFrame: Indexed by the trade's date ( |
DataFrame
|
|
DataFrame
|
(quantity times price), the cost and the part of it that was market |
DataFrame
|
impact, all in the book's currency; why the trade was made |
DataFrame
|
( |
DataFrame
|
as |
daily_cash ¶
Where the run's cash went, and what it was owed and charged, by day.
Balances, at the end of each day:
cash: the cash held.dividends_receivable: dividends gone ex and not yet paid, net of withholding, which the NAV includes.fees_payable: the management fee charged and not yet paid, which the NAV is net of.futures_exposure: what any index futures were exposed to.
Cash that moved, positive into the fund and negative out of it:
interest, dividends_received (net of withholding, on the pay
date), distributions, subscriptions, redemptions,
fees_paid, futures_settled (their daily margin and roll costs),
swap_settled and hedge_settled.
Charged or owed without cash moving:
dividends_gross: dividends the book became owed on their ex-dates, before tax.withholding_tax: the tax withheld from them, on the same days.fee_charged: the management fee charged for the day.flows_dealt: investors' flows as dealt, positive in.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
result
|
BacktestResult
|
The finished run. |
required |
Returns:
| Type | Description |
|---|---|
DataFrame
|
pd.DataFrame: Dates of the NAV by kind, in the book's currency, 0 |
DataFrame
|
where nothing of a kind happened. |
capacity ¶
Capacity: how much of each name a backtest may hold at its size.
Implementation(caps=[OwnershipCap(0.05), LiquidityCap(days=5)],
minimum_position=MinimumPosition(value=50_000))
A cap limits a position; it does not exclude the name. Each cap gives the largest value a position may have on a rebalance date, and the weight that leaves at the book's size then is the name's cap. A name over its cap is cut to it, and the excess is spread across the names under their caps in proportion to their weights, repeated until none is over. As the fund grows, a position shrinks smoothly rather than jumping to zero at a threshold.
Under redistribution="cash" the excess stays in cash instead, and so does
the weight of a position too small to keep.
A name a cap cannot value (no free float, no volume) is not limited by it.
CapacityCap ¶
Bases: ABC
The largest value a position may have on a date.
OwnershipCap ¶
Bases: CapacityCap
At most a share of a name's free-float market cap, or of its whole market cap.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
max_share
|
float
|
The share, as a decimal: 0.05 holds at most 5% of the free float. |
required |
free_float
|
bool
|
Measure against the free-float market cap (the default) or, when False, against every share outstanding. |
True
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If max_share is not above 0 and at most 1. |
LiquidityCap ¶
LiquidityCap(
days: float,
participation: float = 0.2,
lookback_days: int = DEFAULT_LIQUIDITY_DAYS,
)
Bases: CapacityCap
At most what could be sold over a number of days.
The position's value is limited to days times the name's average daily traded value times participation, the share of each day's trading the fund could be.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
days
|
float
|
Days to liquidate. |
required |
participation
|
float
|
The share of a day's traded value, as a decimal. |
0.2
|
lookback_days
|
int
|
Trading days in the average. |
DEFAULT_LIQUIDITY_DAYS
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If days or participation is not positive. |
WeightCap ¶
Bases: CapacityCap
At most a share of the book, whatever the name.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
max_weight
|
float
|
The share, as a decimal. |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If max_weight is not above 0 and at most 1. |
MinimumPosition ¶
Positions too small to keep, by value or by weight.
A position below either is dropped and its weight redistributed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
float | None
|
The smallest position value, in the book's currency. |
None
|
weight
|
float | None
|
The smallest weight, as a decimal. |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If neither is given. |
too_small ¶
Whether a position of weight in a book of book_value is.
cash_band ¶
Keeping a fund's cash within a band around its target, every day.
A backtest's Implementation.cash_buffer is the share of the book it means to
hold in cash, and each rebalance invests the rest. Between rebalances the cash
moves: a delisted holding is settled into it, a dividend kept as cash lands in
it, a fee or a futures margin call is paid from it. With a cash_band the run
checks it at the close of every day: above the band's upper edge the cash is
invested back down to the target, below its lower edge holdings are sold pro
rata to bring it back up, and inside the band nothing is traded. Without one,
the cash waits for the next rebalance, as it always has.
CashBandEvent
dataclass
¶
CashBandEvent(
date: Timestamp,
kind: str,
cash_before: float,
cash_after: float,
target: float,
lower: float,
upper: float,
traded: float,
cost: float,
trades: int,
)
One time the cash left its band and was brought back.
Attributes:
| Name | Type | Description |
|---|---|---|
date |
Timestamp
|
The day. |
kind |
str
|
|
cash_before |
float
|
The cash's share of the fund at the close, before. |
cash_after |
float
|
Its share after the trades. |
target |
float
|
The share it was brought back to: the cash buffer. |
lower |
float
|
The band's lower edge. |
upper |
float
|
The band's upper edge. |
traded |
float
|
The value bought or sold. |
cost |
float
|
What the trades cost. |
trades |
int
|
How many trades it took. |
CashBandMixin ¶
Holds the cash within its band between rebalances, mixed into BacktestEngine.
band_events_frame ¶
The events as a frame indexed by date.
class_benchmark ¶
What a share class is measured against: the index in the class's currency.
A class in the index's own currency is measured against the index as it is. An unhedged class in another currency is measured against the index converted into its currency at each day's rate, so the currency move is in both. A hedged class is measured against the index hedged the same way: each month the index's value in its own currency is sold forward for the class's, at the class's hedge ratio, on the class hedge's terms. So a hedged class's tracking difference is its fees, costs and hedge slippage, not the interest rate differential its hedge earns or pays.
The hedged index. At the first session of each month the index's value I0 is converted at the day's spot S0 (class currency per unit of the index's) and a forward to the first business day of the next month is struck at covered interest parity:
F0 = S0 x (1 + r_class x t_class) / (1 + r_index x t_index)
On each day t of the month the hedged level moves by the index's return in the class's currency, plus the forward's gain per unit of that value:
H_t = H0 x ((I_t x S_t) / (I0 x S0) + h x (F0 - F_t) / (S0 x D_t))
where F_t is the day's forward to the same date, D_t discounts its gain at the class currency's rate and h is the hedge ratio. The month's level is the base of the next. The rates are each currency's short rate in the data on its own day count, else the cash rate on ACT/360, as the class's hedge prices them.
index_in_class_currency ¶
index_in_class_currency(
levels: Series,
index_currency: str,
currency: str,
data_provider: DataFetcher,
hedge_ratio: float | None = None,
cash_rate: float | None = None,
) -> pd.Series
The index's level in a share class's currency, converted or hedged.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
levels
|
Series
|
The index's levels, in index_currency, by date. |
required |
index_currency
|
str
|
The currency the levels are in. |
required |
currency
|
str
|
The class's currency. |
required |
data_provider
|
DataFetcher
|
The data the exchange and short rates are read from. |
required |
hedge_ratio
|
float | None
|
The class's hedge ratio for a hedged class; None for an unhedged one. |
None
|
cash_rate
|
float | None
|
The rate for a currency with no short rate in the data. |
None
|
Returns:
| Type | Description |
|---|---|
Series
|
pd.Series: The levels in currency, on the same dates. |
Raises:
| Type | Description |
|---|---|
CalculationError
|
If there is no rate between the two currencies on the first date. |
class_tracking ¶
class_tracking(
book: ShareClassBook,
benchmark: Series,
periods_per_year: int = DEFAULT_PERIODS_PER_YEAR,
) -> dict[str, float | None]
A class's total return, and its tracking against its benchmark.
Defined as a backtest's tracking is: the tracking difference is the class's total return per unit, with what it paid out added back, less the benchmark's over the run; the tracking error is the standard deviation of the two's daily return difference on their common dates, annualised.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
book
|
ShareClassBook
|
The class's record of the run. |
required |
benchmark
|
Series
|
Its benchmark's levels, in its currency (see
:func: |
required |
periods_per_year
|
int
|
How many daily returns make a year. |
DEFAULT_PERIODS_PER_YEAR
|
Returns:
| Name | Type | Description |
|---|---|---|
dict |
dict[str, float | None]
|
total_return, tracking_difference and tracking_error; the last |
dict[str, float | None]
|
two None without a benchmark. |
costs ¶
What trading costs beyond the fixed basis points, and how fast it can happen.
MarketImpact charges a trade for its size against what the name trades in a
day, by the square-root law. ExecutionLimit caps how much of an order can
trade in one day, by participation in the day's volume or by spreading it over
a number of days. Both are set on a backtest's Implementation; see
beacon.backtest.execution for how a rebalance uses them.
MarketImpact ¶
The cost of a trade's size, by the square-root law.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
coefficient
|
float
|
Scales the impact; around 1 is typical. |
1.0
|
lookback_days
|
int
|
Trading days in the volatility and the average traded value. |
DEFAULT_LIQUIDITY_DAYS
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If coefficient is negative. |
rate ¶
The impact of trading trade_value of asset_id, as a fraction of the value. 0 when the name's volatility or traded value is unknown.
ExecutionLimit ¶
ExecutionLimit(
participation: float | None = None,
days: int | None = None,
lookback_days: int = DEFAULT_LIQUIDITY_DAYS,
)
How much of an order can trade in one day.
Under a participation limit, a name with a volume of 0 on the day trades
nothing. A blank volume is replaced by the last one reported, if no older
than the run's volume_backfill_days, and otherwise by the name's
average daily volume over lookback_days. A name with no volume at all
is not limited by participation, and the run logs a warning.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
participation
|
float | None
|
At most this share of the day's volume, as a decimal. |
None
|
days
|
int | None
|
Spread each order evenly over this many trading days. |
None
|
lookback_days
|
int
|
Trading days in the average daily volume that stands in for a blank one. |
DEFAULT_LIQUIDITY_DAYS
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If neither is given, or either is out of range. |
volume ¶
The shares asset_id is taken to trade on date: the day's volume, else the last reported within backfill_days calendar days, else the average daily volume. None when it has no volume at all.
allowed ¶
How many shares of order may trade on a day the name trades volume shares; None when its volume is unknown.
WorkingOrder
dataclass
¶
An order still being worked over the sessions after a rebalance.
dealing ¶
How an open-ended fund prices the units investors buy and sell.
Vehicle(pricing=SwingPricing(threshold=0.02))
When investors buy or sell, the fund trades to invest or raise the money, and that costs money. Left alone, the cost comes out of the fund, so the investors who stay pay for the ones who came or went: dilution. A pricing method decides who pays. NAV per unit is calculated first; the method then sets the price units are created or cancelled at, or adds a charge, and whatever the dealing investor pays above NAV, or receives below it, stays in the fund.
SinglePricing: everyone deals at NAV, and the fund bears the trading cost.DualPricing: buyers pay NAV plus an offer adjustment, sellers receive NAV less a bid adjustment.SwingPricing: the price swings up on a net inflow and down on a net outflow, when the flow is larger than a threshold.DilutionLevy: deals are at NAV, and a deal larger than a threshold pays a charge into the fund.
An adjustment, swing factor or levy rate left unset is estimated from the run's cost model: the fixed cost plus the market impact of trading the whole flow, as a share of it.
A run deals one net flow a day, so a dual price is charged to the day's net buyers or net sellers rather than to each deal.
Deal
dataclass
¶
One day's flow, priced.
Attributes:
| Name | Type | Description |
|---|---|---|
units |
float
|
Units created (positive) or cancelled (negative). |
cash |
float
|
Cash into the fund (positive) or out of it (negative). |
price |
float
|
The price per unit the units were dealt at. |
adjustment |
float
|
What the dealing investors paid into the fund, through the price or a levy, to cover the trading their flow causes. |
Pricing ¶
Bases: ABC
How a fund prices its dealing.
estimated
property
¶
Whether the method needs the run's estimate of the flow's trading cost.
deal
abstractmethod
¶
Price a flow.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
amount
|
float
|
The flow at NAV, in the book's currency: positive for money paid in, negative for the value of units redeemed. |
required |
nav_per_unit
|
float
|
Today's NAV per unit, before the flow. |
required |
net_assets
|
float
|
The fund's net assets before the flow. |
required |
cost_rate
|
float
|
The estimated cost of trading the flow, as a share of it, for a method that estimates. |
required |
DualPricing ¶
Bases: Pricing
Buyers pay an offer price above NAV, sellers receive a bid price below it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
offer_bps
|
float | None
|
The offer price's premium over NAV, in basis points. None estimates it from the cost of buying the flow. |
None
|
bid_bps
|
float | None
|
The bid price's discount to NAV, in basis points. None estimates it from the cost of selling. |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If either is negative. |
SwingPricing ¶
Bases: Pricing
A single price that swings with the day's net flow.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
factor_bps
|
float | None
|
How far the price swings, in basis points. None estimates it from the cost of trading the flow. |
None
|
threshold
|
float
|
The net flow, as a share of the fund, above which the price swings. 0 (the default) swings on every flow: full swing. |
0.0
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If either is negative. |
DilutionLevy ¶
Bases: Pricing
A charge into the fund on deals larger than a threshold.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
rate_bps
|
float | None
|
The levy, in basis points of the deal. None estimates it from the cost of trading the flow. |
None
|
threshold
|
float
|
The deal, as a share of the fund, above which it is charged. 0 (the default) charges every deal. |
0.0
|
on
|
str
|
Which deals: |
BOTH
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If rate_bps or threshold is negative, or on is not one of the three. |
dividends ¶
Cash distributions: what a backtest's holdings are paid, and what happens to it.
A holding is entitled to a cash distribution if it is held at the start of the
ex-date, before that day's trades. The cash arrives on the pay date, or on the
ex-date when the data gives none, converted into the book's currency on the
day it arrives and net of withholding tax: by the fund's domicile and the
dividend's source country when the vehicle has a domicile (see
beacon.backtest.withholding), otherwise at the flat rate in the run's
modelling assumptions. A holding sold between the two dates is still paid.
What happens to the cash is the run's dividend policy:
"reinvest"(the default): it is invested the day it arrives. Where is the run'sreinvest_into:"payer"(the default) buys more of the name that paid it, and"holdings"buys the current holdings in proportion to their value, as a total-return index assumes. A payer no longer held, or with no price that day, leaves its dividend in cash."cash": it stays in the book as cash and is invested at the next rebalance."distribute": it is paid out of the book. The NAV falls by it and the returns add it back, so performance is still measured as total return.
"reinvest" and "cash" both keep the income in the book, as an
accumulating share class does; they differ only in when it is invested.
"distribute" pays it out, as a distributing share class does.
Every payment is a CashFlow on portfolio.cash_flows: a DIVIDEND for
each name paid, and a DISTRIBUTION (negative) when it is paid out.
The price path must already contain the ex-date drop, as an unadjusted feed and Beacon's synthetic data do. Paying cash against a price that never fell would count the distribution twice.
CashEvent
dataclass
¶
One cash distribution in the data: who pays, how much, when.
Entitlement
dataclass
¶
A distribution the book is owed: an event and the shares held for it.
DividendsMixin ¶
Dividend entitlement and payment, mixed into BacktestEngine.
entitled_frame ¶
Each day's gross dividends and tax withheld, as dividends_entitled.
cash_events ¶
Every cash distribution the data holds, by ex-date.
Cancelled distributions are left out. A pay date before its ex-date is read as the ex-date, since nothing can be paid before it is owed.
engine ¶
BacktestEngine: simulates portfolio execution against a target weight schedule.
BacktestEngine ¶
BacktestEngine(
start_date: str,
end_date: str,
initial_capital: float | Mapping[str, float],
data_provider: DataFetcher,
index_result: IndexResult,
price_column: str = "CLOSE",
currency: str | None = None,
modelling_assumptions: ModellingAssumptions
| None = None,
transaction_cost_bps: float = 0.0,
dividends: str = REINVEST,
implementation: Implementation | None = None,
modifiers: list[BacktestModifier] | None = None,
benchmark: IndexResult | Series | None = None,
target_index: IndexResult | None = None,
calendar: str | None = None,
flows: Flows
| list[Flows]
| Mapping[str, Flows | list[Flows]]
| None = None,
vehicle: Vehicle | None = None,
schedule: dict[Timestamp, dict[str, float]]
| None = None,
swap: SwapTerms | None = None,
futures: IndexFutures | None = None,
share_classes: Sequence[ShareClass] | None = None,
reinvest_into: str = PAYER,
rebalance_tags: tuple[str, ...] = (),
)
Bases: PricingMixin, ShareClassesMixin, DividendsMixin, ExecutionMixin, AccountingMixin, CashBandMixin, ListingsMixin, SwapMixin, FuturesMixin, HedgingMixin
Simulates portfolio execution against a target weight schedule.
The engine consumes target weights from an IndexResult and simulates
trading over a date range using prices from a DataFetcher. On each
rebalance date it sells before it buys, charges costs in basis points of
notional, and sizes a buy down to the cash available rather than dropping
it. Holdings whose listing has ended are settled into cash at their last
price, without cost.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
start_date
|
str
|
The start date of the backtest (YYYY-MM-DD). |
required |
end_date
|
str
|
The end date of the backtest (YYYY-MM-DD). |
required |
initial_capital
|
float | Mapping[str, float]
|
The starting capital for the backtest. With share
classes, each class's by name, such as |
required |
data_provider
|
DataFetcher
|
Data source for market prices. |
required |
index_result
|
IndexResult
|
The IndexResult whose weight_snapshots provide the rebalance schedule and target weights. |
required |
price_column
|
str
|
Column name to read from market data. Defaults to
|
'CLOSE'
|
currency
|
str | None
|
The portfolio's currency. Prices quoted in another currency are converted into it with the FX rate on or before each day. None, the default, uses the index's currency, or USD when the index does not record one. Pass another to keep the book in a different currency from the index, such as a dollar investor tracking a euro index. |
None
|
modelling_assumptions
|
ModellingAssumptions | None
|
What the run takes as given about markets and data, laid over the process-wide default. Data is read under its data treatment, cash earns its cash rate, and the result records them resolved. When index_result was calculated under a different data treatment, the run goes ahead and logs a warning naming each setting that differs. |
None
|
transaction_cost_bps
|
float
|
Transaction cost in basis points applied to each trade's notional value. Defaults to 0 (no cost). |
0.0
|
dividends
|
str
|
What happens to cash distributions the holdings are
paid: |
REINVEST
|
reinvest_into
|
str
|
Where reinvested dividends go: |
PAYER
|
implementation
|
Implementation | None
|
The screens and redistribution rule applied at each
rebalance (see :class: |
None
|
flows
|
Flows | list[Flows] | Mapping[str, Flows | list[Flows]] | None
|
Money arriving and leaving: one scenario or several, added
together (see :mod: |
None
|
vehicle
|
Vehicle | None
|
The fund's fee and unit price at launch (see
:class: |
None
|
schedule
|
dict[Timestamp, dict[str, float]] | None
|
The weights to trade to at each rebalance, when they
differ from the index's own: a replication of it (see
:mod: |
None
|
swap
|
SwapTerms | None
|
For a synthetic fund, the total return swap that exchanges the
schedule's basket's return for the index's (see
:class: |
None
|
futures
|
IndexFutures | None
|
For a futures-based fund, the index futures that carry its
whole exposure while its assets stay in cash (see
:class: |
None
|
share_classes
|
Sequence[ShareClass] | None
|
The fund's share classes, sharing one book, each with
its own fee, flows and distribution policy (see
:mod: |
None
|
modifiers
|
list[BacktestModifier] | None
|
Optional hooks that can skip rebalances or adjust trades. |
None
|
benchmark
|
IndexResult | Series | None
|
The benchmark of record, stored on the result so every reader quotes excess return against the same comparator. |
None
|
target_index
|
IndexResult | None
|
The calculated index the traded schedule was derived
from, when it differs from the schedule itself (the
derived-index shape): index_result is an optimised
calculation and this is its parent, and they land in
|
None
|
calendar
|
str | None
|
The exchange MIC the traded index schedules on, which every index definition carries. It decides which days the run steps onto and how a missing bar on one of them is read: on a day the calendar says was closed the market was shut and the previous session's price is what the position was worth, while on a day it says was open the data is missing something and the carried price is recorded as a gap. With None the run steps onto Monday to Friday, and a missing bar is judged against the data's own sessions (a day the store has bars for is treated as open), which is all a caller assembling an engine by hand can offer. |
None
|
rebalance_tags
|
tuple[str, ...]
|
Tags every rebalance trade carries beyond its
reason, as |
()
|
run ¶
Execute the backtest and return a :class:BacktestResult.
Calling run() again on the same engine starts a fresh run.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
progress
|
Callable[[int, int, Timestamp], None] | None
|
Called about a hundred times a run, and on the last day, with the trading day reached, the days in the run and its date. |
None
|
Returns:
| Type | Description |
|---|---|
BacktestResult
|
BacktestResult |
Raises:
| Type | Description |
|---|---|
CalculationError
|
If the dataset has no column to price positions from, checked before the first trade rather than discovered at it. Also raised when a position cannot be priced: a date outside the market data's coverage, or a holding with no FX rate into the book's currency on or before the day. |
etf ¶
An exchange-traded fund: creations in units, and a market price on an exchange.
Backtest(initial_capital=5e8, vehicle=ucits_etf(), flows=...)
The primary market. Authorised participants (APs) create and redeem shares with the fund in creation units, at NAV. The run's flows are their net demand: each day it is rounded toward zero to whole creation units, and what is left over carries to the next day. A cash creation delivers money, which the fund invests, and a variable creation fee charges the AP the cost; an in-kind creation delivers the holdings, so the fund does not trade.
The secondary market. Everyone else trades shares on an exchange. After
each close the run quotes them, as EtfMarket describes:
- The premium (or discount, when negative) persists from day to day, is pushed by the day's creations and by the NAV's return, wanders with seeded noise, and is held inside the arbitrage band: the cost of creating above NAV and of redeeming below it, which is the AP fee plus the cost of one creation unit's basket (or the creation fee, for cash).
- The spread is a floor plus the basket's trading cost and the NAV's recent volatility, and never less than one price tick.
market price = NAV per unit x (1 + premium)
ask = market price x (1 + spread / 2), bid = market price x (1 - spread / 2)
The basket's cost comes from the run's cost model: the fixed cost plus market impact for one creation unit's worth of every holding, at each holding's weight. With market impact on, it is refreshed at each rebalance and at the start of each week rather than every day, which would read every holding's history daily.
EtfMarket ¶
EtfMarket(
spread_floor_bps: float = 2.0,
basket_sensitivity: float = 1.0,
volatility_sensitivity: float = 0.05,
tick: float = 0.01,
persistence: float = 0.5,
flow_sensitivity: float = 0.1,
return_sensitivity: float = 0.05,
noise_bps: float = 2.0,
volatility_days: int = 21,
seed: int = 0,
)
How an ETF's shares are quoted on the exchange.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
spread_floor_bps
|
float
|
The narrowest spread a market maker quotes, in basis points. |
2.0
|
basket_sensitivity
|
float
|
How much of the basket's trading cost the spread passes on: 1 adds the whole cost. |
1.0
|
volatility_sensitivity
|
float
|
How much the spread widens per unit of the NAV's daily volatility: 0.05 adds 5 basis points at 1% a day. |
0.05
|
tick
|
float
|
The smallest price step, in the book's currency. |
0.01
|
persistence
|
float
|
How much of yesterday's premium remains today, from 0 up to 1: 0.5 halves it each day. |
0.5
|
flow_sensitivity
|
float
|
The premium per unit of the day's net creations as a share of the fund: 0.1 turns 1% of creations into a 10 basis point premium. |
0.1
|
return_sensitivity
|
float
|
The premium per unit of the NAV's daily return, so the price leads NAV on large moves: 0.05 turns a 4% fall into a 20 basis point discount. |
0.05
|
noise_bps
|
float
|
The standard deviation of the premium's daily wander, in basis points. |
2.0
|
volatility_days
|
int
|
Simulated days the volatility is measured over. |
21
|
seed
|
int
|
The random seed, so a run repeats exactly. |
0
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If a setting is out of range. |
Quote
dataclass
¶
Quote(
date: Timestamp,
nav_per_unit: float,
premium: float,
spread: float,
market_price: float,
bid: float,
ask: float,
create_cost: float,
redeem_cost: float,
)
One day's quote for an ETF's shares.
Attributes:
| Name | Type | Description |
|---|---|---|
date |
Timestamp
|
The day. |
nav_per_unit |
float
|
NAV per share at the close. |
premium |
float
|
The market price over NAV, less one; negative for a discount. |
spread |
float
|
The full bid-ask spread, as a share of the market price. |
market_price |
float
|
The mid price on the exchange. |
bid |
float
|
What a seller receives. |
ask |
float
|
What a buyer pays. |
create_cost |
float
|
The arbitrage band's upper edge, as a share of NAV. |
redeem_cost |
float
|
Its lower edge, as a share of NAV. |
EtfVehicle ¶
EtfVehicle(
creation_unit: int = DEFAULT_CREATION_UNIT,
in_kind: bool = False,
ap_fee: float = 500.0,
creation_fee_bps: float | None = None,
market: EtfMarket | None = None,
**settings: Any,
)
Bases: Vehicle
A vehicle whose shares are created in units and traded on an exchange.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
creation_unit
|
int
|
Shares in a creation unit. |
DEFAULT_CREATION_UNIT
|
in_kind
|
bool
|
Create and redeem by delivering the holdings, so the fund does not trade, rather than in cash. |
False
|
ap_fee
|
float
|
What an AP pays per creation or redemption, in the book's currency. It covers the fund's administration, so it is not paid into the fund, but it is part of the cost of arbitrage. |
500.0
|
creation_fee_bps
|
float | None
|
For cash creations, the variable fee charged to the AP and paid into the fund, in basis points. None estimates it from the cost of trading the basket. |
None
|
market
|
EtfMarket | None
|
How the shares are quoted; |
None
|
**settings
|
Any
|
Any other |
{}
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If creation_unit is below 1 or ap_fee is negative. |
creation_units ¶
How many whole creation units amount deals, toward zero.
MarketMaker ¶
Quotes an ETF's shares each day, as its EtfMarket says.
quote ¶
quote(
date: Timestamp,
nav_per_unit: float,
previous: float,
flow_share: float,
basket_cost: float,
) -> Quote
The day's quote.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
date
|
Timestamp
|
The day. |
required |
nav_per_unit
|
float
|
NAV per share at the close. |
required |
previous
|
float
|
NAV per share at the previous close. |
required |
flow_share
|
float
|
The day's net creations as a share of the fund. |
required |
basket_cost
|
float
|
The cost of trading one creation unit's basket, as a share of it. |
required |
quotes_frame ¶
The quotes as a frame indexed by date.
execution ¶
Trading: what a rebalance trades, what it costs, and how fast it can happen.
Implementation(impact=MarketImpact(coefficient=1.0),
execution=ExecutionLimit(participation=0.1))
Costs¶
Every trade pays the fixed cost in basis points of its value. With a
MarketImpact it also pays market impact, which grows with the trade's size
against what the name trades in a day (the square-root law):
impact = coefficient x daily volatility x sqrt(trade value / average
daily traded value)
as a fraction of the trade's value. Without impact a backtest's return does not depend on its size; with it, the same trade costs a large fund more.
Sizing buys¶
A rebalance sells first and buys with what is left. Buys are sized so that they and their costs fit the cash the sells leave, so a rebalance fills in full and the book ends fully invested, short of its target weights only by the costs.
Execution limits¶
With an ExecutionLimit, a name trades at most a share of the day's volume,
or an order is spread evenly over a number of days, or both. Whatever cannot
trade on the rebalance day is a working order, traded on the following
sessions under the same limit. A new rebalance replaces any order still
working. A replaced order, or one still working when the run ends, is
recorded in result.unfilled with the reason "execution limit".
A target name with no price on the rebalance day is recorded in
result.unfilled with the reason "no price".
ExecutionMixin ¶
Trade generation, costs and execution, mixed into BacktestEngine.
flows ¶
Money arriving in a backtest and leaving it, as a scenario.
Backtest(initial_capital=1e8,
flows=[PeriodicFlows(amount=5e6, frequency="MONTHLY"),
DatedFlows({"2024-06-03": -2e7})])
Flows are their own part of a run, separate from the strategy, the implementation and the vehicle, so the same flows can go into different vehicles. Each scenario says how much arrives (positive) or leaves (negative) on a simulated day, in the book's currency; several are added together.
| Scenario | A flow day's amount |
|---|---|
DatedFlows |
The amounts dated on or before the day, since the last one |
PeriodicFlows |
A fixed amount, or a share of the fund's assets |
RandomFlows |
A share of the assets drawn from a normal distribution, seeded |
PerformanceChasingFlows |
A share of the assets that follows the trailing return |
A periodic scenario's flow days are the first simulated day of each period (each week, month, quarter or year) after the first day of the run, which the initial capital already funds.
Each flow creates or cancels units at that day's NAV per unit, so performance
is measured per unit and a flow is not mistaken for a return. See
beacon.backtest.accounting for how a run deals and invests them.
FlowContext
dataclass
¶
What a scenario is told on each simulated day.
Attributes:
| Name | Type | Description |
|---|---|---|
previous |
Timestamp
|
The last simulated day, or the eve of the run on the first. |
date |
Timestamp
|
Today. |
aum |
float
|
The fund's net assets before today's flows, in the book's currency. |
nav_per_unit |
Series
|
NAV per unit on each simulated day so far, today's before its flows included. |
FlowRecord
dataclass
¶
FlowRecord(
date: Timestamp,
amount: float,
nav_per_unit: float,
units: float,
dealing_price: float,
adjustment: float = 0.0,
creation_units: float | None = None,
)
One day's flow, as the run dealt it.
Attributes:
| Name | Type | Description |
|---|---|---|
date |
Timestamp
|
The day it was dealt. |
amount |
float
|
What was paid in (positive) or out (negative), in the book's currency. A redemption larger than the fund is cut to the fund. |
nav_per_unit |
float
|
The day's NAV per unit, before the flow. |
units |
float
|
Units created (positive) or cancelled (negative). |
dealing_price |
float
|
The price per unit they were dealt at: NAV per unit, or NAV moved by the vehicle's pricing method. |
adjustment |
float
|
What the dealing investors paid into the fund, through the price or a levy, toward the trading their flow caused. 0 under single pricing. |
creation_units |
float | None
|
For an ETF, the creation units created (positive) or redeemed (negative); None for any other vehicle. |
Flows ¶
Bases: ABC
A flow scenario: how much arrives or leaves on each simulated day.
DatedFlows ¶
Bases: Flows
Amounts on given dates. One dated on a day the run does not simulate arrives on the next simulated day.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
amounts
|
Mapping[str | Timestamp, float]
|
Date (YYYY-MM-DD or a timestamp) to amount, positive in and negative out. |
required |
PeriodicFlows ¶
PeriodicFlows(
amount: float | None = None,
fraction: float | None = None,
frequency: str = MONTHLY,
)
Bases: _Periodic
A fixed amount, or a fixed share of the fund's assets, each period.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
amount
|
float | None
|
In the book's currency, positive in and negative out. |
None
|
fraction
|
float | None
|
A share of the fund's assets, such as 0.01 for 1% in or -0.01 for 1% out. |
None
|
frequency
|
str
|
|
MONTHLY
|
Raises:
| Type | Description |
|---|---|
ValueError
|
Unless exactly one of amount and fraction is given. |
RandomFlows ¶
Bases: _Periodic
Flows drawn at random as a share of the fund's assets, seeded so a run repeats exactly.
Each flow day's share is normal, with the annual drift and volatility scaled to the frequency: a mean of drift / n and a standard deviation of volatility / sqrt(n), for n flow days a year.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
drift
|
float
|
The expected net flow a year, as a share of the assets. |
0.0
|
volatility
|
float
|
Its annual standard deviation. |
0.1
|
frequency
|
str
|
How often money moves ( |
DAILY
|
seed
|
int
|
The random seed. |
0
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If volatility is negative. |
PerformanceChasingFlows ¶
PerformanceChasingFlows(
sensitivity: float = 0.1,
base: float = 0.0,
lookback_days: int = 63,
frequency: str = MONTHLY,
)
Bases: _Periodic
Flows that follow the fund's trailing return, as investors chase performance.
Each flow day's share of the assets is base + sensitivity x trailing
return, the trailing return being the NAV per unit's change over the
last lookback_days simulated days. No flow until the run has that much
history.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
sensitivity
|
float
|
The share of the assets that flows per unit of trailing return: 0.1 turns a 10% trailing return into a 1% inflow. |
0.1
|
base
|
float
|
A share of the assets that flows each flow day whatever the return. |
0.0
|
lookback_days
|
int
|
Simulated days the trailing return covers. |
63
|
frequency
|
str
|
How often money moves ( |
MONTHLY
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If lookback_days is below 1. |
futures ¶
Index futures held beside the books: for a futures-based fund, its whole exposure; for a physical fund, the cash buffer it equitises.
The position's profit and loss is settled in cash every day, as variation margin is, so the fund's net assets need nothing added for it: the futures are worth nothing after each day's settlement, and what they made or lost is already in the cash. What is recorded is the notional they carried, the rolls and what each cost.
Each contract's fair value is F = S x exp((r - q) x T): S the level of
the index the futures are on (the fund's own, unless they name another), r
the run's cash rate, q the index's dividend yield and T the time to expiry
over calendar days (ACT/365). Its price is the data's, when the terms name a
series of futures prices and it has one that day, and otherwise fair value
moved by the assumed mispricing, 0 unless the terms give it a size. See
beacon.strategy.futures.
The position opens at the first rebalance, rolls to the next contract the set number of sessions before expiry (paying the roll cost on its notional), and is resized to its target at each rebalance, each roll, on any day money flows in or out, and on any other day its exposure has drifted from the target by more than the band: the fund's net assets for a futures-based fund, its uninvested cash for one equitising. Each contract is worth its price times the multiplier, and the contracts are whole unless the terms say otherwise.
Every trade is recorded with what it cost: per contract, and in basis points
of its notional between rolls (see beacon.strategy.futures); so is each
day's position, its price and the fair value it was priced from.
FuturesRoll
dataclass
¶
FuturesRoll(
date: Timestamp,
expiry: Timestamp,
notional: float,
cost: float,
dividend_yield: float,
contracts: float = 0.0,
price: float = 0.0,
multiplier: float = 1.0,
)
One roll of a fund's index futures to the next contract.
Attributes:
| Name | Type | Description |
|---|---|---|
date |
Timestamp
|
The day it rolled. |
expiry |
Timestamp
|
The expiry of the contract rolled into. |
notional |
float
|
The exposure carried into the new contract. |
cost |
float
|
What the roll cost, paid from cash. |
dividend_yield |
float
|
The dividend yield the new contract is priced with. |
contracts |
float
|
How many contracts of the new expiry were held; the contracts times the price times the multiplier is the notional. |
price |
float
|
The new contract's price, at fair value. |
multiplier |
float
|
The value of one index point per contract. |
FuturesTrade
dataclass
¶
FuturesTrade(
date: Timestamp,
reason: str,
kind: str,
before: float,
after: float,
traded: float,
price: float,
notional: float,
cost: float,
)
One trade of a fund's index futures.
Attributes:
| Name | Type | Description |
|---|---|---|
date |
Timestamp
|
The day. |
reason |
str
|
Why it was made, as |
kind |
str
|
What it did: |
before |
float
|
The contracts held before. |
after |
float
|
The contracts held after. |
traded |
float
|
The contracts traded: for a roll both legs, the old contracts and the new. |
price |
float
|
The price of the contract held after. |
notional |
float
|
The notional traded: the contracts traded times the price times the multiplier. |
cost |
float
|
What the trade cost, paid from cash: for a roll, the roll cost and the cost per contract. |
FuturesMixin ¶
Marks, rolls and sizes a fund's index futures.
futures_trades_frame ¶
The trades as a frame indexed by date.
futures_days_frame ¶
Each day's position as a frame indexed by date.
hedging ¶
The currency hedge a hedged share class carries.
A hedged class is denominated in another currency from the fund's book and hedges its share of the assets back into its own: it sells the book's currency forward, for its own, in the amount of its net assets times its hedge ratio. The forward's gains and losses are the class's alone; the other classes never see them.
Pricing. A forward to T is priced at covered interest parity, from the day's spot rate X (class currency per unit of the book's) and the two currencies' short-term interest rates over the days left, each on its own day count (ACT/360 unless its reference data says ACT/365):
F = X x (1 + r_class x t_class) / (1 + r_book x t_book)
Its value to the class, in its own currency, is the notional times the
contracted rate less today's forward for the same date, discounted at the
class currency's rate. A currency's rate is its short rate in the data (see
beacon.data.kinds), else the run's cash rate.
Rolls. A one-month forward is opened at the first session of the run, and rolled at the first session of each month: settled in cash, and a new one opened for the next month, sized to the class's net assets. Between rolls the hedge ratio drifts as the assets move, and when it leaves the class's band (its hedge ratio plus or minus hedge_band) the forward is settled and reopened to the same date at the right size.
Settlement moves the forward's value into the book's cash, and the class keeps it: its net assets do not change, because the value was already counted in them.
The carry this earns or costs is the interest rate differential: a class in a higher-rate currency than the book's earns about the difference a year. Each trade records the carry it locks in over its term: its notional times the forward rate less spot, in the book's currency.
Costs. A class's hedge_cost_bps is charged on the notional each trade deals: all of it when a forward is opened or rolled, the change in it when one is resized. It is paid from the book's cash and borne by the class alone.
Forward
dataclass
¶
An open forward: the book's currency sold for the class's.
Attributes:
| Name | Type | Description |
|---|---|---|
notional |
float
|
How much of the book's currency is sold forward. |
rate |
float
|
The contracted rate, class currency per unit of the book's. |
expiry |
Timestamp
|
The day it settles. |
value |
float
|
What it is worth to the class today, in the book's currency. |
HedgeTrade
dataclass
¶
HedgeTrade(
date: Timestamp,
reason: str,
kind: str,
notional: float,
rate: float,
settled: float,
spot: float = 0.0,
expiry: Timestamp | None = None,
carry: float = 0.0,
cost: float = 0.0,
)
One trade of a class's hedge.
Attributes:
| Name | Type | Description |
|---|---|---|
date |
Timestamp
|
The day. |
reason |
str
|
Why it was made, as |
kind |
str
|
What it did: |
notional |
float
|
The new forward's notional, in the book's currency. |
rate |
float
|
The new forward's contracted rate, class currency per unit of the book's. |
settled |
float
|
What the old forward was settled for, in the class's currency: gained when positive. 0 when one was opened. |
spot |
float
|
The spot rate when it was struck, class currency per unit of the book's. |
expiry |
Timestamp | None
|
The day the new forward settles. |
carry |
float
|
The carry the new forward locks in over its term, in the book's currency: its notional times the forward rate less spot, over spot. Earned when positive. |
cost |
float
|
What the trade cost, in the book's currency. |
HedgingMixin ¶
Marks, rolls and resizes each hedged share class's forward.
histories ¶
Each name's volume and converted closes, read once per run rather than daily.
Market impact, the liquidity screen and cap, and execution limits each ask of a name, on a day, about its last few months of trading: its average traded value, its volatility, its volume. They used to read and convert that window from the market data every time they asked, one name and one day at a time, recomputing windows that overlap almost entirely from one day to the next.
Here a name's whole history is read and converted once, and each question is answered by slicing it to the window the question always used: the same calendar span back from the day, then the same last bars. The answers are the ones the windowed reads gave. The FX conversion is day by day, so converting the whole history converts each day as converting its window did.
History
dataclass
¶
One name's reported volume and its closes in one currency, by date.
volume_between ¶
Its reported volume from start to end, both included; None when it reports no volume at all.
closes_between ¶
Its closes from start to end, both included, blanks dropped.
average_traded ¶
average_traded(
start: Timestamp,
end: Timestamp,
days: int,
by_value: bool = True,
) -> float | None
Its average daily traded value (close times volume) or share volume over its last days reported days from start to end; None when it has none.
impact_inputs ¶
What market impact reads of the window from start to end: the average traded value over its last days reported days (None when there is none), the number of daily returns in its last days, and their sample standard deviation (0 when fewer than two).
last_bar ¶
Its last close from start to end, both included, and its date; None when it printed none.
returns ¶
Its last days daily returns from its closes from start to end.
history ¶
asset_id's history in currency, read on the first ask in a run.
implementation ¶
How a strategy is carried out at a fund's size: what it may hold, and where the weight of what it may not goes.
implementation = Implementation(
screens=[LiquidityScreen(min_traded_value=5e7)],
redistribution="pro_rata")
Backtest(initial_capital=1e11, implementation=implementation)
Separate from the strategy (what to hold) and from the vehicle (how money is
held): the same index can be run under the limits of a small fund and of a
large one, and the difference is the cost of size. See
decisions/0006-backtest-implementation-and-fund-vehicles.md.
The rebalance, as stages¶
- Target: the strategy's weights, as published.
- Screens: names the data says are stale are removed first, then each screen in order removes what it rejects.
- Redistribution: removed weight is spread across the remaining names in
proportion to their weights (
"pro_rata"), or held as cash ("cash"). - Capacity: each name is cut to its caps at the book's size, the excess
spread across the names under their caps until none is over (or held as
cash), and positions too small to keep are dropped and their weight
redistributed. See
beacon.backtest.capacity. - Trades, generated from the result and sized so they and their costs fit the cash, then any modifiers.
- Execution: every trade at once, or as much as an execution limit
allows each day, with market impact on top of the fixed cost. See
beacon.backtest.execution.
Each rebalance's stages are recorded on the result as a RebalanceStep.
Cash and flows¶
A cash_buffer keeps that share of the book in cash: each rebalance invests
the rest, and an outflow is paid from cash before anything is sold. Money
arriving between rebalances is invested the day it arrives, toward the last
rebalance's weights (invest_flows="target", the default) or in proportion
to the holdings ("holdings"), keeping the buffer topped up first. A
cash_band around the buffer is checked every day: cash above it is
invested back down to the buffer, and cash below it raised back up to it
(see beacon.backtest.cash_band).
Implementation ¶
Implementation(
screens: Iterable[Screen] = (),
caps: Iterable[CapacityCap] = (),
minimum_position: MinimumPosition | None = None,
impact: MarketImpact | None = None,
execution: ExecutionLimit | None = None,
redistribution: str = PRO_RATA,
cash_buffer: float = 0.0,
invest_flows: str = TARGET,
equitise: IndexFutures | None = None,
cash_band: tuple[float, float] | None = None,
)
A backtest's screens, capacity limits and redistribution rule.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
screens
|
Iterable[Screen]
|
Applied in order at each rebalance; a name must pass all. |
()
|
caps
|
Iterable[CapacityCap]
|
Capacity caps; a name is held at no more than the smallest. |
()
|
minimum_position
|
MinimumPosition | None
|
Positions too small to keep, or None to keep all. |
None
|
impact
|
MarketImpact | None
|
Market impact charged on every trade, beside the fixed cost, or None for none. |
None
|
execution
|
ExecutionLimit | None
|
How much of an order can trade in a day, or None to trade every order in full on the rebalance day. |
None
|
redistribution
|
str
|
Where removed or capped weight goes: |
PRO_RATA
|
cash_buffer
|
float
|
The share of the book kept in cash, such as 0.02 for 2%. Outflows are paid from cash first. |
0.0
|
cash_band
|
tuple[float, float] | None
|
The lowest and highest share of the book the cash may be before it is brought back to cash_buffer, checked every day; None (the default) leaves it to the next rebalance. |
None
|
invest_flows
|
str
|
What money arriving between rebalances buys: the last
rebalance's weights ( |
TARGET
|
equitise
|
IndexFutures | None
|
Index futures held on the cash the fund keeps uninvested,
so its buffer earns the index's return, or None to leave the cash
as cash. See :mod: |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If redistribution or invest_flows is not one of its choices, cash_buffer is not from 0 up to 1, or cash_band does not hold it. |
RebalanceStep
dataclass
¶
RebalanceStep(
date: Timestamp,
target: dict[str, float],
removed: dict[str, str] = dict(),
capped: dict[str, float] = dict(),
weights: dict[str, float] = dict(),
cash_weight: float = 0.0,
diversified: dict[str, float] = dict(),
screened: dict[str, float] = dict(),
)
What each stage of one rebalance did.
Attributes:
| Name | Type | Description |
|---|---|---|
date |
Timestamp
|
The rebalance date. |
target |
dict[str, float]
|
The strategy's weights, before any stage. |
removed |
dict[str, str]
|
Each name taken out, and the screen that took it out
( |
capped |
dict[str, float]
|
Each name cut to its capacity, and the weight it was cut to. |
screened |
dict[str, float]
|
The weights after the screens and redistribution, before the cash buffer and the caps. |
weights |
dict[str, float]
|
What the trades aimed at, after redistribution. |
cash_weight |
float
|
The share of the book the weights leave in cash. |
buffered ¶
weights with cash_buffer, a share of the book, set aside in cash: each scaled by the share left invested.
plan ¶
plan(
implementation: Implementation,
target: dict[str, float],
date: Timestamp,
held: set[str],
stale: set[str],
context: ScreenContext,
book_value: float = 0.0,
limits: Iterable[DiversificationLimit] = (),
) -> RebalanceStep
Run the screening, redistribution and capacity stages for one rebalance.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
implementation
|
Implementation
|
The screens, caps and redistribution rule. |
required |
target
|
dict[str, float]
|
The strategy's weights. |
required |
date
|
Timestamp
|
The rebalance date. |
required |
held
|
set[str]
|
Names the book holds now, for buffered screens. |
required |
stale
|
set[str]
|
Names the data says have gone stale. |
required |
context
|
ScreenContext
|
The run's data and book currency. |
required |
book_value
|
float
|
The book's value before the rebalance, which caps and minimum positions are measured against. |
0.0
|
limits
|
Iterable[DiversificationLimit]
|
The vehicle's diversification limits, applied with the caps. |
()
|
Returns:
| Name | Type | Description |
|---|---|---|
RebalanceStep |
RebalanceStep
|
The weights to trade to, and how they were reached. |
limits ¶
A fund structure's diversification limits, applied as capacity limits.
Vehicle(limits=[UcitsLimits()])
A structure's rules on how concentrated a fund may be are applied at each rebalance with the capacity caps, so a fund near a limit holds less of a name rather than breaking it. The weight a limit removes is redistributed as the implementation says, until no limit is broken.
- UCITS: at most 10% in one issuer, and the holdings above 5% may add up to at most 40%. A fund replicating an index may instead hold up to 20% of one issuer, or 35% of the single largest.
- 1940 Act diversified: for 75% of the fund, at most 5% in one issuer and at most 10% of an issuer's voting shares. Applied as: the holdings above 5% may add up to at most 25%, and no holding is more than 10% of the company's shares outstanding.
Each name is treated as its own issuer.
DiversificationLimit ¶
UcitsLimits ¶
Bases: DiversificationLimit
The UCITS diversification rules.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
index_tracking
|
bool
|
Apply the rules for a fund replicating an index (20% in one issuer, 35% in the largest) rather than the standard 5/10/40. |
True
|
Act1940Limits ¶
listings ¶
What happens to a holding when its share count changes or its listing ends.
A split, reverse split or stock dividend multiplies the shares held by its
ratio on the ex-date, so the holding's value does not jump. A holding past
its last listed date is settled into cash at its last price, without cost.
Mixed into BacktestEngine.
ListingsMixin ¶
Share-count changes and delistings, mixed into BacktestEngine.
main ¶
Backtest: the one-call front door composing calculator, cache and engine.
The class a user, the fund and the server all call. The construction holds the
modelling assumptions (capital, costs, currency, the benchmark of record) and
each :meth:run supplies the subject: an :class:IndexDefinition and a window.
Inside a run the passes are strictly sequential: fingerprint the calculation
and reuse a cached :class:IndexResult when the cache allows it, otherwise
calculate and store; optionally optimise the published weights; then hand the
schedule to :class:BacktestEngine to simulate. Calculate-then-simulate
produces numbers identical to a fused daily loop while keeping the
calculation a separable, cacheable artifact.
An optimised run is the same composition one derivation deeper: the
definition-plus-config becomes an ephemeral
:class:~beacon.index.derived.OptimisedIndexDefinition, calculated through
the same cache-assisted path a stored one takes (the parent's calculation is
one cache entry shared with every plain run of the same definition, and the
derived calculation is another when the whole chain can be keyed), and the
engine receives the solved calculation as a real IndexResult. Ad-hoc and
stored optimised indices therefore produce bit-identical numbers.
scipy is required only when a solve actually runs, and the default cache
location needs platformdirs; without it a Backtest simply runs uncached, so
the front door works on the core install exactly as it does on a full one.
Backtest ¶
Backtest(
initial_capital: float | Mapping[str, float],
transaction_cost_bps: float = 0.0,
price_column: str = "CLOSE",
currency: str | None = None,
modelling_assumptions: ModellingAssumptions
| None = None,
dividends: str = REINVEST,
implementation: Implementation | None = None,
modifiers: list[BacktestModifier] | None = None,
benchmark: IndexResult | Series | None = None,
data_provider: DataFetcher | None = None,
cache: IndexResultCache | Literal[False] | None = None,
flows: Flows
| list[Flows]
| Mapping[str, Flows | list[Flows]]
| None = None,
vehicle: Vehicle | None = None,
share_classes: Sequence[ShareClass] | None = None,
reinvest_into: str = PAYER,
)
One-call backtests: assumptions on the object, the index per run.
The constructor mirrors :class:BacktestEngine's parameters (what stays
fixed across runs) and :meth:run takes the definition and window, so a
parameter sweep is one object per assumption set over one shared (cached)
calculation::
bt = Backtest(initial_capital=1_000_000, transaction_cost_bps=5.0)
result = bt.run(definition, start="2023-01-03", end="2023-12-29")
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
initial_capital
|
float | Mapping[str, float]
|
The starting capital for each run. With share classes, each class's by name. |
required |
transaction_cost_bps
|
float
|
Transaction cost in basis points applied to each trade's notional value. Defaults to 0 (no cost). |
0.0
|
price_column
|
str
|
Market-data column both the calculator and the engine
read. Defaults to |
'CLOSE'
|
currency
|
str | None
|
The simulated book's currency. None, the default, uses the index's currency. Pass another to keep the book in a different currency from the index. |
None
|
modelling_assumptions
|
ModellingAssumptions | None
|
What the run takes as given about markets and
data, laid over the process-wide default (see
:func: |
None
|
dividends
|
str
|
What happens to cash distributions: |
REINVEST
|
reinvest_into
|
str
|
Where reinvested dividends go: |
PAYER
|
implementation
|
Implementation | None
|
The screens and redistribution rule applied at each rebalance. None screens nothing beyond stale prices. |
None
|
share_classes
|
Sequence[ShareClass] | None
|
The fund's share classes, sharing one book, each with
its own fee, flows and distribution policy (see
:mod: |
None
|
flows
|
Flows | list[Flows] | Mapping[str, Flows | list[Flows]] | None
|
Money arriving and leaving: one scenario or several, added
together (see :mod: |
None
|
vehicle
|
Vehicle | None
|
The fund's fee and unit price at launch. None charges no fee. |
None
|
modifiers
|
list[BacktestModifier] | None
|
Optional hooks that can skip rebalances or adjust trades. |
None
|
benchmark
|
IndexResult | Series | None
|
The benchmark of record, stored on every result this object produces. |
None
|
data_provider
|
DataFetcher | None
|
Data source for both the calculation and the
simulation. None resolves the process's ambient source
(:func: |
None
|
cache
|
IndexResultCache | Literal[False] | None
|
Where calculated IndexResults are kept between runs. None
uses the default location when |
None
|
run ¶
run(
definition: AnyIndexDefinition
| IndexTracking
| ActiveStrategy,
start: str | None = None,
end: str | None = None,
optimised: bool = False,
optimisation_config: OptimisationConfig | None = None,
progress: Progress | None = None,
analysis: AnalysisRequest | None = None,
) -> BacktestResult
Calculate (or reuse) the index, then simulate tracking it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
definition
|
AnyIndexDefinition | IndexTracking | ActiveStrategy
|
The index to calculate and track: a plain
:class: |
required |
start
|
str | None
|
First date (YYYY-MM-DD). Defaults to the definition's base date. |
None
|
end
|
str | None
|
Last date (YYYY-MM-DD). Required. |
None
|
optimised
|
bool
|
Ad-hoc optimisation of definition: build an ephemeral derived index over it from optimisation_config (same solve, same chained levels as a stored one) and trade that. Requires the config; needs scipy only on this path. |
False
|
optimisation_config
|
OptimisationConfig | None
|
What the ad-hoc derivation is asked to do
(objective, constraints, reserved risk model). Only
meaningful, and only allowed, with |
None
|
progress
|
Progress | None
|
Called with the share of the run done, from 0 to 1, and what is happening: the index's calculation, the strategy's weights, then each trading day simulated, by date. |
None
|
analysis
|
AnalysisRequest | None
|
What to analyse with the run: its standard portfolio, risk and attribution figures are worked out before it returns, as well as its performance, and the result keeps the request and how long each part took. Without it every part is still built on first read, with default settings. |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
BacktestResult |
BacktestResult
|
The engine's result: portfolio kept whole, books |
BacktestResult
|
filled, data bound to the run's own source. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If end is not provided, or optimised and optimisation_config contradict each other (a flag with no config, or a config with no flag). |
CalculationError
|
If a calculation comes back empty (no level was computed, every level is zero, or the index never held a constituent), the objective is unknown, or an optimisation is infeasible. |
DataSourceError
|
If no data source is bound and the process has no ambient one. |
calculate ¶
calculate(
definition: AnyIndexDefinition,
start: str | None = None,
end: str | None = None,
) -> IndexResult
Calculate (or reuse) an index alone, as a run would calculate it.
The same calculation run starts from, with this backtest's data and
modelling assumptions, through the same cache: an index calculated
here is reused by a run over the same dates, and the other way round.
No portfolio is simulated.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
definition
|
AnyIndexDefinition
|
The index: a plain :class: |
required |
start
|
str | None
|
First date (YYYY-MM-DD). Defaults to the definition's base date. |
None
|
end
|
str | None
|
Last date (YYYY-MM-DD). Required. |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
IndexResult |
IndexResult
|
Its levels, divisors and the weights at each |
IndexResult
|
rebalance. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If end is not provided. |
CalculationError
|
If the calculation comes back empty. |
metrics ¶
What a backtest's result says about its performance: returns, risk, tracking
against its index, and, when money flowed, a unit's performance and the
money-weighted return. Mixed into BacktestResult.
MetricsMixin ¶
Returns, risk and tracking for a backtest's result.
trading_nav
property
¶
Provided by BacktestResult; declared so this one type-checks.
aum
property
¶
The fund's assets under management each simulated day: the trading NAV, which flows move as well as the market.
units_outstanding
property
¶
Units outstanding at the end of each simulated day.
nav_per_unit
property
¶
NAV per unit each simulated day: the performance of a unit held throughout, whatever money arrived or left. A day with no units outstanding carries the last NAV per unit.
get_returns ¶
The portfolio's daily returns.
The first runs from the initial capital to the first day's close, so
it carries the cost of the opening trades; each later one is from one
close to the next. Cash paid out of the book (a "distribute"
dividend policy) is added back on the day it was paid, so returns
stay total returns.
With flows, returns are those of a unit (time-weighted): NAV per unit from one day to the next, so money arriving is not a return.
Returns:
| Type | Description |
|---|---|
Series
|
pd.Series: Returns by date, one per simulated day. |
performance_levels ¶
The run's performance as a level series: the trading NAV, or with flows the NAV per unit, which they do not move.
money_weighted_return ¶
The annual internal rate of return of the investors' money.
Counts the initial capital, every subscription and redemption, any distribution paid out, and the final NAV, each on its date (ACT/365). Unlike the time-weighted figures it depends on when money arrived.
Returns:
| Type | Description |
|---|---|
float | None
|
float or None: The rate a year, or None when it does not exist |
float | None
|
(no money at risk, say). |
get_annual_returns ¶
The portfolio's calendar-year returns.
Each year runs from the previous year's last close, and the first from the initial capital, so the years compound to the whole run's return. A partial first or last year covers only its days.
Returns:
| Type | Description |
|---|---|
Series
|
pd.Series: Return by calendar year, indexed by the year as an int. |
get_tracking_error ¶
Calculate annualised tracking error against the tracked index.
Tracking error is the standard deviation of the difference between daily portfolio returns and index returns on their common dates, annualised by the square root of the run's periods per year (252 unless its modelling assumptions say otherwise). The first day compares the portfolio's return from its initial capital (the opening trades' cost) with the index's return of zero from its starting level.
Returns:
| Type | Description |
|---|---|
float | None
|
float or None: Annualised tracking error, or None if the run |
float | None
|
tracked no index or they share fewer than two days. |
get_tracking_difference ¶
Calculate cumulative tracking difference against the tracked index.
Tracking difference is the portfolio's cumulative return, from its initial capital, less the index's cumulative return, from its level on the first day, over the whole run. The cost of the opening trades is in it.
Returns:
| Type | Description |
|---|---|
float | None
|
float or None: Tracking difference, or None if the run tracked |
float | None
|
no index. |
summary ¶
Calculate key performance metrics for the backtest.
Returns are daily and annualised over the run's periods per year, and the Sharpe ratio is the annualised return in excess of its risk-free rate, over the volatility. Both come from the run's modelling assumptions: 252 and 0 unless they say otherwise. Total return is measured against the portfolio's initial capital, with any dividends paid out of the book added back.
With flows every figure is a unit's, and the money-weighted return
is added. The figures are beacon.analysis.performance.PerformanceAnalytics's
over the whole run, with an ETF's trading and an active strategy's
own figures added.
Returns:
| Name | Type | Description |
|---|---|---|
dict |
dict[str, float | None]
|
Dictionary containing: total_return, annualised_return, |
dict[str, float | None]
|
volatility, sharpe_ratio, max_drawdown, hit_rate_monthly, and, |
|
dict[str, float | None]
|
when the run tracked an index, tracking_error and |
|
dict[str, float | None]
|
tracking_difference; with flows, money_weighted_return. |
information_ratio ¶
Annualised active return over annualised tracking error, against the tracked index. None without an index, or with no active risk.
active_summary ¶
An active strategy's figures: its information ratio, average active share and average ex-ante tracking error.
active_attribution ¶
Which names produced the active return: each name's active weight (the portfolio's less the index's, as both drift) times its return, linked over the run so they add up to the portfolio's compounded return less the index's. Costs, cash and fees are in the residual.
Raises:
| Type | Description |
|---|---|
CalculationError
|
If the run has no index to compare with, or the weights and returns share no dates. |
market_return ¶
An exchange investor's return on an ETF: bought at the ask on the first day and sold at the bid on the last. None for another vehicle. Cash the fund paid out is not included.
market_summary ¶
An ETF's trading on the exchange: the investor's return at market prices, the average premium and spread, and the share of days at a premium.
presets ¶
Vehicles for real fund structures, ready to use.
Backtest(initial_capital=1e8, vehicle=uk_oeic(management_fee_bps=15))
Backtest(initial_capital=1e8, vehicle=preset("luxembourg_sicav"))
A preset is a Vehicle with the settings a structure usually has: how it
prices dealing and the diversification limits it must keep. Every setting
can be changed by passing it, so a preset is a starting point, not a rule:
uk_oeic(pricing=DilutionLevy(threshold=0.05))
| Preset | Pricing | Diversification limits | Domicile |
|---|---|---|---|
uk_oeic |
Full swing pricing | UCITS | United Kingdom |
luxembourg_sicav |
Partial swing pricing, above 2% of the fund | UCITS | Luxembourg |
irish_icav |
Anti-dilution levy, above 2% of the fund | UCITS | Ireland |
us_mutual_fund |
Single pricing, with an optional redemption fee | 1940 Act | United States |
ucits_etf |
Cash creations with a creation fee; an exchange price | UCITS | Ireland |
us_etf |
In-kind creations; an exchange price | 1940 Act | United States |
Swing factors and levies are estimated from the run's cost model unless set.
The UCITS limits are those for a fund replicating an index (20% in one
issuer, 35% in the largest), since a backtest tracks an index. The domicile
sets the withholding tax on the fund's dividends (see
beacon.backtest.withholding); a UCITS ETF is Irish by default, as most are.
Preset
dataclass
¶
A preset by name, for listing.
Attributes:
| Name | Type | Description |
|---|---|---|
key |
str
|
The name |
name |
str
|
What the structure is called. |
build |
Callable[..., Vehicle]
|
Makes the vehicle; takes any |
uk_oeic ¶
A UK open-ended investment company: daily dealing, a full swinging single price, and the UCITS limits.
luxembourg_sicav ¶
A Luxembourg SICAV under UCITS: a partial swing above 2% of the fund, and the UCITS limits.
irish_icav ¶
An Irish ICAV under UCITS: an anti-dilution levy on deals above 2% of the fund, and the UCITS limits.
us_mutual_fund ¶
A US mutual fund: single pricing, an optional redemption fee, and the 1940 Act's test for a diversified fund.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
redemption_fee_bps
|
float
|
A fee on redemptions, paid into the fund, in basis points. Charged on every redemption, since a run does not track how long each investor held. |
0.0
|
**settings
|
Any
|
Any other |
{}
|
ucits_etf ¶
A UCITS ETF: cash creations, a creation fee estimated from the basket's trading cost, and the UCITS limits.
us_etf ¶
A US ETF: in-kind creations and the 1940 Act's test for a diversified fund.
preset ¶
The vehicle for a preset, by name, with any settings changed.
Raises:
| Type | Description |
|---|---|
KeyError
|
If there is no preset by that name; the message lists them. |
pricing ¶
PricingMixin: what a price is, to the backtest engine: a date, a session, a currency.
Everything here answers one question (what is this name worth, in the book's currency, on this day), and the engine above it decides what to do with the answer. Two conversions sit between a stored bar and a usable price:
- Currency. Prices are stored in the currency the company is quoted in,
while a portfolio has one currency, so each price is converted with the FX
rate on or before the day. A missing rate raises
CalculationError. - Date. A requested date is not always a session. Within the data's
coverage it resolves back to the last session on or before it; beyond the
coverage it raises
CalculationError. The trading calendar tells a closed market (the previous close is carried silently) apart from a hole in the data (the previous close is carried and recorded as aPriceGap).
PricingMixin ¶
Price resolution and currency conversion, mixed into BacktestEngine.
redistribution ¶
Moving weight from names that cannot hold it to names that can.
redistributed scales what is left back up to the total, or leaves it for
cash. water_filled cuts every name over its limit and spreads the excess
across the names under theirs, in proportion to their weights, until none is
over. The capacity caps and a vehicle's diversification limits both use
them.
water_filled ¶
water_filled(
weights: dict[str, float],
limits: dict[str, float],
rule: str,
capped: dict[str, float],
) -> dict[str, float]
Cut every name over its limit to it and spread the excess across the names under theirs, pro rata, until none is over; under the cash rule the excess stays in cash. Records each cut name in capped.
redistributed ¶
kept scaled back up to total, or left as it is for cash.
result ¶
BacktestResult: the record of a run, holding books rather than fields.
The result is an orchestrator: it holds the Portfolio (kept whole, not flattened into series) plus the run-level facts (the index tracked, the target index, the benchmark of record, unfilled orders) and its methods answer questions by comparing books:
result.portfolio.nav what the money did
result.index.target.levels what the index being aimed at did
result.index.optimised the solved index's own calculation, when one exists
result.index.tracked the book the engine actually traded toward
result.benchmark.levels the benchmark of record, when one was given
result.against(other) any comparator, after the fact
Parallel structure is the point: several books, same question, same spelling. Each fact has one home.
The benchmark of record versus a question asked later¶
benchmark= given to the engine is a fact about the run: stored here,
serialised, reproducible, so every reader of this result quotes excess
return against the same comparator. against(other) is a question asked
afterwards: it computes and returns, and never mutates the stored
record, because otherwise the benchmark of record would be whatever
somebody last idly compared against.
Day zero and the trading NAV¶
The portfolio's NAV opens with initial capital on the eve of the first
trading day. :attr:trading_nav is the same series with that opening row
dropped, net of any management fee the fund owes, and the wire format reads
it. The return metrics start from the initial capital instead: the first
day's return runs from the capital to the first close, so the cost of buying
in counts.
Flows and units¶
With flows the NAV is the fund's size, which money arriving and leaving moves
as much as the market does. Performance is then measured per unit:
:attr:nav_per_unit is the NAV over the units outstanding, every return
metric is computed from it (time-weighted), and the summary adds the
money-weighted return, which does count when the money arrived. Without flows
the units never change and the metrics are computed exactly as before.
UnfilledOrder
dataclass
¶
UnfilledOrder(
date: Timestamp,
asset_id: str,
requested_quantity: float,
filled_quantity: float,
price: float,
shortfall_value: float,
reason: str = CASH_SHORT,
)
An order the simulation could not execute in full.
Recorded on the result rather than only logged: a partially filled rebalance leaves the portfolio off its target weights, and a caller comparing tracking error against expectations needs to know that happened rather than reading it as a modelling result.
Attributes:
| Name | Type | Description |
|---|---|---|
date |
Timestamp
|
The rebalance date. |
asset_id |
str
|
Asset that could not be fully traded. |
requested_quantity |
float
|
Quantity the rebalance asked for; 0.0 when the name could not be priced to size an order. |
filled_quantity |
float
|
Quantity actually traded; 0.0 when nothing was. |
price |
float
|
Execution price used; NaN when there was none. |
shortfall_value |
float
|
Notional value that went unfilled; for a name with no price, the value the rebalance aimed to hold. NaN for an order an execution limit left unfinished. |
reason |
str
|
Why: |
RebalancePricing
dataclass
¶
What one rebalance priced from.
A rebalance scheduled on a day the market was shut still trades: it prices
from the session in force through the closure. Recording both dates lets a
reader tell that case apart from an ordinary rebalance. date and
priced_from are equal for the ordinary rebalance, which is what makes an
unequal pair worth reading.
Attributes:
| Name | Type | Description |
|---|---|---|
date |
Timestamp
|
The rebalance date from the weight schedule. |
priced_from |
Timestamp
|
The session its prices were read from. |
BacktestResult
dataclass
¶
BacktestResult(
portfolio: Portfolio,
index: IndexBooks = IndexBooks(),
benchmark: Book | None = None,
unfilled: list[UnfilledOrder] = list(),
price_gaps: list[PriceGap] = list(),
rebalance_pricing: list[RebalancePricing] = list(),
rebalance_steps: list[RebalanceStep] = list(),
currency: str = "USD",
modelling_assumptions: ModellingAssumptions
| None = None,
flows: list[FlowRecord] = list(),
units: Series = (lambda: pd.Series(dtype=float))(),
fees_payable: Series = (
lambda: pd.Series(dtype=float)
)(),
launch_price: float = 1.0,
market: DataFrame = pd.DataFrame(),
replication: list[ReplicationStep] = list(),
active: list[ActiveStep] = list(),
swap_value: Series = (lambda: pd.Series(dtype=float))(),
swap_resets: list[SwapReset] = list(),
futures_notional: Series = (
lambda: pd.Series(dtype=float)
)(),
futures_rolls: list[FuturesRoll] = list(),
cash_band_events: DataFrame = pd.DataFrame(),
futures_trades: list[FuturesTrade] = list(),
futures_days: DataFrame = pd.DataFrame(),
hedge_value: Series = (
lambda: pd.Series(dtype=float)
)(),
dividends_receivable: Series = (
lambda: pd.Series(dtype=float)
)(),
dividends_entitled: DataFrame = (
lambda: pd.DataFrame(
columns=["GROSS", "WITHHELD"], dtype=float
)
)(),
share_classes: dict[str, ShareClassBook] = dict(),
analysis_request: Any = None,
analysis_timings: dict[str, float] = dict(),
analysis_block: dict[str, Any] | None = None,
_data_fetcher: DataFetcher | None = None,
)
Bases: MetricsMixin, AnalyticsMixin
The record of one backtest run.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
portfolio
|
Portfolio
|
The books (positions, weights, cash, NAV, transactions), kept whole and frozen by the engine on completion. |
required |
index
|
IndexBooks
|
The run's calculated indices, as an :class: |
IndexBooks()
|
benchmark
|
Book | None
|
The benchmark of record, when one was given to the engine. |
None
|
unfilled
|
list[UnfilledOrder]
|
Buys the simulation could not execute in full. Empty for a run where every rebalance leg filled, so a non-empty list is itself the signal that the portfolio drifted off target for a reason other than price movement. |
list()
|
price_gaps
|
list[PriceGap]
|
Days a name had no bar on a session its calendar says was open, and was therefore marked at a carried-forward price. Empty for a run with complete data. A market holiday is not a gap, since nothing is missing on a day nothing traded. |
list()
|
rebalance_pricing
|
list[RebalancePricing]
|
What each rebalance priced from, in date order.
|
list()
|
rebalance_steps
|
list[RebalanceStep]
|
What the screening and redistribution stages did at each rebalance: the target, the names removed and by what, and the weights traded to. |
list()
|
currency
|
str
|
The book's currency, which the NAV, costs and holdings' values are in. |
'USD'
|
modelling_assumptions
|
ModellingAssumptions | None
|
What the run assumed, every field resolved. None for a result built by hand, which the summary then measures with a zero risk-free rate over 252 periods a year. |
None
|
flows
|
list[FlowRecord]
|
Each day's flow as dealt: the amount, and the units created or cancelled at that day's NAV per unit. Empty without flows. |
list()
|
units
|
Series
|
Units outstanding at the end of each day, from the eve of the first trading day. |
(lambda: Series(dtype=float))()
|
fees_payable
|
Series
|
The management fee owed and not yet paid at the end of each day, which the NAV is net of. |
(lambda: Series(dtype=float))()
|
launch_price
|
float
|
NAV per unit at launch. |
1.0
|
market
|
DataFrame
|
For an ETF, each day's quote on the exchange: NAV per share, premium, spread, market price, bid and ask, and the arbitrage band. Empty for any other vehicle. |
DataFrame()
|
replication
|
list[ReplicationStep]
|
For an index held through |
list()
|
active
|
list[ActiveStep]
|
For an |
list()
|
swap_value
|
Series
|
For a synthetic fund, its swap's value at the end of each day: owed to the fund when positive, by it when negative. The NAV includes it. Empty for any other fund. |
(lambda: Series(dtype=float))()
|
swap_resets
|
list[SwapReset]
|
For a synthetic fund, each reset of its swap: the day, the value settled, why, and the notional it was reset to. |
list()
|
futures_notional
|
Series
|
For a fund holding index futures (futures-based, or equitising its cash buffer), the exposure they carried at the end of each day. Their profit and loss is already in the cash. Empty for any other fund. |
(lambda: Series(dtype=float))()
|
futures_rolls
|
list[FuturesRoll]
|
Each roll of those futures to the next contract: the day, the new expiry, the notional, the cost and the dividend yield it was priced with. |
list()
|
futures_trades
|
list[FuturesTrade]
|
Each trade of those futures: opened, rolled or resized, the contracts before, after and traded, the price, the notional traded and the cost. |
list()
|
futures_days
|
DataFrame
|
Each day the futures were held: the contracts, their price, their fair value, the level of the index they are on and of the fund's own, the assumed mispricing and their expiry. Empty for any other fund. |
DataFrame()
|
cash_band_events
|
DataFrame
|
For a fund with a cash band, each time its cash left the band and was brought back, by date: whether it was a raise or a spend-down, the cash's share of the fund before and after, the target and the band, the value traded, its cost and the number of trades. Empty otherwise. |
DataFrame()
|
hedge_value
|
Series
|
For a fund with currency-hedged share classes, the value of their open forwards at the end of each day, in the book's currency. The NAV includes it. Empty otherwise. |
(lambda: Series(dtype=float))()
|
dividends_receivable
|
Series
|
The dividends the book is owed at the end of each day: gone ex and not yet paid, at the day's exchange rates and net of withholding tax. The NAV includes it, as a fund's accounts do; nothing is bought with it until it is paid. |
(lambda: Series(dtype=float))()
|
dividends_entitled
|
DataFrame
|
What the book became owed on each day a holding
went ex: |
(lambda: DataFrame(columns=['GROSS', 'WITHHELD'], dtype=float))()
|
share_classes
|
dict[str, ShareClassBook]
|
For a fund run with share classes, each class's book by name: its net assets, units, NAV per unit, fees, distributions and flows. The result's own NAV and metrics are the whole fund's. Empty otherwise. |
dict()
|
trading_nav
property
¶
NAV over the simulated days, with the day-zero row excluded.
The portfolio's own nav opens with initial capital on the eve of
the first trading day, the record of what the run started with.
Every metric derives from this series instead: the eve row is a
starting fact, not a day the simulation traded. Net of the management
fee owed and not yet paid, when the run had a vehicle charging one.
counterparty_exposure
property
¶
For a synthetic fund, what it would lose if its swap counterparty failed, as a share of NAV at the end of each day: the swap's value when owed to the fund, otherwise zero. Empty for any other fund.
futures_exposure
property
¶
For a fund holding index futures, the exposure they carried as a share of NAV at the end of each day. Empty for any other fund.
total_unfilled_value
property
¶
Total notional that went unfilled across the run.
with_data ¶
Bind a DataFetcher for asset-level queries. Returns self for chaining.
asset ¶
Return a BacktestAssetView for an asset the run ever held.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
asset_id
|
str
|
Identifier of the asset. |
required |
Returns:
| Type | Description |
|---|---|
BacktestAssetView
|
BacktestAssetView |
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If no DataFetcher has been bound via
:meth: |
KeyError
|
If the run's books never held asset_id. Membership is judged from the positions panel (the record of holdings) rather than from a weight column, so a position too small to round to a visible weight still counts as held. |
against ¶
Compare this run's NAV against any comparator, after the fact.
The run-time benchmark is a fact about the run; this is a question asked later, so it computes and returns, and stores nothing. Ask against ten comparators and the result is byte-for-byte what it was.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
other
|
Comparable
|
Another result, a book, an index result, or a bare level series. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
RelativeMetrics |
RelativeMetrics
|
Excess return, tracking error, beta and |
RelativeMetrics
|
correlation over the common window, as |
|
RelativeMetrics
|
computes them. |
rules ¶
BacktestModifier: optional hooks that alter rebalance behaviour.
A modifier can skip a scheduled rebalance or adjust its trade list. Ships with
DriftThresholdModifier (rebalance only when weights have drifted far enough).
Deciding which names may be held is a screen's job, not a modifier's: see
beacon.backtest.screens.
BacktestModifier ¶
Bases: ABC
Abstract base class for modifiers that alter rebalance behaviour.
A modifier can veto a rebalance entirely via :meth:should_skip_rebalance
or adjust the trade list via :meth:adjust_trades.
should_skip_rebalance
abstractmethod
¶
should_skip_rebalance(
date: Timestamp,
portfolio: Portfolio,
target_weights: dict[str, float],
) -> bool
Return True to skip the scheduled rebalance on date.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
date
|
Timestamp
|
The rebalance date. |
required |
portfolio
|
Portfolio
|
Current portfolio state (prices already updated). |
required |
target_weights
|
dict[str, float]
|
Target weights for this rebalance. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
bool |
adjust_trades
abstractmethod
¶
adjust_trades(
trades: list[TradeInstruction],
date: Timestamp,
portfolio: Portfolio,
) -> list[TradeInstruction]
Optionally modify the trade list before execution.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
trades
|
list[TradeInstruction]
|
The trades the engine generated for this rebalance, sells first, then buys. |
required |
date
|
Timestamp
|
The rebalance date. |
required |
portfolio
|
Portfolio
|
Current portfolio state. |
required |
Returns:
| Type | Description |
|---|---|
list[TradeInstruction]
|
list of TradeInstruction: The (possibly modified) trade list. |
DriftThresholdModifier ¶
Bases: BacktestModifier
Only rebalance when max weight drift exceeds a threshold.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
threshold
|
float
|
Maximum tolerable absolute drift between current and target weights. If every asset's drift is within this threshold the rebalance is skipped. For example, 0.05 means 5%. |
required |
adjust_trades ¶
adjust_trades(
trades: list[TradeInstruction],
date: Timestamp,
portfolio: Portfolio,
) -> list[TradeInstruction]
Pass-through: no trade adjustment.
screens ¶
Screens: which names a backtest may hold, decided at each rebalance.
Implementation(screens=[
LiquidityScreen(min_traded_value=5e6, exit=4e6),
MarketCapScreen(min_cap=1e9),
ExclusionScreen(sectors=["Tobacco"]),
])
A screen runs after the index's target weights and before any trade is
generated. A name it rejects leaves the target, so it is sold if held and
never bought, and its weight is redistributed (see Implementation). The
index itself is untouched.
Buffers¶
A threshold screen takes an entry level and an optional exit level. A name not held must clear the entry level to come in; a held name stays until it falls below the exit level. With the exit below the entry, a name hovering near the threshold does not flip in and out at every rebalance. Without an exit level, both are the entry level.
Currency¶
Money thresholds (market cap, traded value, price) are in the book's currency, and every value is converted into it before it is compared, so a threshold means the same money for every name.
A name a screen cannot value (no price, no share count) is rejected unless
the screen is built with on_missing=True.
ScreenContext
dataclass
¶
What a screen is told about the run it screens for.
Attributes:
| Name | Type | Description |
|---|---|---|
fetcher |
DataFetcher
|
The data the run reads. |
currency |
str
|
The book's currency, which money thresholds are in. |
Screen ¶
Bases: ABC
Decides whether a name may be held on a rebalance date.
prepare ¶
Check the screen against the data before the run starts.
admits
abstractmethod
¶
Whether asset_id may be held on date.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
asset_id
|
str
|
The name. |
required |
date
|
Timestamp
|
The rebalance date. |
required |
held
|
bool
|
Whether the book holds it now, which decides whether a buffered screen applies its entry or its exit level. |
required |
context
|
ScreenContext
|
The run's data and book currency. |
required |
ThresholdScreen ¶
Bases: Screen
A minimum on a value, with an optional lower exit level.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entry
|
float
|
The level a name not held must reach. |
required |
exit
|
float | None
|
The level a held name must stay at or above. None uses entry. Must not be above entry. |
None
|
on_missing
|
bool
|
Whether a name with no value passes. |
False
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If exit is above entry. |
value
abstractmethod
¶
The value compared against the levels, or None when unknown.
MarketCapScreen ¶
MarketCapScreen(
min_cap: float,
exit: float | None = None,
float_adjusted: bool = False,
on_missing: bool = False,
)
Bases: ThresholdScreen
A minimum market cap, in the book's currency.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
min_cap
|
float
|
The entry level. |
required |
exit
|
float | None
|
The exit level for a held name. None uses min_cap. |
None
|
float_adjusted
|
bool
|
Measure the free-float market cap instead. |
False
|
on_missing
|
bool
|
Whether a name with no market cap passes. |
False
|
MinimumPriceScreen ¶
Bases: ThresholdScreen
A minimum price, in the book's currency.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
min_price
|
float
|
The entry level. |
required |
exit
|
float | None
|
The exit level for a held name. None uses min_price. |
None
|
on_missing
|
bool
|
Whether a name with no recent price passes. |
False
|
LiquidityScreen ¶
LiquidityScreen(
min_traded_value: float | None = None,
min_volume: float | None = None,
exit: float | None = None,
lookback_days: int = DEFAULT_LIQUIDITY_DAYS,
on_missing: bool = False,
)
Bases: ThresholdScreen
A minimum average traded value (book currency) or volume (shares).
Exactly one of min_traded_value and min_volume is given.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
min_traded_value
|
float | None
|
The entry level for the average of close times volume, converted into the book's currency day by day. |
None
|
min_volume
|
float | None
|
The entry level for the average share volume. |
None
|
exit
|
float | None
|
The exit level for a held name, on the same measure. |
None
|
lookback_days
|
int
|
How many of the name's most recent trading days are averaged. |
DEFAULT_LIQUIDITY_DAYS
|
on_missing
|
bool
|
Whether a name with no volume passes. |
False
|
Raises:
| Type | Description |
|---|---|
ValueError
|
Unless exactly one minimum is given. |
ListingAgeScreen ¶
Bases: Screen
A minimum time since a name's first price in the data.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
min_days
|
int
|
Calendar days since the first price. |
required |
ExclusionScreen ¶
ExclusionScreen(
identifiers: Iterable[str] = (),
sectors: Iterable[str] = (),
regions: Iterable[str] = (),
)
Bases: Screen
Names, sectors or regions a backtest may not hold.
Sector and region are read from the reference record in force on the rebalance date, so a name that moved sector is judged by the sector it was in then.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
identifiers
|
Iterable[str]
|
Names excluded outright. |
()
|
sectors
|
Iterable[str]
|
|
()
|
regions
|
Iterable[str]
|
|
()
|
ExpressionScreen ¶
Bases: Screen
Names that pass an expression, resolved at each rebalance.
Money fields in the expression, such as data.market.market_cap, are in
the book's currency.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
expression
|
Expression
|
The screen. |
required |
on_missing
|
bool
|
Whether a name with no value for a field passes. |
False
|
prepare ¶
Refuse a field the data does not have, before the run starts.
Raises:
| Type | Description |
|---|---|
ExpressionError
|
Naming each field that does not resolve. |
average_traded ¶
average_traded(
asset_id: str,
date: Timestamp,
context: ScreenContext,
lookback_days: int = DEFAULT_LIQUIDITY_DAYS,
by_value: bool = True,
) -> float | None
A name's average daily traded value in the book's currency (close times volume, converted day by day), or its average share volume, over its last lookback_days trading days to date. None when it has no volume.
converted_closes ¶
converted_closes(
asset_id: str,
start: Timestamp,
end: Timestamp,
context: ScreenContext,
) -> pd.Series
asset_id's closes in the book's currency, blanks dropped.
settings ¶
How a backtest engine's settings are resolved and checked before it runs: the book's currency, whether the index was calculated under the same modelling assumptions, and that no screen was passed as a modifier.
book_currency ¶
The book's currency: the one asked for, else the index's, else USD.
A book in a different currency from its index is a real case (a dollar investor tracking a euro index) and is allowed, but said, because its tracking figures then include exchange-rate moves the index does not see.
warn_if_assumptions_differ ¶
Log each data-treatment setting the index was calculated under that differs from the one this run reads data under.
refuse_screens_as_modifiers ¶
Say where a screen goes, if one was passed as a modifier.
share_classes ¶
Share classes: several classes of one fund, sharing one pool of assets.
Backtest(initial_capital={"Acc": 6e7, "Dist": 4e7},
share_classes=[ShareClass("Acc"),
ShareClass("Dist", distribution="distributing")],
flows={"Dist": PeriodicFlows(fraction=0.01)},
vehicle=uk_oeic(management_fee_bps=50))
A fund's classes own slices of one book. Every class's money is traded in that book, so capacity caps, market impact and execution limits see the whole fund's size, and each class keeps its own units, net assets and NAV per unit.
Each day the book's gains, losses and income (price moves, dividends, interest, trading costs) are split between the classes in proportion to their net assets at the previous close, the usual method for a multi-class fund. What belongs to one class is then applied to it alone:
- its fee, its own
management_fee_bps(the vehicle's when None), accrued on its own net assets; - its flows, dealt at its own NAV per unit, sized against its own net assets, and moved by the vehicle's pricing (a swing or a levy) as the fund's net flow sets it;
- its distributions: a distributing class's share of the dividends is paid out, while an accumulating class's stays in the book, invested as the run's dividend policy says.
A class's currency. A class may be denominated in another currency
from the book's (currency=): its NAV per unit, capital, flows, fees and
distributions are in that currency, converted at each day's rate, so its
returns carry the currency's moves. A hedged class (hedged=True) carries a
currency forward that takes those moves out, and earns or pays the interest
rate differential instead (see beacon.backtest.hedging).
result.share_classes holds each class's book. The result's own NAV and
metrics are the whole fund's.
ShareClass
dataclass
¶
ShareClass(
name: str,
management_fee_bps: float | None = None,
distribution: str = ACCUMULATING,
currency: str | None = None,
hedged: bool = False,
hedge_ratio: float = 1.0,
hedge_band: float | None = 0.03,
hedge_cost_bps: float = 0.0,
)
One share class of a fund.
Attributes:
| Name | Type | Description |
|---|---|---|
name |
str
|
What the class is called, such as |
management_fee_bps |
float | None
|
The class's annual management fee in basis points. None uses the vehicle's. |
distribution |
str
|
|
currency |
str | None
|
The class's currency, such as |
hedged |
bool
|
Whether it hedges its share of the assets into its currency with a monthly forward. |
hedge_ratio |
float
|
The share of its net assets it hedges, 1.0 for all. |
hedge_band |
float | None
|
How far the hedge ratio may drift between monthly rolls, such as 0.03 for 97% to 103%, before the forward is resized. None resizes only at the rolls. |
hedge_cost_bps |
float
|
What trading its forwards costs, in basis points of the notional each trade deals. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the distribution is not one of the two, the fee is negative, the currency is not a three-letter code, a hedge has no currency, or the hedge ratio, band or cost is out of range. |
ShareClassBook
dataclass
¶
ShareClassBook(
name: str,
distribution: str,
management_fee_bps: float,
net_assets: Series,
units: Series,
nav_per_unit: Series,
fees: Series,
distributions: Series,
distributions_per_unit: Series,
flows: list[FlowRecord] = list(),
currency: str = "",
hedged: bool = False,
book_net_assets: Series = (
lambda: pd.Series(dtype=float)
)(),
hedge_value: Series = (
lambda: pd.Series(dtype=float)
)(),
hedges: list[HedgeTrade] = list(),
)
One share class's record of a run.
Attributes:
| Name | Type | Description |
|---|---|---|
name |
str
|
The class's name. |
distribution |
str
|
|
management_fee_bps |
float
|
The fee it was charged, in basis points a year. |
net_assets |
Series
|
Its net assets at the end of each day, from the eve of the first. |
units |
Series
|
Its units outstanding at the end of each day. |
nav_per_unit |
Series
|
Its NAV per unit at the end of each day. |
fees |
Series
|
The fee it accrued each day. |
distributions |
Series
|
What it paid out each day, in total. |
distributions_per_unit |
Series
|
What it paid out each day, per unit. |
flows |
list[FlowRecord]
|
Its own flows as dealt. |
currency |
str
|
The currency its NAV, flows, fees and distributions are in. |
hedged |
bool
|
Whether it hedged into that currency. |
book_net_assets |
Series
|
Its net assets in the book's currency, which add up to the fund's across the classes. |
hedge_value |
Series
|
Its forward's value at the end of each day, in its currency. Empty for an unhedged class. |
hedges |
list[HedgeTrade]
|
Each trade of its hedge: opened, rolled or resized. |
get_returns ¶
The class's daily returns per unit: its NAV per unit from one day to the next, with what it paid out added back, so a distributing class's returns are total returns. The first runs from the launch price to the first close.
total_return ¶
The class's total return over the run, distributions reinvested.
ShareClassesMixin ¶
Keeps each share class's books beside the fund's.
Before AccountingMixin and DividendsMixin in BacktestEngine's bases, whose declarations of these methods it provides; what it uses from them is annotated here, never defined, so it shadows nothing.
class_terms ¶
class_terms(
share_classes: Sequence[ShareClass] | None,
initial_capital: float | Mapping[str, float],
flows: Flows
| list[Flows]
| Mapping[str, Flows | list[Flows]]
| None,
vehicle: Vehicle | None,
) -> list[_Ledger]
Each class's capital, fee and flows, checked; empty without classes.
Raises:
| Type | Description |
|---|---|
ValueError
|
If classes share a name, a capital or flows name a class there is not, or a single capital or flow list is given for more than one class (or a mapping without classes). |
swap ¶
The total return swap a synthetic fund holds, valued beside its books.
The fund's portfolio holds the substitute basket; the swap is not a holding. Its value is kept here, owed to the fund when positive and by it when negative, and the fund's net assets are the portfolio's value, less the fee owed, plus this. It is settled in cash at each reset.
Between resets, on a notional N set at the last reset:
value = N x (index return) - N x (basket return) - spread accrued
The index return is the tracked index's level against its level at the reset. The basket return is chained day by day from the basket's own moves: each day the holdings carried in from the day before, at today's prices, plus the dividends they paid today, over their value at yesterday's close. So money the fund adds or takes out, its fee, its trading costs and its cash do not count as the basket's return; only what the basket itself earned does. The spread accrues on the notional over calendar days (ACT/360).
The swap opens at the first rebalance, when the basket is first bought, and is reset:
- when its value owed to the fund reaches the exposure limit of net assets,
- at every rebalance, settled before the rebalance trades,
- on any day money flows in or out, settled after the flows are dealt.
A positive value settled is cash the fund then invests in the basket; a negative one is paid from cash, selling the basket pro rata for what the cash does not cover.
SwapReset
dataclass
¶
One reset of a synthetic fund's swap.
Attributes:
| Name | Type | Description |
|---|---|---|
date |
Timestamp
|
The day it was reset. |
settled |
float
|
The swap's value paid in cash: received by the fund when positive, paid by it when negative. 0 when the swap opened. |
reason |
str
|
Why: |
notional |
float
|
The notional it was reset to: the fund's net assets. |
SwapMixin ¶
Values, settles and resets a synthetic fund's swap.
vehicle ¶
How a backtest's money is held: the vehicle.
Backtest(initial_capital=1e8, vehicle=Vehicle(management_fee_bps=20))
A vehicle says nothing about what is held (the strategy) or what money
arrives (the flows). It carries what the fund itself charges, how it prices
the units investors deal in, the diversification limits its structure
imposes, and its unit price at launch. A preset (see
beacon.backtest.presets) fills these in for a real fund structure.
The management fee accrues every calendar day on the fund's net assets
(ACT/365): net assets x fee / 10,000 x days / 365, owed as a liability
that the NAV is net of and paid from cash as soon as there is cash to pay it.
Vehicle ¶
Vehicle(
management_fee_bps: float = 0.0,
launch_price: float = DEFAULT_LAUNCH_PRICE,
pricing: Pricing | None = None,
limits: Iterable[DiversificationLimit] = (),
name: str = "Vehicle",
domicile: str | None = None,
withholding_rates: Mapping[str, float] | None = None,
)
How a fund holds its money.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
management_fee_bps
|
float
|
The annual management fee in basis points, such as 20 for 0.20%. Accrued daily on net assets, ACT/365. |
0.0
|
launch_price
|
float
|
NAV per unit when the fund launches, which sets how many units the initial capital buys. |
DEFAULT_LAUNCH_PRICE
|
pricing
|
Pricing | None
|
How the fund prices dealing (see |
None
|
limits
|
Iterable[DiversificationLimit]
|
The structure's diversification limits (see
|
()
|
name
|
str
|
What the vehicle is called, such as a preset's name. |
'Vehicle'
|
domicile
|
str | None
|
Where the fund is domiciled, as a two-letter country code
such as |
None
|
withholding_rates
|
Mapping[str, float] | None
|
The fund's own withholding rates by the dividend's
source country, such as |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If the fee is negative, the launch price not positive, the domicile not a two-letter code, or a withholding rate not a share from 0 up to 1. |
daily_fee ¶
The fee net_assets accrue over days calendar days.
withholding ¶
Withholding tax on a fund's dividends, by its domicile and the dividend's source.
A fund's domicile sets the tax it suffers on each market's dividends, because treaties are between countries and not every fund can claim them. On US dividends an Irish fund suffers 15%, a Luxembourg SICAV usually 30% (the treaty's limitation on benefits keeps most from its 15% rate) and a US fund nothing. A net total return index assumes one rate for every holder, so the difference is part of a fund's tracking difference against it.
The rate for a dividend is, in order:
- the vehicle's own
withholding_ratesfor the dividend's source country, - the table below, for the vehicle's domicile and the source country,
- the run's flat
withholding_tax_rate(zero unless set).
The table holds only rates that are well established for funds, since a wrong rate is worse than an honest fallback:
| Dividend from | Ireland | Luxembourg | United Kingdom | United States |
|---|---|---|---|---|
| United States | 15% | 30% | 15% | 0% |
| United Kingdom | 0% | 0% | 0% | 0% |
| Ireland | 0% |
The United Kingdom withholds nothing on dividends, and an Irish fund is exempt
from Irish dividend withholding tax. Other markets' rates for a fund depend
on whether it can claim a treaty and on reclaims, so they are left to
withholding_rates and the flat rate.
The source country is the company's own: a COUNTRY_DOMICILE or
COUNTRY reference column when the data has one, otherwise the country of
the exchange it lists on (XNYS is the United States, XLON the United Kingdom).
Countries are ISO 3166 two-letter codes.
withholding_rate ¶
withholding_rate(
domicile: str | None,
source: str | None,
overrides: Mapping[str, float] | None,
fallback: float,
) -> float
The share of a dividend from source withheld from a fund domiciled in domicile: the fund's own override for the source, then the table, then fallback.
source_country ¶
Where identifier's dividends come from: its country column, else its listing exchange's country, else None.
checked_rates ¶
A vehicle's withholding rates by source country, checked: each a share from 0 up to 1, each key a two-letter country code (upper-cased).
Daily breakdown¶
What a run cost each day, by kind (fixed trading costs, market impact, futures rolls and creation fees), and where its cash went: balances, cash that moved, and what was charged or owed without cash moving.
breakdown ¶
A run's money, day by day: what it cost, and where its cash went.
costs = daily_costs(result)
cash = daily_cash(result)
Each table has a row for every day of the run's NAV and a column for each kind, in the book's currency, and is 0 on a day nothing of that kind happened. Summed over the run, each column is the run's total of its kind.
daily_costs ¶
What the run cost, by day and kind.
Columns:
fixed: trading costs at the run's fixed rate, on every trade that paid them.impact: the market impact trades paid, when the run models it.futures_rolls: what rolling index futures to the next contract cost: the roll cost and the cost per contract of both legs.futures_trading: what trading them between rolls cost: opening the position and each resize.creation_fees: what investors dealing in or out paid into the fund toward the trading their flow caused, through the dealing price or a levy (an ETF's creation fee among them). Received by the fund, so it offsets the costs above rather than adding to them.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
result
|
BacktestResult
|
The finished run. |
required |
Returns:
| Type | Description |
|---|---|
DataFrame
|
pd.DataFrame: Dates of the NAV by kind, in the book's currency, 0 |
DataFrame
|
where nothing of a kind happened. |
trades ¶
Every trade the run made, in the order it made them.
Returns:
| Type | Description |
|---|---|
DataFrame
|
pd.DataFrame: Indexed by the trade's date ( |
DataFrame
|
|
DataFrame
|
(quantity times price), the cost and the part of it that was market |
DataFrame
|
impact, all in the book's currency; why the trade was made |
DataFrame
|
( |
DataFrame
|
as |
daily_cash ¶
Where the run's cash went, and what it was owed and charged, by day.
Balances, at the end of each day:
cash: the cash held.dividends_receivable: dividends gone ex and not yet paid, net of withholding, which the NAV includes.fees_payable: the management fee charged and not yet paid, which the NAV is net of.futures_exposure: what any index futures were exposed to.
Cash that moved, positive into the fund and negative out of it:
interest, dividends_received (net of withholding, on the pay
date), distributions, subscriptions, redemptions,
fees_paid, futures_settled (their daily margin and roll costs),
swap_settled and hedge_settled.
Charged or owed without cash moving:
dividends_gross: dividends the book became owed on their ex-dates, before tax.withholding_tax: the tax withheld from them, on the same days.fee_charged: the management fee charged for the day.flows_dealt: investors' flows as dealt, positive in.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
result
|
BacktestResult
|
The finished run. |
required |
Returns:
| Type | Description |
|---|---|
DataFrame
|
pd.DataFrame: Dates of the NAV by kind, in the book's currency, 0 |
DataFrame
|
where nothing of a kind happened. |