Cost Models#

Pluggable commission, slippage and trading-record models used by the backtester to simulate execution costs and capture intermediate trading state.

Commission Models#

Commission model implementations module.

This module provides concrete implementations of CommissionModelInterface for various commission structures. The default PerShareCommission matches the original backtester behavior.

Available models: - PerShareCommission: Fixed rate per share (default) - ZeroCommission: No commission for testing - PercentageCommission: Percentage of notional value - FixedCommission: Fixed amount per trade - TieredCommission: Volume-based tiered rates

All commission models are immutable after construction, ensuring consistent behavior throughout a backtest.

class FixedCommission(amount: float)#

Bases: CommissionModelInterface

Fixed commission amount per trade regardless of size.

Charges the same flat fee for every trade, independent of the number of shares traded. Common with discount brokers that charge a flat rate per trade.

Variables:

amount (float) – Fixed commission per trade (read-only).

Examples

>>> model = FixedCommission(amount=4.95)
>>> model.calculate(shares=1)
4.95
>>> model.calculate(shares=10000)
4.95
property amount: float#

Get the fixed commission amount.

calculate(context: CommissionContext) → float#

Return fixed commission amount.

Returns the same fixed amount regardless of the trade context. This is typical of flat-fee broker pricing.

Parameters:

context (CommissionContext) – Trade context. Ignored since commission is fixed.

Returns:

Fixed commission amount in currency units.

Return type:

float

Examples

>>> from kaxanuk.backtest_engine.entities.commission_context import CommissionContext
>>> model = FixedCommission(amount=4.95)
>>> model.calculate(CommissionContext(shares=1, price=50.0))
4.95
>>> model.calculate(CommissionContext(shares=10000, price=50.0))
4.95
class PerShareCommission(rate: float)#

Bases: CommissionModelInterface

Commission as a fixed rate per share traded.

This is the default commission model matching the original backtester behavior where commission = abs(shares) * rate.

Variables:

rate (float) – Commission rate per share (read-only). For example, 0.01 means $0.01 per share.

Examples

Basic usage:

>>> model = PerShareCommission(rate=0.01)
>>> model.calculate(shares=100)
1.0
>>> model.calculate(shares=-100)  # Sell order
1.0

Zero commission rate:

>>> model = PerShareCommission(rate=0.0)
>>> model.calculate(shares=1000)
0.0

Notes

This matches the original _calculate_commission method: return abs(shares) * self.commission_pct_float

See also

ZeroCommission

Convenience class for zero commission

calculate(context: CommissionContext) → float#

Calculate the commission for a trade based on share count.

Computes abs(context.shares) * rate and rounds the result to two decimal places (cents). The absolute value is used because commission is charged on both buys and sells; the sign of shares only carries information about trade direction, which is irrelevant to fee computation. context.price is ignored by this per-share model.

Parameters:

context (CommissionContext) – Trade context. Only context.shares is used (positive for buys, negative for sells; zero yields zero commission). Fractional values are supported (e.g. fractional-share brokers).

Returns:

Commission amount in currency units, rounded to two decimal places. Always non-negative.

Return type:

float

Examples

Buy and sell of the same size produce the same fee:

>>> from kaxanuk.backtest_engine.entities.commission_context import CommissionContext
>>> model = PerShareCommission(rate=0.01)
>>> model.calculate(CommissionContext(shares=100, price=50.0))
1.0
>>> model.calculate(CommissionContext(shares=-100, price=50.0))
1.0

Zero shares is a no-op:

>>> model.calculate(CommissionContext(shares=0, price=50.0))
0.0

Notes

Rounding is applied at the end with two decimal precision (round(value, 2)), matching the original backtester to the cent. Callers that need higher precision should bypass this model and reimplement the multiplication.

See also

CommissionModelInterface.calculate

Abstract contract.

PercentageCommission

Notional-based commission.

ZeroCommission

No-fee model used as a baseline.

property rate: float#

Get the commission rate per share.

Returns:

Commission rate in currency units per share.

Return type:

float

class PercentageCommission(pct: float)#

Bases: CommissionModelInterface

Commission as a percentage of traded notional value.

Calculates commission as abs(shares) x price x pct using the actual per-unit price carried by the CommissionContext. This is the natural model for crypto and other maker/taker fee schedules expressed in basis points.

Variables:

pct (float) – Commission percentage as decimal (read-only). For example, 0.001 for 0.1% (10 basis points).

Examples

>>> from kaxanuk.backtest_engine.entities.commission_context import CommissionContext
>>> model = PercentageCommission(pct=0.001)  # 10 bps
>>> model.calculate(CommissionContext(shares=1, price=60_000.0))
60.0  # 1 unit x $60,000 x 0.001
calculate(context: CommissionContext) → float#

Calculate commission as a percentage of traded notional.

Computes commission as abs(context.shares) x context.price x pct and rounds to two decimal places.

Parameters:

context (CommissionContext) – Trade context. Uses shares (absolute value) and price to form the traded notional.

Returns:

Commission amount in currency units. Always non-negative.

Return type:

float

Examples

>>> from kaxanuk.backtest_engine.entities.commission_context import CommissionContext
>>> model = PercentageCommission(pct=0.001)
>>> model.calculate(CommissionContext(shares=1, price=60_000.0))
60.0
>>> model.calculate(CommissionContext(shares=-1, price=60_000.0))
60.0
property pct: float#

Get the commission percentage.

class TieredCommission(tiers: tuple[tuple[float, float], ...], minimum: float = 0.0)#

Bases: CommissionModelInterface

Commission with volume-based tiered per-share rates.

Applies different per-share rates based on the trade size. Larger trades typically receive better (lower) rates. A minimum commission can be specified.

Variables:
  • tiers (tuple[tuple[float, float], ...]) – Tuple of (share_threshold, rate) tuples (read-only). Each tier specifies the rate that applies when shares >= threshold.

  • minimum (float) – Minimum commission per trade (read-only). Default is 0.0.

Examples

>>> tiers = ((0, 0.01), (1000, 0.008), (5000, 0.005))
>>> model = TieredCommission(tiers=tiers, minimum=1.0)
>>> model.calculate(shares=500)    # Uses 0.01 rate
5.0
>>> model.calculate(shares=2000)   # Uses 0.008 rate
16.0
>>> model.calculate(shares=10000)  # Uses 0.005 rate
50.0

Notes

The tier with the highest threshold that is <= shares is used. If the calculated commission is less than minimum, minimum is returned.

calculate(context: CommissionContext) → float#

Calculate commission using applicable tier rate.

Determines the appropriate rate based on share count, then calculates commission as abs(shares) x rate. Returns at least the minimum commission if specified. context.price is ignored by this share-count-based model.

Parameters:

context (CommissionContext) – Trade context. Uses context.shares (absolute value) for tier selection and calculation.

Returns:

Commission amount in currency units. At least the minimum commission if specified.

Return type:

float

Examples

>>> from kaxanuk.backtest_engine.entities.commission_context import CommissionContext
>>> tiers = ((0, 0.01), (1000, 0.008), (5000, 0.005))
>>> model = TieredCommission(tiers=tiers, minimum=1.0)
>>> model.calculate(CommissionContext(shares=500, price=50.0))
5.0  # 500 x 0.01 = 5.0
>>> model.calculate(CommissionContext(shares=50, price=50.0))
1.0  # 50 x 0.01 = 0.5, but minimum is 1.0
>>> model.calculate(CommissionContext(shares=2000, price=50.0))
16.0  # 2000 x 0.008 = 16.0
property minimum: float#

Get the minimum commission amount.

property tiers: tuple[tuple[float, float], ...]#

Get the commission tiers.

class ZeroCommission#

Bases: CommissionModelInterface

Commission model that charges no fees.

Useful for backtesting scenarios where transaction costs should be ignored, such as: - Testing strategy logic without cost friction - Simulating zero-commission brokers - Baseline performance comparison

Examples

>>> model = ZeroCommission()
>>> model.calculate(shares=1000)
0.0
>>> model.calculate(shares=1_000_000)
0.0

Notes

This is equivalent to PerShareCommission(rate=0.0) but provides clearer intent in code.

calculate(context: CommissionContext) → float#

Return zero commission for any trade.

Always returns 0.0 regardless of the trade context. This model is useful for backtesting without transaction costs.

Parameters:

context (CommissionContext) – Trade context. Ignored since no commission is charged.

Returns:

Always returns 0.0.

Return type:

float

Examples

>>> from kaxanuk.backtest_engine.entities.commission_context import CommissionContext
>>> model = ZeroCommission()
>>> model.calculate(CommissionContext(shares=100, price=50.0))
0.0

Slippage Models#

Slippage model implementations module.

This module provides concrete implementations of SlippageModelInterface for various slippage scenarios. NoSlippage is the default, maintaining backward compatibility with the original backtester.

Available models: - NoSlippage: Zero slippage (default) - FixedSlippage: Constant percentage - BasisPointSlippage: Slippage in basis points - VolumeBasedSlippage: Order-size dependent - SquareRootSlippage: Academic market impact model - RandomSlippage: Stochastic slippage for testing

All slippage models are immutable after construction (except RandomSlippage which maintains internal RNG state for reproducibility).

class BasisPointSlippage(basis_points: float)#

Bases: SlippageModelInterface

Slippage specified in basis points.

Convenience class that accepts slippage in basis points (bp) rather than decimal percentage.

Variables:

basis_points (float) – Slippage in basis points (read-only). For example, 10 for 10bp = 0.1%.

Examples

>>> model = BasisPointSlippage(basis_points=10)
>>> model.apply(price=100.0, shares=100, is_buy=True)
100.1  # 10bp = 0.1% increase

Notes

1 basis point = 0.01% = 0.0001 in decimal form.

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

Apply basis point slippage.

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

  • shares (float) – Number of shares in the order. Ignored.

  • is_buy (bool) – True for buy orders, False for sell orders.

Returns:

Adjusted execution price with slippage applied.

Return type:

float

property basis_points: float#

Get the slippage in basis points.

class FixedSlippage(slippage_pct: float)#

Bases: SlippageModelInterface

Fixed percentage slippage regardless of order size.

Applies a constant percentage adjustment to the price. Buys execute above the quoted price (unfavorable). Sells execute below the quoted price (unfavorable).

Variables:

slippage_pct (float) – Slippage as decimal (read-only). For example, 0.001 for 0.1% or 10 basis points.

Examples

>>> model = FixedSlippage(slippage_pct=0.001)
>>> model.apply(price=100.0, shares=100, is_buy=True)
100.1  # Price increased by 0.1%
>>> model.apply(price=100.0, shares=100, is_buy=False)
99.9   # Price decreased by 0.1%
apply(price: float, shares: float, is_buy: bool) → float#

Apply fixed percentage slippage.

Adjusts the price by a fixed percentage in the direction unfavorable to the trader.

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

  • shares (float) – Number of shares in the order. This parameter is ignored since slippage is fixed.

  • is_buy (bool) – True for buy orders (price increases). False for sell orders (price decreases).

Returns:

Adjusted execution price with slippage applied.

Return type:

float

Examples

>>> model = FixedSlippage(slippage_pct=0.001)
>>> model.apply(price=100.0, shares=100, is_buy=True)
100.1
>>> model.apply(price=100.0, shares=100, is_buy=False)
99.9
property slippage_pct: float#

Get the slippage percentage.

class HalfSpreadSlippage(default_half_spread: float = 0.0)#

Bases: _MarketDataSlippage

Half the bid-ask spread, the cost of crossing it.

A buy lifts the ask and a sell hits the bid, so one side of a round trip pays half the quoted spread against the midpoint. This is a real cost even when the execution price is a daily VWAP, since a VWAP is an average of prints that themselves crossed the spread.

The half spread comes from the trade context, which the pipeline fills from a mapped spread column or from an estimate off daily bars. When a ticker has none on a day, the configured constant applies and the fallback is logged once for that ticker.

Variables:

default_half_spread (float) – Half spread as a fraction of the price (0.0005 for 5 basis points), used for a ticker and day the market data has none.

Examples

>>> model = HalfSpreadSlippage(default_half_spread=0.0005)
>>> model.apply(price=100.0, shares=1000, is_buy=True)
100.05
>>> model.apply(price=100.0, shares=1000, is_buy=False)
99.95

Notes

The cost does not depend on order size: crossing the spread costs the same fraction whether the order is one share or ten thousand. Size is what market impact prices, which is a separate term; see SpreadPlusImpactSlippage for the two together.

See also

SpreadPlusImpactSlippage

Half spread plus square-root market impact.

BasisPointSlippage

The same shape with a single rate for every ticker.

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

Apply the fallback half spread, with no ticker to look one up for.

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

  • shares (float) – Number of shares in the order. Ignored: crossing the spread costs the same fraction at any size.

  • is_buy (bool) – True for buy orders, False for sell orders.

Returns:

Adjusted execution price.

Return type:

float

apply_to(context: SlippageContext) → float#

Apply the ticker’s own half spread on the day.

Parameters:

context (SlippageContext) – The order, with the ticker’s half spread when the market data provides one.

Returns:

Adjusted execution price.

Return type:

float

property default_half_spread: float#

Get the fallback half spread.

class NoSlippage#

Bases: SlippageModelInterface

Slippage model with no price impact.

Returns the original price unchanged. This is the default behavior matching the original backtester where slippage was not modeled.

This model is useful for: - Baseline backtesting without market impact - Highly liquid markets where slippage is negligible - Comparing strategy performance with/without slippage

Examples

>>> model = NoSlippage()
>>> model.apply(price=100.0, shares=1000, is_buy=True)
100.0
>>> model.apply(price=100.0, shares=1000, is_buy=False)
100.0

Notes

This is the default slippage model used when none is specified.

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

Return price unchanged.

This method returns the input price without any modification, simulating a market with zero slippage or price impact.

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

  • shares (float) – Number of shares in the order. This parameter is ignored since no slippage is applied.

  • is_buy (bool) – True for buy orders, False for sell orders. This parameter is ignored since no slippage is applied.

Returns:

The original price, unchanged.

Return type:

float

Examples

>>> model = NoSlippage()
>>> model.apply(price=150.25, shares=1000, is_buy=True)
150.25
>>> model.apply(price=150.25, shares=1000, is_buy=False)
150.25
class RandomSlippage(min_slippage: float = 0.0, max_slippage: float = 0.001, seed: int | None = None)#

Bases: SlippageModelInterface

Random slippage within specified bounds.

Generates random slippage uniformly distributed between minimum and maximum percentages.

Variables:
  • min_slippage (float) – Minimum slippage percentage. Default is 0.0.

  • max_slippage (float) – Maximum slippage percentage. Default is 0.001.

  • seed (int | None) – Random seed for reproducibility. Default is None.

Examples

>>> model = RandomSlippage(min_slippage=0.0, max_slippage=0.001, seed=42)
>>> model.apply(price=100.0, shares=100, is_buy=True)
100.06...  # Random slippage between 0% and 0.1%

Notes

Using a seed ensures reproducible results across runs.

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

Apply random slippage within bounds.

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

  • shares (float) – Number of shares in the order. Ignored.

  • is_buy (bool) – True for buy orders, False for sell orders.

Returns:

Adjusted execution price with random slippage applied.

Return type:

float

property max_slippage: float#

Get the maximum slippage percentage.

property min_slippage: float#

Get the minimum slippage percentage.

property seed: int | None#

Get the random seed.

class SpreadPlusImpactSlippage(default_half_spread: float = 0.0, impact_coefficient: float = 0.1, volatility: float = 0.02, average_daily_volume: float = 1000000, max_participation: float = 1.0)#

Bases: _MarketDataSlippage

Half the bid-ask spread plus square-root market impact.

The standard decomposition of execution cost outside commission: every order pays half the spread for crossing it, and an order large relative to the day’s volume also moves the price against itself. The two are added and applied to the execution price:

cost = half_spread + impact_coefficient * volatility * sqrt(participation)

Each input comes from the trade context when the market data provides it, and from the configured constant otherwise, logged once per ticker. The spread term is flat in order size and the impact term grows with its square root, which is what the empirical literature finds.

Variables:
  • default_half_spread (float) – Half spread as a fraction of the price, for a ticker and day without one.

  • impact_coefficient (float) – Scaling factor of the impact term. Values around 0.1 to 1 appear in the literature. Default 0.1.

  • volatility (float) – Daily volatility as a decimal, for a ticker without one. Default 0.02.

  • average_daily_volume (float) – Reference daily volume, for a ticker without one. Default 1,000,000.

  • max_participation (float) – Largest share of daily volume an order is priced as taking. Default 1.0.

Examples

>>> model = SpreadPlusImpactSlippage(
...     default_half_spread=0.0005,
...     impact_coefficient=0.1,
... )
>>> model.apply(price=100.0, shares=10_000, is_buy=True)
100.07  # 5bp of spread plus 2bp of impact

References

Almgren, R., Thum, C., Hauptmann, E. and Li, H. (2005). “Direct Estimation of Equity Market Impact”. Risk.

Kissell, R. (2013). The Science of Algorithmic Trading and Portfolio Management.

See also

HalfSpreadSlippage

The spread term on its own.

SquareRootSlippage

The impact term on its own.

Notes

Do not combine this with an execution price that already carries a cost, such as a vendor VWAP the provider has already penalised: the spread would be paid twice.

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

Apply spread plus impact using only the configured constants.

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

  • shares (float) – Number of shares in the order.

  • is_buy (bool) – True for buy orders, False for sell orders.

Returns:

Adjusted execution price.

Return type:

float

apply_to(context: SlippageContext) → float#

Apply spread plus impact using the ticker’s own market data.

Parameters:

context (SlippageContext) – The order, with the ticker’s half spread, volatility and average daily volume when the market data provides them.

Returns:

Adjusted execution price. Identical to apply() when the context carries none of the three.

Return type:

float

Notes

The three inputs fall back independently. A ticker whose file has highs and lows but no volume gets a real spread and a constant impact, which is better than discarding the spread too.

property average_daily_volume: float#

Get the fallback average daily volume.

property default_half_spread: float#

Get the fallback half spread.

property impact_coefficient: float#

Get the impact coefficient.

property volatility: float#

Get the fallback volatility.

class SquareRootSlippage(impact_coefficient: float = 0.1, volatility: float = 0.02, average_daily_volume: float = 1000000, max_participation: float = 1.0)#

Bases: _MarketDataSlippage

Square root market impact model.

Implements the widely-used square root law of market impact, where price impact scales with the square root of order size.

Variables:
  • impact_coefficient (float) – Scaling factor for impact. Default is 0.1.

  • volatility (float) – Daily volatility as decimal. Default is 0.02. Used only for a ticker and day the market data has no volatility for.

  • average_daily_volume (float) – Reference daily volume. Default is 1,000,000. Used only for a ticker and day the market data has no volume for; the fallback is logged once per ticker.

  • max_participation (float) – Largest share of daily volume an order is priced as taking. Default is 1.0.

Examples

>>> model = SquareRootSlippage(
...     impact_coefficient=0.1,
...     volatility=0.02,
...     average_daily_volume=1_000_000,
... )
>>> model.apply(price=100.0, shares=10_000, is_buy=True)
100.02  # sqrt(10000/1000000) * 0.1 * 0.02 impact

References

Almgren, R., et al. (2005). “Direct Estimation of Equity Market Impact”

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

Apply square root market impact.

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

  • shares (float) – Number of shares in the order.

  • is_buy (bool) – True for buy orders, False for sell orders.

Returns:

Adjusted execution price with market impact applied.

Return type:

float

apply_to(context: SlippageContext) → float#

Apply square root impact using the ticker’s own volume and volatility.

Parameters:

context (SlippageContext) – The order, with the ticker’s average daily volume and volatility when the market data provides them.

Returns:

Adjusted execution price. Identical to apply() when the context carries neither, since the same constants are used.

Return type:

float

Notes

The two inputs fall back independently: a ticker can have a volatility from its price series and no volume, which is what happens when its file simply has no volume column.

property average_daily_volume: float#

Get the average daily volume.

property impact_coefficient: float#

Get the impact coefficient.

property volatility: float#

Get the volatility.

class VolumeBasedSlippage(base_slippage: float = 0.0005, volume_impact: float = 0.01, average_daily_volume: float = 1000000, max_participation: float = 1.0)#

Bases: _MarketDataSlippage

Volume-proportional slippage model.

Slippage increases linearly with order size relative to average daily volume (ADV).

Variables:
  • base_slippage (float) – Minimum slippage percentage. Default is 0.0005 (5bp).

  • volume_impact (float) – Additional slippage per unit of volume ratio. Default is 0.01.

  • average_daily_volume (float) – Reference volume for normalizing order size. Default is 1,000,000. Used only for a ticker and day the market data has no volume for; otherwise the ticker’s own average daily volume is used and the fallback is logged once per ticker.

  • max_participation (float) – Largest share of daily volume an order is priced as taking. Default is 1.0, i.e. the whole day’s volume.

Examples

>>> model = VolumeBasedSlippage(
...     base_slippage=0.0005,
...     volume_impact=0.01,
...     average_daily_volume=1_000_000,
... )
>>> model.apply(price=100.0, shares=100_000, is_buy=True)
100.15  # 0.05% base + 0.1% volume impact
apply(price: float, shares: float, is_buy: bool) → float#

Apply volume-proportional slippage.

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

  • shares (float) – Number of shares in the order.

  • is_buy (bool) – True for buy orders, False for sell orders.

Returns:

Adjusted execution price with slippage applied.

Return type:

float

apply_to(context: SlippageContext) → float#

Apply volume-proportional slippage using the ticker’s own volume.

Parameters:

context (SlippageContext) – The order, with the ticker’s average daily volume when the market data provides one.

Returns:

Adjusted execution price. Identical to apply() when the context carries no volume, since the same constant is used.

Return type:

float

property average_daily_volume: float#

Get the average daily volume reference.

property base_slippage: float#

Get the base slippage percentage.

property volume_impact: float#

Get the volume impact factor.

Trading Records#

Trading data structures module.

This module defines data classes for representing trade orders, rebalancing results, and portfolio records. These are entities (pure data containers) with minimal derived properties.

The module provides: - PriceData: Container for execution and commission prices - TradeOrder: Immutable representation of executed trade - RebalanceResult: Aggregate result of rebalancing operation - DailyRecord: Daily portfolio snapshot - Record types for commission, shares, and orders

Type aliases for portfolio structures are also defined here to ensure consistent typing across the codebase.

class CommissionRecord(date: Timestamp, ticker: str, commission: float)#

Bases: object

Record of commission payment for a single ticker.

Used for tracking and reporting commission breakdown by ticker and date.

Variables:
  • date (pd.Timestamp) – Date of the commission payment.

  • ticker (str) – Ticker symbol associated with the commission.

  • commission (float) – Commission amount in currency units.

Examples

>>> record = CommissionRecord(
...     date=pd.Timestamp('2024-01-15'),
...     ticker='AAPL',
...     commission=1.50,
... )
class DailyRecord(date: Timestamp, portfolio_value: float, total_value: float, cash: float, weights: dict[str, float]=<factory>, financing_cost: float = 0.0)#

Bases: object

Daily portfolio status record.

Immutable snapshot of portfolio state at end of trading day. Created for each day in the backtest period.

Variables:
  • date (pd.Timestamp) – Date of the record. Normalized to midnight (00:00:00).

  • portfolio_value (float) – Value of holdings (excluding cash). Sum of (shares x price) for all positions.

  • total_value (float) – Holdings plus cash position. This is the total portfolio value.

  • cash (float) – Cash position in currency units. Can be negative if validation is disabled.

  • weights (dict[str, float]) – Portfolio weights by ticker. Includes ‘CASH_RESERVE’ key for cash weight. Weights should sum to approximately 1.0.

  • financing_cost (float) – Net financing deducted from cash on this day (borrow fee plus margin interest minus interest earned on cash). 0.0 when no financing model is active.

Examples

Creating a daily record:

>>> record = DailyRecord(
...     date=pd.Timestamp('2024-01-15'),
...     portfolio_value=95000.0,
...     total_value=100000.0,
...     cash=5000.0,
...     weights={'AAPL': 0.50, 'MSFT': 0.45, 'CASH_RESERVE': 0.05},
... )
>>> record.total_value
100000.0

Calculating return:

>>> daily_return = (today.total_value / yesterday.total_value) - 1

Notes

The weights dictionary always includes a ‘CASH_RESERVE’ key representing the cash portion of the portfolio. Ticker weights are calculated as (shares x price) / total_value.

class FinancingRecord(date: Timestamp, days: int, borrow_fee: float, margin_interest: float, cash_interest: float, total: float)#

Bases: object

One trading day’s financing, split by concept.

Variables:
  • date (pd.Timestamp) – Day the charge was booked.

  • days (int) – Calendar days financed (until the next trading day).

  • borrow_fee (float) – Fee charged on short positions.

  • margin_interest (float) – Interest charged on a negative cash balance.

  • cash_interest (float) – Interest earned on a positive cash balance (a credit).

  • total (float) – Net amount deducted from cash, borrow_fee + margin_interest - cash_interest, as computed by the model.

class ForcedExitRecord(date: Timestamp, ticker: str, reason: str, last_valid_date: date, shares: float, price: float, value: float, commission: float)#

Bases: object

One position closed by the delisting policy.

Variables:
  • date (pd.Timestamp) – Trading day the exit was executed on.

  • ticker (str) – Ticker of the closed position.

  • reason (str) – "sell_at_last_price" or "mark_to_zero".

  • last_valid_date (datetime.date) – Last date on which the ticker had a price.

  • shares (float) – Position closed, signed: negative for a short that was bought back or written off.

  • price (float) – Execution price of the sale (after slippage), or the last mark-to-market price for a write-off.

  • value (float) – Proceeds of the sale (before commission), or the value written off. Negative for a short.

  • commission (float) – Commission paid on the sale; 0.0 for a write-off.

class OrderRecord(date: Timestamp, ticker: str, shares: float, commission: float)#

Bases: object

Record of an executed order.

Simplified order representation for reporting and analysis.

Variables:
  • date (pd.Timestamp) – Date of order execution.

  • ticker (str) – Ticker symbol.

  • shares (float) – Number of shares traded. Signed: negative for sells, positive for buys.

  • commission (float) – Commission charged for the order.

Examples

Buy order:

>>> record = OrderRecord(
...     date=pd.Timestamp('2024-01-15'),
...     ticker='AAPL',
...     shares=100.0,  # Positive for buy
...     commission=1.00,
... )

Sell order:

>>> record = OrderRecord(
...     date=pd.Timestamp('2024-01-15'),
...     ticker='MSFT',
...     shares=-50.0,  # Negative for sell
...     commission=0.50,
... )
type PortfolioSchedule = dict[Timestamp, TickerWeights]#

Mapping of rebalance dates to ticker weight dictionaries.

Keys are pd.Timestamp objects representing rebalance dates. Values are TickerWeights dictionaries for that date.

Examples

>>> schedule: PortfolioSchedule = {
...     pd.Timestamp('2024-01-01'): {'AAPL': 0.5, 'MSFT': 0.5},
...     pd.Timestamp('2024-04-01'): {'AAPL': 0.6, 'GOOG': 0.4},
... }
class PriceData(execution_price: float, vwap_price: float, average_daily_volume: float | None = None, volatility: float | None = None, half_spread: float | None = None)#

Bases: object

Container for prices used in trade execution.

Separates the price used for execution from the price used for commission calculation. This supports realistic scenarios where execution happens at one price (e.g., close) but commission is calculated on another (e.g., VWAP).

Variables:
  • execution_price (float) – Price at which the trade is executed. This is the price used to calculate the trade value. Must be positive.

  • vwap_price (float) – Volume-weighted average price. Typically used for commission calculation to match institutional trading practices. Must be positive.

  • average_daily_volume (float | None) – Average daily volume in shares over the execution-cost window, ending on the day before this one. None (the default) when the market data carries no volume column or the ticker has no history yet; slippage models then fall back to their configured constant.

  • volatility (float | None) – Daily return standard deviation over the same window, as a decimal. None under the same conditions.

  • half_spread (float | None) – Half the relative bid-ask spread as a fraction of the price, mapped from a column or estimated from daily highs and lows. None when neither source is available.

Examples

Creating price data:

>>> prices = PriceData(execution_price=150.25, vwap_price=150.10)
>>> prices.execution_price
150.25
>>> prices.vwap_price
150.10

Using in trade execution:

>>> shares_value = shares * prices.execution_price
>>> commission = commission_model.calculate(
...     shares=math.floor(amount / prices.vwap_price)
... )

Notes

The separation of execution_price and vwap_price matches the original backtester behavior where shares are calculated using execution price but commission uses VWAP-derived shares.

The three market-data fields all default to None, so every existing PriceData(execution_price=..., vwap_price=...) construction is unaffected. The engines fill them only when the bundle carries the corresponding tables.

class RebalanceResult(date: Timestamp, buy_orders: list[TradeOrder] = <factory>, sell_orders: list[TradeOrder] = <factory>, total_commissions: float = 0.0, total_slippage: float = 0.0, total_buy_value: float = 0.0, total_sale_value: float = 0.0, status: RebalanceStatus = RebalanceStatus.SUCCESS, error_message: str = '', reason: RebalanceReason = RebalanceReason.SCHEDULED)#

Bases: object

Result of a portfolio rebalancing operation.

Aggregates all trades and metrics from a single rebalancing event. This is a mutable dataclass to allow updating status and error messages during execution.

Variables:
  • date (pd.Timestamp) – Date of rebalancing. Normalized to midnight (00:00:00).

  • buy_orders (list[TradeOrder]) – List of executed buy orders. Empty list if no buys were executed.

  • sell_orders (list[TradeOrder]) – List of executed sell orders. Empty list if no sells were executed.

  • total_commissions (float) – Sum of all commissions from all orders. Default is 0.0.

  • total_slippage (float) – Sum of the slippage cost of all orders, i.e. how much worse the fills were than the reference prices. Zero under the default NoSlippage, and zero for any order built without a reference price. Default is 0.0.

  • total_buy_value (float) – Total value of all purchases (shares x price). Default is 0.0.

  • total_sale_value (float) – Total value of all sales (shares x price). Default is 0.0.

  • status (RebalanceStatus) – Outcome status of the rebalancing. Default is SUCCESS.

  • error_message (str) – Error description if status is not SUCCESS. Empty string for successful rebalancing.

  • reason (RebalanceReason) – Why the result exists: a rebalancing date from the portfolio file (SCHEDULED, the default) or a position the delisting policy closed at its last price (FORCED_LIQUIDATION).

Examples

Creating a rebalance result:

>>> result = RebalanceResult(
...     date=pd.Timestamp('2024-01-15'),
...     buy_orders=[buy_order],
...     sell_orders=[sell_order],
...     total_commissions=2.00,
...     total_buy_value=15000.0,
...     total_sale_value=10000.0,
... )
>>> result.status
<RebalanceStatus.SUCCESS: 'success'>

Handling failure:

>>> result.status = RebalanceStatus.INSUFFICIENT_CASH
>>> result.error_message = "Cash went negative: -$500.00"

Notes

This is not frozen because status and error_message may need to be updated after initial creation (e.g., when cash validation fails after trades are calculated).

The default_factory=list ensures each instance gets its own list objects, avoiding shared state issues.

class SharesRecord(date: Timestamp, ticker: str, shares: float)#

Bases: object

Record of share holdings for a single ticker.

Used for tracking share positions after rebalancing events.

Variables:
  • date (pd.Timestamp) – Date of the record (typically a rebalance date).

  • ticker (str) – Ticker symbol.

  • shares (float) – Number of shares held. Always non-negative after valid rebalancing.

Examples

>>> record = SharesRecord(
...     date=pd.Timestamp('2024-01-15'),
...     ticker='AAPL',
...     shares=100.0,
... )
class SlippageRecord(date: Timestamp, ticker: str, slippage: float)#

Bases: object

Record of the slippage cost of a single ticker’s orders.

The counterpart of CommissionRecord for the other half of execution cost, used to report the breakdown by ticker and date.

Variables:
  • date (pd.Timestamp) – Date of the trade.

  • ticker (str) – Ticker symbol associated with the cost.

  • slippage (float) – Cost in currency units: the gap between the fill price and the reference price, times the shares traded. Never negative.

Examples

>>> record = SlippageRecord(
...     date=pd.Timestamp('2024-01-15'),
...     ticker='AAPL',
...     slippage=15.0,
... )
type TickerWeights = dict[str, float]#

Mapping of ticker symbols to their portfolio weights.

Keys are ticker symbols (e.g., ‘AAPL’, ‘MSFT’). Values are decimal weights that should sum to 1.0 (or less with cash reserve).

Examples

>>> weights: TickerWeights = {'AAPL': 0.4, 'MSFT': 0.3, 'GOOG': 0.3}
class TradeOrder(ticker: str, shares: float, side: OrderSide, execution_price: float, commission: float, reference_price: float | None = None)#

Bases: object

Immutable representation of an executed trade order.

This is a pure entity capturing trade execution details. Created by ExecutionBroker when processing rebalancing orders.

Variables:
  • ticker (str) – Security ticker symbol (e.g., ‘AAPL’, ‘MSFT’). Must be non-empty.

  • shares (float) – Number of shares traded. Always positive regardless of order side.

  • side (OrderSide) – Direction of the trade (BUY or SELL). Determines if shares are added or removed from portfolio.

  • execution_price (float) – Price per share at execution. After slippage adjustment if applicable.

  • commission (float) – Commission charged for this trade. Non-negative value in currency units.

  • reference_price (float | None) – Execution price before slippage, i.e. what the order would have filled at with no market friction. None (the default) when the caller did not record one, in which case the slippage cost is reported as zero rather than guessed.

Examples

Creating a buy order:

>>> order = TradeOrder(
...     ticker='AAPL',
...     shares=100.0,
...     side=OrderSide.BUY,
...     execution_price=150.00,
...     commission=1.00,
... )
>>> order.ticker
'AAPL'
>>> order.shares
100.0

Calculating trade value:

>>> trade_value = order.shares * order.execution_price
>>> print(f"Trade value: ${trade_value:,.2f}")
Trade value: $15,000.00

Notes

This is an immutable dataclass (frozen=True) to ensure trade records cannot be modified after creation, maintaining data integrity in backtest results.

property slippage_cost: float#

Currency cost of the slippage applied to this order.

The difference between the filled price and the reference price, times the shares traded. Always non-negative: slippage works against the trader on both sides, so a buy fills above the reference and a sell below it.

Returns:

abs(execution_price - reference_price) * shares, or 0.0 when no reference price was recorded.

Return type:

float

Examples

>>> order = TradeOrder(
...     ticker='AAPL',
...     shares=100.0,
...     side=OrderSide.BUY,
...     execution_price=150.15,
...     commission=1.00,
...     reference_price=150.00,
... )
>>> round(order.slippage_cost, 2)
15.0
>>> TradeOrder(
...     ticker='AAPL',
...     shares=100.0,
...     side=OrderSide.BUY,
...     execution_price=150.15,
...     commission=1.00,
... ).slippage_cost
0.0