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:
TypedDictTypedDict for backtest result dictionaries.
Defines the structure of the dictionary returned by PyArrowBacktester.run() and related methods.
- class BaseBacktesterInterface(backtest_config: BacktestConfiguration)#
Bases:
objectAbstract 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
PyArrowBacktesterConcrete PyArrow-based implementation
BacktestConfigurationConfiguration 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:
- 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, *, equity: float | None = None) CashValidationResult#
Validate that the portfolio is still solvent.
Which invariant that means depends on whether short positions are open, and the caller decides by passing
equityor not:Long-only (
equity is None): the check iscash >= 0. Negative cash means the portfolio bought more than it could pay for, which is a real defect worth stopping on.With shorts (
equityprovided): the check isequity > 0. Once shorts are open,cash >= 0stops being meaningful — short-sale proceeds inflate cash, so a book can hold cash worth 119% of its own value while being one adverse move from insolvency. Solvency lives in equity, not liquidity.
- 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.
equity (float | None, optional) – Net liquidation value (holdings plus cash). Pass this only when short positions are open, which switches the check from liquidity to solvency. Default is None, preserving the original long-only behaviour exactly.
- Returns:
Structured result containing: - is_valid: True if the applicable invariant holds - 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
Non-positive equity on a long-short book typically indicates: - Short positions that moved against the strategy - Gross exposure too high for the capital base
- class PerformanceStatsDict#
Bases:
TypedDictTypedDict 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 from a trade context (shares + price) - 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:
ABCAbstract interface for commission calculation models.
This interface defines the contract for all commission models used in the backtesting engine. Models receive a
CommissionContextcarrying the trade’s shares and price, so percentage/basis-point models can price against the real notional while per-share models simply read the share count.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, context: CommissionContext) -> float: ... return abs(context.shares) * self._rate
Using the model:
>>> from kaxanuk.backtest_engine.entities.commission_context import CommissionContext >>> model = MyCommission(rate=0.01) >>> model.calculate(CommissionContext(shares=100, price=50.0)) 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
PerShareCommissionDefault per-share rate model
ZeroCommissionNo-fee model for testing
PercentageCommissionNotional-based percentage model
- abstractmethod calculate(context: CommissionContext) float#
Calculate commission for a trade.
Computes the commission amount for a single trade from its context. Implementations should use the absolute value of
context.sharesto ensure consistent results for buys and sells.- Parameters:
context (CommissionContext) – Trade context carrying
shares(signed; absolute value used) andprice(per-unit commission basis). Percentage/basis-point models multiply shares by price; per-share/fixed models ignoreprice.- Returns:
Commission amount in currency units. Must be non-negative (>= 0.0).
- Return type:
float
Examples
>>> from kaxanuk.backtest_engine.entities.commission_context import CommissionContext >>> model.calculate(CommissionContext(shares=100, price=50.0)) 1.0 >>> model.calculate(CommissionContext(shares=-100, price=50.0)) # Sell 1.0 >>> model.calculate(CommissionContext(shares=0, price=50.0)) 0.0
- class FinancingModelInterface#
Bases:
ABCAbstract interface for daily financing models.
Financing is the cost of holding positions: the borrow fee on shorts, the interest on a margin loan, the interest earned on cash. Unlike commissions and slippage it accrues every trading day, so both engines call
accrue()once per day with the balances being financed and the calendar days until the next trading day, and deduct the returned charge from cash.Implementations must be stateless with respect to calculation: the same context must always produce the same charge, and the arithmetic must live inside
accrueso the scalar and the vectorized engine, which both call it, stay bit-identical.Examples
Creating a flat borrow-fee model:
>>> class FlatBorrow(FinancingModelInterface): ... def __init__(self, annual_rate: float): ... self._rate = annual_rate ... ... def accrue(self, context: FinancingContext) -> FinancingCharge: ... return FinancingCharge( ... borrow_fee=context.short_market_value * self._rate * context.days / 360 ... )
See also
ZeroFinancingDefault model that charges nothing.
BenchmarkSpreadFinancingBenchmark-plus-spread model built from FinancingTerms.
- abstractmethod accrue(context: FinancingContext) FinancingCharge#
Compute the financing charge for one trading day.
- Parameters:
context (FinancingContext) – Short market value (and, when available, the short positions with their own borrow fees), signed cash balance, the date and the calendar days they are financed for.
- Returns:
Charges and credits for the day.
totalis deducted from cash by the engine.- Return type:
FinancingCharge
- benchmark_rate_on(date: object) float | None#
The annual benchmark (risk-free) rate this model uses on
date.The engine averages it over the trading days of the backtest and reports it as the risk-free rate of the Sharpe ratio. The default is
None: the model has no notion of a benchmark and the Sharpe ratio uses zero.- Parameters:
date (object) – Date-like value (a
pandas.Timestampordatetime.date).- Returns:
Annual rate as a fraction, or
None.- Return type:
float | None
- is_active() bool#
Whether this model can ever charge or credit anything.
The engines skip the daily accrual entirely when this is
False, which keeps a backtest without financing free of any extra floating-point operation on cash. The default isTrue; models that are provably a no-op should override it.
- class SlippageModelInterface#
Bases:
ABCAbstract 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).
applyis the only abstract method, and the broker never calls it directly: it callsapply_to()with aSlippageContext, which by default forwards toapply. A model that needs the ticker, the date or the day’s market data overridesapply_toand keepsapplyas its context-free fallback.See also
NoSlippageZero slippage for baseline testing
FixedSlippageConstant percentage slippage
VolumeBasedSlippageOrder-size dependent slippage
SquareRootSlippageAcademic 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
- apply_to(context: SlippageContext) float#
Apply slippage to the order described by
context.This is the entry point the execution broker calls. The default implementation forwards to
apply(), so a model written against the three-argument interface keeps working untouched; models that read market data (the ticker’s spread, volume or volatility on the day) override this method instead.- Parameters:
context (SlippageContext) – The order, plus the ticker’s average daily volume, volatility and half spread when the market data provides them.
- Returns:
Adjusted execution price, on the same terms as
apply(): at or abovecontext.pricefor a buy, at or below it for a sell.- Return type:
float
Examples
>>> model.apply_to(SlippageContext( ... ticker='AAPL', ... date=None, ... price=100.0, ... shares=1000.0, ... is_buy=True, ... )) 100.1 # 10bp higher for buy