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:
CommissionModelInterfaceFixed 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
Bases:
CommissionModelInterfaceCommission 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_floatSee also
ZeroCommissionConvenience class for zero commission
Calculate the commission for a trade based on share count.
Computes
abs(context.shares) * rateand rounds the result to two decimal places (cents). The absolute value is used because commission is charged on both buys and sells; the sign ofsharesonly carries information about trade direction, which is irrelevant to fee computation.context.priceis ignored by this per-share model.- Parameters:
context (CommissionContext) – Trade context. Only
context.sharesis 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.calculateAbstract contract.
PercentageCommissionNotional-based commission.
ZeroCommissionNo-fee model used as a baseline.
Get the commission rate per share.
- Returns:
Commission rate in currency units per share.
- Return type:
float
- class PercentageCommission(pct: float)#
Bases:
CommissionModelInterfaceCommission as a percentage of traded notional value.
Calculates commission as
abs(shares) x price x pctusing the actual per-unit price carried by theCommissionContext. 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 pctand rounds to two decimal places.- Parameters:
context (CommissionContext) – Trade context. Uses
shares(absolute value) andpriceto 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:
CommissionModelInterfaceCommission 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.priceis 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:
CommissionModelInterfaceCommission 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:
SlippageModelInterfaceSlippage 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:
SlippageModelInterfaceFixed 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:
_MarketDataSlippageHalf 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.0005for 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
SpreadPlusImpactSlippagefor the two together.See also
SpreadPlusImpactSlippageHalf spread plus square-root market impact.
BasisPointSlippageThe 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:
SlippageModelInterfaceSlippage 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:
SlippageModelInterfaceRandom 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:
_MarketDataSlippageHalf 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
HalfSpreadSlippageThe spread term on its own.
SquareRootSlippageThe 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:
_MarketDataSlippageSquare 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:
_MarketDataSlippageVolume-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:
objectRecord 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:
objectDaily 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.0when 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:
objectOne 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:
objectOne 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.0for a write-off.
- class OrderRecord(date: Timestamp, ticker: str, shares: float, commission: float)#
Bases:
objectRecord 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:
objectContainer 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.
Noneunder 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.
Nonewhen 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 existingPriceData(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:
objectResult 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.
Bases:
objectRecord 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:
objectRecord of the slippage cost of a single ticker’s orders.
The counterpart of
CommissionRecordfor 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:
objectImmutable 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, or0.0when 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