Skip to main content

Position Groups

A position group is a named strategy assembled from individual positions. A leveraged loop spread across a collateral position and a borrow, or a cash-and-carry split between spot and a perp, is one strategy to an investor and several rows to a ledger. Groups are the mapping between the two. They matter for reporting because an LP report’s strategies can be backed by groups: the strategy’s NAV, returns and allocation slice then come from the group’s legs rather than from a single share token.
A group is a view, never a ledger write. Every endpoint here reads positions the reconciliation already produced; grouping them changes presentation, not accounting.

Base path

Different service and prefix from the Report Studio, so a key issued with a path allowlist needs /portfolio-management on it. Every endpoint requires the admin, trader, or pms_user role plus access to the portfolio.
portfolio_id goes on the query string for every verb, and additionally in the body on POST. That is not redundancy. The access decorator special-cases POST: when a JSON body is present it reads portfolio_id from the body only, with no fallback to the query string. A create request carrying the id solely as a query parameter is rejected with The portfolio_id parameter is required! before the handler runs. Send it in both places on POST.

Response envelope

Success responses return the payload under a named key (groups, group, legs, membership) with no wrapper. Errors use the platform-wide shape:
Rejected writes may also carry a details object explaining what failed validation.

Reading groups

GET /strategy-types

The closed vocabulary for strategy_type. No portfolio needed.

GET /position-groups

string
required
The portfolio whose groups to list.
Returns { "groups": [ ... ] }, each group carrying group_id, portfolio_id, name, strategy_type, legs, mandate, notes and creation metadata.

GET /position-groups/{group_id}

One group. 404 when the id doesn’t belong to that portfolio.

GET /position-groups/legs

The leg catalog: every groupable position in the portfolio, each annotated with the group that already owns it (group, or null when unassigned). Sorted by absolute USD value, largest first. This is what a “group these positions” picker reads. A leg is identified by leg_uid, derived from account_id, chain and key, where key is a contract address, native, or a synthetic id such as a Morpho market.

GET /position-groups/membership

The same relationship inverted: leg_uid -> group summary, so a positions table can badge each row with the strategy that claims it without walking every group.

Writing groups

POST /position-groups

string
required
Also required in the body, see the warning above.
string
required
The portfolio this group belongs to.
string
required
Display name, and the name the LP report’s strategy will carry.
string
required
One of the ids from /strategy-types.
array
required
The member positions, each { account_id, chain, key }. leg_uid is derived server-side.
string
Free text describing what the strategy is for. Carried through to the LP report.
string
Internal note, not rendered on statements.
Returns 201 with the created group.
The “Unreconciled (capital basis incomplete)” row cannot be grouped. It’s a disclosure that the books don’t fully close, not a position, and attempting to include it is rejected.

PUT /position-groups/{group_id}

Update name, type, legs, mandate or notes. portfolio_id on the query string.

DELETE /position-groups/{group_id}

Removes the group. The positions are untouched; they simply become ungrouped again.

Derived values

Everything below is computed from ledger rows the reconciliation already produced. None of it writes.

GET /position-groups/aggregate

Every group’s combined value, cost, P&L and return, plus the ungrouped remainder, so the caller sees the whole portfolio partitioned rather than just the parts someone remembered to group.
The ungrouped block is the honest half of this endpoint. A dashboard that charts only groups will silently understate the portfolio by whatever hasn’t been assigned yet.

GET /position-groups/{group_id}/history

The group’s daily value curve, summed from its legs’ existing series. No backfill: days a leg has no data for are days the curve doesn’t claim a value, rather than being interpolated into one.

GET /position-groups/foot-check

Asserts that portfolio NAV equals the sum of groups plus ungrouped legs, and reports the residual with the reason when it doesn’t.
Worth surfacing rather than hiding. This check exists because a partition that doesn’t add up is a real condition, not a rounding artifact, and it names what broke rather than absorbing the difference into a slice. If you show group allocations to an LP, show them from a portfolio whose foot check is clean.

How groups reach an LP report

A report strategy backed by a group carries these fields in /config under strategies[]: The strategy’s returns, allocation slice and per-strategy detail on the statement then derive from the group’s legs.
Re-sending strategies without these fields strips the group’s identity and its NAV. The array replaces wholesale on PUT /config, so a client that reads the config, edits one strategy and sends the array back will drop them unless it preserves every field it didn’t mean to change. The backend rescues the four fields when it can match the strategy by position, but do not rely on that: read, modify in place, and send back what you received.