pybroker.strategy module

Contains implementation for backtesting trading strategies.

class BacktestMixin[source]

Bases: object

Mixin implementing backtesting functionality.

backtest_executions(config: StrategyConfig, executions: set[Execution], before_exec_fn: Callable[[Mapping[str, ExecContext]], None] | None, after_exec_fn: Callable[[Mapping[str, ExecContext]], None] | None, sessions: Mapping[str, MutableMapping], models: Mapping[ModelSymbol, TrainedModel], indicator_data: Mapping[IndicatorSymbol, Series], test_data: DataFrame, portfolio: Portfolio, exit_dates: Mapping[str, datetime64], backtest_settings: BacktestSettings = BacktestSettings(max_long_positions=None, max_short_positions=None, worst_rank_held=None), rotation_sizer: Callable[[RotationContext], None] | None = None, train_only: bool = False, slippage_model: SlippageModel | None = None, enable_fractional_shares: bool = False, round_fill_price: bool = True, warmup: int | None = None, interval_data: IntervalData = IntervalData(compressed={}), history_col_scope: ColumnScope | None = None, test_col_scope: ColumnScope | None = None, run_hyperparams: dict[str, Any] | None = None, pending_order_scope: PendingOrderScope | None = None, master_col_scope: ColumnScope | None = None) dict[str, DataFrame][source]

Backtests a set of Executions that implement trading logic.

Parameters:
Returns:

Dictionary of pandas.DataFrames containing bar data, indicator data, and model predictions for each symbol when pybroker.config.StrategyConfig.return_signals is True. Signals contain base-timeframe values only; an interval-bound indicator or model appears only when 'base' is included in its binding.

class BacktestSettings(max_long_positions: int | None = None, max_short_positions: int | None = None, worst_rank_held: int | None = None)[source]

Bases: object

class Execution(id: int, symbols: frozenset[str] | Callable[[DataFrame], Sequence[str]], fn: Callable[[ExecContext], None] | None, model_names: frozenset[str], indicator_names: frozenset[str], intervals: frozenset[int | Literal['daily', 'weekly', 'monthly', 'quarterly', 'yearly'] | str] = frozenset({}), hyperparam_names: frozenset[str] = frozenset({}), args: tuple[Any, ...] = (), kwargs: tuple[tuple[str, Any], ...] = ())[source]

Bases: NamedTuple

Represents an execution of a Strategy. Holds a reference to a Callable that implements trading logic.

id

Unique ID.

Type:

int

symbols

Ticker symbols used for execution of fn.

Type:

frozenset[str] | Callable[[pandas.DataFrame], Sequence[str]]

fn

Implements trading logic.

Type:

Callable[[pybroker.context.ExecContext], None] | None

model_names

Names of pybroker.model.ModelSources used for execution of fn, including the suffixed per-interval names of interval-bound models.

Type:

frozenset[str]

indicator_names

Names of pybroker.indicator.Indicators used for execution of fn, including the suffixed per-interval names of interval-bound indicators.

Type:

frozenset[str]

intervals

Compression intervals available to fn through pybroker.context.ExecContext.interval(): the union of the intervals declared on pybroker.strategy.Strategy.add_execution() and the intervals bound to the execution’s models and indicators.

Type:

frozenset[int | Literal[‘daily’, ‘weekly’, ‘monthly’, ‘quarterly’, ‘yearly’] | str]

args

Additional positional arguments for fn.

Type:

tuple[Any, …]

kwargs

Additional keyword arguments for fn.

Type:

tuple[tuple[str, Any], …]

class Strategy(data_source: DataSource | DataFrame, start_date: str | datetime, end_date: str | datetime, config: StrategyConfig | None = None)[source]

Bases: BacktestMixin, EvaluateMixin, IndicatorsMixin, ModelsMixin, WalkforwardMixin, OptimizeMixin

Class representing a trading strategy to backtest.

Parameters:
add_execution(fn: ~typing.Callable[[~typing.Concatenate[~pybroker.context.ExecContext, ~P]], None] | None, symbols: str | ~typing.Iterable[str] | ~typing.Callable[[~pandas.DataFrame], ~typing.Sequence[str]], models: ~pybroker.model.ModelSource | ~pybroker.model.IntervalBoundModel | ~typing.Iterable[~pybroker.model.ModelSource | ~pybroker.model.IntervalBoundModel] | None = None, indicators: ~pybroker.indicator.Indicator | ~pybroker.indicator.IntervalBoundIndicator | ~typing.Iterable[~pybroker.indicator.Indicator | ~pybroker.indicator.IntervalBoundIndicator] | None = None, hyperparams: ~typing.Iterable[~pybroker.optimize.Hyperparam] | None = None, intervals: int | ~typing.Literal['daily', 'weekly', 'monthly', 'quarterly', 'yearly'] | str | ~typing.Iterable[int | ~typing.Literal['daily', 'weekly', 'monthly', 'quarterly', 'yearly'] | str] | None = None, *args: ~typing.~P, **kwargs: ~typing.~P)[source]

Adds an execution to backtest.

A TimeframeInterval passed to intervals is one of the following:

  • Every-n-bars (int): Compress every n base bars into one bar, where n > 1. On 1-minute data, 5 yields 5-bar bins (approximately 5-minute bars).

  • Duration (str): Fixed time span as digits plus one unit letter — "1m", "5m", "1h", "30s", or "1d".

  • Calendar (str): Calendar buckets — "daily", "weekly", "monthly", "quarterly", or "yearly". Weeks start on Monday, months on the first of the month, quarters in January, April, July, and October, and years on January 1.

For example, to read 5-bar bins and 1-hour duration bars on a 1-minute feed, and additionally compute sma on weekly bars:

strategy.add_execution(
    fn,
    "SPY",
    indicators=sma.intervals("weekly"),
    intervals=[5, "1h"],
)
strategy.walkforward(windows=1, timeframe="1m")
Parameters:
  • fnCallable invoked on every bar of data during the backtest and passed an pybroker.context.ExecContext for each ticker symbol in symbols.

  • symbols – Ticker symbols used to run fn, where fn is called separately for each symbol. Can also be a pybroker.common.SymbolSelector — a Callable (df) -> Sequence[str] that picks the symbols to trade once per walkforward window, so the universe changes over the backtest. It receives the window’s training data, never test data, and therefore requires a training window: backtest() and train_size=0 raise ValueError. The candidate universe must be supplied as a pandas.DataFrame rather than a pybroker.data.DataSource, since the symbols to query are unknown until a window is split. A position in a symbol that a later window drops is closed at the first bar of that window; if the symbol has no bars left, it is closed at its final bar. Note that shuffle=True randomizes the training frame’s row order, so avoid it with a selector that depends on bar order.

  • modelsIterable of pybroker.model.ModelSources to train/load for backtesting. A model passed directly is trained on the base timeframe. Bind a model to compression intervals with pybroker.model.ModelSource.intervals() to instead train it on exactly the listed intervals’ compressed bars, together with the indicators registered on it; include the literal 'base' in the binding to also train on the base timeframe. Per-interval predictions are read with pybroker.context.IntervalContext.preds(). An interval-bound model honors the lookahead passed to backtest(), walkforward(), or optimize() in the interval’s units: lookahead compressed bars are held out between its train and test rows.

  • indicatorsIterable of pybroker.indicator.Indicators to compute for backtesting. An indicator passed directly is computed on the base timeframe. Bind an indicator to compression intervals with pybroker.indicator.Indicator.intervals() to instead compute it on exactly the listed intervals’ compressed bars, read with pybroker.context.IntervalContext.indicator(); include the literal 'base' in the binding to also compute it on the base timeframe.

  • hyperparamsIterable of pybroker.optimize.Hyperparams that fn can read with pybroker.context.ExecContext.hyperparam().

  • intervals – One or more compression intervals whose bars are made available to fn through pybroker.context.ExecContext.interval(). Declaring an interval here provides compressed bars only. It does not compute indicators or train models on that interval. Bind those per source with pybroker.indicator.Indicator.intervals() and pybroker.model.ModelSource.intervals() instead; bound intervals are automatically made available through interval() without repeating them here. Each interval must be strictly coarser than the base bar spacing passed as timeframe to backtest() or walkforward(); invalid combinations raise ValueError when the backtest runs. Intervals are scoped to this execution, so another execution’s pybroker.context.ExecContext cannot read them — including inside a set_before_exec() or set_after_exec() callback, which receives contexts from every execution.

  • args – Positional arguments passed to fn.

  • kwargs – Keyword arguments passed to fn.

backtest(start_date: str | datetime | None = None, end_date: str | datetime | None = None, timeframe: str = '', between_time: tuple[str, str] | None = None, days: str | Day | Iterable[str | Day] | None = None, lookahead: int = 1, train_size: float = 0, shuffle: bool = False, calc_bootstrap: bool = False, parallel_indicators: bool = False, parallel_models: bool = False, warmup: int | None = None, portfolio: Portfolio | None = None, adjust: Any | None = None, seed: int | None = 42, params: dict[str, Any] | None = None) TestResult[source]

Backtests the trading strategy by running executions that were added with add_execution().

Parameters:
  • start_date – Starting date of the backtest (inclusive). Must be within start_date and end_date range that was passed to Strategy.

  • end_date – Ending date of the backtest (inclusive). Must be within start_date and end_date range that was passed to Strategy.

  • timeframe

    Formatted string that specifies the timeframe resolution of the backtesting data. The timeframe string supports the following units:

    • "s"/"sec": seconds

    • "m"/"min": minutes

    • "h"/"hour": hours

    • "d"/"day": days

    • "w"/"week": weeks

    An example timeframe string is 1h 30m. Required when any execution declares intervals, since it defines the base bar spacing that compression intervals are validated and aligned against.

  • between_timetuple[str, str] of times of day e.g. (‘9:30’, ‘16:00’) used to filter the backtesting data (inclusive).

  • days – Days (e.g. "mon", "tues" etc.) used to filter the backtesting data.

  • lookahead – Number of bars in the future of the target prediction. For example, predicting returns for the next bar would have a lookahead of 1. This quantity is needed to prevent training data from leaking into the test boundary. It is expressed in the bars of the timeframe each model is fitted on: a model bound to an interval with pybroker.model.ModelSource.intervals() holds out lookahead bars of that interval, not of the base timeframe. No bar in a window’s test set is ever used to fit that window’s models.

  • train_size – Amount of pybroker.data.DataSource data to use for training, where the max train_size is 1. For example, a train_size of 0.9 would result in 90% of data being used for training and the remaining 10% of data being used for testing.

  • shuffle – Whether to randomly shuffle the data used for training. Defaults to False. Disabled when model caching is enabled via pybroker.cache.enable_model_cache().

  • calc_bootstrap – Whether to compute randomized bootstrap evaluation metrics. Defaults to False.

  • parallel_indicators – If True, pybroker.indicator.Indicator data is computed in parallel using multiple processes. Defaults to False.

  • parallel_models – If True, pybroker.model.ModelTrainer models are trained in parallel using multiple processes. Defaults to False.

  • warmup – Number of bars that need to pass before running the executions.

  • portfolio – Custom pybroker.portfolio.Portfolio to use for backtests.

  • adjust – The type of adjustment to make to the pybroker.data.DataSource.

  • seed – Random seed used for reproducibility. Defaults to 42.

Returns:

TestResult containing portfolio balances, order history, and evaluation metrics.

clear_executions()[source]

Clears executions that were added with add_execution().

enable_rotation(worst_rank_held: int | Hyperparam | None, sizer: Callable[[RotationContext], None] | None = None) None[source]

Enables rotational hold-band logic and optional custom sizing.

Each bar, held positions ranked worse than worst_rank_held are liquidated, and the top-ranked symbols are entered to fill the position slots that remain free. Without a sizer, entries are equal-weighted across set_max_long_positions() plus set_max_short_positions() slots.

Rotation is exclusive: trading is driven entirely by pybroker.context.ExecContext.long_score and pybroker.context.ExecContext.short_score, and orders placed by an Execution are ignored. Fill prices and stops set during an execution are kept and applied to the orders rotation places.

Ranking spans the whole portfolio, so a held position without a rankable score is liquidated even when another Execution opened it.

Parameters:
  • worst_rank_held – Worst score rank at which a held position is kept, a searchable pybroker.optimize.Hyperparam, or None to disable rotation. Must be greater than or equal to the maximum long and short position counts.

  • sizer – Optional Callable that takes a pybroker.context.RotationContext to override equal-weight entry sizing after rotation decisions are made. Do not override sell or cover signals set by rotation.

set_after_exec(fn: Callable[[Mapping[str, ExecContext]], None] | None)[source]

Callable[[Mapping[str, ExecContext]], None] that runs after all execution functions.

Parameters:

fnCallable that takes a Mapping of all ticker symbols to ExecContexts.

set_before_exec(fn: Callable[[Mapping[str, ExecContext]], None] | None)[source]

Callable[[Mapping[str, ExecContext]], None] that runs before all execution functions.

Parameters:

fnCallable that takes a Mapping of all ticker symbols to ExecContexts.

set_max_long_positions(max_long: int | Hyperparam | None) None[source]

Sets the maximum number of long positions held at any time.

Parameters:

max_long – Maximum long positions, a searchable pybroker.optimize.Hyperparam, or None for unlimited.

set_max_short_positions(max_short: int | Hyperparam | None) None[source]

Sets the maximum number of short positions held at any time.

Parameters:

max_short – Maximum short positions, a searchable pybroker.optimize.Hyperparam, or None for unlimited.

set_slippage_model(slippage_model: SlippageModel | None)[source]

Sets pybroker.slippage.SlippageModel.

Built-in models are pybroker.slippage.FixedSlippageModel (fixed basis points), pybroker.slippage.VolatilitySlippageModel (ATR-scaled), and pybroker.slippage.VolumeSlippageModel (participation cap and square-law price impact). Pass None to disable slippage.

Fill-time slippage applies to scheduled orders, stop exits, and position exits. Stop and position exits use the adjusted fill price only; share adjustments are ignored on those paths because they exit an entry in full.

walkforward(windows: int, lookahead: int = 1, start_date: str | datetime | None = None, end_date: str | datetime | None = None, timeframe: str = '', between_time: tuple[str, str] | None = None, days: str | Day | Iterable[str | Day] | None = None, train_size: float = 0.5, shuffle: bool = False, calc_bootstrap: bool = False, parallel_indicators: bool = False, parallel_models: bool = False, warmup: int | None = None, portfolio: Portfolio | None = None, adjust: Any | None = None, seed: int | None = 42, params: dict[str, Any] | None = None) TestResult[source]

Backtests the trading strategy using Walkforward Analysis. Backtesting data supplied by the pybroker.data.DataSource is divided into windows number of equal sized time windows, with each window split into train and test data as specified by train_size. The backtest “walks forward” in time through each window, running executions that were added with add_execution().

Parameters:
  • windows – Number of walkforward time windows.

  • start_date – Starting date of the Walkforward Analysis (inclusive). Must be within start_date and end_date range that was passed to Strategy.

  • end_date – Ending date of the Walkforward Analysis (inclusive). Must be within start_date and end_date range that was passed to Strategy.

  • timeframe

    Formatted string that specifies the timeframe resolution of the backtesting data. The timeframe string supports the following units:

    • "s"/"sec": seconds

    • "m"/"min": minutes

    • "h"/"hour": hours

    • "d"/"day": days

    • "w"/"week": weeks

    An example timeframe string is 1h 30m. Required when any execution declares intervals, since it defines the base bar spacing that compression intervals are validated and aligned against.

  • between_timetuple[str, str] of times of day e.g. (‘9:30’, ‘16:00’) used to filter the backtesting data (inclusive).

  • days – Days (e.g. "mon", "tues" etc.) used to filter the backtesting data.

  • lookahead – Number of bars in the future of the target prediction. For example, predicting returns for the next bar would have a lookahead of 1. This quantity is needed to prevent training data from leaking into the test boundary. It is expressed in the bars of the timeframe each model is fitted on: a model bound to an interval with pybroker.model.ModelSource.intervals() holds out lookahead bars of that interval, not of the base timeframe. No bar in a window’s test set is ever used to fit that window’s models.

  • train_size – Amount of pybroker.data.DataSource data to use for training, where the max train_size is 1. For example, a train_size of 0.9 would result in 90% of data being used for training and the remaining 10% of data being used for testing.

  • shuffle – Whether to randomly shuffle the data used for training. Defaults to False. Disabled when model caching is enabled via pybroker.cache.enable_model_cache().

  • calc_bootstrap – Whether to compute randomized bootstrap evaluation metrics. Defaults to False.

  • parallel_indicators – If True, pybroker.indicator.Indicator data is computed in parallel using multiple processes. Defaults to False.

  • parallel_models – If True, pybroker.model.ModelTrainer models are trained in parallel using multiple processes. Defaults to False.

  • warmup – Number of bars that need to pass before running the executions.

  • portfolio – Custom pybroker.portfolio.Portfolio to use for backtests.

  • adjust – The type of adjustment to make to the pybroker.data.DataSource.

  • seed – Random seed used for reproducibility. Defaults to 42.

Returns:

TestResult containing portfolio balances, order history, and evaluation metrics.

class TestResult(start_date: datetime, end_date: datetime, portfolio: DataFrame, positions: DataFrame, orders: DataFrame, trades: DataFrame, metrics: EvalMetrics, metrics_df: DataFrame, bootstrap: BootstrapResult | None, signals: dict[str, DataFrame] | None, stops: DataFrame | None, symbols: frozenset[str] = frozenset({}))[source]

Bases: object

Contains the results of backtesting a Strategy.

start_date

Starting date of backtest.

Type:

datetime.datetime

end_date

Ending date of backtest.

Type:

datetime.datetime

portfolio

pandas.DataFrame of pybroker.portfolio.Portfolio balances for every bar.

Type:

pandas.DataFrame

positions

pandas.DataFrame of pybroker.portfolio.Position balances for every bar.

Type:

pandas.DataFrame

orders

pandas.DataFrame of all orders that were placed.

Type:

pandas.DataFrame

trades

pandas.DataFrame of all trades that were made.

Type:

pandas.DataFrame

metrics

Evaluation metrics.

Type:

pybroker.eval.EvalMetrics

metrics_df

pandas.DataFrame of evaluation metrics.

Type:

pandas.DataFrame

bootstrap

Randomized bootstrap evaluation metrics.

Type:

pybroker.eval.BootstrapResult | None

signals

Dictionary of pandas.DataFrames containing bar data, indicator data, and model predictions for each symbol when pybroker.config.StrategyConfig.return_signals is True. Signals contain base-timeframe values only; an interval-bound indicator or model appears only when 'base' is included in its binding.

Type:

dict[str, pandas.DataFrame] | None

stops

pandas.DataFrame containing stop data per-bar when pybroker.config.StrategyConfig.return_stops is True.

Type:

pandas.DataFrame | None

symbols

Ticker symbols that were backtested.

Type:

frozenset[str]

to_json(*, include: frozenset[str] = frozenset({'bootstrap', 'metrics', 'orders', 'trades'}), max_rows: int | None = 100, symbols: frozenset[str] | None = None) dict[str, Any][source]

Returns JSON-serializable backtest results.

By default includes start_date, end_date, metrics, trades, orders, and bootstrap (when present). Large time series such as portfolio, positions, signals, and stops are opt-in via include. Dates serialize as naive UTC, NaN as null, and infinite metric values as the string sentinels "Infinity"/"-Infinity".

Parameters:
  • include – Names of optional result sections to include. Valid names are metrics, metrics_df, trades, orders, portfolio, positions, bootstrap, signals, and stops. Note that positions has rows only when pybroker.config.StrategyConfig.record_position_bars is True, and signals/stops only with their matching config flags.

  • max_rows – Maximum rows per tabular section. None for no limit.

  • symbols – When set, filter symbol-specific sections to these tickers. Must be a non-empty subset of symbols.

to_json_str(*, include: frozenset[str] = frozenset({'bootstrap', 'metrics', 'orders', 'trades'}), max_rows: int | None = 100, symbols: frozenset[str] | None = None) str[source]

Returns strict JSON text from to_json().

class WalkforwardMixin[source]

Bases: object

Mixin implementing logic for Walkforward Analysis.

walkforward_split(df: DataFrame, windows: int, lookahead: int, train_size: float = 0.9, shuffle: bool = False) Iterator[WalkforwardWindow][source]

Splits a pandas.DataFrame containing data for multiple ticker symbols into an Iterator of train/test time windows for Walkforward Analysis.

Parameters:
  • dfpandas.DataFrame of data to split into train/test windows for Walkforward Analysis.

  • windows – Number of walkforward time windows.

  • lookahead – Number of bars in the future of the target prediction. For example, predicting returns for the next bar would have a lookahead of 1. This quantity is needed to prevent training data from leaking into the test boundary. It is expressed in the bars of the timeframe each model is fitted on: a model bound to an interval with pybroker.model.ModelSource.intervals() holds out lookahead bars of that interval, not of the base timeframe. No bar in a window’s test set is ever used to fit that window’s models.

  • train_size – Amount of data in df to use for training, where the max train_size is 1. For example, a train_size of 0.9 would result in 90% of data in df being used for training and the remaining 10% of data being used for testing.

  • shuffle – Whether to randomly shuffle the data used for training. Defaults to False.

Returns:

Iterator of WalkforwardWindows containing train and test data.

class WalkforwardWindow(train_data: ndarray[tuple[Any, ...], dtype[int64]], test_data: ndarray[tuple[Any, ...], dtype[int64]])[source]

Bases: NamedTuple

Contains train/test row indices for a walkforward window.

train_data

Integer row indices into the master frame for training.

Type:

numpy.ndarray[tuple[Any, …], numpy.dtype[numpy.int64]]

test_data

Integer row indices into the master frame for testing.

Type:

numpy.ndarray[tuple[Any, …], numpy.dtype[numpy.int64]]