Execution#
Execution-layer components that drive a backtest day-to-day: portfolio state tracking, order routing through cost models, and structured output reporting.
Portfolio State Manager#
Portfolio state management module.
This module provides the PortfolioStateManager class which encapsulates portfolio state and provides controlled methods for updating positions, cash, and calculating values.
Key components: - AvailableCapital: Immutable result dataclass for capital calculations - PortfolioStateManager: Stateful service managing portfolio holdings
The manager follows the principle of encapsulation, exposing controlled methods for state mutation while protecting invariants.
- class AvailableCapital(capital_to_invest: float, cash_reserve: float, total_available: float)#
Bases:
objectResult of available capital calculation.
This is an immutable entity representing the breakdown of capital available for investment. Returned by the PortfolioStateManager.get_available_capital() method.
- Variables:
capital_to_invest (float) – Amount available for investment after cash reserve deduction. This is what can actually be allocated to positions.
cash_reserve (float) – Amount held back as cash reserve. Calculated as total_available * cash_reserve_pct.
total_available (float) – Total capital before reserve allocation. On first rebalance: initial_capital. On subsequent rebalances: current_cash + portfolio_value.
Examples
>>> capital = AvailableCapital( ... capital_to_invest=98000.0, ... cash_reserve=2000.0, ... total_available=100000.0, ... ) >>> capital.capital_to_invest 98000.0 >>> capital.cash_reserve 2000.0
Notes
The relationship is: capital_to_invest + cash_reserve = total_available
- class PortfolioStateManager(tickers: list[str], initial_capital: float, cash_reserve_pct: float = 0.0)#
Bases:
objectManages portfolio state with controlled mutation methods.
This class encapsulates portfolio holdings, cash position, and commission tracking. It provides methods for updating state and calculating derived values like portfolio value and weights.
The manager maintains the following state: - Holdings: shares per ticker - Cash: current cash position - Commissions: cumulative commissions paid - Initialization flag: whether first rebalance has occurred
- Variables:
holdings (dict[str, float]) – Current shares held by ticker (read-only property).
cash (float) – Current cash position.
initial_capital (float) – Starting capital amount (read-only).
cash_reserve_pct (float) – Percentage of total value to maintain as cash (read-only).
total_commissions (float) – Cumulative commissions paid (read-only property).
is_initialized (bool) – Whether first rebalancing has occurred (read-only property).
Examples
Creating and using a manager:
>>> manager = PortfolioStateManager( ... tickers=['AAPL', 'MSFT'], ... initial_capital=100000.0, ... cash_reserve_pct=0.02, ... ) >>> manager.update_position('AAPL', delta_shares=100.0) >>> manager.get_shares('AAPL') 100.0
Getting available capital:
>>> def get_price(ticker): ... return {'AAPL': 150.0, 'MSFT': 300.0}[ticker] >>> capital = manager.get_available_capital(get_price) >>> capital.capital_to_invest 98000.0
Notes
The manager distinguishes between first rebalance (uses initial_capital) and subsequent rebalances (uses current_cash + portfolio_value). This matches the original backtester behavior.
- add_commission(amount: float) None#
Add commission to cumulative total.
- Parameters:
amount (float) – Commission amount to add.
Examples
>>> manager.add_commission(5.50) >>> manager.total_commissions 5.5
- apply_financing(charge: FinancingCharge) None#
Deduct one day’s financing from cash and accumulate it by concept.
Both engines call this once per trading day with the charge the financing model returned, so cash follows the same sequential
cash - totalfold in each and stays bit-identical.- Parameters:
charge (FinancingCharge) – The day’s borrow fee, margin interest and cash interest.
Examples
>>> manager.cash = 1000.0 >>> manager.apply_financing(FinancingCharge(borrow_fee=2.0, cash_interest=0.5)) >>> manager.cash 998.5 >>> manager.total_financing_costs 1.5
- calculate_portfolio_value(price_getter: Callable[[str], float]) float#
Calculate total value of holdings (excluding cash).
- Parameters:
price_getter (Callable[[str], float]) – Function that returns price for a ticker. Should raise ValueError or KeyError if unavailable.
- Returns:
Sum of
shares * priceover every open position. Short positions contribute negatively, so the result is negative whenever short exposure exceeds long exposure. That is a valid state, not an error — solvency is measured on total value (holdings plus cash), not on holdings alone.- Return type:
float
Examples
>>> def get_price(ticker): ... return {'AAPL': 150.0, 'MSFT': 300.0}[ticker] >>> manager.calculate_portfolio_value(get_price) 30000.0
- calculate_short_market_value(price_getter: Callable[[str], float]) float#
Market value of the short positions, as a non-negative number.
- Parameters:
price_getter (Callable[[str], float]) – Function that returns price for a ticker. Should raise ValueError or KeyError if unavailable.
- Returns:
-sum(shares * price)over positions with negative shares, reduced withmath.fsumso the result does not depend on iteration order;0.0with no shorts open.- Return type:
float
Examples
>>> manager.set_position('TSLA', -100.0) >>> manager.calculate_short_market_value(lambda ticker: 200.0) 20000.0
- calculate_short_positions(price_getter: Callable[[str], float]) tuple[tuple[str, float], ...]#
Market value of each short position, as non-negative numbers.
The same
shares * priceproductscalculate_short_market_value()sums, one per ticker, for models that price each short on its own.- Parameters:
price_getter (Callable[[str], float]) – Function that returns price for a ticker. Should raise ValueError or KeyError if unavailable.
- Returns:
(ticker, -(shares * price))for every position with negative shares whose price is available, in holdings order.- Return type:
tuple[tuple[str, float], …]
Examples
>>> manager.set_position('TSLA', -100.0) >>> manager.calculate_short_positions(lambda ticker: 200.0) (('TSLA', 20000.0),)
- calculate_total_value(price_getter: Callable[[str], float]) float#
Calculate total portfolio value including cash.
- Parameters:
price_getter (Callable[[str], float]) – Function that returns price for a ticker.
- Returns:
Portfolio value plus cash position.
- Return type:
float
Examples
>>> manager.calculate_total_value(get_price) 35000.0 # 30000 holdings + 5000 cash
- calculate_weights(price_getter: Callable[[str], float], *, total_value: float | None = None) dict[str, float]#
Calculate current portfolio weights including cash.
- Parameters:
price_getter (Callable[[str], float]) – Function that returns price for a ticker.
total_value (float, optional) – Pre-computed total portfolio value (holdings + cash) for the same prices. When provided, it is used directly instead of recomputing
calculate_total_value(price_getter)— this avoids a redundant full pass over every position when the caller has already valued the portfolio (e.g. the daily snapshot inPyArrowBacktester._record_daily_status). The value must be computed from the sameprice_getter; passing an inconsistent value yields inconsistent weights. WhenNone(default), it is computed internally, preserving the original behavior.
- Returns:
Weights by ticker, including ‘CASH_RESERVE’. Weights sum to 1.0 and are rounded to 12 decimals. Short positions carry a negative weight, and in that case ‘CASH_RESERVE’ can exceed 1.0 because short-sale proceeds sit in cash. The weights still sum to 1.0.
- Return type:
dict[str, float]
Examples
>>> weights = manager.calculate_weights(get_price) >>> weights['AAPL'] 0.75 >>> weights['CASH_RESERVE'] 0.25
With a short position open:
>>> weights = manager.calculate_weights(get_price) >>> weights['TSLA'] -0.2948 >>> weights['CASH_RESERVE'] 0.5979
- property cash: float#
Get current cash position.
- Returns:
Cash amount in currency units.
- Return type:
float
- property cash_reserve_pct: float#
Get the cash reserve percentage.
- Returns:
Percentage of portfolio to maintain as cash (0.0 to 1.0).
- Return type:
float
- get_active_positions() dict[str, float]#
Get every open position, long or short.
A position is active when its share count is non-zero. Short positions (negative share counts) are included: they are open exposure that must be valued, reported, and closed like any other. Only tickers sitting at exactly zero are omitted.
- Returns:
Dictionary of tickers with
shares != 0. Values are negative for short positions.- Return type:
dict[str, float]
Examples
A long-only portfolio:
>>> manager.get_active_positions() {'AAPL': 100.0, 'MSFT': 50.0}
A long-short portfolio:
>>> manager.get_active_positions() {'AAPL': 100.0, 'TSLA': -150.0}
Notes
For a long-only portfolio this is identical to filtering on
shares > 0, because share counts can never go negative when every target weight is non-negative.
- get_available_capital(price_getter: Callable[[str], float]) AvailableCapital#
Calculate available capital for investment.
Handles the distinction between first rebalancing (uses initial capital) and subsequent rebalancing (uses current portfolio value).
- Parameters:
price_getter (Callable[[str], float]) – Function that returns price for a ticker.
- Returns:
Capital breakdown with amounts.
- Return type:
Examples
First rebalancing (not initialized):
>>> capital = manager.get_available_capital(get_price) >>> capital.capital_to_invest 98000.0 >>> capital.cash_reserve 2000.0
Notes
This matches the original backtester logic:
if rebalance_date == self.first_date: total_available = self.initial_capital_float else: total_available = self.previous_cash + portfolio_value
Get the current share count for a ticker.
Returns the number of shares currently held in the portfolio for the requested ticker. The method is safe to call for any symbol: unknown tickers return
0.0rather than raising, which lets callers treat missing positions and zeroed-out positions uniformly.- Parameters:
ticker (str) – Ticker symbol to query. Case-sensitive; must match the casing used when the position was created.
- Returns:
Number of shares held. Returns
0.0when the ticker has no position recorded.- Return type:
float
Examples
Querying a held ticker:
>>> manager.get_shares('AAPL') 100.0
Querying an unknown ticker:
>>> manager.get_shares('UNKNOWN') 0.0
Notes
Fractional shares are supported (the return type is
float). Negative values are not normally possible during a backtest but are not validated here.See also
update_positionAdjust a position by a delta.
set_positionSet the absolute share count for a ticker.
- property holdings: dict[str, float]#
Get current holdings.
- Returns:
Reference to holdings dict (mutable for internal use).
- Return type:
dict[str, float]
Notes
Returns direct reference for performance. Use get_shares() for safe read access or get_active_positions() for filtered view.
- property initial_capital: float#
Get the initial capital amount.
- Returns:
Starting capital in currency units.
- Return type:
float
- property is_initialized: bool#
Check if portfolio has been initialized.
- Returns:
True if first rebalancing has occurred.
- Return type:
bool
- mark_initialized() None#
Mark portfolio as initialized after first rebalancing.
This changes the behavior of get_available_capital() from using initial_capital to using current_cash + portfolio_value.
Examples
>>> manager.mark_initialized() >>> manager.is_initialized True
- set_position(ticker: str, shares: float) None#
Set absolute position for a ticker.
- Parameters:
ticker (str) – Ticker symbol to set.
shares (float) – Absolute number of shares to hold.
Examples
>>> manager.set_position('AAPL', shares=100.0) >>> manager.get_shares('AAPL') 100.0
- property total_borrow_fees: float#
Cumulative borrow fees charged on short positions.
- property total_cash_interest: float#
Cumulative interest earned on positive cash balances.
- property total_commissions: float#
Get cumulative commissions paid.
- Returns:
Total commissions in currency units.
- Return type:
float
- property total_financing_costs: float#
Net financing deducted from cash so far.
- Returns:
Borrow fees plus margin interest minus cash interest earned.
- Return type:
float
- property total_margin_interest: float#
Cumulative interest charged on negative cash balances.
- update_position(ticker: str, delta_shares: float) None#
Adjust a position by adding or removing shares.
Applies
delta_sharesto the current share count for the ticker, creating the position if it did not exist. This is the method used by the trade broker after every fill: buys produce a positive delta, sells a negative one. Cash adjustments are handled separately by the broker, not by this method.- Parameters:
ticker (str) – Ticker symbol to update. Case-sensitive.
delta_shares (float) – Change in share count. Positive adds shares (e.g. a buy); negative removes shares (e.g. a sell). Fractional values are allowed.
Examples
Buy followed by partial sell:
>>> manager.update_position('AAPL', delta_shares=50.0) >>> manager.update_position('AAPL', delta_shares=-30.0) >>> manager.get_shares('AAPL') 20.0
First-time entry:
>>> manager.update_position('MSFT', delta_shares=10.0) >>> manager.get_shares('MSFT') 10.0
Notes
This method does NOT touch cash, commissions, or transaction history. The trade broker is responsible for keeping cash and share counts in sync. Calling this method directly outside of the broker can desynchronise portfolio value calculations.
See also
set_positionSet an absolute share count instead of a delta.
get_sharesInspect the current share count for a ticker.
Trade Broker#
Trade execution broker module.
This module provides the ExecutionBroker class for calculating and executing trades during portfolio rebalancing. It supports pluggable commission and slippage models while preserving the original calculation logic for shares.
The broker encapsulates all trade execution logic, keeping it separate from portfolio state management. This separation enables independent testing and potential future enhancements like multi-broker support.
IMPORTANT: The share calculation logic in this module matches the original backtester exactly. Changes to this logic would affect backtest results.
- class ExecutionBroker(commission_model: CommissionModelInterface, slippage_model: SlippageModelInterface)#
Bases:
objectExecutes trades for portfolio rebalancing with cost models.
This broker handles the mechanics of determining required trades to move from current positions to target weights, applying commission and slippage models.
The broker is stateless - it does not maintain any position or account information. All state is passed in and results are returned.
- Variables:
commission_model (CommissionModelInterface) – Model for calculating commissions (read-only).
slippage_model (SlippageModelInterface) – Model for calculating slippage (read-only).
Examples
Creating a broker:
>>> from kaxanuk.backtest_engine.backtest.models import ( ... PerShareCommission, ... NoSlippage, ... ) >>> broker = ExecutionBroker( ... commission_model=PerShareCommission(rate=0.01), ... slippage_model=NoSlippage(), ... )
Executing rebalance:
>>> result = broker.execute_rebalance( ... date=pd.Timestamp('2024-01-15'), ... current_holdings={'AAPL': 100, 'MSFT': 50}, ... target_weights={'AAPL': 0.6, 'MSFT': 0.4}, ... capital=100000.0, ... price_getter=lambda t: PriceData(150.0, 150.0), ... )
Notes
The broker preserves the original calculation logic for shares: - Shares are calculated using math.floor - Commission is calculated on shares derived from VWAP price - Execution uses the execution price
# For sells shares = math.floor(abs(diff) / execution_price) shares_commission = math.floor(abs(diff) / vwap_price) # For buys shares = math.floor(diff / execution_price) shares_commission = math.floor(diff / vwap_price) # Liquidation uses current_shares directly if weight == 0: shares = current_shares
- property commission_model: CommissionModelInterface#
Get the commission model.
- execute_rebalance(date: Timestamp, current_holdings: dict[str, float], target_weights: dict[str, float], capital: float, price_getter: Callable[[str], PriceData]) RebalanceResult#
Calculate and execute the trades needed to reach target weights.
Compares the current holdings against the target weights and produces buy / sell orders such that, after execution at the provided prices, the portfolio reaches the target exposure for each ticker. Slippage is applied to the execution price by the configured slippage model and commissions are computed by the configured commission model; both are recorded on each resulting
TradeOrder.The method is pure with respect to the broker state — it does not mutate any internal cache. The caller is expected to apply the returned orders to the portfolio state manager and update cash separately.
- Parameters:
date (pandas.Timestamp) – Date of the rebalancing operation. Used only to stamp the resulting orders.
current_holdings (dict[str, float]) – Current shares by ticker. Tickers absent from this mapping are treated as zero positions.
target_weights (dict[str, float]) – Target weights by ticker. Weights should sum to approximately 1.0 (or 1.0 minus the cash reserve). A weight of 0.0 on a currently held ticker triggers a full liquidation.
capital (float) – Available capital for investment after the cash reserve has been deducted. Used to translate weights into target dollar exposures.
price_getter (Callable[[str], PriceData]) – Function returning
PriceData(execution price + VWAP) for a ticker. May raiseValueErrororKeyErrorif data is unavailable; those errors are caught and the ticker is skipped with a warning.
- Returns:
Complete result with
buy_orders,sell_orders,total_commissions,total_buy_value,total_sale_valueandstatus.- Return type:
Examples
>>> result = broker.execute_rebalance( ... date=pd.Timestamp('2024-01-15'), ... current_holdings={'AAPL': 100, 'MSFT': 50}, ... target_weights={'AAPL': 0.6, 'MSFT': 0.4}, ... capital=100000.0, ... price_getter=get_prices, ... ) >>> result.status <RebalanceStatus.SUCCESS: 'success'> >>> len(result.buy_orders) + len(result.sell_orders) 2
Notes
Share calculation matches the original backtester logic exactly:
# For sells shares = math.floor(abs(diff) / execution_price) shares_commission = math.floor(abs(diff) / vwap_price) # For buys shares = math.floor(diff / execution_price) shares_commission = math.floor(diff / vwap_price) # Liquidation uses current_shares directly if weight == 0: shares = current_shares
Sell orders are executed first to free up capital; buy orders then consume the available capital. Commissions are computed on every order regardless of side. Tickers whose price cannot be resolved are skipped (logged at WARNING level) but do not cause the whole rebalance to fail.
See also
CommissionModelInterface.calculateApplied to every order.
SlippageModelInterface.applyApplied to execution prices.
RebalanceResultStructure of the returned object.
- execute_rebalance_vectorized(*, date: Timestamp, target_tickers: list[str], weights: ndarray, current_shares: ndarray, execution_prices: ndarray, commission_prices: ndarray, capital: float, average_daily_volumes: ndarray | None = None, volatilities: ndarray | None = None, half_spreads: ndarray | None = None) RebalanceResult#
Array-based equivalent of
execute_rebalance().Produces byte-for-byte identical orders and totals to
execute_rebalancebut computes the per-ticker exposure diffs in one vectorized pass instead of calling a scalarprice_getteronce per target ticker. Order creation (share flooring, slippage, commission) is delegated unchanged to_create_sell_order/_create_buy_order, so the pluggable-model contract is preserved.All array parameters are 1-D and aligned to
target_tickers(same index = same ticker), matching the caller’s target-weight iteration order so that float accumulation of the totals is order-identical to the scalar path. Prices must already have nulls mapped to0.0(mirroringto_python_float), which the caller does when slicing the dense price matrices.- Parameters:
date (pandas.Timestamp) – Rebalance date, used only to stamp the orders.
target_tickers (list[str]) – Tickers in target-weight order.
weights (np.ndarray) – Target weight per ticker.
current_shares (np.ndarray) – Currently held shares per ticker (0 when not held).
execution_prices (np.ndarray) – Execution price per ticker.
commission_prices (np.ndarray) – Commission (VWAP) price per ticker.
capital (float) – Capital available for investment after the cash reserve.
average_daily_volumes (np.ndarray | None) – Average daily volume per ticker over the execution-cost window, ending the day before
date.None(the default) when the bundle carries no volume table; a NaN element means the ticker has no value on the day. Only slippage models that read the context use it.volatilities (np.ndarray | None) – Daily return standard deviation per ticker over the same window, on the same terms.
half_spreads (np.ndarray | None) – Half the relative bid-ask spread per ticker, on the same terms.
- Returns:
Same structure as
execute_rebalance().- Return type:
- property slippage_model: SlippageModelInterface#
Get the slippage model.
Reporting Engine#
Backtest reporting engine module.
This module provides the ReportingEngine class for converting raw backtest records into structured DataFrames and comprehensive reports.
Key components: - BacktestReport: Immutable dataclass containing all report outputs - ReportingEngine: Stateless service for generating reports
The reporting engine transforms lists of DailyRecord and RebalanceResult objects into pandas DataFrames suitable for analysis, visualization, and export to Excel.
- class BacktestReport(register_df: ~pandas.DataFrame, total_commissions: float, commission_list: tuple[float, ...], final_value: float, initial_value: float, years: float, start_date: ~datetime.date | None, end_date: ~datetime.date | None, commission_df: ~pandas.DataFrame, shares_df: ~pandas.DataFrame, orders_df: ~pandas.DataFrame, daily_weights_df: ~pandas.DataFrame, portfolio_df: ~pandas.DataFrame, benchmark: ~pyarrow.lib.Table | None = None, total_slippage_costs: float = 0.0, slippage_df: ~pandas.DataFrame = <factory>, execution_costs_df: ~pandas.DataFrame = <factory>, total_financing_costs: float = 0.0, financing_df: ~pandas.DataFrame = <factory>, forced_liquidations_df: ~pandas.DataFrame = <factory>, risk_free_rate: float = 0.0)#
Bases:
objectComplete backtest results in structured format.
This immutable dataclass contains all outputs from a backtest execution, organized into DataFrames and summary statistics. It serves as the final deliverable from a backtest run.
- Variables:
register_df (pd.DataFrame) – Time series of portfolio values indexed by date. Contains columns: Portfolio_Value, Total_Portfolio_Value, Returns. The Returns column is calculated as percentage change of Total_Portfolio_Value.
total_commissions (float) – Sum of all commissions paid during the entire backtest period. Rounded to 2 decimal places.
commission_list (tuple[float, ...]) – Tuple of commission amounts per rebalancing event. Each value is rounded to 2 decimal places. Immutable tuple ensures report integrity.
final_value (float) – Portfolio total value at end of backtest. Includes both holdings value and cash position.
initial_value (float) – Portfolio value at start of backtest (initial capital). Rounded to 2 decimal places.
years (float) – Duration of backtest in years. Calculated using trading days (typically 252 per year).
commission_df (pd.DataFrame) – Commission payments pivoted by ticker and date. Rows are dates, columns are tickers. Values represent commission paid for each ticker on each date.
shares_df (pd.DataFrame) – Share holdings pivoted by ticker and date. Rows are dates, columns are tickers. Values represent number of shares held.
orders_df (pd.DataFrame) – Executed orders pivoted by ticker and date. Rows are dates, columns are tickers. Positive values are buys, negative values are sells.
start_date (datetime.date | None) – Start date of the backtest period. None if no explicit start date was configured.
end_date (datetime.date | None) – End date of the backtest period. None if no explicit end date was configured.
daily_weights_df (pd.DataFrame) – Daily portfolio weights indexed by date. Columns are tickers plus ‘CASH_RESERVE’. Values sum to approximately 1.0 for each date.
portfolio_df (pd.DataFrame) – Original portfolio definition DataFrame. Contains the input portfolio weights schedule.
benchmark (pa.Table | None) – Benchmark time series data if provided during backtest. None if no benchmark was specified.
total_slippage_costs (float) – Total execution cost attributable to slippage, rounded to cents: the gap between what the orders filled at and the reference prices they were measured against. Zero under the default
NoSlippage.slippage_df (pd.DataFrame) – Slippage cost by date and ticker, the counterpart of
commission_df. Empty when nothing was traded.execution_costs_df (pd.DataFrame) – Commissions, slippage and their total per rebalancing date, so the two halves of execution cost can be read side by side.
total_financing_costs (float) – Net financing deducted from cash over the whole backtest.
financing_df (pd.DataFrame) – Daily financing ledger by concept.
forced_liquidations_df (pd.DataFrame) – Positions closed by the delisting policy, one row per exit, indexed by date. Empty with the default
failpolicy.risk_free_rate (float) – Annual risk-free rate for the Sharpe ratio: the financing benchmark rate averaged over the trading days of the backtest. Zero when no benchmark is configured.
Examples
Accessing basic metrics:
>>> report.final_value 125000.0 >>> report.total_commissions 450.50
Converting to dictionary for legacy compatibility:
>>> results = report.to_dict() >>> results['final_total_portfolio_value'] 125000.0
Notes
This class is frozen (immutable) to ensure report integrity after generation. All DataFrames should be treated as read-only.
- to_dict() BacktestResultDict#
Convert report to dictionary format.
Provides backward compatibility with legacy code that expects dictionary output format from the backtester. Keys match the original PyArrowBacktester return format.
- Returns:
Dictionary with all report components using legacy keys: - Register_df: Time series DataFrame - Total_commissions: Total commissions paid - lista_comisiones: List of commissions per rebalance - final_total_portfolio_value: Final portfolio value - initial_portfolio_value: Initial capital - years: Backtest duration - Commission_df: Commission DataFrame - Shares_df: Shares DataFrame - orders_df: Orders DataFrame - Daily_Weights: Weights DataFrame - portfolio_df: Portfolio definition - benchmark: Benchmark data - Total_financing_costs: Net financing deducted from cash - Financing_df: Daily financing ledger by concept - Forced_liquidations_df: Positions closed by the delisting policy - Risk_free_rate: Annual rate the Sharpe ratio subtracts
- Return type:
Examples
>>> results = report.to_dict() >>> results['final_total_portfolio_value'] 125000.0 >>> results['lista_comisiones'] [10.50, 8.25, 12.00]
- type CommissionRecord = dict[str, date | Timestamp | str | float]#
Record for a single commission event: date, ticker, commission.
- type OrderRecord = dict[str, date | Timestamp | str | float]#
Record for a single order event: date, ticker, shares, commission.
- class ReportingEngine#
Bases:
objectGenerates structured reports from backtest records.
This stateless service class handles all data aggregation and DataFrame construction from raw backtest tracking records. It transforms lists of record objects into organized DataFrames suitable for analysis and visualization.
The engine is stateless - it does not maintain any internal state between calls. All data is passed in and results are returned.
Examples
Basic usage:
>>> engine = ReportingEngine() >>> report = engine.generate_report( ... daily_records=records, ... rebalance_results=results, ... initial_capital=100000.0, ... years=2.5, ... portfolio_df=portfolio, ... ) >>> report.final_value 125000.0
Accessing generated DataFrames:
>>> report.register_df.head() Portfolio_Value Total_Portfolio_Value Returns date 2024-01-02 98500.0 100000.0 NaN 2024-01-03 99200.0 100700.0 0.0070
Notes
This class is intentionally stateless. All methods either operate on passed parameters or are static methods for pure transformations.
- generate_report(daily_records: list[DailyRecord], rebalance_results: list[RebalanceResult], initial_capital: float, years: float, portfolio_df: DataFrame, benchmark: Table | None = None, shares_records: list[SharesRecord] | None = None, start_date: date | None = None, end_date: date | None = None, financing_records: list[FinancingRecord] | None = None, forced_exit_records: list[ForcedExitRecord] | None = None, risk_free_rate: float = 0.0) BacktestReport#
Build a complete backtest report from raw tracking records.
Main entry point of the reporting layer. Takes the streams of daily snapshots and rebalance results produced during execution and turns them into a single
BacktestReportdataclass with DataFrames suitable for downstream analysis, Excel export, or serialization to dict for the publicBacktestResultDict.The method is intentionally stateless: every call produces a fresh report, the engine instance is not modified, and inputs are never mutated.
- Parameters:
daily_records (list[DailyRecord]) – Daily portfolio status records. Each record contains the date, portfolio value, total value, cash position and weights for that day. Must be non-empty for a meaningful report.
rebalance_results (list[RebalanceResult]) – One entry per rebalance event. Each contains the buy/sell orders, commissions paid and execution status.
initial_capital (float) – Starting portfolio capital in currency units. Used for return-based metrics.
years (float) – Duration of the backtest in years (trading-day based, typically
trading_days / 252). Used to annualize return statistics.portfolio_df (pandas.DataFrame) – Original portfolio definition. Preserved on the report as-is for reference / round-tripping.
benchmark (pyarrow.Table | None, optional) – Optional benchmark time series. Default is None.
shares_records (list[SharesRecord] | None, optional) – Share holding records captured at each rebalance event. When None, the
shares_dfon the report is empty. Default is None.start_date (datetime.date | None, optional) – Start date of the backtest period, preserved on the report. Default is None.
end_date (datetime.date | None, optional) – End date of the backtest period, preserved on the report. Default is None.
financing_records (list[FinancingRecord] | None, optional) – Daily financing ledger. When None,
financing_dfis empty.forced_exit_records (list[ForcedExitRecord] | None, optional) – Positions closed by the delisting policy. When None,
forced_liquidations_dfis empty.risk_free_rate (float, optional) – Annual risk-free rate for the Sharpe ratio. Default 0.0.
- Returns:
Immutable dataclass with every structured DataFrame plus the metadata fields (start/end dates, initial/final value, years, total commissions).
- Return type:
Examples
Typical use from inside
PyArrowBacktester.run:>>> engine = ReportingEngine() >>> report = engine.generate_report( ... daily_records=daily_records, ... rebalance_results=rebalance_results, ... initial_capital=100000.0, ... years=2.5, ... portfolio_df=portfolio_df, ... benchmark=benchmark_table, ... shares_records=shares_records, ... ) >>> report.final_value 125000.0 >>> result_dict = report.to_dict() # for the public API
Notes
The returned
BacktestReportis frozen, so callers can pass it around safely without defensive copies. To customize a report (e.g. inject extra columns), build a new report from modified inputs instead of mutating the returned one.See also
BacktestReportStructure of the returned report.
PyArrowBacktester.runPrimary caller of this method.
- type SlippageRecord = dict[str, date | Timestamp | str | float]#
Record for a single order’s slippage cost: date, ticker, slippage.