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; |
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
|
None
|
rebalancing
|
str
|
|
'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. |
rebalance_dates ¶
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.
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 ¶
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 ¶
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
¶
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
¶
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. |
ActiveShare ¶
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 ¶
Bases: ActiveConstraint
No more than maximum names held.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
maximum
|
int
|
The most names. |
required |
RelativePositionBounds ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 |
required |
higher_is_better
|
bool
|
Whether a larger value is better. |
True
|
Momentum ¶
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 ¶
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
|
FullReplication ¶
IndexTracking ¶
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 |
()
|
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.
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 ¶
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:
- 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.
- Picks its candidates: the
universe(the benchmark's constituents by default), less any failing thescreen, a condition such asdata.market.market_cap > 1e9. A benchmark name that is not a candidate cannot be held. - Scores the candidates with its signal (see
beacon.strategy.signals). - 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.
-
Constructs the long-only, fully invested portfolio, within its constraints (see
beacon.strategy.constraints): -
MaxAlpha(tracking_error): the most exposure to the scores that a tracking-error budget allows. MeanVariance(risk_aversion): the best trade-off of expected active return (the scores timesalpha_per_score) against active variance timesrisk_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.
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 ¶
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 ¶
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; |
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
|
None
|
rebalancing
|
str
|
|
'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. |
rebalance_dates ¶
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
¶
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 ¶
The shrunk covariance of names, over the days all have returns.
covariance_of ¶
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. |
ActiveConstraint ¶
Bases: ABC
A constraint on an active portfolio.
build
abstractmethod
¶
The optimiser constraints this becomes for problem.
TrackingErrorBudget ¶
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 ¶
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 ¶
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 ¶
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 ¶
Bases: ActiveConstraint
No more than maximum names held.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
maximum
|
int
|
The most names. |
required |
TurnoverLimit ¶
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 ¶
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
|
FieldSignal ¶
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 |
required |
higher_is_better
|
bool
|
Whether a larger value is better. |
True
|
Momentum ¶
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 ¶
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.
FullReplication ¶
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 |
()
|
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 ¶
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 ¶
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.