Skip to content

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.