Interfaces#

Abstract interfaces and protocol classes that define the contracts implemented by backtesters and cost models. Use these as base classes when building custom implementations.

Backtester Interface#

Base backtest interface module.

This module provides the abstract base class for implementing backtesting strategies.

Key components: - BaseBacktesterInterface: Abstract base class for backtesters - BacktestResultDict: TypedDict for backtester output

class BacktestResultDict#

Bases: TypedDict

TypedDict for backtest result dictionaries.

Defines the structure of the dictionary returned by PyArrowBacktester.run() and related methods.

class BaseBacktesterInterface(backtest_config: BacktestConfiguration)#

Bases: object

Abstract base class for implementing backtesting strategies.

This class provides the fundamental structure and common methods for backtesting trading strategies. It handles configuration management and defines the interface for concrete implementations.

All concrete backtester implementations must inherit from this class and implement the abstract run() method.

Variables:
  • backtest_config (BacktestConfiguration) – Configuration object containing all backtest parameters. Accessed via the _backtest_config private attribute.

  • initial_capital (float) – Get the initial capital amount from configuration.

  • cash_reserve_pct (float) – Get the cash reserve percentage from configuration.

Examples

Creating a custom backtester:

>>> class MyBacktester(BaseBacktesterInterface):
...     def __init__(self, config: BacktestConfiguration):
...         super().__init__(backtest_config=config)
...         self._data = None
...
...     def run(self) -> BacktestResultDict:
...         # Implementation here
...         return {"results": ...}

Using the validation method:

>>> result = BaseBacktesterInterface.validate_cash_position(
...     cash=1000.0,
...     validation_date=pd.Timestamp("2024-01-15"),
...     logger=logger,
... )
>>> result.is_valid
True

Notes

This is an abstract base class and cannot be instantiated directly. Subclasses must implement the run() method.

The class uses metaclass=abc.ABCMeta to enforce the abstract method contract at class definition time.

See also

PyArrowBacktester

Concrete PyArrow-based implementation

BacktestConfiguration

Configuration dataclass

calculate_years(start_date: date | str, end_date: date | str) float#

Calculate duration in years using trading days.

Computes the number of years between two dates based on business days, using the configured trading days per year. This is used for annualizing performance metrics.

Parameters:
  • start_date (datetime.date | str) – Start date of the period. Can be datetime.date object or ISO format string (YYYY-MM-DD).

  • end_date (datetime.date | str) – End date of the period. Can be datetime.date object or ISO format string.

Returns:

Duration in years calculated as: business_days / trading_days_per_year Returns 1.0 if calculation fails (fallback).

Return type:

float

Examples

Using date objects:

>>> from datetime import date
>>> years = backtester.calculate_years(
...     start_date=date(2022, 1, 1),
...     end_date=date(2024, 1, 1),
... )
>>> years
2.0  # Approximately 2 years

Using string dates:

>>> years = backtester.calculate_years(
...     start_date="2022-01-01",
...     end_date="2022-12-31",
... )
>>> years
0.996  # Approximately 1 year

Notes

Uses numpy’s busday_count for accurate business day calculation. The trading_days_per_year default (252) is standard in US finance.

The method returns 1.0 as a fallback if date parsing fails, ensuring calculations don’t fail due to date format issues.

property cash_reserve_pct: float#

Get the cash reserve percentage.

Returns:

Percentage of total value to maintain as cash (0.0 to 1.0).

Return type:

float

Examples

>>> backtester.cash_reserve_pct
0.02
property initial_capital: float#

Get the initial capital amount.

Returns:

Starting capital for the backtest in currency units.

Return type:

float

Examples

>>> backtester.initial_capital
100000.0
abstractmethod run() BacktestResultDict#

Execute the backtesting strategy.

This abstract method must be implemented by subclasses to define the specific logic for running the backtest. It should contain the main loop that processes market data and makes trading decisions.

Returns:

Dictionary containing backtest results including: - Register_df: Time series of portfolio values - Total_commissions: Total commissions paid - final_total_portfolio_value: Final portfolio value - Other metrics and DataFrames

Return type:

BacktestResultDict

Raises:
  • NotImplementedError – If not implemented by a subclass.

  • RuntimeError – If backtest execution fails.

Examples

>>> results = backtester.run()
>>> results['final_total_portfolio_value']
125000.0
>>> results['Register_df'].head()
static validate_cash_position(cash: float, validation_date: DateLike, logger: Logger) CashValidationResult#

Validate that the cash position is non-negative.

This static method checks if the current cash position is valid (non-negative) and returns a structured result. Logs an error message if the cash position is negative.

Parameters:
  • cash (float) – The cash amount to validate. Can be a PyArrow scalar or regular float. PyArrow scalars are converted via .as_py() method.

  • validation_date (DateLike) – The date associated with this cash position. Used for error reporting and debugging. Can be datetime.date, pd.Timestamp, or ISO format string.

  • logger (logging.Logger) – Logger instance for recording error messages.

Returns:

Structured result containing: - is_valid: True if cash >= 0 - cash_amount: The validated amount as float - validation_date: The date provided - error_message: Error description or empty string

Return type:

CashValidationResult

Examples

Valid cash position:

>>> import logging
>>> logger = logging.getLogger('test')
>>> result = BaseBacktesterInterface.validate_cash_position(
...     cash=1000.0,
...     validation_date="2024-01-15",
...     logger=logger,
... )
>>> result.is_valid
True
>>> result.error_message
''

Invalid (negative) cash position:

>>> result = BaseBacktesterInterface.validate_cash_position(
...     cash=-500.0,
...     validation_date="2024-01-15",
...     logger=logger,
... )
>>> result.is_valid
False
>>> 'Cash error' in result.error_message
True

Notes

This method uses PyArrow’s compute functions for comparison, allowing it to work with both regular Python floats and PyArrow scalars.

A negative cash position typically indicates: - Insufficient initial capital for the strategy - Commission costs higher than expected - Problems with the trading logic - Strategy too aggressive for available capital

class PerformanceStatsDict#

Bases: TypedDict

TypedDict for portfolio/benchmark performance statistics.

All fields are optional (total=False) because not all metrics are computed in every context.

Cost Model Interfaces#

Cost model interfaces module.

This module defines abstract base classes for commission and slippage models used in backtesting. These interfaces establish contracts that all cost model implementations must follow.

The interfaces are designed for the backtesting context where: - Commission models calculate fees based on share count - Slippage models adjust execution prices based on market impact

Implementing custom cost models allows for realistic simulation of different broker fee structures and market conditions.

class CommissionModelInterface#

Bases: ABC

Abstract interface for commission calculation models.

This interface defines the contract for all commission models used in the backtesting engine. The default signature calculates commission based on share count, matching the original backtester behavior.

All implementations must be stateless with respect to calculation - the same inputs should always produce the same outputs.

Examples

Creating a custom commission model:

>>> class MyCommission(CommissionModelInterface):
...     def __init__(self, rate: float):
...         self._rate = rate
...
...     def calculate(self, shares: float) -> float:
...         return abs(shares) * self._rate

Using the model:

>>> model = MyCommission(rate=0.01)
>>> model.calculate(shares=100)
1.0

Notes

Implementations should always use the absolute value of shares since commission is charged regardless of buy/sell direction. The calculate() method should never raise exceptions for valid numeric inputs.

See also

PerShareCommission

Default per-share rate model

ZeroCommission

No-fee model for testing

PercentageCommission

Notional-based percentage model

abstractmethod calculate(shares: float) float#

Calculate commission for a trade.

This method computes the commission amount for a trade based on the number of shares. Implementations should use the absolute value of shares to ensure consistent results for buys and sells.

Parameters:

shares (float) – Number of shares being traded. Can be positive (buy) or negative (sell). The absolute value should be used for calculation.

Returns:

Commission amount in currency units. Must be non-negative (>= 0.0).

Return type:

float

Examples

>>> model.calculate(shares=100)
1.0
>>> model.calculate(shares=-100)  # Sell order
1.0
>>> model.calculate(shares=0)
0.0
class SlippageModelInterface#

Bases: ABC

Abstract interface for price slippage models.

Slippage represents the difference between expected and actual execution prices due to market impact, liquidity constraints, and other trading frictions.

All slippage models must work against the trader: - Buy orders execute at prices >= original - Sell orders execute at prices <= original

Examples

Creating a custom slippage model:

>>> class MySlippage(SlippageModelInterface):
...     def __init__(self, basis_points: float):
...         self._bp = basis_points / 10000
...
...     def apply(self, price: float, shares: float, is_buy: bool) -> float:
...         if is_buy:
...             return price * (1 + self._bp)
...         return price * (1 - self._bp)

Using the model:

>>> model = MySlippage(basis_points=10)
>>> model.apply(price=100.0, shares=1000, is_buy=True)
100.1  # 10bp higher for buy
>>> model.apply(price=100.0, shares=1000, is_buy=False)
99.9   # 10bp lower for sell

Notes

Slippage should always work against the trader to model realistic market conditions. The apply() method should never raise exceptions for valid numeric inputs.

Volume-dependent models can use the shares parameter to scale slippage with order size (larger orders have more market impact).

See also

NoSlippage

Zero slippage for baseline testing

FixedSlippage

Constant percentage slippage

VolumeBasedSlippage

Order-size dependent slippage

SquareRootSlippage

Academic market impact model

abstractmethod apply(price: float, shares: float, is_buy: bool) float#

Apply slippage to an execution price.

Adjusts the quoted price to simulate realistic execution conditions where trades don’t fill at the exact quoted price.

Parameters:
  • price (float) – Original quoted price before slippage. Must be positive.

  • shares (float) – Number of shares in the order. Used by volume-dependent slippage models. The absolute value should be used for calculation.

  • is_buy (bool) – True for buy orders, False for sell orders. Determines direction of price adjustment: - True: price increases (unfavorable for buyer) - False: price decreases (unfavorable for seller)

Returns:

Adjusted execution price with slippage applied. For buys: result >= price For sells: result <= price

Return type:

float

Examples

>>> model.apply(price=100.0, shares=1000, is_buy=True)
100.1  # Price increased for buy
>>> model.apply(price=100.0, shares=1000, is_buy=False)
99.9   # Price decreased for sell