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(shares: float) float#
Return fixed commission amount.
Returns the same fixed amount regardless of how many shares are traded. This is typical of flat-fee broker pricing.
- Parameters:
shares (float) – Number of shares traded. This parameter is ignored since commission is fixed.
- Returns:
Fixed commission amount in currency units.
- Return type:
float
Examples
>>> model = FixedCommission(amount=4.95) >>> model.calculate(shares=1) 4.95 >>> model.calculate(shares=10000) 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(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.- Parameters:
shares (float) – Number of shares traded. Positive for buys, negative for sells. Zero shares 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:
>>> model = PerShareCommission(rate=0.01) >>> model.calculate(shares=100) 1.0 >>> model.calculate(shares=-100) 1.0
Zero shares is a no-op:
>>> model.calculate(shares=0) 0.0
Fractional shares are honoured:
>>> model = PerShareCommission(rate=0.02) >>> model.calculate(shares=12.5) 0.25
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, notional_per_share: float = 100.0)#
Bases:
CommissionModelInterfaceCommission as percentage of notional value.
Calculates commission based on the estimated notional value of the trade (shares x price). Since the interface only receives shares, an approximate notional_per_share is used by default.
- Variables:
pct (float) – Commission percentage as decimal (read-only). For example, 0.001 for 0.1% (10 basis points).
notional_per_share (float) – Approximate price per share for commission calculation (read-only). Default is 100.0.
Examples
>>> model = PercentageCommission(pct=0.001, notional_per_share=50.0) >>> model.calculate(shares=100) 5.0 # 100 shares x $50 x 0.001
>>> model = PercentageCommission(pct=0.001) # Default $100/share >>> model.calculate(shares=100) 10.0 # 100 shares x $100 x 0.001
Notes
For accurate commission with actual trade price, use the calculate_with_price() method when price is available.
- calculate(shares: float) float#
Calculate commission as percentage of estimated notional.
Computes commission as: abs(shares) x notional_per_share x pct. This provides an estimate when actual trade price is not available.
- Parameters:
shares (float) – Number of shares traded. Can be positive (buy) or negative (sell). The absolute value is used for calculation.
- Returns:
Estimated commission amount in currency units. Always non-negative.
- Return type:
float
Examples
>>> model = PercentageCommission(pct=0.001, notional_per_share=50.0) >>> model.calculate(shares=100) 5.0 >>> model.calculate(shares=-100) 5.0
- calculate_with_price(shares: float, price: float) float#
Calculate commission with actual price.
Computes commission as: abs(shares) x price x pct. Use this when the actual trade price is known.
- Parameters:
shares (float) – Number of shares traded.
price (float) – Actual price per share for the trade.
- Returns:
Actual commission amount in currency units.
- Return type:
float
Examples
>>> model = PercentageCommission(pct=0.001) >>> model.calculate_with_price(shares=100, price=150.0) 15.0 # 100 x $150 x 0.001
Get the notional per share value.
- 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(shares: float) 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.
- Parameters:
shares (float) – Number of shares traded. The absolute value is used for tier selection and calculation.
- Returns:
Commission amount in currency units. At least the minimum commission if specified.
- Return type:
float
Examples
>>> tiers = ((0, 0.01), (1000, 0.008), (5000, 0.005)) >>> model = TieredCommission(tiers=tiers, minimum=1.0) >>> model.calculate(shares=500) 5.0 # 500 x 0.01 = 5.0 >>> model.calculate(shares=50) 1.0 # 50 x 0.01 = 0.5, but minimum is 1.0 >>> model.calculate(shares=2000) 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(shares: float) float#
Return zero commission for any trade.
Always returns 0.0 regardless of the number of shares traded. This model is useful for backtesting without transaction costs.
- Parameters:
shares (float) – Number of shares traded. This parameter is ignored since no commission is charged.
- Returns:
Always returns 0.0.
- Return type:
float
Examples
>>> model = ZeroCommission() >>> model.calculate(shares=100) 0.0 >>> model.calculate(shares=1_000_000) 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 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 SquareRootSlippage(impact_coefficient: float = 0.1, volatility: float = 0.02, average_daily_volume: float = 1000000)#
Bases:
SlippageModelInterfaceSquare 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.
average_daily_volume (float) – Reference daily volume. Default is 1,000,000.
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
- 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)#
Bases:
SlippageModelInterfaceVolume-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.
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
- 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>)#
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.
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 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)#
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.
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.
- class RebalanceResult(date: Timestamp, buy_orders: list[TradeOrder] = <factory>, sell_orders: list[TradeOrder] = <factory>, total_commissions: float = 0.0, total_buy_value: float = 0.0, total_sale_value: float = 0.0, status: RebalanceStatus = RebalanceStatus.SUCCESS, error_message: str = '')#
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_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.
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, ... )
- 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)#
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.
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.