Skip to content

Portfolio

Portfolio, the accounting record of holdings, cash and transactions, and its history. It knows identifiers and prices, not assets or data sources.

portfolio

Portfolio accounting: holdings, cash and the transactions that change them.

Portfolio is a ledger of Holdings keyed by asset identifier, with a cash balance and a list of Transaction records. It is marked with a mapping of prices, reports its value and weights, accepts trade instructions, and keeps a dated record of its positions, cash and NAV. Its accounting needs no asset objects or data source. ReportGenerator writes holdings and performance reports to Excel (this needs the excel extra).

Holding dataclass

Holding(
    asset_id: str,
    quantity: float,
    average_cost_price: float,
    current_price: float | None = None,
    market_value: float | None = None,
)

Represents a holding of a specific asset in the portfolio. Mutable as quantity and market value change.

update_market_data

update_market_data(
    current_price: float, update_date: Timestamp
) -> None

Updates the holding with the latest market price and recalculates market value.

Portfolio

Portfolio(
    portfolio_id: str,
    initial_cash: float = 0.0,
    inception: Timestamp | None = None,
    source: DataFetcher | None = None,
)

Manages a collection of asset holdings, cash balance, and transaction history.

Holdings are keyed by string asset identifiers. The accounting needs no Asset objects or data source: callers pass simple strings and prices.

It is also the store of record: what it started with, and what the books said on every date something changed them, are kept here rather than flattened onto whatever ran it. See :attr:positions, :attr:cash and :attr:nav.

Parameters:

Name Type Description Default
portfolio_id str

Identifier for these books.

required
initial_cash float

Opening cash balance. Retained as :attr:initial_capital, because cash_balance goes on mutating and without it a portfolio could not say what it started with.

0.0
inception Timestamp | None

Optional day zero. When given, the books open on that date with NAV and cash equal to the initial capital, before anything trades. A backtest passes the business day before its first trading day; a hand-built portfolio can leave it out, and its history then starts at its first event.

None
source DataFetcher | None

Data source that :meth:asset reads market data from. None uses the process's ambient source (:func:beacon.sources.resolve) at the time of the call.

None

Raises:

Type Description
ValueError

If portfolio_id is empty or initial_cash is negative.

positions property

positions: DataFrame

What was held, long-form: DATE, ASSET_ID, QUANTITY, PRICE, MARKET_VALUE, WEIGHT.

A row per held asset per date on which the books changed (a trade or a mark). WEIGHT was computed and stored at write time, so it records what the portfolio believed then rather than what recomputing it now would say.

weights property

weights: DataFrame

The stored weights, wide: dates by asset.

A pivot of the positions panel's WEIGHT column: the same recorded numbers, shaped for cross-book arithmetic. portfolio.weights subtracts cleanly against an index book's weights because both are date-by-asset frames.

cash property

cash: Series

The cash balance on every recorded date.

nav property

nav: Series

Total value on every recorded date.

Starts at :attr:initial_capital on the inception date when one was given.

asset

asset(asset_id: str) -> PortfolioAssetView

One asset's position and market data, read live.

Parameters:

Name Type Description Default
asset_id str

An asset this portfolio holds or has ever held.

required

Returns:

Name Type Description
PortfolioAssetView PortfolioAssetView

Position numbers from these books, market

PortfolioAssetView

facts from the resolved data source.

Raises:

Type Description
KeyError

If the books have never seen asset_id, matching IndexResult.asset for a non-constituent.

DataSourceError

If no source is bound and the process has none.

freeze

freeze() -> None

Close the books: from here on, any write raises.

Called by the backtest engine when a run ends. The portfolio is then the record of that run, and a later trade against it would restate a result somebody may already have read. Idempotent.

batch

batch() -> Iterator[None]

Record the books once, as the block ends, for every change made in it.

Each call inside still applies at once; only the snapshot of the books waits. The history keeps the last snapshot written for a date, so a run of trades on one date records the same history either way, without rewriting every holding after each trade. Every change in the block must be dated the same day. A batch inside a batch is part of the outer one.

trading

trading(
    reason: str,
    by_name: Mapping[str, tuple[str, ...]] | None = None,
    tags: tuple[str, ...] = (),
) -> Iterator[None]

Record every trade made in the block as made for reason, with tags, and each name's with its tags in by_name as well. The innermost block's reason wins; a trade's own reason wins over any block's.

receive_cash

receive_cash(
    amount: float,
    date: Timestamp,
    kind: str,
    asset_id: str | None = None,
) -> None

Add cash that did not come from a trade, and record it.

Parameters:

Name Type Description Default
amount float

How much. Negative pays cash out, as interest at a negative rate does; the balance never goes below zero.

required
date Timestamp

When it moved.

required
kind str

What it was, such as "INTEREST" or "DIVIDEND".

required
asset_id str | None

The holding it came from, for a dividend.

None

Raises:

Type Description
FrozenPortfolioError

If the books are closed.

execute_buy

execute_buy(
    asset_id: str,
    quantity: float,
    price: float,
    cost: float = 0.0,
    date: Timestamp | None = None,
    impact: float = 0.0,
    reason: str | None = None,
    tags: tuple[str, ...] = (),
) -> None

Buy an asset: deduct cash, create/update holding, record transaction.

The holding's average cost is updated as a quantity-weighted average and it is marked at price. If cash (allowing a tiny tolerance for float error) does not cover quantity * price + cost, the buy is not made: an error is logged and nothing changes.

Parameters:

Name Type Description Default
asset_id str

String identifier for the asset.

required
quantity float

Number of units to buy (must be positive).

required
price float

Execution price per unit.

required
cost float

Optional transaction cost (brokerage, taxes, etc.).

0.0
date Timestamp | None

Optional execution date. Defaults to now, and the history row is written under that same timestamp.

None

Raises:

Type Description
ValueError

If quantity is not positive or price is negative.

FrozenPortfolioError

If the books have been frozen.

execute_sell

execute_sell(
    asset_id: str,
    quantity: float,
    price: float,
    cost: float = 0.0,
    date: Timestamp | None = None,
    impact: float = 0.0,
    reason: str | None = None,
    tags: tuple[str, ...] = (),
) -> None

Sell an asset: add cash proceeds, reduce/remove holding, record transaction.

Cash rises by quantity * price - cost. A holding reduced to (almost) zero is removed; what remains of a partly sold holding is re-marked at price. If the portfolio holds less than quantity, the sell is not made: an error is logged and nothing changes.

Parameters:

Name Type Description Default
asset_id str

String identifier for the asset.

required
quantity float

Number of units to sell (must be positive).

required
price float

Execution price per unit.

required
cost float

Optional transaction cost (brokerage, taxes, etc.).

0.0
date Timestamp | None

Optional execution date. Defaults to now, and the history row is written under that same timestamp.

None

Raises:

Type Description
ValueError

If quantity is not positive or price is negative.

FrozenPortfolioError

If the books have been frozen.

apply

apply(
    trade: TradeInstruction, date: Timestamp | None = None
) -> None

Record one trade in the books.

The entry point for anything that has already decided a trade, such as the engine after sizing and pricing it. Dispatches to :meth:execute_buy or :meth:execute_sell, so the accounting stays on the portfolio: weighted average cost, closing at ~zero quantity, and declining (with a logged error) entries that would push cash or holdings negative.

Parameters:

Name Type Description Default
trade TradeInstruction

The instruction, as the decider issued it.

required
date Timestamp | None

Execution date. Defaults to now, as the underlying accounting does.

None

Raises:

Type Description
ValueError

If the side is neither "BUY" nor "SELL" (case-insensitive). Refused rather than guessed, because silently ignoring an unknown side would drop a trade from the record.

FrozenPortfolioError

If the books have been frozen. Checked here as well as in the accounting, so the message names the entry point the caller actually used.

update_prices

update_prices(
    prices: dict[str, float], date: Timestamp | None = None
) -> None

Update current prices for holdings from a dictionary.

A mark changes what the books say the portfolio is worth, so it is recorded like a trade is.

Parameters:

Name Type Description Default
prices dict[str, float]

Mapping of asset_id to current price. Holdings whose asset_id is not in the dict are left unchanged with a warning.

required
date Timestamp | None

Optional date to mark as of. Defaults to now.

None

Raises:

Type Description
FrozenPortfolioError

If the books have been frozen.

apply_ratio

apply_ratio(asset_id: str, ratio: float) -> None

Change a holding's share count for a split or stock dividend.

The quantity is multiplied by ratio and the per-share average cost and price divided by it, so the holding's cost and value are unchanged. Nothing is traded and no transaction is recorded. A name not held is left alone.

Parameters:

Name Type Description Default
asset_id str

The holding.

required
ratio float

The share-count multiplier: 2.0 for a 2-for-1 split, 0.5 for a 1-for-2 reverse split.

required

Raises:

Type Description
ValueError

If ratio is not positive.

FrozenPortfolioError

If the books have been frozen.

get_total_value

get_total_value() -> float

Calculates the total current market value of the portfolio (holdings + cash).

Relies on prices having been set via :meth:update_prices, :meth:execute_buy, or :meth:execute_sell beforehand.

Returns:

Type Description
float

The total portfolio value as a float.

get_weights

get_weights() -> dict[str, float]

Calculates the current weight of each asset in the portfolio. Weights are based on last-updated market values.

Returns:

Type Description
dict[str, float]

A dictionary mapping asset_id strings to weight floats.

get_holdings_summary

get_holdings_summary() -> pd.DataFrame

Returns a DataFrame summarizing current holdings.

Returns:

Type Description
DataFrame

A pandas DataFrame with columns: AssetID, Quantity,

DataFrame

AvgCostPrice, CurrentPrice, MarketValue, Weight.

Transaction dataclass

Transaction(
    asset_id: str,
    quantity: float,
    price: float,
    transaction_type: str,
    transaction_date: Timestamp,
    transaction_cost: float = 0.0,
    impact_cost: float = 0.0,
    reason: str = "",
    tags: tuple[str, ...] = (),
)

Represents a single transaction (buy or sell) of an asset.

ReportGenerator

Generates various reports for a portfolio or backtest results.

Excel output needs the 'excel' extra; the dependency is checked when a report is generated, so the class itself is always constructible.

generate_holdings_report_excel

generate_holdings_report_excel(
    portfolio: Portfolio,
    report_path: str | PathLike[str],
    valuation_date: Timestamp,
) -> None

Generates an Excel report summarizing the current portfolio holdings.

The report is built from the portfolio's own state, so the caller must have called portfolio.update_prices(...) beforehand for the holdings to carry current prices (and therefore current market values/weights).

The workbook has a HoldingsSummary sheet (the output of portfolio.get_holdings_summary(), including a cash row, with a leading valuation_date column) and, when the portfolio has transactions, a TransactionHistory sheet. If report_path does not end in .xlsx the extension is appended.

Parameters:

Name Type Description Default
portfolio Portfolio

The Portfolio object to report on.

required
report_path str | PathLike[str]

Where to save the workbook, as a string or a path.

required
valuation_date Timestamp

The date the holdings are reported as of, written into every row of the holdings sheet.

required

Raises:

Type Description
ReportingError

If there's an issue writing the file.

ValueError

If portfolio is None.

generate_performance_report_excel

generate_performance_report_excel(
    performance_data: DataFrame,
    report_path: str | PathLike[str],
    report_title: str | None = "Performance Report",
) -> None

Generates an Excel report from a DataFrame of performance data. The performance_data DataFrame is typically the output of a backtest (e.g., daily portfolio values, returns) or specific analysis results.

The frame is written with its index to a single sheet. If report_path does not end in .xlsx the extension is appended.

Parameters:

Name Type Description Default
performance_data DataFrame

A pandas DataFrame containing performance metrics over time. Expected to have a DatetimeIndex.

required
report_path str | PathLike[str]

Where to save the workbook, as a string or a path.

required
report_title str | None

Used as the sheet name, with spaces replaced by underscores and truncated to 30 characters. None names the sheet PerformanceData.

'Performance Report'

Raises:

Type Description
ReportingError

If there's an issue writing the file.

ValueError

If performance_data is not a non-empty DataFrame.

asset_view

PortfolioAssetView: one asset seen from a portfolio, position and market.

p.asset("AAA").quantity            # from the books
p.asset("AAA").unrealised_pnl
p.asset("AAA").prices(start, end)  # from the data source
p.asset("AAA").sector(on=date)     # point-in-time, not today's

The view reads everything live (position numbers from the portfolio object, market facts from the data source), so nothing is cached that can go stale: a copied fact can quietly disagree with its source, a read cannot.

PortfolioAssetView

PortfolioAssetView(
    asset_id: str,
    data_fetcher: DataFetcher,
    portfolio: _PortfolioLike,
)

Bases: AssetView

One asset's position and market data, read live.

Parameters:

Name Type Description Default
asset_id str

The identifier, as the portfolio and the data source both know it.

required
data_fetcher DataFetcher

The data provider instance.

required
portfolio _PortfolioLike

The books the position numbers come from.

required
quantity property
quantity: float

Units currently held; 0.0 for a position that was sold out.

average_cost property
average_cost: float | None

Weighted average cost of the current position, or None when the asset is not currently held.

market_value property
market_value: float | None

The position's last marked value, or None when not held.

weight property
weight: float

The position's share of the portfolio right now; 0.0 when not held or unpriced.

unrealised_pnl property
unrealised_pnl: float | None

What the current position has made against its average cost.

None when the asset is not held or has never been priced: an unknown P&L is not a zero P&L.

position_history
position_history() -> pd.DataFrame

This asset's rows of the positions panel, indexed by date.

sector
sector(on: Timestamp | str | None = None) -> str | None

The asset's sector as it stood on a date.

Point-in-time through the fetcher's classification path: a name that moved sectors answers with the one in force on on, not today's.

Parameters:

Name Type Description Default
on Timestamp | str | None

The date to stand on; None reads the latest record.

None

Returns:

Type Description
str | None

str or None: The sector, or None when reference data has none.

base

Portfolio accounting: Portfolio, the Holdings it keeps, the Transactions it records, and the TradeInstructions it accepts.

Transaction dataclass

Transaction(
    asset_id: str,
    quantity: float,
    price: float,
    transaction_type: str,
    transaction_date: Timestamp,
    transaction_cost: float = 0.0,
    impact_cost: float = 0.0,
    reason: str = "",
    tags: tuple[str, ...] = (),
)

Represents a single transaction (buy or sell) of an asset.

TradeInstruction dataclass

TradeInstruction(
    asset_id: str,
    side: str,
    quantity: float,
    price: float,
    cost: float,
    impact: float = 0.0,
    reason: str | None = None,
    tags: tuple[str, ...] = (),
)

A single trade for a portfolio to record.

Produced by whatever decides trades (the backtest engine sizes, prices and costs an order) and consumed by :meth:Portfolio.apply, which does the accounting.

Attributes:

Name Type Description
asset_id str

Asset identifier.

side str

"SELL" or "BUY".

quantity float

Number of units to trade.

price float

Execution price per unit.

cost float

Transaction cost in currency terms.

impact float

Of cost, the part that was market impact; the rest is the fixed cost.

reason str | None

Why it was made; the portfolio's current reason when None (see Portfolio.trading).

tags tuple[str, ...]

Tags it carries beyond its reason's.

Holding dataclass

Holding(
    asset_id: str,
    quantity: float,
    average_cost_price: float,
    current_price: float | None = None,
    market_value: float | None = None,
)

Represents a holding of a specific asset in the portfolio. Mutable as quantity and market value change.

update_market_data
update_market_data(
    current_price: float, update_date: Timestamp
) -> None

Updates the holding with the latest market price and recalculates market value.

Portfolio

Portfolio(
    portfolio_id: str,
    initial_cash: float = 0.0,
    inception: Timestamp | None = None,
    source: DataFetcher | None = None,
)

Manages a collection of asset holdings, cash balance, and transaction history.

Holdings are keyed by string asset identifiers. The accounting needs no Asset objects or data source: callers pass simple strings and prices.

It is also the store of record: what it started with, and what the books said on every date something changed them, are kept here rather than flattened onto whatever ran it. See :attr:positions, :attr:cash and :attr:nav.

Parameters:

Name Type Description Default
portfolio_id str

Identifier for these books.

required
initial_cash float

Opening cash balance. Retained as :attr:initial_capital, because cash_balance goes on mutating and without it a portfolio could not say what it started with.

0.0
inception Timestamp | None

Optional day zero. When given, the books open on that date with NAV and cash equal to the initial capital, before anything trades. A backtest passes the business day before its first trading day; a hand-built portfolio can leave it out, and its history then starts at its first event.

None
source DataFetcher | None

Data source that :meth:asset reads market data from. None uses the process's ambient source (:func:beacon.sources.resolve) at the time of the call.

None

Raises:

Type Description
ValueError

If portfolio_id is empty or initial_cash is negative.

positions property
positions: DataFrame

What was held, long-form: DATE, ASSET_ID, QUANTITY, PRICE, MARKET_VALUE, WEIGHT.

A row per held asset per date on which the books changed (a trade or a mark). WEIGHT was computed and stored at write time, so it records what the portfolio believed then rather than what recomputing it now would say.

weights property
weights: DataFrame

The stored weights, wide: dates by asset.

A pivot of the positions panel's WEIGHT column: the same recorded numbers, shaped for cross-book arithmetic. portfolio.weights subtracts cleanly against an index book's weights because both are date-by-asset frames.

cash property
cash: Series

The cash balance on every recorded date.

nav property
nav: Series

Total value on every recorded date.

Starts at :attr:initial_capital on the inception date when one was given.

asset
asset(asset_id: str) -> PortfolioAssetView

One asset's position and market data, read live.

Parameters:

Name Type Description Default
asset_id str

An asset this portfolio holds or has ever held.

required

Returns:

Name Type Description
PortfolioAssetView PortfolioAssetView

Position numbers from these books, market

PortfolioAssetView

facts from the resolved data source.

Raises:

Type Description
KeyError

If the books have never seen asset_id, matching IndexResult.asset for a non-constituent.

DataSourceError

If no source is bound and the process has none.

freeze
freeze() -> None

Close the books: from here on, any write raises.

Called by the backtest engine when a run ends. The portfolio is then the record of that run, and a later trade against it would restate a result somebody may already have read. Idempotent.

batch
batch() -> Iterator[None]

Record the books once, as the block ends, for every change made in it.

Each call inside still applies at once; only the snapshot of the books waits. The history keeps the last snapshot written for a date, so a run of trades on one date records the same history either way, without rewriting every holding after each trade. Every change in the block must be dated the same day. A batch inside a batch is part of the outer one.

trading
trading(
    reason: str,
    by_name: Mapping[str, tuple[str, ...]] | None = None,
    tags: tuple[str, ...] = (),
) -> Iterator[None]

Record every trade made in the block as made for reason, with tags, and each name's with its tags in by_name as well. The innermost block's reason wins; a trade's own reason wins over any block's.

receive_cash
receive_cash(
    amount: float,
    date: Timestamp,
    kind: str,
    asset_id: str | None = None,
) -> None

Add cash that did not come from a trade, and record it.

Parameters:

Name Type Description Default
amount float

How much. Negative pays cash out, as interest at a negative rate does; the balance never goes below zero.

required
date Timestamp

When it moved.

required
kind str

What it was, such as "INTEREST" or "DIVIDEND".

required
asset_id str | None

The holding it came from, for a dividend.

None

Raises:

Type Description
FrozenPortfolioError

If the books are closed.

execute_buy
execute_buy(
    asset_id: str,
    quantity: float,
    price: float,
    cost: float = 0.0,
    date: Timestamp | None = None,
    impact: float = 0.0,
    reason: str | None = None,
    tags: tuple[str, ...] = (),
) -> None

Buy an asset: deduct cash, create/update holding, record transaction.

The holding's average cost is updated as a quantity-weighted average and it is marked at price. If cash (allowing a tiny tolerance for float error) does not cover quantity * price + cost, the buy is not made: an error is logged and nothing changes.

Parameters:

Name Type Description Default
asset_id str

String identifier for the asset.

required
quantity float

Number of units to buy (must be positive).

required
price float

Execution price per unit.

required
cost float

Optional transaction cost (brokerage, taxes, etc.).

0.0
date Timestamp | None

Optional execution date. Defaults to now, and the history row is written under that same timestamp.

None

Raises:

Type Description
ValueError

If quantity is not positive or price is negative.

FrozenPortfolioError

If the books have been frozen.

execute_sell
execute_sell(
    asset_id: str,
    quantity: float,
    price: float,
    cost: float = 0.0,
    date: Timestamp | None = None,
    impact: float = 0.0,
    reason: str | None = None,
    tags: tuple[str, ...] = (),
) -> None

Sell an asset: add cash proceeds, reduce/remove holding, record transaction.

Cash rises by quantity * price - cost. A holding reduced to (almost) zero is removed; what remains of a partly sold holding is re-marked at price. If the portfolio holds less than quantity, the sell is not made: an error is logged and nothing changes.

Parameters:

Name Type Description Default
asset_id str

String identifier for the asset.

required
quantity float

Number of units to sell (must be positive).

required
price float

Execution price per unit.

required
cost float

Optional transaction cost (brokerage, taxes, etc.).

0.0
date Timestamp | None

Optional execution date. Defaults to now, and the history row is written under that same timestamp.

None

Raises:

Type Description
ValueError

If quantity is not positive or price is negative.

FrozenPortfolioError

If the books have been frozen.

apply
apply(
    trade: TradeInstruction, date: Timestamp | None = None
) -> None

Record one trade in the books.

The entry point for anything that has already decided a trade, such as the engine after sizing and pricing it. Dispatches to :meth:execute_buy or :meth:execute_sell, so the accounting stays on the portfolio: weighted average cost, closing at ~zero quantity, and declining (with a logged error) entries that would push cash or holdings negative.

Parameters:

Name Type Description Default
trade TradeInstruction

The instruction, as the decider issued it.

required
date Timestamp | None

Execution date. Defaults to now, as the underlying accounting does.

None

Raises:

Type Description
ValueError

If the side is neither "BUY" nor "SELL" (case-insensitive). Refused rather than guessed, because silently ignoring an unknown side would drop a trade from the record.

FrozenPortfolioError

If the books have been frozen. Checked here as well as in the accounting, so the message names the entry point the caller actually used.

update_prices
update_prices(
    prices: dict[str, float], date: Timestamp | None = None
) -> None

Update current prices for holdings from a dictionary.

A mark changes what the books say the portfolio is worth, so it is recorded like a trade is.

Parameters:

Name Type Description Default
prices dict[str, float]

Mapping of asset_id to current price. Holdings whose asset_id is not in the dict are left unchanged with a warning.

required
date Timestamp | None

Optional date to mark as of. Defaults to now.

None

Raises:

Type Description
FrozenPortfolioError

If the books have been frozen.

apply_ratio
apply_ratio(asset_id: str, ratio: float) -> None

Change a holding's share count for a split or stock dividend.

The quantity is multiplied by ratio and the per-share average cost and price divided by it, so the holding's cost and value are unchanged. Nothing is traded and no transaction is recorded. A name not held is left alone.

Parameters:

Name Type Description Default
asset_id str

The holding.

required
ratio float

The share-count multiplier: 2.0 for a 2-for-1 split, 0.5 for a 1-for-2 reverse split.

required

Raises:

Type Description
ValueError

If ratio is not positive.

FrozenPortfolioError

If the books have been frozen.

get_total_value
get_total_value() -> float

Calculates the total current market value of the portfolio (holdings + cash).

Relies on prices having been set via :meth:update_prices, :meth:execute_buy, or :meth:execute_sell beforehand.

Returns:

Type Description
float

The total portfolio value as a float.

get_weights
get_weights() -> dict[str, float]

Calculates the current weight of each asset in the portfolio. Weights are based on last-updated market values.

Returns:

Type Description
dict[str, float]

A dictionary mapping asset_id strings to weight floats.

get_holdings_summary
get_holdings_summary() -> pd.DataFrame

Returns a DataFrame summarizing current holdings.

Returns:

Type Description
DataFrame

A pandas DataFrame with columns: AssetID, Quantity,

DataFrame

AvgCostPrice, CurrentPrice, MarketValue, Weight.

cash_flows

Cash a portfolio receives or pays that is not a trade.

Interest on cash, dividends, money investors put in or take out, and fees. A trade moves cash and a holding together and is a Transaction; a cash flow moves cash alone, so it is recorded on its own list rather than stretched into a trade.

CashFlow dataclass

CashFlow(
    date: Timestamp,
    amount: float,
    kind: str,
    asset_id: str | None = None,
)

One amount of cash received (positive) or paid (negative).

Attributes:

Name Type Description
date Timestamp

When the cash moved.

amount float

How much, in the portfolio's currency.

kind str

What it was: "INTEREST", "DIVIDEND" (received from a holding), "DISTRIBUTION" (paid out to investors), "SUBSCRIPTION" or "REDEMPTION" (money investors put in or took out), "FEE" (the fund's charges), "SWAP" (a swap's value settled at a reset), "FUTURES" (index futures' daily margin and roll costs) or "HEDGE" (a hedged share class's currency forward settled).

asset_id str | None

The holding it came from, for a dividend. None for interest.

history

The portfolio's books over time: positions, cash and NAV, event by event.

The recorder is part of the Portfolio, composed rather than inherited, and nothing outside the portfolio layer writes to one. Read the history through Portfolio.positions, Portfolio.cash and Portfolio.nav.

MarkedHolding

Bases: Protocol

The three fields the recorder reads off a holding.

A structural type rather than an import of :class:beacon.portfolio.base.Holding, which would make the two modules import each other.

PortfolioHistory

PortfolioHistory()

Records what the books said, on every event that changed them.

Written as plain dicts during a run and converted to a DataFrame or Series on read. There is deliberately no Position class: the panel rests as a frame and lookups return frame slices.

Keyed by date, and the last write for a date wins. A mark followed by a trade on the same day therefore leaves the post-trade state on record, which is what end-of-day books mean; the intermediate state is not something that survived the day.

positions property
positions: DataFrame

Every recorded position, long-form and sorted by date.

cash property
cash: Series

The cash balance on each recorded date.

nav property
nav: Series

The total value of the books on each recorded date.

record
record(
    date: Timestamp,
    holdings: Mapping[str, MarkedHolding],
    cash: float,
) -> None

Write the current state of the books, dated date.

The weight on each row is computed here and stored, rather than derived on read from the market value and the NAV. That is the point of the panel: it records what the books said at the time, so a later change to the derivation, or to the price data underneath, cannot silently restate history. A holding with no market value is recorded with weight 0.0 and left out of the NAV.

Parameters:

Name Type Description Default
date Timestamp

The date the state belongs to. Re-recording a date replaces what was there.

required
holdings Mapping[str, MarkedHolding]

The live holdings, read but not retained.

required
cash float

The cash balance to record beside them.

required

reasons

Why a trade was made.

Each trade has one reason, the event that triggered it, and a set of tags: the reason, what it implies, and anything else that shaped it. On a rebalance the reason is the rebalance, and the tags name the stages that changed the name's target (an issuer cap, a liquidity screen). A trade made to move cash in or out of the book is tagged a cash raise when it sells and a cash spend-down when it buys, whatever moved the cash.

Figures by reason add up to the run's; figures by tag may count a trade more than once, as one trade can carry several.

tags_for

tags_for(
    reason: str, side: str, extra: tuple[str, ...] = ()
) -> tuple[str, ...]

A trade's tags: its reason, a cash raise or spend-down when its reason moves cash, and extra, each once, in that order.

reporting

Excel reports from portfolio data: a holdings report and a performance report.

Writing a report needs the excel extra (openpyxl).

ReportingError

ReportingError(details: str)

Bases: BeaconError

Raised when a report cannot be generated or written.

A BeaconError rather than a bare Exception so it reaches a client through the API's error envelope with a stable code, like every other library failure, instead of as an unlabelled 500.

ReportGenerator

Generates various reports for a portfolio or backtest results.

Excel output needs the 'excel' extra; the dependency is checked when a report is generated, so the class itself is always constructible.

generate_holdings_report_excel
generate_holdings_report_excel(
    portfolio: Portfolio,
    report_path: str | PathLike[str],
    valuation_date: Timestamp,
) -> None

Generates an Excel report summarizing the current portfolio holdings.

The report is built from the portfolio's own state, so the caller must have called portfolio.update_prices(...) beforehand for the holdings to carry current prices (and therefore current market values/weights).

The workbook has a HoldingsSummary sheet (the output of portfolio.get_holdings_summary(), including a cash row, with a leading valuation_date column) and, when the portfolio has transactions, a TransactionHistory sheet. If report_path does not end in .xlsx the extension is appended.

Parameters:

Name Type Description Default
portfolio Portfolio

The Portfolio object to report on.

required
report_path str | PathLike[str]

Where to save the workbook, as a string or a path.

required
valuation_date Timestamp

The date the holdings are reported as of, written into every row of the holdings sheet.

required

Raises:

Type Description
ReportingError

If there's an issue writing the file.

ValueError

If portfolio is None.

generate_performance_report_excel
generate_performance_report_excel(
    performance_data: DataFrame,
    report_path: str | PathLike[str],
    report_title: str | None = "Performance Report",
) -> None

Generates an Excel report from a DataFrame of performance data. The performance_data DataFrame is typically the output of a backtest (e.g., daily portfolio values, returns) or specific analysis results.

The frame is written with its index to a single sheet. If report_path does not end in .xlsx the extension is appended.

Parameters:

Name Type Description Default
performance_data DataFrame

A pandas DataFrame containing performance metrics over time. Expected to have a DatetimeIndex.

required
report_path str | PathLike[str]

Where to save the workbook, as a string or a path.

required
report_title str | None

Used as the sheet name, with spaces replaced by underscores and truncated to 30 characters. None names the sheet PerformanceData.

'Performance Report'

Raises:

Type Description
ReportingError

If there's an issue writing the file.

ValueError

If performance_data is not a non-empty DataFrame.

valuation

A book's weights from what it holds: each name's units at its price over the book's net asset value.

held_weights

held_weights(
    units: dict[str, float], prices: Series, nav: float
) -> dict[str, tuple[float, float]]

Each name's units and weight: its units at its price in prices over nav. A name with no price is left out.