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 ¶
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: |
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: |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If portfolio_id is empty or initial_cash is negative. |
positions
property
¶
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
¶
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.
nav
property
¶
Total value on every recorded date.
Starts at :attr:initial_capital on the inception date when one was
given.
asset ¶
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
|
DataSourceError
|
If no source is bound and the process has none. |
freeze ¶
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 ¶
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 ¶
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 |
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 ¶
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 |
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 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 ¶
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 ¶
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 ¶
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 ¶
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 |
'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 ¶
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 |
average_cost
property
¶
Weighted average cost of the current position, or None when the asset is not currently held.
market_value
property
¶
The position's last marked value, or None when not held.
weight
property
¶
The position's share of the portfolio right now; 0.0 when not held or unpriced.
unrealised_pnl
property
¶
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 ¶
This asset's rows of the positions panel, indexed by date.
sector ¶
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
|
|
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 |
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 ¶
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: |
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: |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If portfolio_id is empty or initial_cash is negative. |
positions
property
¶
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
¶
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.
nav
property
¶
Total value on every recorded date.
Starts at :attr:initial_capital on the inception date when one was
given.
asset ¶
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
|
DataSourceError
|
If no source is bound and the process has none. |
freeze ¶
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 ¶
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 ¶
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 |
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 ¶
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 |
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 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 ¶
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 ¶
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 ¶
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 ¶
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
¶
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: |
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 ¶
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.
record ¶
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 ¶
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 ¶
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 |
'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.