Read portfolio values¶
Use portfolio inside strategy functions to read cash, equity and profit/loss. These values are read-only.
| Value | Meaning |
|---|---|
portfolio.cash |
Cash, or account balance in margin accounting. |
portfolio.value / portfolio.equity |
Marked account-currency value. |
portfolio.unrealized_pnl |
Account-currency marked P&L. |
portfolio.realized_pnl |
Cumulative closed-trade P&L. |
portfolio.starting_capital |
Initial funding when recorded. |
portfolio.return_pct |
Return fraction; 0.05 means 5%. |
A margin account's cash is not automatically its buying power. Gross exposure is not equity. Missing conversion or current-price data can affect values; a price fallback can lag a bar.
Author-facing equity, sizing, risk and CLI account summaries use the account's base currency in cash as well as margin accounts. With USD 1,000, buying ten units priced at JPY 100 at 0.01 USD per JPY leaves USD 990 cash and USD 10 of assets: equity is still USD 1,000 before fees, not 1,990. The required finite, positive conversion must exist before a cash fill; missing FX is not a 1:1 rate.
A fully closed, settled foreign position does not need a new FX quote merely because its historical zero-quantity row remains. For example, buy and sell one EUR-priced unit at EUR 100 with EUR/USD 1.1 and no fees: a USD 1,000 account is back to USD 1,000 and can value that flat state without EUR/USD. An open foreign position, deferred foreign realised P&L or unsettled foreign cash still needs a finite positive conversion. Closing a position is not the same as settling all of its foreign-currency obligations.
An accrued borrow charge of USD 10 reduces both cash and equity by 10, while realized P&L records -10. Funding credits work in the opposite direction. A cash trade's ordinary closing profit is already reflected in its buy/sell cash flows and is not credited twice. Financing uses the configured model and historical exposure; it is not a complete venue financing statement.
Low-level Portfolio.total_value and Portfolio.unrealized_pnl retain their
legacy raw cash-account aggregation for compatibility. Integrators needing
account units use total_value_in_base() and unrealized_pnl_in_base();
the injected author portfolio.value already uses the strict account value.
If starting capital is not recorded, return may not be meaningful. Use cumulative realised P&L as a running figure, not a daily loss measure. See position and account reference.
Keep quantities and money distinct¶
Suppose a fully funded, unleveraged strategy began with 10,000, now holds assets marked at 2,000 and has cash of 8,200. Its equity is 10,200 and return fraction is 0.02, or 2%. Gross exposure, cash and equity describe different things.
In margin accounting, cash/balance is not automatically free margin. Currency conversion affects account-currency equity and P&L. Use the accounting regime supplied by the execution environment rather than adding signed market values as if all accounts were unleveraged cash accounts.
def on_tick():
plot("equity", portfolio.equity)
plot("return_percent", 100 * portfolio.return_pct)
Multiply the return fraction by 100 only for a percent-number display. Do not pass that displayed value back to an order argument that expects a fraction.
Scope and fallback values¶
portfolio is the portfolio view supplied to the run; it is not a promise of a broker-wide account balance. Values can be zero when no portfolio is bound. If original funding is missing, starting_capital can fall back to current cash, making return unsuitable for performance interpretation. Check that the run recorded its original funding.
Realised P&L is cumulative closed-trade P&L; it does not reset daily. For a daily equity-loss gate, use risk checks. For individual holdings and their quantities, use positions.
Model delayed sale proceeds¶
The event-driven simulator defaults to immediate settlement
(settlement_days=0). Selecting market="US" or another market does not
choose a historical settlement rule. Configure a delay explicitly for a cash
account and supply its settlement calendar:
from atom.contracts.calendar import SessionSchedule
def on_start():
return {
"settlement_days": 1,
"settlement_calendar": SessionSchedule("SIM", timezone_name="UTC"),
}, {}
This illustrative calendar has weekdays and no holidays; replace it with the
calendar appropriate to the experiment. Positive delays require a supplied
SessionSchedule. settlement_days is an integer from zero through 366.
Days advance in the supplied timezone, skipping weekends and the calendar's
holidays; always_open=True counts every date. Proceeds become eligible at
midnight on that date and move to spendable cash at the first processed bar
at or after it, before that bar's pending orders are evaluated.
For example, start with 1,000 and buy ten shares at 100. Cash becomes zero. Selling them at 100 with a one-day delay adds 1,000 to unsettled proceeds, not spendable cash. A same-day buy needing those funds is rejected. On the next eligible date, settlement makes 1,000 available for a new buy. Equity includes unsettled proceeds throughout; they are not a trading loss. Fees still consume cash. A weekend or supplied holiday can extend the wait.
The policy applies to simulated cash sale proceeds, including composite legs. Sale proceeds from one leg cannot fund a simultaneous new-risk leg before settlement. Margin accounts and external order clients reject this setting: their adapters own settlement. Keep cash enforcement enabled when you want funding constraints. This model is not a jurisdiction-specific settlement calendar or broker buying-power guarantee.
Checkpoints retain unsettled proceeds and the policy identity. Resume with the same delay, timezone and holiday set; a changed policy is rejected. Earlier checkpoints have the immediate-settlement policy. The portfolio's lower-level foreign-currency unsettled ledger converts eligible amounts at the available FX rate and refuses settlement without a required rate.
Accrue financing over observed history¶
Borrow uses Actual/365: every elapsed calendar date is chargeable, including
weekends and holidays. At a transition from date D to E, the engine charges
D through Eā1 before marking the new bar. Each date requests its own annual
borrow rate in basis points. Missing dates carry the last observed holdings and
close; no unobserved trades are invented. The final observed date is still open
and is not charged at shutdown. For two short shares last marked at 100, an
annual rate of 365 bps costs 0.02 per calendar day. Friday to Monday therefore
costs 0.06, even with no weekend bars; previously it cost only 0.02. Supply the
appropriate rates to the injected get_borrow_rate(symbol, date) provider.
Perpetual funding requires event-time holdings and marks. The engine requests
get_funding_symbols(date) first. Return the complete list of symbols with
funding obligations for that date, including closed/removed lots; [] explicitly
means none. This hook is mandatory whenever get_funding_schedule is configured,
even when the current book has no rows. The engine deduplicates symbols, requests
get_funding_schedule(symbol, date) for each, and resolves every event through the host's
get_funding_exposure(symbol, timestamp). This function returns a list of rows
with signed qty, mark_price and currency, representing holdings immediately
before that event. Include lots that have since closed. Return [] for known
zero exposure and None for unavailable history. Events may supply their own
mark as (timestamp, rate, mark_price); otherwise each row supplies its mark.
The helper can also be called directly:
from datetime import datetime, timezone
from atom.models import Portfolio
from atom.helpers.funding import accrue_funding
ts = datetime(2026, 1, 5, 8, tzinfo=timezone.utc)
history = {ts: [{"qty": 2, "mark_price": 100, "currency": "USD"}]}
portfolio = Portfolio(cash=1000)
adjustment = accrue_funding(
portfolio, "EXAMPLE-PERP", [(ts, 0.01)],
exposure_at=lambda symbol, timestamp: history.get(timestamp),
)
assert adjustment == -2
assert portfolio.cash == 998
The host owns this historical record; a daily close alone cannot establish
intraday funding exposure. Missing history raises ValueError with
funding requires event-time exposure and marks or
funding event-time exposure unavailable, and the engine stops instead of
reporting P&L with omitted charges. All rows/events are validated before a
helper call books charges. Static histories used in older integrations must be
replaced by actual event records; current holdings are never an automatic
fallback. This does not add exchange settlement, inverse-contract valuation or
non-unit contract support.
Financing fails closed: with a configured borrow or funding provider, unavailable
rates/schedules, invalid non-finite quantities/rates, nonpositive required marks,
or missing exposure/currency raise ValueError and stop the run. They are never
converted to zero charges or warnings. A missing provider leaves that financing
model disabled; a configured provider must return an explicit zero rate or empty
schedule when no charge is due. Signed finite rates are allowed (rebates).
Validation is atomic within one borrow date or one symbol's funding helper call;
charges already completed for earlier dates/symbols remain if a later one fails.
A failed run is incomplete and must not be reported as a completed backtest.
For example, get_borrow_rate returning float("nan") previously charged zero;
it now raises borrow requires finite rate/exposure, positive mark and explicit currency.
A flat book with a declared CLOSED funding symbol and historical quantity 2,
mark 100 and rate 0.01 pays 2; previously the engine silently paid zero. Without
get_funding_symbols(date), a configured schedule raises
funding requires get_funding_symbols(date), including closed lots.
Checkpoints now save portfolio.last_borrow_accrual_date as an ISO date (or
null when no date has been charged). Restore replaces the in-memory marker;
repeat or earlier accrual dates do nothing. The marker cannot follow the snapshot
date. Legacy snapshots without it reset the marker to null and resume from the
snapshot's observed date, which the engine left open. A Thursday/Friday checkpoint
followed by Monday charges Friday, Saturday and Sunday using Friday's holdings
and close and each date's rate. Resume does not reconstruct unobserved trades.
Use the canonical CRYPTO_PERP instrument type. PERPETUAL is not an Order enum
alias; permissive raw Portfolio test rows did not establish such a contract.
Funding symbol enumeration comes from host history, independently of current
row labels. The test-only static_exposure fixture is invalid as a host provider
whenever holdings or marks change between events.