Skip to content

Strategies

IndexTracking holds an index in full or through an optimised or sampled subset. See Strategies.

strategy

Strategies: what a backtest holds, whatever the vehicle that holds it.

A bare index definition passed to Backtest.run is tracked in full. IndexTracking holds an index another way: through an optimised subset or a stratified sample (see beacon.strategy.tracking). ActiveStrategy builds its own portfolio from a signal, against a benchmark (see beacon.strategy.active).

ActiveStep dataclass

ActiveStep(
    date: Timestamp,
    weights: dict[str, float],
    benchmark: dict[str, float],
    tracking_error: float,
    active_share: float,
    scores: dict[str, float] = dict(),
    unmeasured: tuple[str, ...] = tuple(),
)

What an active strategy did at one rebalance.

Attributes:

Name Type Description
date Timestamp

The rebalance date.

weights dict[str, float]

The weights the portfolio traded to.

benchmark dict[str, float]

The benchmark's weights that day.

tracking_error float

The ex-ante annualised tracking error to the benchmark, from the day's covariance.

active_share float

Half the summed absolute differences from the benchmark.

scores dict[str, float]

Each candidate's signal score.

unmeasured tuple[str, ...]

Candidates held at their benchmark weight because their history was too short to measure.

ActiveStrategy

ActiveStrategy(
    benchmark: AnyIndexDefinition,
    signal: Signal,
    construction: Construction | None = None,
    constraints: Sequence[ActiveConstraint] = (),
    universe: Sequence[str] | None = None,
    screen: Expression | None = None,
    rebalancing: str = "MONTHLY",
    lookback_days: int = DEFAULT_LOOKBACK_DAYS,
    minimum_observations: int = MINIMUM_OBSERVATIONS,
    name: str = "Active strategy",
)

A strategy that builds its own portfolio from a signal, against a benchmark.

Parameters:

Name Type Description Default
benchmark AnyIndexDefinition

The index it is measured against.

required
signal Signal

What it believes about each name.

required
construction Construction | None

How it builds the portfolio; MaxAlpha() by default.

None
constraints Sequence[ActiveConstraint]

What the portfolio must also satisfy.

()
universe Sequence[str] | None

The names it may hold; the benchmark's constituents when None.

None
screen Expression | None

A condition every holding must pass, such as data.market.market_cap > 1e9.

None
rebalancing str

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

'MONTHLY'
lookback_days int

Trading days of returns the covariance is estimated over.

DEFAULT_LOOKBACK_DAYS
minimum_observations int

The fewest returns a name needs to be measured.

MINIMUM_OBSERVATIONS
name str

What the strategy is called.

'Active strategy'

Raises:

Type Description
ValueError

If rebalancing is not one of the four.

index property

index: AnyIndexDefinition

The index the run calculates and measures against.

rebalance_dates

rebalance_dates(start: str, end: str) -> list[pd.Timestamp]

The first session of each period from start to end.

steps

steps(
    benchmark: IndexResult,
    start: str,
    end: str,
    context: StrategyContext,
) -> list[ActiveStep]

The portfolio at each rebalance from start to end.

rebalanced

rebalanced(
    date: Timestamp,
    benchmark: dict[str, float],
    previous: dict[str, float],
    context: StrategyContext,
) -> ActiveStep

One rebalance: the portfolio for benchmark on date.

Construction

Bases: ABC

How an active portfolio is built from scores, a benchmark and a covariance.

name property

name: str

How the run's record names this construction.

build abstractmethod

build(
    problem: ActiveProblem,
    alpha: ndarray,
    rules: list[Constraint],
    hint: ndarray,
) -> np.ndarray

The weights, aligned to the problem's names.

Parameters:

Name Type Description Default
problem ActiveProblem

The names, benchmark and covariance.

required
alpha ndarray

Each name's score; 0 for a name that cannot be held.

required
rules list[Constraint]

The constraints, including long-only bounds and full investment.

required
hint ndarray

A feasible place to start.

required

MaxAlpha

MaxAlpha(tracking_error: float = 0.03)

Bases: Construction

The most exposure to the scores within a tracking-error budget.

Solved through its equivalent trade-off: maximising exposure within a budget has the same answer as maximising exposure less some risk aversion times the active variance. The risk aversion is found by bisection so the tracking error meets the budget, or falls short of it when the other constraints keep the portfolio closer to the benchmark anyway. Each step is a quadratic problem the optimiser solves reliably, where the linear objective against a quadratic limit did not.

Parameters:

Name Type Description Default
tracking_error float

The ex-ante annualised budget, as a decimal.

0.03

Raises:

Type Description
ValueError

If tracking_error is not positive.

MeanVariance

MeanVariance(
    risk_aversion: float = 10.0,
    alpha_per_score: float = 0.02,
)

Bases: Construction

The best trade-off of expected active return against active variance.

Expected active return is each name's score times alpha_per_score; the portfolio maximises it less risk_aversion times the active variance.

Parameters:

Name Type Description Default
risk_aversion float

How heavily active variance counts: higher holds closer to the benchmark.

10.0
alpha_per_score float

The expected annual return of one score, as a decimal.

0.02

Raises:

Type Description
ValueError

If either is not positive.

StrategyContext dataclass

StrategyContext(fetcher: DataFetcher, currency: str)

What a strategy may read at a rebalance.

Attributes:

Name Type Description
fetcher DataFetcher

The run's data.

currency str

The book's currency, which returns and money fields are in.

ActiveConstraint

Bases: ABC

A constraint on an active portfolio.

build abstractmethod

build(problem: ActiveProblem) -> list[Constraint]

The optimiser constraints this becomes for problem.

ActiveProblem dataclass

ActiveProblem(
    assets: list[str],
    benchmark: ndarray,
    covariance: ndarray,
    sectors: dict[str, str] = dict(),
    previous: dict[str, float] = dict(),
)

One rebalance's problem, aligned to its names.

Attributes:

Name Type Description
assets list[str]

The names the solve allocates over, in order.

benchmark ndarray

Their benchmark weights.

covariance ndarray

Their annualised covariance.

sectors dict[str, str]

Each name's sector, for sector bounds.

previous dict[str, float]

The last rebalance's weights, for a turnover limit.

active

active(weights: ndarray) -> np.ndarray

Weights less the benchmark's.

tracking_error

tracking_error(weights: ndarray) -> float

Ex-ante annualised tracking error of weights.

active_share

active_share(weights: ndarray) -> float

Half the summed absolute differences from the benchmark.

ActiveShare

ActiveShare(
    minimum: float | None = None,
    maximum: float | None = None,
)

Bases: ActiveConstraint

An active share in a range.

Parameters:

Name Type Description Default
minimum float | None

The least, as a decimal, or None.

None
maximum float | None

The most, as a decimal, or None.

None

Raises:

Type Description
ValueError

If neither is given, or they are outside 0 to 1 or cross.

HoldingsLimit

HoldingsLimit(maximum: int)

Bases: ActiveConstraint

No more than maximum names held.

Parameters:

Name Type Description Default
maximum int

The most names.

required

RelativePositionBounds

RelativePositionBounds(within: float)

Bases: ActiveConstraint

Each name within within of its benchmark weight.

Parameters:

Name Type Description Default
within float

The largest difference, as a decimal.

required

Raises:

Type Description
ValueError

If within is negative.

RelativeSectorBounds

RelativeSectorBounds(within: float, scheme: str = 'SECTOR')

Bases: ActiveConstraint

Each sector's weight within within of the benchmark's.

Parameters:

Name Type Description Default
within float

The largest difference, as a decimal: 0.05 for 5 points.

required
scheme str

The classification sectors are read from.

'SECTOR'

Raises:

Type Description
ValueError

If within is negative.

TrackingErrorBudget

TrackingErrorBudget(maximum: float)

Bases: ActiveConstraint

An ex-ante tracking error to the benchmark of at most maximum.

Parameters:

Name Type Description Default
maximum float

The annualised limit, as a decimal: 0.03 for 3%.

required

Raises:

Type Description
ValueError

If maximum is not positive.

TurnoverLimit

TurnoverLimit(maximum: float)

Bases: ActiveConstraint

At most maximum traded one way from the last rebalance's weights. The first rebalance, with nothing held, is not limited.

Parameters:

Name Type Description Default
maximum float

The one-way limit, as a decimal.

required

FieldSignal

FieldSignal(field: Field, higher_is_better: bool = True)

Bases: Signal

A field's value: a market column, a reference value or a feature.

Parameters:

Name Type Description Default
field Field

The field, such as data.features.fundamentals.earnings_yield.

required
higher_is_better bool

Whether a larger value is better.

True

FunctionSignal

FunctionSignal(
    function: Callable[
        [str, Timestamp, DataFetcher], float | None
    ],
    higher_is_better: bool = True,
)

Bases: Signal

A value from a function of the name, the date and the data.

Parameters:

Name Type Description Default
function Callable[[str, Timestamp, DataFetcher], float | None]

Called as function(name, date, fetcher); returns a number, or None when the name has no value.

required
higher_is_better bool

Whether a larger value is better.

True

Momentum

Momentum(lookback_days: int = 252, skip_days: int = 21)

Bases: Signal

Price momentum: the return over a lookback, leaving out the most recent days, where short-term reversal works against it.

Parameters:

Name Type Description Default
lookback_days int

Trading days the return is measured over.

252
skip_days int

The most recent trading days left out.

21

Raises:

Type Description
ValueError

If skip_days is not below lookback_days.

Signal

Signal(higher_is_better: bool = True)

Bases: ABC

A value for each name at a rebalance.

Parameters:

Name Type Description Default
higher_is_better bool

Whether a larger value is a stronger case to hold the name.

True

name property

name: str

How the run's record names this signal.

values abstractmethod

values(
    names: list[str],
    date: Timestamp,
    context: StrategyContext,
) -> dict[str, float | None]

Each name's value on date, None where it has none.

scores

scores(
    names: list[str],
    date: Timestamp,
    context: StrategyContext,
) -> dict[str, float]

Each name's standardised score on date.

FullReplication

Bases: Replication

Every constituent at its index weight.

IndexTracking

IndexTracking(
    index: AnyIndexDefinition,
    replication: Replication | None = None,
)

A strategy that holds an index.

Parameters:

Name Type Description Default
index AnyIndexDefinition

The index to track.

required
replication Replication | None

How it is held; in full by default.

None

replicated

replicated(
    snapshots: dict[Timestamp, dict[str, float]],
    context: StrategyContext,
) -> list[ReplicationStep]

The replication at each of the index's rebalances, in date order.

OptimisedReplication

OptimisedReplication(
    holdings: int | None = None,
    constraints: Sequence[Constraint] = (),
    lookback_days: int = DEFAULT_LOOKBACK_DAYS,
    minimum_observations: int = MINIMUM_OBSERVATIONS,
)

Bases: Replication

A subset weighted to minimise tracking error to the index.

Parameters:

Name Type Description Default
holdings int | None

The most names to hold, or None for no limit.

None
constraints Sequence[Constraint]

Further optimiser constraints, such as GroupBounds. Long-only bounds and full investment are always applied.

()
lookback_days int

Trading days of returns the covariance is estimated over.

DEFAULT_LOOKBACK_DAYS
minimum_observations int

The fewest returns a name needs to be measured; a name with fewer is held at its index weight.

MINIMUM_OBSERVATIONS

Raises:

Type Description
ValueError

If holdings is below 1 or the windows are too short.

Replication

Bases: ABC

How an index is held: from its weights at a rebalance to the portfolio's.

name property

name: str

How the run's record names this replication.

replicate abstractmethod

replicate(
    target: dict[str, float],
    date: Timestamp,
    context: StrategyContext,
) -> ReplicationStep

The portfolio's weights for the index's target on date.

ReplicationStep dataclass

ReplicationStep(
    date: Timestamp,
    weights: dict[str, float],
    holdings: int,
    tracking_error: float | None = None,
    unmeasured: tuple[str, ...] = tuple(),
)

What a replication did at one rebalance.

Attributes:

Name Type Description
date Timestamp

The rebalance date.

weights dict[str, float]

The weights the portfolio traded to.

holdings int

How many names they hold.

tracking_error float | None

The ex-ante annualised tracking error to the index, for an optimised replication; None when it was not estimated.

unmeasured tuple[str, ...]

Names held at their index weight because their history was too short to measure.

SampledReplication

SampledReplication(
    holdings: int,
    size_buckets: int = 3,
    scheme: str = "SECTOR",
)

Bases: Replication

The largest names in each sector and size cell, each cell at its index weight.

Parameters:

Name Type Description Default
holdings int

How many names to hold.

required
size_buckets int

How many size groups (by market cap) each sector is split into: 3 for terciles.

3
scheme str

The classification the sectors are read from.

'SECTOR'

Raises:

Type Description
ValueError

If holdings or size_buckets is below 1.

active

Active strategies: a universe, a signal and a way of building a portfolio from it, measured against a benchmark.

strategy = ActiveStrategy(
    benchmark=my_index,
    signal=FieldSignal(data.features.fundamentals.earnings_yield),
    construction=MaxAlpha(tracking_error=0.03),
    constraints=[RelativeSectorBounds(within=0.05), HoldingsLimit(60)])

Backtest(initial_capital=1e8).run(strategy, start="2023-01-03", end="2024-12-31")

At each rebalance the strategy:

  1. Takes the benchmark's weights in force that day. The benchmark is an index, calculated as any index is, and the run is measured against it.
  2. Picks its candidates: the universe (the benchmark's constituents by default), less any failing the screen, a condition such as data.market.market_cap > 1e9. A benchmark name that is not a candidate cannot be held.
  3. Scores the candidates with its signal (see beacon.strategy.signals).
  4. Estimates the covariance from a year of returns, shrunk toward constant correlation. A candidate with too little history is held at its benchmark weight, outside the solve.
  5. Constructs the long-only, fully invested portfolio, within its constraints (see beacon.strategy.constraints):

  6. MaxAlpha(tracking_error): the most exposure to the scores that a tracking-error budget allows.

  7. MeanVariance(risk_aversion): the best trade-off of expected active return (the scores times alpha_per_score) against active variance times risk_aversion; the tracking error is whatever results.

The constructions share one interface, Construction, so others can be added.

Rebalances fall on the first session of each period ("MONTHLY" by default). Each records the weights, the benchmark's, the ex-ante tracking error and the active share in result.active.

ActiveStep dataclass

ActiveStep(
    date: Timestamp,
    weights: dict[str, float],
    benchmark: dict[str, float],
    tracking_error: float,
    active_share: float,
    scores: dict[str, float] = dict(),
    unmeasured: tuple[str, ...] = tuple(),
)

What an active strategy did at one rebalance.

Attributes:

Name Type Description
date Timestamp

The rebalance date.

weights dict[str, float]

The weights the portfolio traded to.

benchmark dict[str, float]

The benchmark's weights that day.

tracking_error float

The ex-ante annualised tracking error to the benchmark, from the day's covariance.

active_share float

Half the summed absolute differences from the benchmark.

scores dict[str, float]

Each candidate's signal score.

unmeasured tuple[str, ...]

Candidates held at their benchmark weight because their history was too short to measure.

Construction

Bases: ABC

How an active portfolio is built from scores, a benchmark and a covariance.

name property
name: str

How the run's record names this construction.

build abstractmethod
build(
    problem: ActiveProblem,
    alpha: ndarray,
    rules: list[Constraint],
    hint: ndarray,
) -> np.ndarray

The weights, aligned to the problem's names.

Parameters:

Name Type Description Default
problem ActiveProblem

The names, benchmark and covariance.

required
alpha ndarray

Each name's score; 0 for a name that cannot be held.

required
rules list[Constraint]

The constraints, including long-only bounds and full investment.

required
hint ndarray

A feasible place to start.

required

MaxAlpha

MaxAlpha(tracking_error: float = 0.03)

Bases: Construction

The most exposure to the scores within a tracking-error budget.

Solved through its equivalent trade-off: maximising exposure within a budget has the same answer as maximising exposure less some risk aversion times the active variance. The risk aversion is found by bisection so the tracking error meets the budget, or falls short of it when the other constraints keep the portfolio closer to the benchmark anyway. Each step is a quadratic problem the optimiser solves reliably, where the linear objective against a quadratic limit did not.

Parameters:

Name Type Description Default
tracking_error float

The ex-ante annualised budget, as a decimal.

0.03

Raises:

Type Description
ValueError

If tracking_error is not positive.

MeanVariance

MeanVariance(
    risk_aversion: float = 10.0,
    alpha_per_score: float = 0.02,
)

Bases: Construction

The best trade-off of expected active return against active variance.

Expected active return is each name's score times alpha_per_score; the portfolio maximises it less risk_aversion times the active variance.

Parameters:

Name Type Description Default
risk_aversion float

How heavily active variance counts: higher holds closer to the benchmark.

10.0
alpha_per_score float

The expected annual return of one score, as a decimal.

0.02

Raises:

Type Description
ValueError

If either is not positive.

ActiveStrategy

ActiveStrategy(
    benchmark: AnyIndexDefinition,
    signal: Signal,
    construction: Construction | None = None,
    constraints: Sequence[ActiveConstraint] = (),
    universe: Sequence[str] | None = None,
    screen: Expression | None = None,
    rebalancing: str = "MONTHLY",
    lookback_days: int = DEFAULT_LOOKBACK_DAYS,
    minimum_observations: int = MINIMUM_OBSERVATIONS,
    name: str = "Active strategy",
)

A strategy that builds its own portfolio from a signal, against a benchmark.

Parameters:

Name Type Description Default
benchmark AnyIndexDefinition

The index it is measured against.

required
signal Signal

What it believes about each name.

required
construction Construction | None

How it builds the portfolio; MaxAlpha() by default.

None
constraints Sequence[ActiveConstraint]

What the portfolio must also satisfy.

()
universe Sequence[str] | None

The names it may hold; the benchmark's constituents when None.

None
screen Expression | None

A condition every holding must pass, such as data.market.market_cap > 1e9.

None
rebalancing str

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

'MONTHLY'
lookback_days int

Trading days of returns the covariance is estimated over.

DEFAULT_LOOKBACK_DAYS
minimum_observations int

The fewest returns a name needs to be measured.

MINIMUM_OBSERVATIONS
name str

What the strategy is called.

'Active strategy'

Raises:

Type Description
ValueError

If rebalancing is not one of the four.

index property
index: AnyIndexDefinition

The index the run calculates and measures against.

rebalance_dates
rebalance_dates(start: str, end: str) -> list[pd.Timestamp]

The first session of each period from start to end.

steps
steps(
    benchmark: IndexResult,
    start: str,
    end: str,
    context: StrategyContext,
) -> list[ActiveStep]

The portfolio at each rebalance from start to end.

rebalanced
rebalanced(
    date: Timestamp,
    benchmark: dict[str, float],
    previous: dict[str, float],
    context: StrategyContext,
) -> ActiveStep

One rebalance: the portfolio for benchmark on date.

base

What every strategy shares: what it may read at a rebalance, and the returns and covariance it measures risk with.

The covariance is re-estimated at each rebalance from the names' trailing daily returns in the book's currency (a year by default), shrunk toward constant correlation. A name with too few returns to measure is left out of it; each strategy says what it does with such a name.

StrategyContext dataclass

StrategyContext(fetcher: DataFetcher, currency: str)

What a strategy may read at a rebalance.

Attributes:

Name Type Description
fetcher DataFetcher

The run's data.

currency str

The book's currency, which returns and money fields are in.

trailing_returns

trailing_returns(
    names: list[str],
    date: Timestamp,
    context: StrategyContext,
    lookback_days: int = DEFAULT_LOOKBACK_DAYS,
) -> pd.DataFrame

Daily returns in the book's currency over the trading days before and including date, one column per name.

measured

measured(
    names: list[str],
    returns: DataFrame,
    minimum_observations: int = MINIMUM_OBSERVATIONS,
) -> list[str]

The names with enough returns to estimate their risk, in order.

estimated_risk

estimated_risk(
    returns: DataFrame, names: list[str]
) -> RiskModel

The shrunk covariance of names, over the days all have returns.

covariance_of

covariance_of(
    risk: RiskModel, names: list[str]
) -> np.ndarray

The annualised covariance matrix, aligned to names.

constraints

What an active portfolio must satisfy, mostly measured against its benchmark.

ActiveStrategy(..., constraints=[ActiveShare(minimum=0.3),
                                 RelativeSectorBounds(within=0.05),
                                 HoldingsLimit(60)])
  • TrackingErrorBudget(maximum): an ex-ante tracking error to the benchmark of at most maximum a year.
  • ActiveShare(minimum, maximum): an active share in the range.
  • RelativeSectorBounds(within): each sector within within of the benchmark's weight in it.
  • RelativePositionBounds(within): each name within within of its benchmark weight.
  • HoldingsLimit(maximum): no more than maximum names.
  • TurnoverLimit(maximum): at most maximum traded one way from the last rebalance's weights.

Active share is half the summed absolute differences from the benchmark's weights. Every portfolio is also long-only and fully invested.

ActiveProblem dataclass

ActiveProblem(
    assets: list[str],
    benchmark: ndarray,
    covariance: ndarray,
    sectors: dict[str, str] = dict(),
    previous: dict[str, float] = dict(),
)

One rebalance's problem, aligned to its names.

Attributes:

Name Type Description
assets list[str]

The names the solve allocates over, in order.

benchmark ndarray

Their benchmark weights.

covariance ndarray

Their annualised covariance.

sectors dict[str, str]

Each name's sector, for sector bounds.

previous dict[str, float]

The last rebalance's weights, for a turnover limit.

active
active(weights: ndarray) -> np.ndarray

Weights less the benchmark's.

tracking_error
tracking_error(weights: ndarray) -> float

Ex-ante annualised tracking error of weights.

active_share
active_share(weights: ndarray) -> float

Half the summed absolute differences from the benchmark.

ActiveConstraint

Bases: ABC

A constraint on an active portfolio.

build abstractmethod
build(problem: ActiveProblem) -> list[Constraint]

The optimiser constraints this becomes for problem.

TrackingErrorBudget

TrackingErrorBudget(maximum: float)

Bases: ActiveConstraint

An ex-ante tracking error to the benchmark of at most maximum.

Parameters:

Name Type Description Default
maximum float

The annualised limit, as a decimal: 0.03 for 3%.

required

Raises:

Type Description
ValueError

If maximum is not positive.

ActiveShare

ActiveShare(
    minimum: float | None = None,
    maximum: float | None = None,
)

Bases: ActiveConstraint

An active share in a range.

Parameters:

Name Type Description Default
minimum float | None

The least, as a decimal, or None.

None
maximum float | None

The most, as a decimal, or None.

None

Raises:

Type Description
ValueError

If neither is given, or they are outside 0 to 1 or cross.

RelativeSectorBounds

RelativeSectorBounds(within: float, scheme: str = 'SECTOR')

Bases: ActiveConstraint

Each sector's weight within within of the benchmark's.

Parameters:

Name Type Description Default
within float

The largest difference, as a decimal: 0.05 for 5 points.

required
scheme str

The classification sectors are read from.

'SECTOR'

Raises:

Type Description
ValueError

If within is negative.

RelativePositionBounds

RelativePositionBounds(within: float)

Bases: ActiveConstraint

Each name within within of its benchmark weight.

Parameters:

Name Type Description Default
within float

The largest difference, as a decimal.

required

Raises:

Type Description
ValueError

If within is negative.

HoldingsLimit

HoldingsLimit(maximum: int)

Bases: ActiveConstraint

No more than maximum names held.

Parameters:

Name Type Description Default
maximum int

The most names.

required

TurnoverLimit

TurnoverLimit(maximum: float)

Bases: ActiveConstraint

At most maximum traded one way from the last rebalance's weights. The first rebalance, with nothing held, is not limited.

Parameters:

Name Type Description Default
maximum float

The one-way limit, as a decimal.

required

signals

Signals: what an active strategy believes about each name, as a score.

FieldSignal(data.features.fundamentals.earnings_yield)
Momentum(lookback_days=252, skip_days=21)
FunctionSignal(my_function, higher_is_better=False)

A signal gives each candidate a value at a rebalance. The values are turned into scores: standardised across the candidates (mean 0, standard deviation 1), capped at 3 either side so one outlier cannot dominate, and negated when lower is better. A name with no value scores 0, so it is held neither over nor under on the signal's account.

Signal

Signal(higher_is_better: bool = True)

Bases: ABC

A value for each name at a rebalance.

Parameters:

Name Type Description Default
higher_is_better bool

Whether a larger value is a stronger case to hold the name.

True
name property
name: str

How the run's record names this signal.

values abstractmethod
values(
    names: list[str],
    date: Timestamp,
    context: StrategyContext,
) -> dict[str, float | None]

Each name's value on date, None where it has none.

scores
scores(
    names: list[str],
    date: Timestamp,
    context: StrategyContext,
) -> dict[str, float]

Each name's standardised score on date.

FieldSignal

FieldSignal(field: Field, higher_is_better: bool = True)

Bases: Signal

A field's value: a market column, a reference value or a feature.

Parameters:

Name Type Description Default
field Field

The field, such as data.features.fundamentals.earnings_yield.

required
higher_is_better bool

Whether a larger value is better.

True

FunctionSignal

FunctionSignal(
    function: Callable[
        [str, Timestamp, DataFetcher], float | None
    ],
    higher_is_better: bool = True,
)

Bases: Signal

A value from a function of the name, the date and the data.

Parameters:

Name Type Description Default
function Callable[[str, Timestamp, DataFetcher], float | None]

Called as function(name, date, fetcher); returns a number, or None when the name has no value.

required
higher_is_better bool

Whether a larger value is better.

True

Momentum

Momentum(lookback_days: int = 252, skip_days: int = 21)

Bases: Signal

Price momentum: the return over a lookback, leaving out the most recent days, where short-term reversal works against it.

Parameters:

Name Type Description Default
lookback_days int

Trading days the return is measured over.

252
skip_days int

The most recent trading days left out.

21

Raises:

Type Description
ValueError

If skip_days is not below lookback_days.

standardised

standardised(
    values: dict[str, float | None],
) -> dict[str, float]

Values as scores: standardised across the names that have one, capped at three standard deviations, and 0 for a name with none.

tracking

Index tracking: holding an index, in full or through a subset.

Backtest(initial_capital=1e8).run(
    IndexTracking(my_index, replication=OptimisedReplication(holdings=50)),
    start="2023-01-03", end="2024-12-31")

A bare index definition passed to run() is tracked in full. IndexTracking says how else to hold it. The index itself is still calculated and is what the run is measured against; at each rebalance the replication turns the index's weights into the weights the portfolio trades to.

  • FullReplication: every constituent at its index weight.
  • OptimisedReplication: a subset weighted by the optimiser to minimise tracking error.
  • SampledReplication: the largest names in each sector and size cell, each cell at its index weight.

Optimised. At each rebalance the covariance of the constituents is estimated from their trailing daily returns in the book's currency (a year by default), shrunk toward constant correlation, and the optimiser finds the long-only weights closest to the index in tracking error, within a holdings limit and any other constraints. A name with too little history to measure is held at its index weight, outside the optimisation.

Sampled (stratified). The constituents are split into cells by sector and by size (terciles of market cap by default). Each cell is given holdings in proportion to its index weight, at least one where the limit allows, holds its largest names by index weight, and is scaled to its index weight. A limit smaller than the number of cells keeps the heaviest cells, and the others' weight is spread across them.

ReplicationStep dataclass

ReplicationStep(
    date: Timestamp,
    weights: dict[str, float],
    holdings: int,
    tracking_error: float | None = None,
    unmeasured: tuple[str, ...] = tuple(),
)

What a replication did at one rebalance.

Attributes:

Name Type Description
date Timestamp

The rebalance date.

weights dict[str, float]

The weights the portfolio traded to.

holdings int

How many names they hold.

tracking_error float | None

The ex-ante annualised tracking error to the index, for an optimised replication; None when it was not estimated.

unmeasured tuple[str, ...]

Names held at their index weight because their history was too short to measure.

Replication

Bases: ABC

How an index is held: from its weights at a rebalance to the portfolio's.

name property
name: str

How the run's record names this replication.

replicate abstractmethod
replicate(
    target: dict[str, float],
    date: Timestamp,
    context: StrategyContext,
) -> ReplicationStep

The portfolio's weights for the index's target on date.

FullReplication

Bases: Replication

Every constituent at its index weight.

OptimisedReplication

OptimisedReplication(
    holdings: int | None = None,
    constraints: Sequence[Constraint] = (),
    lookback_days: int = DEFAULT_LOOKBACK_DAYS,
    minimum_observations: int = MINIMUM_OBSERVATIONS,
)

Bases: Replication

A subset weighted to minimise tracking error to the index.

Parameters:

Name Type Description Default
holdings int | None

The most names to hold, or None for no limit.

None
constraints Sequence[Constraint]

Further optimiser constraints, such as GroupBounds. Long-only bounds and full investment are always applied.

()
lookback_days int

Trading days of returns the covariance is estimated over.

DEFAULT_LOOKBACK_DAYS
minimum_observations int

The fewest returns a name needs to be measured; a name with fewer is held at its index weight.

MINIMUM_OBSERVATIONS

Raises:

Type Description
ValueError

If holdings is below 1 or the windows are too short.

SampledReplication

SampledReplication(
    holdings: int,
    size_buckets: int = 3,
    scheme: str = "SECTOR",
)

Bases: Replication

The largest names in each sector and size cell, each cell at its index weight.

Parameters:

Name Type Description Default
holdings int

How many names to hold.

required
size_buckets int

How many size groups (by market cap) each sector is split into: 3 for terciles.

3
scheme str

The classification the sectors are read from.

'SECTOR'

Raises:

Type Description
ValueError

If holdings or size_buckets is below 1.

IndexTracking

IndexTracking(
    index: AnyIndexDefinition,
    replication: Replication | None = None,
)

A strategy that holds an index.

Parameters:

Name Type Description Default
index AnyIndexDefinition

The index to track.

required
replication Replication | None

How it is held; in full by default.

None
replicated
replicated(
    snapshots: dict[Timestamp, dict[str, float]],
    context: StrategyContext,
) -> list[ReplicationStep]

The replication at each of the index's rebalances, in date order.