trader.performance.benchmark module

Account ROI vs. a market benchmark (requirements §12.1, GitLab issue #19).

Not the same feature as the backtest’s buy-and-hold baseline (MR !41, trader.backtest.metrics.buy_and_hold + trader.backtest.engine). That one answers “did this strategy beat holding the SAME symbol it traded”, computed from simulated bars over a backtest window. This module answers a different question: “did the whole real ACCOUNT, over its real account_snapshots history (requirements §10), beat a market INDEX” — QQQ/VOO by default, configurable via config/reporting.yaml. It is a portfolio-level report over live data, not a per-symbol simulation.

It deliberately REUSES total_return_pct and buy_and_hold from trader.backtest.metrics rather than reimplementing them: both are pure functions over two Decimal numbers with no backtest-specific assumption baked in (no slippage, no whole-share constraint), so there is nothing here to duplicate — only to call.

class trader.performance.benchmark.AccountReturn(start_at, end_at, starting_value, ending_value, return_pct, snapshot_count)[source]

Bases: object

The account’s real return between the earliest and latest snapshot in a window.

Parameters:
  • start_at (datetime)

  • end_at (datetime)

  • starting_value (Decimal)

  • ending_value (Decimal)

  • return_pct (Decimal)

  • snapshot_count (int)

start_at: datetime
end_at: datetime
starting_value: Decimal
ending_value: Decimal
return_pct: Decimal
snapshot_count: int

How many snapshots the window actually spanned — surfaced so a report built from exactly 2 sparse snapshots can be told apart from one built from a dense history, even though both produce one first/last pair.

class trader.performance.benchmark.BenchmarkReturn(ticker, start_at, end_at, starting_price, ending_price, ending_value, return_pct)[source]

Bases: object

A buy-and-hold return for one ticker over the SAME window as an AccountReturn.

Parameters:
  • ticker (str)

  • start_at (datetime)

  • end_at (datetime)

  • starting_price (Decimal)

  • ending_price (Decimal)

  • ending_value (Decimal)

  • return_pct (Decimal)

ticker: str
start_at: datetime
end_at: datetime
starting_price: Decimal
ending_price: Decimal
ending_value: Decimal

What starting_value (an AccountReturn’s) would be worth today had it bought this ticker instead, fractional shares — see buy_and_hold’s own docstring for why fractional, not whole, shares are used here too.

return_pct: Decimal
trader.performance.benchmark.account_return(snapshots)[source]

The account’s return from its earliest to its latest snapshot in snapshots.

Ordered by (captured_at, id) before taking first/last — the same tie-break SnapshotRepository itself uses — so a caller that already queried in order is not silently trusted, and one that did not is corrected rather than producing a return over the wrong pair.

Raises:

PerformanceError – fewer than 2 snapshots. A single point cannot show a return, and reporting 0.00% would read as “flat” rather than “unmeasurable” — the same reasoning run_backtest uses for a too-short window.

Parameters:

snapshots (Sequence[AccountSnapshot])

Return type:

AccountReturn

trader.performance.benchmark.benchmark_return(ticker, bars, capital)[source]

The buy-and-hold return ticker would have produced over bars.

Marked close-to-close, not open-to-open or with slippage applied: this is a REPORT of what a lump-sum buy-and-hold would have returned over the account’s snapshot window, not an executable order, so there is no fill price to model.

Raises:

PerformanceError – fewer than 2 bars. Same reasoning as account_return — a single bar cannot show a return.

Parameters:
  • ticker (str)

  • bars (Sequence[Bar])

  • capital (Decimal)

Return type:

BenchmarkReturn

trader.performance.benchmark.excess_return_pct(account, benchmark)[source]

How much the account beat (or lagged) benchmark. Negative is a real answer.

Both operands are already display-rounded Decimal`s (via `_pct), so this subtraction loses no precision worth quantizing again — unlike BacktestResult.excess_return_pct (MR !41), which is a @property on one object because both figures live together there; here the two figures come from two different functions, so this is a plain function instead.

Parameters:
Return type:

Decimal