Skip to content

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

PriceGap(
    date: Timestamp, asset_id: str, priced_from: Timestamp
)

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

"SELL" or "BUY".

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 Portfolio.trading).

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 source snapshots: what each rebalance decided, which is the comparison slippage is about.

None

trades

trades() -> pd.DataFrame

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

total_cost() -> float

Sum of all transaction costs for this asset.

Returns:

Name Type Description
float float

Total transaction costs incurred for this asset.

holding_periods

holding_periods() -> list[dict[str, pd.Timestamp]]

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 "start" and "end" keys with

list[dict[str, Timestamp]]

Timestamps. An open position at the end of the run has "end"

list[dict[str, Timestamp]]

set to the last recorded date.

weight_series

weight_series() -> pd.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

target_weight_series() -> pd.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

slippage_vs_target() -> pd.Series

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

weight_on_date(date: Timestamp) -> float | None

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.

name property

name: str

How the run's record names this cap.

max_value abstractmethod

max_value(
    asset_id: str,
    date: Timestamp,
    book_value: float,
    context: ScreenContext,
) -> float | None

The largest position value in the book's currency, or None when this cap does not limit the name.

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

MinimumPosition(
    value: float | None = None, weight: float | None = None
)

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

too_small(weight: float, book_value: float) -> bool

Whether a position of weight in a book of book_value is.

OwnershipCap

OwnershipCap(max_share: float, free_float: bool = True)

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

WeightCap(max_weight: float)

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

volume(
    asset_id: str,
    date: Timestamp,
    fetcher: DataFetcher,
    backfill_days: int,
) -> float | None

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

allowed(order: WorkingOrder, volume: float | None) -> float

How many shares of order may trade on a day the name trades volume shares; None when its volume is unknown.

MarketImpact

MarketImpact(
    coefficient: float = 1.0,
    lookback_days: int = DEFAULT_LIQUIDITY_DAYS,
)

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

rate(
    asset_id: str,
    trade_value: float,
    date: Timestamp,
    context: ScreenContext,
) -> float

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

Deal(
    units: float,
    cash: float,
    price: float,
    adjustment: float,
)

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

DilutionLevy(
    rate_bps: float | None = None,
    threshold: float = 0.0,
    on: str = BOTH,
)

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" (the default), "subscriptions" or "redemptions". A redemption fee is a levy on redemptions.

BOTH

Raises:

Type Description
ValueError

If rate_bps or threshold is negative, or on is not one of the three.

DualPricing

DualPricing(
    offer_bps: float | None = None,
    bid_bps: float | None = None,
)

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

estimated: bool

Whether the method needs the run's estimate of the flow's trading cost.

deal abstractmethod

deal(
    amount: float,
    nav_per_unit: float,
    net_assets: float,
    cost_rate: float,
) -> Deal

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

SinglePricing

Bases: Pricing

Everyone deals at NAV per unit.

SwingPricing

SwingPricing(
    factor_bps: float | None = None, threshold: float = 0.0
)

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 {"Acc": 6e7}; a class left out starts empty.

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".

'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" (the default) buys the current holdings with them the day they arrive, "cash" keeps them as cash until the next rebalance, and "distribute" pays them out of the book, with returns adding them back. See :mod:beacon.backtest.dividends.

REINVEST
reinvest_into str

Where reinvested dividends go: "payer" (the default) buys more of the name that paid each one, and "holdings" buys the current holdings in proportion to value.

PAYER
implementation Implementation | None

The screens and redistribution rule applied at each rebalance (see :class:~beacon.backtest.Implementation). None screens nothing beyond stale prices and redistributes pro rata, which is how a run behaved before it had stages.

None
flows Flows | list[Flows] | Mapping[str, Flows | list[Flows]] | None

Money arriving and leaving: one scenario or several, added together (see :mod:beacon.backtest.flows). None for a fixed amount of capital. With share classes, each class's by name.

None
vehicle Vehicle | None

The fund's fee and unit price at launch (see :class:~beacon.backtest.Vehicle). None charges no fee.

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:beacon.strategy). None trades the index's snapshots. The run is measured against the index either way.

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:~beacon.strategy.SyntheticReplication). None for a fund that holds what it tracks.

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:~beacon.strategy.FuturesReplication). None otherwise; a physical fund equitises its cash buffer through Implementation(equitise=...) instead.

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:beacon.backtest.share_classes). None runs the fund as one class. With classes, dividends applies to the accumulating classes' income and may not be "distribute".

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 index.optimised and index.target respectively. Omitted on a plain run, whose own calculation fills the target book.

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 beacon.portfolio.reasons names them: an active strategy's are model-driven.

()

run

run(
    progress: Callable[[int, int, Timestamp], None]
    | None = None,
) -> BacktestResult

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; EtfMarket() when None.

None
**settings Any

Any other Vehicle setting. The launch price defaults to 100 a share.

{}

Raises:

Type Description
ValueError

If creation_unit is below 1 or ap_fee is negative.

creation_units

creation_units(amount: float, nav_per_unit: float) -> float

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

DatedFlows(amounts: Mapping[str | Timestamp, float])

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

FlowContext(
    previous: Timestamp,
    date: Timestamp,
    aum: float,
    nav_per_unit: Series,
)

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.

start

start(first_day: Timestamp) -> None

Get ready for a run whose first simulated day is first_day.

amount abstractmethod

amount(context: FlowContext) -> float

Today's flow in the book's currency: positive in, negative out.

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" by default).

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

"DAILY", "WEEKLY", "MONTHLY" (the default), "QUARTERLY" or "ANNUALLY".

MONTHLY

Raises:

Type Description
ValueError

Unless exactly one of amount and fraction is given.

RandomFlows

RandomFlows(
    drift: float = 0.0,
    volatility: float = 0.1,
    frequency: str = DAILY,
    seed: int = 0,
)

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" by default).

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" (the default) across the remaining names, or "cash".

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", the default) or the holdings in proportion to their value ("holdings").

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:beacon.strategy.futures.

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 ("stale price" for one the data says has gone stale, "MinimumPosition" for one too small to keep).

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

Bases: DiversificationLimit

The US Investment Company Act's test for a diversified fund.

DiversificationLimit

Bases: ABC

A limit on how concentrated a fund may be.

name property

name: str

How the run's record names this limit.

caps

caps() -> list[CapacityCap]

Per-name capacity caps the limit adds, such as a share of a company's shares outstanding.

limited abstractmethod

limited(
    weights: dict[str, float],
    rule: str,
    capped: dict[str, float],
) -> dict[str, float]

weights within the limit, redistributed by rule. Records each name cut, and the weight it was cut to, in capped.

UcitsLimits

UcitsLimits(index_tracking: bool = True)

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".

'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:beacon.use_modelling_assumptions). The index calculation reads data under the same assumptions as the simulation.

None
dividends str

What happens to cash distributions: "reinvest" (the default), "cash" or "distribute". See :mod:beacon.backtest.dividends.

REINVEST
reinvest_into str

Where reinvested dividends go: "payer" (the default), the name that paid each one, or "holdings", the current holdings in proportion to value.

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:beacon.backtest.share_classes). None runs one class.

None
flows Flows | list[Flows] | Mapping[str, Flows | list[Flows]] | None

Money arriving and leaving: one scenario or several, added together (see :mod:beacon.backtest.flows). None for a fixed amount of capital.

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:beacon.sources.resolve) at each run. Resolution is per run, not at construction, so beacon.use() after construction is honoured.

None
cache IndexResultCache | Literal[False] | None

Where calculated IndexResults are kept between runs. None uses the default location when platformdirs is available and degrades to no caching when it is not. False turns caching off, so every run calculates its index afresh.

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:IndexDefinition, or a stored :class:~beacon.index.derived.OptimisedIndexDefinition, which always fills both index books: its parent's calculation as index.target and its own as index.optimised. Or an :class:~beacon.strategy.IndexTracking, which holds the index through a replication: the index is calculated and measured against, and the replicated weights are traded. Or an :class:~beacon.strategy.ActiveStrategy, which builds its own portfolio and is measured against the index as its benchmark.

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 optimised=True.

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:IndexDefinition or a derived one, whose source is calculated first.

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

Preset(key: str, name: str, build: Callable[..., Vehicle])

A preset by name, for listing.

Attributes:

Name Type Description
key str

The name preset() takes.

name str

What the structure is called.

build Callable[..., Vehicle]

Makes the vehicle; takes any Vehicle setting to change.

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 container: always present, its books None when the run calculated none. index.target is the index aimed at, index.optimised the solved calculation when one exists, and index.tracked the book the engine traded toward.

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. date and priced_from differ only where the schedule landed on a day the market was shut, so the run can state which session its trades were struck at rather than leaving it inferable.

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 IndexTracking, what the replication did at each rebalance: the weights, the holdings and the ex-ante tracking error. Empty otherwise.

list()
active list[ActiveStep]

For an ActiveStrategy, what it did at each rebalance: the weights, the benchmark's, the ex-ante tracking error, the active share and the scores. Empty otherwise.

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: GROSS, the dividends before tax, and WITHHELD, the tax withheld from them, at that day's exchange rates. Only days with a dividend have a row.

(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

trading_nav: Series

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

counterparty_exposure: Series

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

futures_exposure: Series

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.

fees_paid property

fees_paid: float

The management fees paid out of the book over the run.

total_costs property

total_costs: float

The transaction costs paid over the run, in its currency.

total_unfilled_value property

total_unfilled_value: float

Total notional that went unfilled across the run.

with_data

with_data(data_fetcher: DataFetcher) -> BacktestResult

Bind a DataFetcher for asset-level queries. Returns self for chaining.

asset

asset(asset_id: str) -> BacktestAssetView

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:with_data.

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

against(other: Comparable) -> RelativeMetrics

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 analysis.relative

RelativeMetrics

computes them.

RebalancePricing dataclass

RebalancePricing(date: Timestamp, priced_from: Timestamp)

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: "cash" (not enough to buy it all), "no price" (the name could not be priced on the day) or "execution limit" (still working when the next rebalance replaced it or the run ended).

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

DriftThresholdModifier(threshold: float)

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]

SECTOR values excluded.

()
regions Iterable[str]

REGION values excluded.

()

ExpressionScreen

ExpressionScreen(
    expression: Expression, on_missing: bool = False
)

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

prepare(fetcher: DataFetcher) -> None

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

ListingAgeScreen(min_days: int)

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

MinimumPriceScreen(
    min_price: float,
    exit: float | None = None,
    on_missing: bool = False,
)

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.

name property

name: str

How the run's record names this screen.

prepare

prepare(fetcher: DataFetcher) -> None

Check the screen against the data before the run starts.

admits abstractmethod

admits(
    asset_id: str,
    date: Timestamp,
    held: bool,
    context: ScreenContext,
) -> bool

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

ScreenContext(fetcher: DataFetcher, currency: str)

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

ThresholdScreen(
    entry: float,
    exit: float | None = None,
    on_missing: bool = False,
)

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

value(
    asset_id: str, date: Timestamp, context: ScreenContext
) -> float | None

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 "Acc" or "I GBP Dist".

management_fee_bps float | None

The class's annual management fee in basis points. None uses the vehicle's.

distribution str

"accumulating" (the default) keeps its income in the fund, "distributing" pays it out.

currency str | None

The class's currency, such as "EUR". None for the book's.

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

"accumulating" or "distributing".

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.

fees_paid property

fees_paid: float

The fees it accrued over the run.

distributions_paid property

distributions_paid: float

What it paid out over the run.

get_returns

get_returns() -> pd.Series

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

total_return() -> float

The class's total return over the run, distributions reinvested.

SwapReset dataclass

SwapReset(
    date: Timestamp,
    settled: float,
    reason: str,
    notional: float,
)

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: "limit" (its value owed to the fund reached the exposure limit), "rebalance" or "flows".

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 beacon.backtest.dealing). Single pricing by default.

None
limits Iterable[DiversificationLimit]

The structure's diversification limits (see beacon.backtest.limits), applied at each rebalance with the capacity caps.

()
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 "IE". It sets the withholding tax the fund suffers on each market's dividends (see beacon.backtest.withholding). None applies the run's flat rate to every dividend.

None
withholding_rates Mapping[str, float] | None

The fund's own withholding rates by the dividend's source country, such as {"US": 0.15}, used before the built-in table for its domicile.

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

daily_fee(net_assets: float, days: int) -> float

The fee net_assets accrue over days calendar days.

irish_icav

irish_icav(**settings: Any) -> Vehicle

An Irish ICAV under UCITS: an anti-dilution levy on deals above 2% of the fund, and the UCITS limits.

luxembourg_sicav

luxembourg_sicav(**settings: Any) -> Vehicle

A Luxembourg SICAV under UCITS: a partial swing above 2% of the fund, and the UCITS limits.

preset

preset(key: str, **settings: Any) -> Vehicle

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

ucits_etf(**settings: Any) -> EtfVehicle

A UCITS ETF: cash creations, a creation fee estimated from the basket's trading cost, and the UCITS limits.

uk_oeic

uk_oeic(**settings: Any) -> Vehicle

A UK open-ended investment company: daily dealing, a full swinging single price, and the UCITS limits.

us_etf

us_etf(**settings: Any) -> EtfVehicle

A US ETF: in-kind creations and the 1940 Act's test for a diversified fund.

us_mutual_fund

us_mutual_fund(
    redemption_fee_bps: float = 0.0, **settings: Any
) -> Vehicle

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 Vehicle setting.

{}

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_flows says, 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.

AccountingMixin

Units, flows, fees and interest, mixed into BacktestEngine.

launch_price property
launch_price: float

NAV per unit at launch.

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.

run_data property
run_data: Any

The run as beacon.analysis.RunData, which every analysis reads.

analytics property
analytics: Any

The run's performance (beacon.analysis.performance.PerformanceAnalytics).

portfolio_analytics property
portfolio_analytics: Any

The run's portfolio figures (beacon.analysis.performance.portfolio .PortfolioAnalytics).

risk property
risk: Any

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
attribution: Any

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 source snapshots: what each rebalance decided, which is the comparison slippage is about.

None
trades
trades() -> pd.DataFrame

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
total_cost() -> float

Sum of all transaction costs for this asset.

Returns:

Name Type Description
float float

Total transaction costs incurred for this asset.

holding_periods
holding_periods() -> list[dict[str, pd.Timestamp]]

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 "start" and "end" keys with

list[dict[str, Timestamp]]

Timestamps. An open position at the end of the run has "end"

list[dict[str, Timestamp]]

set to the last recorded date.

weight_series
weight_series() -> pd.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
target_weight_series() -> pd.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
slippage_vs_target() -> pd.Series

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
weight_on_date(date: Timestamp) -> float | None

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

Book(
    levels: Series,
    weights: DataFrame | None = None,
    source: IndexResult | None = None,
)

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.

returns property
returns: Series

Daily returns of the levels; empty when the book is.

from_index classmethod
from_index(result: IndexResult) -> Book

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
from_levels(levels: Series) -> Book

A book over a bare level series, such as a benchmark from raw data.

IndexBooks dataclass

IndexBooks(
    target: Book | None = None,
    optimised: Book | None = None,
)

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
tracked: Book | None

The book the engine traded toward: optimised when solved, else target.

index_books

index_books(
    index_result: IndexResult,
    target_index: IndexResult | None,
) -> IndexBooks

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

benchmark_book(
    benchmark: IndexResult | Series | None,
) -> Book | None

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

daily_costs(result: BacktestResult) -> pd.DataFrame

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

trades(result: BacktestResult) -> pd.DataFrame

Every trade the run made, in the order it made them.

Returns:

Type Description
DataFrame

pd.DataFrame: Indexed by the trade's date (DATE), with the name,

DataFrame

BUY or SELL, the quantity, the price, the value traded

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

(REASON) and every tag it carries, joined by "; " (TAGS),

DataFrame

as beacon.portfolio.reasons names them.

daily_cash

daily_cash(result: BacktestResult) -> pd.DataFrame

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.

name property
name: str

How the run's record names this cap.

max_value abstractmethod
max_value(
    asset_id: str,
    date: Timestamp,
    book_value: float,
    context: ScreenContext,
) -> float | None

The largest position value in the book's currency, or None when this cap does not limit the name.

OwnershipCap

OwnershipCap(max_share: float, free_float: bool = True)

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

WeightCap(max_weight: float)

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

MinimumPosition(
    value: float | None = None, weight: float | None = None
)

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
too_small(weight: float, book_value: float) -> bool

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

raise when the cash fell below the band and holdings were sold, spend-down when it rose above and was invested.

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

band_events_frame(
    events: list[CashBandEvent],
) -> pd.DataFrame

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:index_in_class_currency).

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

MarketImpact(
    coefficient: float = 1.0,
    lookback_days: int = DEFAULT_LIQUIDITY_DAYS,
)

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
rate(
    asset_id: str,
    trade_value: float,
    date: Timestamp,
    context: ScreenContext,
) -> float

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
volume(
    asset_id: str,
    date: Timestamp,
    fetcher: DataFetcher,
    backfill_days: int,
) -> float | None

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
allowed(order: WorkingOrder, volume: float | None) -> float

How many shares of order may trade on a day the name trades volume shares; None when its volume is unknown.

WorkingOrder dataclass

WorkingOrder(
    date: Timestamp,
    asset_id: str,
    side: str,
    quantity: float,
    filled: float = 0.0,
)

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

Deal(
    units: float,
    cash: float,
    price: float,
    adjustment: float,
)

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
estimated: bool

Whether the method needs the run's estimate of the flow's trading cost.

deal abstractmethod
deal(
    amount: float,
    nav_per_unit: float,
    net_assets: float,
    cost_rate: float,
) -> Deal

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

SinglePricing

Bases: Pricing

Everyone deals at NAV per unit.

DualPricing

DualPricing(
    offer_bps: float | None = None,
    bid_bps: float | None = None,
)

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

SwingPricing(
    factor_bps: float | None = None, threshold: float = 0.0
)

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

DilutionLevy(
    rate_bps: float | None = None,
    threshold: float = 0.0,
    on: str = BOTH,
)

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" (the default), "subscriptions" or "redemptions". A redemption fee is a levy on redemptions.

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's reinvest_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

CashEvent(
    asset_id: str,
    per_share: float,
    ex_date: Timestamp,
    pay_date: Timestamp,
)

One cash distribution in the data: who pays, how much, when.

Entitlement dataclass

Entitlement(event: CashEvent, shares: float)

A distribution the book is owed: an event and the shares held for it.

DividendsMixin

Dividend entitlement and payment, mixed into BacktestEngine.

entitled_frame

entitled_frame(
    history: dict[Timestamp, tuple[float, float]],
) -> pd.DataFrame

Each day's gross dividends and tax withheld, as dividends_entitled.

cash_events

cash_events(
    data_provider: DataFetcher,
) -> dict[pd.Timestamp, list[CashEvent]]

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 {"Acc": 6e7}; a class left out starts empty.

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".

'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" (the default) buys the current holdings with them the day they arrive, "cash" keeps them as cash until the next rebalance, and "distribute" pays them out of the book, with returns adding them back. See :mod:beacon.backtest.dividends.

REINVEST
reinvest_into str

Where reinvested dividends go: "payer" (the default) buys more of the name that paid each one, and "holdings" buys the current holdings in proportion to value.

PAYER
implementation Implementation | None

The screens and redistribution rule applied at each rebalance (see :class:~beacon.backtest.Implementation). None screens nothing beyond stale prices and redistributes pro rata, which is how a run behaved before it had stages.

None
flows Flows | list[Flows] | Mapping[str, Flows | list[Flows]] | None

Money arriving and leaving: one scenario or several, added together (see :mod:beacon.backtest.flows). None for a fixed amount of capital. With share classes, each class's by name.

None
vehicle Vehicle | None

The fund's fee and unit price at launch (see :class:~beacon.backtest.Vehicle). None charges no fee.

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:beacon.strategy). None trades the index's snapshots. The run is measured against the index either way.

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:~beacon.strategy.SyntheticReplication). None for a fund that holds what it tracks.

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:~beacon.strategy.FuturesReplication). None otherwise; a physical fund equitises its cash buffer through Implementation(equitise=...) instead.

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:beacon.backtest.share_classes). None runs the fund as one class. With classes, dividends applies to the accumulating classes' income and may not be "distribute".

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 index.optimised and index.target respectively. Omitted on a plain run, whose own calculation fills the target book.

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 beacon.portfolio.reasons names them: an active strategy's are model-driven.

()
run
run(
    progress: Callable[[int, int, Timestamp], None]
    | None = None,
) -> BacktestResult

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; EtfMarket() when None.

None
**settings Any

Any other Vehicle setting. The launch price defaults to 100 a share.

{}

Raises:

Type Description
ValueError

If creation_unit is below 1 or ap_fee is negative.

creation_units
creation_units(amount: float, nav_per_unit: float) -> float

How many whole creation units amount deals, toward zero.

MarketMaker

MarketMaker(vehicle: EtfVehicle)

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

quotes_frame(quotes: list[Quote]) -> pd.DataFrame

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

FlowContext(
    previous: Timestamp,
    date: Timestamp,
    aum: float,
    nav_per_unit: Series,
)

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.

start
start(first_day: Timestamp) -> None

Get ready for a run whose first simulated day is first_day.

amount abstractmethod
amount(context: FlowContext) -> float

Today's flow in the book's currency: positive in, negative out.

DatedFlows

DatedFlows(amounts: Mapping[str | Timestamp, float])

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

"DAILY", "WEEKLY", "MONTHLY" (the default), "QUARTERLY" or "ANNUALLY".

MONTHLY

Raises:

Type Description
ValueError

Unless exactly one of amount and fraction is given.

RandomFlows

RandomFlows(
    drift: float = 0.0,
    volatility: float = 0.1,
    frequency: str = DAILY,
    seed: int = 0,
)

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" by default).

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" by default).

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 beacon.portfolio.reasons names it: futures roll for a roll, futures exposure otherwise.

kind str

What it did: open, roll (the old contract sold and the next bought) or resize (the position brought back to its target).

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

futures_trades_frame(
    trades: list[FuturesTrade],
) -> pd.DataFrame

The trades as a frame indexed by date.

futures_days_frame

futures_days_frame(
    days: dict[Timestamp, dict[str, float]],
) -> pd.DataFrame

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

Forward(
    notional: float,
    rate: float,
    expiry: Timestamp,
    value: float = 0.0,
)

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 beacon.portfolio.reasons names it: fx hedge.

kind str

What it did: "open", "roll" (a new month) or "band" (the hedge ratio left its band).

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

History(volume: Series | None, closes: Series)

One name's reported volume and its closes in one currency, by date.

volume_between
volume_between(
    start: Timestamp, end: Timestamp
) -> pd.Series | None

Its reported volume from start to end, both included; None when it reports no volume at all.

closes_between
closes_between(
    start: Timestamp, end: Timestamp
) -> pd.Series

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
impact_inputs(
    start: Timestamp, end: Timestamp, days: int
) -> tuple[float | None, int, float]

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
last_bar(
    start: Timestamp, end: Timestamp
) -> tuple[pd.Timestamp, float] | None

Its last close from start to end, both included, and its date; None when it printed none.

last_close
last_close(date: Timestamp) -> float | None

Its last close on or before date, or None.

returns
returns(
    start: Timestamp, end: Timestamp, days: int
) -> np.ndarray

Its last days daily returns from its closes from start to end.

history

history(
    fetcher: DataFetcher,
    asset_id: str,
    currency: str | None,
) -> 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
  1. Target: the strategy's weights, as published.
  2. Screens: names the data says are stale are removed first, then each screen in order removes what it rejects.
  3. Redistribution: removed weight is spread across the remaining names in proportion to their weights ("pro_rata"), or held as cash ("cash").
  4. 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.
  5. Trades, generated from the result and sized so they and their costs fit the cash, then any modifiers.
  6. 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" (the default) across the remaining names, or "cash".

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", the default) or the holdings in proportion to their value ("holdings").

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:beacon.strategy.futures.

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 ("stale price" for one the data says has gone stale, "MinimumPosition" for one too small to keep).

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

buffered(
    weights: dict[str, float], cash_buffer: float
) -> dict[str, float]

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

Bases: ABC

A limit on how concentrated a fund may be.

name property
name: str

How the run's record names this limit.

caps
caps() -> list[CapacityCap]

Per-name capacity caps the limit adds, such as a share of a company's shares outstanding.

limited abstractmethod
limited(
    weights: dict[str, float],
    rule: str,
    capped: dict[str, float],
) -> dict[str, float]

weights within the limit, redistributed by rule. Records each name cut, and the weight it was cut to, in capped.

UcitsLimits

UcitsLimits(index_tracking: bool = True)

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

Bases: DiversificationLimit

The US Investment Company Act's test for a diversified fund.

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".

'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:beacon.use_modelling_assumptions). The index calculation reads data under the same assumptions as the simulation.

None
dividends str

What happens to cash distributions: "reinvest" (the default), "cash" or "distribute". See :mod:beacon.backtest.dividends.

REINVEST
reinvest_into str

Where reinvested dividends go: "payer" (the default), the name that paid each one, or "holdings", the current holdings in proportion to value.

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:beacon.backtest.share_classes). None runs one class.

None
flows Flows | list[Flows] | Mapping[str, Flows | list[Flows]] | None

Money arriving and leaving: one scenario or several, added together (see :mod:beacon.backtest.flows). None for a fixed amount of capital.

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:beacon.sources.resolve) at each run. Resolution is per run, not at construction, so beacon.use() after construction is honoured.

None
cache IndexResultCache | Literal[False] | None

Where calculated IndexResults are kept between runs. None uses the default location when platformdirs is available and degrades to no caching when it is not. False turns caching off, so every run calculates its index afresh.

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:IndexDefinition, or a stored :class:~beacon.index.derived.OptimisedIndexDefinition, which always fills both index books: its parent's calculation as index.target and its own as index.optimised. Or an :class:~beacon.strategy.IndexTracking, which holds the index through a replication: the index is calculated and measured against, and the replicated weights are traded. Or an :class:~beacon.strategy.ActiveStrategy, which builds its own portfolio and is measured against the index as its benchmark.

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 optimised=True.

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:IndexDefinition or a derived one, whose source is calculated first.

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
trading_nav: Series

Provided by BacktestResult; declared so this one type-checks.

aum property
aum: Series

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: Series

Units outstanding at the end of each simulated day.

nav_per_unit property
nav_per_unit: Series

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
get_returns() -> pd.Series

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
performance_levels() -> pd.Series

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
money_weighted_return() -> float | None

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
get_annual_returns() -> pd.Series

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
get_tracking_error() -> float | None

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
get_tracking_difference() -> float | None

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
summary() -> dict[str, float | None]

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
information_ratio() -> float | None

Annualised active return over annualised tracking error, against the tracked index. None without an index, or with no active risk.

active_share
active_share() -> pd.Series

An active strategy's active share at each rebalance.

active_summary
active_summary() -> dict[str, float | None]

An active strategy's figures: its information ratio, average active share and average ex-ante tracking error.

active_attribution
active_attribution() -> AttributionResult

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
market_return() -> float | None

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
market_summary() -> dict[str, float | None]

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

Preset(key: str, name: str, build: Callable[..., Vehicle])

A preset by name, for listing.

Attributes:

Name Type Description
key str

The name preset() takes.

name str

What the structure is called.

build Callable[..., Vehicle]

Makes the vehicle; takes any Vehicle setting to change.

uk_oeic

uk_oeic(**settings: Any) -> Vehicle

A UK open-ended investment company: daily dealing, a full swinging single price, and the UCITS limits.

luxembourg_sicav

luxembourg_sicav(**settings: Any) -> Vehicle

A Luxembourg SICAV under UCITS: a partial swing above 2% of the fund, and the UCITS limits.

irish_icav

irish_icav(**settings: Any) -> Vehicle

An Irish ICAV under UCITS: an anti-dilution levy on deals above 2% of the fund, and the UCITS limits.

us_mutual_fund

us_mutual_fund(
    redemption_fee_bps: float = 0.0, **settings: Any
) -> Vehicle

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 Vehicle setting.

{}

ucits_etf

ucits_etf(**settings: Any) -> EtfVehicle

A UCITS ETF: cash creations, a creation fee estimated from the basket's trading cost, and the UCITS limits.

us_etf

us_etf(**settings: Any) -> EtfVehicle

A US ETF: in-kind creations and the 1940 Act's test for a diversified fund.

preset

preset(key: str, **settings: Any) -> Vehicle

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 a PriceGap).

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

redistributed(
    kept: dict[str, float], total: float, rule: str
) -> dict[str, float]

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: "cash" (not enough to buy it all), "no price" (the name could not be priced on the day) or "execution limit" (still working when the next rebalance replaced it or the run ended).

RebalancePricing dataclass

RebalancePricing(date: Timestamp, priced_from: Timestamp)

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 container: always present, its books None when the run calculated none. index.target is the index aimed at, index.optimised the solved calculation when one exists, and index.tracked the book the engine traded toward.

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. date and priced_from differ only where the schedule landed on a day the market was shut, so the run can state which session its trades were struck at rather than leaving it inferable.

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 IndexTracking, what the replication did at each rebalance: the weights, the holdings and the ex-ante tracking error. Empty otherwise.

list()
active list[ActiveStep]

For an ActiveStrategy, what it did at each rebalance: the weights, the benchmark's, the ex-ante tracking error, the active share and the scores. Empty otherwise.

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: GROSS, the dividends before tax, and WITHHELD, the tax withheld from them, at that day's exchange rates. Only days with a dividend have a row.

(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
trading_nav: Series

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
counterparty_exposure: Series

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
futures_exposure: Series

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.

fees_paid property
fees_paid: float

The management fees paid out of the book over the run.

total_costs property
total_costs: float

The transaction costs paid over the run, in its currency.

total_unfilled_value property
total_unfilled_value: float

Total notional that went unfilled across the run.

with_data
with_data(data_fetcher: DataFetcher) -> BacktestResult

Bind a DataFetcher for asset-level queries. Returns self for chaining.

asset
asset(asset_id: str) -> BacktestAssetView

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:with_data.

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
against(other: Comparable) -> RelativeMetrics

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 analysis.relative

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

DriftThresholdModifier(threshold: float)

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

ScreenContext(fetcher: DataFetcher, currency: str)

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.

name property
name: str

How the run's record names this screen.

prepare
prepare(fetcher: DataFetcher) -> None

Check the screen against the data before the run starts.

admits abstractmethod
admits(
    asset_id: str,
    date: Timestamp,
    held: bool,
    context: ScreenContext,
) -> bool

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

ThresholdScreen(
    entry: float,
    exit: float | None = None,
    on_missing: bool = False,
)

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
value(
    asset_id: str, date: Timestamp, context: ScreenContext
) -> float | None

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

MinimumPriceScreen(
    min_price: float,
    exit: float | None = None,
    on_missing: bool = False,
)

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

ListingAgeScreen(min_days: int)

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]

SECTOR values excluded.

()
regions Iterable[str]

REGION values excluded.

()

ExpressionScreen

ExpressionScreen(
    expression: Expression, on_missing: bool = False
)

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
prepare(fetcher: DataFetcher) -> None

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

book_currency(
    currency: str | None, index_result: IndexResult
) -> str

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

warn_if_assumptions_differ(
    index_result: IndexResult, data_provider: DataFetcher
) -> None

Log each data-treatment setting the index was calculated under that differs from the one this run reads data under.

refuse_screens_as_modifiers

refuse_screens_as_modifiers(
    modifiers: list[BacktestModifier],
) -> None

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 "Acc" or "I GBP Dist".

management_fee_bps float | None

The class's annual management fee in basis points. None uses the vehicle's.

distribution str

"accumulating" (the default) keeps its income in the fund, "distributing" pays it out.

currency str | None

The class's currency, such as "EUR". None for the book's.

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

"accumulating" or "distributing".

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.

fees_paid property
fees_paid: float

The fees it accrued over the run.

distributions_paid property
distributions_paid: float

What it paid out over the run.

get_returns
get_returns() -> pd.Series

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
total_return() -> float

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

SwapReset(
    date: Timestamp,
    settled: float,
    reason: str,
    notional: float,
)

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: "limit" (its value owed to the fund reached the exposure limit), "rebalance" or "flows".

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 beacon.backtest.dealing). Single pricing by default.

None
limits Iterable[DiversificationLimit]

The structure's diversification limits (see beacon.backtest.limits), applied at each rebalance with the capacity caps.

()
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 "IE". It sets the withholding tax the fund suffers on each market's dividends (see beacon.backtest.withholding). None applies the run's flat rate to every dividend.

None
withholding_rates Mapping[str, float] | None

The fund's own withholding rates by the dividend's source country, such as {"US": 0.15}, used before the built-in table for its domicile.

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
daily_fee(net_assets: float, days: int) -> float

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:

  1. the vehicle's own withholding_rates for the dividend's source country,
  2. the table below, for the vehicle's domicile and the source country,
  3. 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

source_country(
    fetcher: Any, identifier: str, date: Any
) -> str | None

Where identifier's dividends come from: its country column, else its listing exchange's country, else None.

checked_rates

checked_rates(
    rates: Mapping[str, float] | None,
) -> dict[str, float] | None

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

daily_costs(result: BacktestResult) -> pd.DataFrame

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

trades(result: BacktestResult) -> pd.DataFrame

Every trade the run made, in the order it made them.

Returns:

Type Description
DataFrame

pd.DataFrame: Indexed by the trade's date (DATE), with the name,

DataFrame

BUY or SELL, the quantity, the price, the value traded

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

(REASON) and every tag it carries, joined by "; " (TAGS),

DataFrame

as beacon.portfolio.reasons names them.

daily_cash

daily_cash(result: BacktestResult) -> pd.DataFrame

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.