> ## Documentation Index
> Fetch the complete documentation index at: https://docs.itoflow.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Portfolios and trade approvals

> Read holdings and performance, import a portfolio, measure its risk, link a strategy, and approve trades. Paper portfolios can trade on their own; a live portfolio's trades always wait for your approval.

> [!TIP]
>
> **The easiest way: ask your AI assistant.** With [Itoflow MCP](https://docs.itoflow.ai/mcp/install.md) connected to Claude or Codex, paste:
>
> ```text theme={null}
> Use Itoflow to list my portfolios and summarize the holdings, cash and
> performance of the one I choose. Don't change anything.
> ```
>
> > [card] [Open in Claude](https://claude.ai/new?q=Use%20the%20Itoflow%20plugin%20\(https%3A%2F%2Fgithub.com%2Fitoflow%2Fskills\)%20for%20this.%20If%20it%20isn't%20installed%20yet%2C%20first%20tell%20me%20how%20to%20install%20it%20and%20sign%20in%20to%20Itoflow%2C%20and%20wait%20until%20I%20confirm.%20Then%3A%0A%0AUse%20Itoflow%20to%20list%20my%20portfolios%20and%20summarize%20the%20holdings%2C%20cash%20and%20performance%20of%20the%20one%20I%20choose.%20Don't%20change%20anything.)
> >
> > Start this in Claude with the Itoflow plugin.
>
> > [card] [Open in ChatGPT](https://chatgpt.com/?q=Use%20the%20Itoflow%20plugin%20\(https%3A%2F%2Fgithub.com%2Fitoflow%2Fskills\)%20for%20this.%20If%20it%20isn't%20installed%20yet%2C%20first%20tell%20me%20how%20to%20install%20it%20and%20sign%20in%20to%20Itoflow%2C%20and%20wait%20until%20I%20confirm.%20Then%3A%0A%0AUse%20Itoflow%20to%20list%20my%20portfolios%20and%20summarize%20the%20holdings%2C%20cash%20and%20performance%20of%20the%20one%20I%20choose.%20Don't%20change%20anything.)
> >
> > Start this in ChatGPT with the Itoflow plugin.
>
> Using another client? Copy the prompt with the button on the code block.
>
> No tokens or code needed. You can also do this in the [Itoflow app](https://app.itoflow.ai/portfolios). The rest of this page covers the HTTP API.

A portfolio holds positions and cash. It can be **paper** (simulated) or **live** (linked to a [broker account](https://docs.itoflow.ai/features/brokers.md)). A [strategy](https://docs.itoflow.ai/features/strategies.md) can manage a portfolio, or a [fund-oversight monitor](https://docs.itoflow.ai/features/monitors.md#create-a-fund-oversight-monitor) can watch it, but not both.

You can see your portfolios and approve trades in the [app](https://app.itoflow.ai/portfolios) too.

Proposing a trade, approving it, sending it to the broker and the trade filling are separate steps. Check each one; one doesn't imply the next.

```mermaid theme={null}
flowchart LR
  Strategy[Strategy decision] --> Pending[Pending trades]
  Pending --> Approval[Your approval]
  Approval --> Attempts[Orders sent to the broker]
  Attempts --> Holdings[Updated holdings]
```

## Read portfolios

| Endpoint | Returns |
| - | - |
| `GET /portfolios` | Your portfolios |
| `GET /portfolios/{id}` | One portfolio and its holdings |
| `GET /portfolios/{id}/performance` | Performance over time |
| `GET /portfolios/{id}/holdings/history` | Past holdings |
| `GET /portfolios/{id}/trades` | Trade history |
| `GET /portfolios/{id}/decisions` | Strategy decisions for this portfolio |

Performance returns chain observed portfolio values after taking out external
deposits and withdrawals. Each flow is placed at the preceding reading, an
approximation to valuation at the time of the flow. The first reading starts
at zero. Daily return runs from the last reading at or before midnight UTC;
a baseline older than the preceding UTC day makes it `null` (not known).
Unknown returns stay `null`; readers must not fill them with zero, carry an
older day's return, or infer a gain from a change in portfolio value. Owner
performance points include `daily_return_reason` and `cumulative_return_reason`.
These fields are null for measured returns and explain unknown ones. Status and
detail use `dailyReturnReason` and `cumulativeReturnReason`. A metric without
enough measured returns is null, with an insufficient-data message. Public
strategy summaries use `totalReturn: null` with `totalReturnReason`; their history
points use a null `value` with `reason`.

Cumulative and daily fields use ratios: `0.01` means 1%. Trades and commissions
remain in returns. Paper portfolios do not record dividends.

Holdings and trades carry their own currency; don't guess it from the ticker or exchange. Many amounts are decimal strings, but some are numbers, and a portfolio's `total_value` is in US dollars. Check each field's schema.

For a live broker account with cash in several currencies, `live_broker_state`
reports available, reserved, and total cash for the whole account in the account
currency, beside their dollar values. For example, USD 150 available and EUR 200
available at \$1.20 per euro give 390 available in a USD account. Older snapshots
get these whole-account native totals on their next sync.

A short holding has a negative `quantity` and `current_value_usd`, and `total_value` is cash plus the signed value of every holding. `unrealized_pnl_pct` is the holding's P\&L as a share of its cost, so a short whose price falls shows a gain. In a paper portfolio, a holding moves to `exited_holdings` once the absolute value of its quantity is below 0.0001, so a long or short of 0.00009 shares has exited. In a live portfolio, it moves only when its quantity is zero.

For a portfolio valued from its holdings, an open holding without a usable current price makes the current total and all weights unknown. A live mirror keeps the non-null total from its latest broker snapshot even when a local holding lacks a price. A price placeholder, a holding imported without a price that the starting capital leaves out, is not funded, so it stays out of the total, the weights and the P\&L totals. When the current total is unknown, the list returns `total_value: null`; detail returns `metrics.totalValue: null`. Both include `valuation_unavailable_reason` and `valuationError` with a `code` and `message`. Detail does not use older metrics in place of an unknown current total. Paper status and summaries return `current: null`, empty `holdings`, and `valuationError` with code `PORTFOLIO_VALUE_UNKNOWN` and a reason. Historical rows keep their own dates and values.

Holdings history records the book each time it changes: on import, on each
fill, when cash is reserved or released, on broker sync and when the portfolio
is liquidated. It also records the book every 15 minutes during market hours. A
change and its record are saved together, so a past date reads the book as it
stood then.

Holdings history keeps every open line in an unpriced snapshot. All weights are
null, and `valuation_unavailable_reason` names the lines that lack a usable price,
currency or exchange rate. Those lines also have null prices and values. A
price placeholder is the exception: it stays in each record with
a null value, weight and price and its own reason, and the other lines keep
their values and weights.

A live mirror's broker total does not prove that each local holding has a usable
price. Detail and portfolio tools keep that total and name any missing local
valuation in `valuation_unavailable_reason`. Priced holdings keep their values;
unpriced holdings have `current_value_usd: null`. Portfolio status includes this
reason even when it does not include holding rows. Public strategy holdings keep
priced weights against the broker total and return a null weight with the price
reason for each unpriced line. Trading status and summaries require all local
prices, so they return the whole-book refusal above even with a known broker total.

If any open holding lacks a current value because its price is missing or not above zero, its currency is missing, or its exchange rate is missing or invalid, its `unrealized_pnl` and `unrealized_pnl_pct` are `null`. Detail also returns `null` for `pnl_metrics.total_unrealized_pnl`, `total_pnl`, and `total_pnl_pct`, with the same valuation reason. Recorded realized gains and losses stay available. A dollar holding needs an exchange rate of exactly 1; a holding in another currency needs a positive rate.

A known net asset value of zero or less also has no current valuation or weights. The list returns `total_value: null`; detail returns `metrics: null`. Both include `valuationError` with code `PORTFOLIO_VALUE_NOT_POSITIVE` and a message giving the value, plus the same message in `valuation_unavailable_reason`. They do not substitute older stored metrics.

Transitions refuse zero net asset value while any non-cash position has a nonzero
quantity, including offsetting longs and shorts. With no open positions, a
zero-value transition queues no trades and retires open intent.

Paper status and summaries apply this check even when the portfolio has no open
holdings. An empty book worth \$0 returns `current: null`, empty `holdings`, and
`PORTFOLIO_VALUE_NOT_POSITIVE`, with or without older stored metrics.

Position returns need an entry cost. When that cost is zero, `unrealized_pnl_pct` (or `pnl_pct` in paper status and summaries) is `null`, because the return is not known. The dollar gain or loss still uses the signed quantity.

For a portfolio valued from its holdings, an open holding without a usable current price or exchange rate makes its net asset value and weights unknown. Factor cards use a separate rule: that line counts as zero, gets no weight, and appears in `missing_price_symbols:<count>:<SYMBOLS>`. The other factor-card weights use the valued lines, including cash. For example, $100 cash, one AAPL share at $100 and an unpriced MSFT short give AAPL and cash weights of 0.5 each on the factor card. A missing exchange rate for a line in another currency has the same effect. Forecast backtests leave all weights unavailable and name the unvalued lines; they never use entry cost as a current price. A price placeholder isn't funded: factor cards and backtests leave it out of the figures and name it in `missing_price_symbols`, and a backtest keeps the other weights.

Zero and negative prices are unusable. A dollar cash unit must have a price of exactly one; another currency's cash unit must match its dollar rate within 0.000001. Cash with an unusable price follows the same missing-price rule as any other holding.

A portfolio created by import includes `portfolio_value`: the amount, currency, date, basis and source you accepted at import. It is the value you confirmed then, not a live price. Older portfolios may return `null`.

When a portfolio response includes `factorScorecard`, its `coverage` reports
`longWeight`, `shortWeight`, `grossWeight` and `netWeight` as decimal strings.
They exclude cash: long is the sum of positive weights, short the sum of negative
weights, gross is long minus short, and net is long plus short. A neutral book
with long `"1.00000000"` and short `"-1.00000000"` has gross `"2.00000000"` and
net `"0.00000000"`. Each field is `null` when the stored scorecard lacks it.

A factor card holds two dates. `factorScorecard.asOf` is the model date: prices
and the risk model end on it. The worker calculated the card later, from the
holdings, fund books, peers and benchmark constituents on file at that time, so
a book uploaded after the model date can be in it. A request with
`factor_as_of` returns the newest card whose model date is on or before that
date, and its book can be newer than the date you asked for. The response gives
the calculation time in `scorecard_input_dates.holdings_fund_books_peers_and_benchmark_read_by`
and says what each date covers in `scorecard_input_dates.meaning`. That time is
`null` for the current card, which stores none. A response with no card has no
`scorecard_input_dates`.

After import or a holdings change, the queued factor refresh adds missing
names to the active model before it builds the scorecard, including names in
held funds, the benchmark and the peer portfolio. It fetches a held or peer
fund's book when none is stored. Scorable names need no further action or
daily model run. A name with too little new data stays covered while its
older model rows remain fresh, then becomes uncovered. If adding names
fails, the scorecard uses the covered names and carries a warning. An expired
model or stale stored fund book can still delay the scorecard.

Live model inputs expire after 48 weekday hours, counted in UTC. Saturdays and
Sundays add no age; holidays still count. A Friday model stays fresh all Monday
and expires just after Tuesday 00:00 UTC if no newer inputs replace it. Stored
fund books expire 48 calendar hours after fetch; weekend runs renew them too.
Historical model reads keep their 45-calendar-day limit.

A held fund's derivative counts at its notional, the size of the position it
stands for, and only for risk. A contract for difference on a share the same
fund holds adds its notional to that share. A small future the fund offsets
itself rides on the fund's own returns: at most half a percent of the fund, on
the same side as its other holdings. The scorecard names every other
derivative with its reason, such as "a derivative's underlying is not mapped
to a listing", counts its share as uncovered and prices the rest of the fund.
Where a fund's rows add to one with the derivatives inside, each line is
restated over the fund's net assets, its rows other than derivatives. A
fund's contra rows only offset its derivatives and carry no exposure.

A fund whose weights are shares of a total its rows cannot size is not priced.
The scorecard names the fund with "a derivative's exposure is not known",
counts its weight as uncovered and measures the rest of the book. A
comparison treats such a fund in a benchmark or peer book the same way.
A name the model covers but whose prices the day's read could not get keeps
its modelled risk when nothing else in the book is priced from returns.
Otherwise the scorecard names it with the read's cause and counts its weight
as uncovered, because its link to the returns-priced holdings is not known.
When a holding priced from its own returns needs another holding's link to it,
and a series is too short or the two share too few dates, the scorecard names
the holding whose series causes the gap, with that cause, and counts its
weight as uncovered; the rest of the book is measured. A holding priced from
its own returns with no series at all is named the same way.
A modelled name whose link a returns-priced holding needs is set aside the
same way when it has no series or too few shared dates. Held directly, it is
named with that cause and its weight counts as uncovered. Held through a fund,
it is priced with the rest of the fund from the fund's own returns, and the
scorecard names it; if the fund has no usable series either, the fund is
named instead. The fund's other modelled holdings keep their risk, and only the
part of the fund they do not cover counts as uncovered.
An unread name stays outside the new additions without a missing-data reason;
other names can still be added, and the scorecard carries a warning.
The model trusts prices as they arrive and leaves no name out for the size of a
move. A bad price that comes back costs that day's price, not the name's rows.
An earlier model run recorded the reason `prices_not_usable` for a name whose
price rose a hundredfold or more in one day: "Its price rose more than a
hundredfold in one day, so its risk is not known." The model keeps that answer
for the dates it covers, and older model rows for such a name are not carried
forward. When a held fund holds such a name, the scorecard names it under the
fund with its share of the fund, and the fund's own returns measure that
weight.

Closed residues below 0.0001 shares on either sign do not block performance
refreshes or affect the inferred benchmark. This holds even when the residue
has no current price or currency evidence. An open short still needs usable
valuation evidence and counts when choosing the benchmark.

Stock splits preserve a paper position's direction. A two-for-one split changes
a short of 100 shares at a $100 entry cost per share to a short of 200 shares
at $50 per share. The same adjustment applies when a missed split is backfilled.
Live holdings follow the broker's split records.

## Create a portfolio

`POST /portfolios` takes `name` and `allocated_amount`, plus optional `description` and `strategy_id`. Allocations must be between 1 and 1 billion US dollars, or the equivalent. It returns `202`.

> [!WARNING]
>
> **New paper portfolios execute automatically.** `POST /portfolios` turns on automatic execution, so a linked strategy's paper trades run without an approval step. To review each trade, turn it off with `PATCH /portfolios/{id}/auto-execute`.

To link a strategy later, use `PUT /portfolios/{id}/link`; `DELETE` on the same path unlinks it. To turn automatic execution on or off for a paper portfolio, use `PATCH /portfolios/{id}/auto-execute`. Live portfolios ignore this setting: their trades always wait for your approval.

Linking to a live broker portfolio returns `400 SHORT_TRADE_UNSUPPORTED` if the strategy's latest decision target has any negative non-cash weight or its current book holds an open short. An open short has a negative quantity with an absolute value of at least 0.0001. The response says: “Live short sales are not supported. A strategy whose book or latest target holds a short cannot link to a broker.” The strategy remains unassigned.

## Import holdings

Importing turns a list of holdings into a new paper portfolio in two steps:

A draft accepts up to 3,000 holding rows. Both steps use this limit, including rows left out of the import.

1. **Validate.** `POST /portfolios/import/validate` matches each holding to an instrument, prices it and returns a draft with an `approval_token`.
2. **Commit.** After a person has reviewed the draft, `POST /portfolios/import` saves it. Send the `normalized_draft` from step 1 with `approved_at` set to when it was approved, the `approval_token`, and the output ID with `:approved` added to the end.

Check the body, not only the status code: validation problems come back as `200` with `ok: false` and a list of issues. If the token expires, prices move, or you change the draft, validate again.

An invalid row does not stop the review from pricing valid rows. A failed validation can return `holding_prices` and `row_warnings` beside `row_errors`. When `valuation_scope` is `valid_rows`, `book_summary` covers only those valid rows. It is not the full book's exposure. No approval token is issued while errors remain; fix them and validate again before importing.

In the draft, check `valuation_coverage`. When it is `partial`, some holdings had no current USD price; they are listed in `unvalued_holding_row_ids`. Without a stated `portfolio_value` they are also left out of `allocated_amount_usd`; a stated value already counts them.

Check and import responses also return `row_warnings` for holdings with missing valuation inputs. Each issue has `severity: "warning"` and the holding's `row_id`. Missing-price warnings use `field: "price"`; a missing exchange rate uses `field: "currency"`. A warning does not block confirmation. The codes mean:

| Code | Meaning | Example message |
| - | - | - |
| `price_not_fetched` | The price vendor refused, failed or did not answer. Retry the check. | AAPL.US: the price vendor did not answer; try again. |
| `no_recent_close` | The vendor omitted the name, returned a different or missing exchange, or has no usable positive close in the last 14 days. | BRK-B.US: no close in the last 14 days. |
| `holding_fx_unavailable` | The holding has a native price, but its rate to US dollars is unavailable. | VOD.LSE: exchange rate from GBP to USD is unavailable. |

Pricing adds no missing-close warning when a holding already has a validated usable unit value. Uploaded `unit_value` fields remain ignored; they do not replace provider prices. These warnings leave the existing unknown book value and weight rules below in place.

When the upload states no `portfolio_value`, a holding imported without a price becomes a price placeholder: the starting capital leaves it out, so it is not funded, and the price refresh never prices it. A stated `portfolio_value` already counts every holding, so a US dollar holding imported without a price stays funded, and the next price refresh prices it. A holding in another currency becomes a placeholder either way, because it has no rate to US dollars without a price.

A paper `BUY` or `SELL_SHORT` fill replaces a placeholder's quantity, even when the imported quantity has the opposite sign, and clears the placeholder flag. A `SELL` or `BUY_TO_COVER` cannot close that row because it has no funded position.

A short is a holding with a negative `quantity`. A row can instead keep a positive quantity and set `side` to `short` or `S`; validation makes the quantity negative. Cash can be negative, for borrowed cash. When every holding has a current value, cash plus holdings at current prices must be above zero. `book_summary` in the validate response gives the long, short, gross and net value in US dollars and as a share of net asset value; cash counts in net asset value but not in exposure.

If any included holding has no current US dollar value, `book_summary.nav` and all four weights are `null`. The affected leg's value is `null`; `gross_value` and `net_value` are also `null`. A leg with no holdings has value `"0"`. Show `valuation_unavailable_reason` with these unknown values. Keep `valuation_coverage` and the unvalued row list; `allocated_amount_usd` is the stated `portfolio_value`, or without one the partial allocated amount; it does not establish net asset value. Validation refuses a known net asset value at or below zero. When some holdings have no current value, it refuses a book whose cash plus priced holdings is below zero, because that sum becomes the starting capital.

Both validation and import check the current priced total even when `portfolio_value` declares a positive amount for an earlier date.

```text theme={null}
Unvalued holding → affected leg, gross and net value unknown
                 → net asset value and every weight unknown
```

Importing creates a portfolio only, not a monitor or strategy. Repeating a commit returns the same portfolio.

Paper portfolios can open, add to and cover shorts without a separate setting. A trade keeps a positive quantity and names its side: `BUY`, `SELL`, `SELL_SHORT` or `BUY_TO_COVER`. The planner splits a change across zero: selling 150 shares from a holding of 100 queues `SELL 100`, then `SELL_SHORT 50`. Buying 80 against a short of 50 queues `BUY_TO_COVER 50`, then `BUY 30`. Once the whole change meets the minimum trade amount, the planner keeps the closing leg even below that minimum. It drops an opening leg below the minimum, so the trade only closes the old position.

A short sale adds its proceeds to cash, less fees. The planner funds covers, including fees, from current cash plus net long-sale proceeds after fees in the same basket. Starting cash may be negative if those sales repay the debt and fund the covers. Short-sale proceeds cannot fund a cover in that basket. For opening buys, current cash less any cover cost not funded by net long sales must exceed buy notional times 1.10. Each fill debits its fees.

Fills run in this order: `SELL`, `BUY_TO_COVER`, `SELL_SHORT`, then `BUY`. Closing legs keep their share quantity through price changes; opening legs use their target dollar amount at the fill price. A closing fill cannot exceed the held quantity. A cash refusal fails only that leg while the rest continue, so a basket can settle in part. A cover that lacks cash stays queued while a long sale in its basket is still queued, such as one on a closed exchange, and fails once none is. Live broker trades and mirror sync still refuse shorts.

Live rebalance validation and submission return `SHORT_TRADE_UNSUPPORTED` if any non-cash target weight is negative or the portfolio holds an open short (negative quantity with an absolute value of at least 0.0001). This check runs before the minimum trade amount and no-trade handling, so an unchanged short or a target worth less than \$1 still refuses the rebalance. The refusal includes: “Itoflow cannot yet open, add to or cover a short.”

Trade history returns those four sides and `SPLIT` for a corporate action adjustment. `SPLIT` is a history record, not an order side.

## Compare with a benchmark

`PUT /portfolios/{id}/benchmark` needs both `peer_portfolio_id` and `benchmark_index_id` every time; send `null` to clear one. A peer must be another of your active portfolios. The comparison updates in the background, so new results appear a little later.

## Measure risk through MCP

Through [MCP](https://docs.itoflow.ai/mcp/install.md), your assistant calls the public [risk run operations](https://docs.itoflow.ai/api-reference/risk-runs.md) to measure a saved portfolio or a what-if book against a benchmark and up to 10 peer funds. The result covers risk, exposures, attribution, factor profit and loss, bets, sizing, scenarios and valuation, the same breakdown chat shows. A fund held beside other holdings is measured through its holdings at the line's signed weight. Cash the fund holds counts as the book's cash.

Factor risk excludes shared-specific factors. Specific risk includes each
name's own variance and shared-specific factors: residual moves several names
share. Those shared moves can link separate books, so their specific variances
need not add. Factor and specific variances add to total variance; the squares
of their reported volatilities add to the square of total volatility.

An unresolved fund holding stays outside the model at its signed weight and does not change a direct holding of the same name. If no usable fund book is available, the run measures the other names and gives the fund line's reason. A failed request stays distinct from an absent or expired book. A held fund's book passes the same weight check as a fund measured on its own: when its stated weights differ from what its share counts and prices are worth, the run rebuilds them from those values, or refuses the book and names the refusal as the fund line's reason.

A fund inside a held fund is looked through the same way, at the product of the signed weights, down to three funds. A fund that holds itself, or sits below the third, stays one gap at its weight with the reason code `no_fund_book`.

| Method in `execute` | What it does |
| - | - |
| `api.startRiskRun` | Starts a run and returns its `run_id`. |
| `api.getRiskRun` | Reads the run: `running` until it ends, with `progress` once the run reports it, then a short summary and the full panel. |
| `api.cancelRiskRun` | Stops a run by its `run_id`. |
| `api.startRiskMeasure` | Starts a measure of 1 to 10 books on one factor model version, at one date or over up to 60 dates, and returns its `measure_id`. |
| `api.getRiskMeasure` | Reads the measure: `running` with `progress` until it ends, then one entry per date. |
| `api.cancelRiskMeasure` | Stops a measure by its `measure_id`. |

```js theme={null}
const started = await api.startRiskRun({
  body: {
    request_key: "4f1c2a9e-7b3d-4e8a-9c61-2d5b8f0a6e17",
    request: {
      portfolio: { holdings: [{ symbol: "QQQ", weight: 0.9 }, { symbol: "CASH", weight: 0.1 }] },
      benchmark: "SPY",
      peers: [],
      include_forecast_quality: false,
    },
  },
});
// In a later call:
const run = await api.getRiskRun({ run_id: started.run_id });
```

`include_forecast_quality` defaults to `false`, as in the example. Set it to
`true` to request the historical forecast-quality check and its factor snapshots.
Current risk, coverage checks, attribution, risk change and scenarios remain
included, with the history each needs. The `trust` section says “Forecast quality
not requested.” and appears in `missing_by_design`, not `missing_sections`.
This omission alone does not make the run partial. Monitor runs retain the full
report. Use a new `request_key` when changing this option; omitted and `false`
mean the same request.

`analyses` chooses what the run computes: `["risk"]`, `["performance"]` or
both, the default. Risk covers risk, risk change, bets, sizing, scenarios,
drivers, trust, exposures, liquidity and valuation. Performance covers
performance, attribution and factor P\&L. A run of one analysis skips the
factor and price reads its sections do not use, so a performance-only run
skips covariance reads. Every run reads the same holdings books, so a section
gives the same figures as in a run of both. Only risk reads the current model,
so without it `unmodelled_count` is null and `coverage` is absent. The
other sections appear in `missing_by_design` with the reason `not_requested`,
and they alone do not make the run partial. Compliance and changes come only
with both. Omitting `analyses` and listing both mean the same request; listing
one is a new request and needs a new `request_key`.

When performance is requested, a saved portfolio reads its stored return series,
unless the run looks through it as one listed fund; that path uses the fund's
prices. Empty or unknown stored readings put `performance` in `missing_sections`,
which makes the run `partial`. A risk-only request still lists unrequested
performance in `missing_by_design`. An unlisted what-if has no stored return
series, so its performance remains missing by design.

```mermaid theme={null}
flowchart LR
  Request[Risk run] --> Current[Current risk and other sections]
  Request --> Option{Include forecast quality?}
  Option -->|Yes| History[Historical snapshots and forecast check]
  Option -->|No, default| Omitted[Forecast quality not requested]
```

A what-if holds 1 to 3,000 lines. A saved portfolio may hold up to 3,000 securities. A run refuses more with `invalid_risk_engine_request`. Weights are fractions of the book's net assets; a short is below zero. With a `CASH` line, weights add to 1. Without one, the rest is cash; a book that borrows lists `CASH` below zero. At most three runs may run at once. Each newly launched background job has a 60-minute budget, including coverage and output. Initial validation, startup and stopping a timed-out job can add time before its terminal status is available.

For figures at past dates, over a range of dates, or for several books side by side, your assistant starts a [risk measure](https://docs.itoflow.ai/api-reference/risk-measure.md) instead. One measure covers a single date or a range of up to 60, month ends by default; a year of month ends on a few books takes a minute or two. It reads a saved portfolio at its newest snapshot by the end of each date, unless you name another time, and funds and the benchmark through their stored holdings on or before that date. A measure counts toward the three runs while it runs, and its answer stays for 24 hours after it ends. It does not add names to the model; a risk run on the book does. For a worked example, see [Measure a book's factor risk](https://docs.itoflow.ai/use-cases/measure-factor-risk.md).

Before measuring, the engine asks for names missing from the selected model publication, including holdings inside fund lines and the benchmark and peers. It checks every five seconds for up to ten minutes. A tool run leaves five minutes of its remaining deadline for measurement, so a retry can wait less. Submission, status reads and polling all share this wait budget. Names already covered need no wait. The run measures available coverage when a request fails or times out; fresh older rows can still cover a name under the model's freshness rule. A name the model refused to add at that publication stays outside current measurements even when fresh older rows exist. Its signed weight and refusal reason appear in each book that holds it; those older rows still serve history.

A run whose books hold names the model has not scored first waits up to 10 minutes for coverage within the same job budget. While it waits, `api.getRiskRun` returns `running` with a `progress` object: `{ stage: "waiting_for_coverage", names_asked, waited_seconds, expected_seconds, at_most_seconds }`, then `{ stage: "measuring" }`. `expected_seconds` is about 2 seconds for each name asked, never more than `at_most_seconds`, the most this run waits before it measures what it has. `measuring` means the run is computing figures from the coverage it has. A retry shows plain `running` until it reports fresh progress. A run that asks for no names goes straight to `measuring`. When a run that asked for names ends, its risk section says so in one sentence, for example "Asked the factor model to score 233 names and waited 7 min 0 s." The sentence counts requests and wait time across all attempts. A run that asked for none adds no sentence. Monitor runs add the same sentence.

The result's `coverage` list holds every gap in the subject, benchmark and each peer, with `book`, `symbol`, signed `weight`, `reason_code` and `reason`. `unmodelled` remains a view of the subject's 20 largest gaps by absolute weight, and `unmodelled_count` counts all subject gaps. The receipt groups the full list by book and reason, with each group's share of the book. Reasons include the model's refusal to add a name, `not_added_in_time`, `coverage_failed`, `no_fund_book`, `no_equity_listing`, `cash_currency`, `not_listed` and `invalid_inputs`. A failed provider read is `coverage_failed`, not a model refusal. A held fund with no usable holdings book keeps one gap at its signed outer weight and gives the fetch's reason. A row with no confirmed equity listing stays a separate signed gap even when its label matches a covered holding in another book.

| Reason code | What the run says |
| - | - |
| `not_added_in_time` | The model had not scored this holding before the wait ended. |
| `prices_short_of_tail` | Its prices do not cover the dates the model needs. |
| `prices_not_usable` | Its price rose more than a hundredfold in one day, so its risk is not known. |
| `specific_risk_out_of_range` | Its stored specific risk is too large to use, so its risk is not known. |
| `no_stored_state` | The model lacks the saved inputs needed to score this holding. |
| `earlier_specific_risk_rule` | The model's saved inputs for this date use an earlier risk method, so this date cannot score the holding. A later date can, if the model's daily run covers the holding or a new request asks for it at that date. |
| `invalid_inputs` | This holding could not be valued; its risk is excluded. |
| `no_equity_listing` | A line with no confirmed equity listing, such as a bill or bond; no run can add it. |

A dropped peer's reason states the cause. For example: "Its holdings book is dated 2026-09-17, after the report date 2026-09-16." A provider answer with only sector or region totals says so; it does not count as a holdings book.

Current fund holdings use the book's own date, even when the factor publication
is older. A short fund line therefore measures the same risk as its holdings
written out at that signed weight. A source row with no confirmed listing has the
reason code `no_equity_listing`; the run does not ask the worker to score its
bare ticker. Each
`reason` explains the gap in plain words and names the subject as the portfolio
or fund. The `book` and `reason_code` fields retain their machine identifiers.

A peer whose holdings book cannot be read after fetching is dropped from comparisons and named in `dropped_peers` with the fetch's reason. The remaining peers still take part. A benchmark with no required book keeps the existing refusal rule. Monitor runs use this same measurement and receipt.

Keep one `request_key` for each new run. The same key and request join that run for 24 hours after it ends. Another request with that key returns 409. Finished results and full artifacts stay available for 24 hours even after the sandbox sleeps. Successful runs include a top-level `analytics` reference to the complete schema-version-2 document. Read it with `GET /risk/runs/{run_id}/artifact`; use gzip ranges and verify both hashes for a large copy. All three groups of exposure sides, sizing modelled-book scope and valuation comparisons remain present. A publication failure fails the run. See the [risk-run transport contract](https://docs.itoflow.ai/api-reference/risk-runs.md). Cancellation, timeout and container loss leave a known terminal outcome.

A failed run returns 200 and says why in `failure_code` and `reason`. `engine_refused` means change the request. `rate_limited` and `provider_unavailable` mean the price provider failed: start the run again later, with a new `request_key`.

Risk runs need to be enabled for your account. When they aren't, all three operations return 403 with `feature_not_enabled`. Through `api.*`, a refused call returns `{ ok: false, status, body }`. If the runtime does not answer, the operation returns 503; retry with the same key.

## Approve trades

`GET /portfolios/approvals/pending` lists everything waiting for your approval. For one portfolio:

1. `GET /portfolios/{id}/pending-trades` returns the proposed trades.
2. Show them to the person who will approve them.
3. `POST /portfolios/{id}/pending-trades/approve` approves them.
4. `GET /portfolios/{id}/trades/{tradeId}/attempts` shows what happened at the broker.

To cancel pending trades, `DELETE /portfolios/{id}/pending-trades`.

**Live portfolios.** Approve the exact proposal you reviewed. Copy `metadata.approvalProposal.id` and `metadata.approvalProposal.hash` from the pending-trades response and send `{ approvalProposal: { id, hash } }` in the approve request. If the proposal has changed since you read it, the approval fails. Read it again and get a fresh approval. Paper portfolios accept `{ decisionId? }`.

Live portfolios always need this approval; automatic execution can only be turned on for paper portfolios.

An approval is not a fill. Check the attempts and the portfolio afterward.

## Live portfolio operations

These endpoints act on a live portfolio and its broker account:

| Endpoint | What it does |
| - | - |
| `GET /portfolios/{id}/broker-status` | The broker connection's state |
| `POST /portfolios/{id}/sync-live-state` | Refreshes holdings and cash from the broker |
| `POST /portfolios/{id}/reconcile` | Reconciles Itoflow's records with the broker account |
| `POST /portfolios/{id}/go-paper` | Switches the portfolio to paper trading |
| `POST /portfolios/{id}/liquidate` | Creates sell trades for every position. On a live portfolio they wait for your approval. |
| `DELETE /portfolios/{id}` | Deletes a live portfolio. Its strategy is deactivated, and the broker is disconnected if this was its last live portfolio. |

The liquidation response includes `total_value_usd` and
`valuation_unavailable_reason`. If any open holding lacks a usable price,
currency or exchange rate, the total is `null` and the reason explains why.
The response never gives a partial sum. A queued task still has to pass trade
sizing; follow its status to check whether liquidation finishes.

`liquidate` and `DELETE` are refused while trades are pending or executing (`TRADES_IN_PROGRESS`). `DELETE` can't be undone. Some of these return a task ID; follow it with `GET /transition/{taskId}` until it finishes.

See the [portfolio endpoint reference](https://docs.itoflow.ai/api-reference/coverage.md#portfolios) for every field.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.