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

# Position Groups

> Group raw positions into named strategies, and read their combined value, P&L and history

# 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.

> [!NOTE]
>
> 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

```
https://api.renesis.fi/portfolio-management/position-groups
```

Different service and prefix from the [Report Studio](https://docs.renesis.fi/lp-reporting/api/introduction.md), 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.

> [!WARNING]
>
> **`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:

```json theme={null}
{ "isError": true, "statusCode": 404, "message": "Group not found" }
```

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.

```json theme={null}
{ "types": [
  { "id": "carry",       "label": "Carry" },
  { "id": "loop",        "label": "Leveraged Loop" },
  { "id": "basis",       "label": "Cash and Carry" },
  { "id": "directional", "label": "Directional" },
  { "id": "hedge",       "label": "Hedge" },
  { "id": "liquidity",   "label": "Liquidity Provision" },
  { "id": "yield",       "label": "Yield" },
  { "id": "other",       "label": "Other" }
] }
```

### GET /position-groups

**portfolio\_id** `string` *required*

The portfolio whose groups to list.

```bash theme={null}
curl -H "X-API-Key: $API_KEY" \
  "https://api.renesis.fi/portfolio-management/position-groups?portfolio_id=$PORTFOLIO_ID"
```

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

**portfolio\_id** `string` *required*

Also required in the body, see the warning above.

**portfolio\_id** `string` *required*

The portfolio this group belongs to.

**name** `string` *required*

Display name, and the name the LP report's strategy will carry.

**strategy\_type** `string` *required*

One of the ids from `/strategy-types`.

**legs** `array` *required*

The member positions, each `{ account_id, chain, key }`. `leg_uid` is derived server-side.

**mandate** `string`

Free text describing what the strategy is for. Carried through to the LP report.

**notes** `string`

Internal note, not rendered on statements.

Returns `201` with the created group.

> [!NOTE]
>
> 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.

```json theme={null}
{
  "groups": [ ],
  "ungrouped": { },
  "foot_check": { }
}
```

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.

> [!WARNING]
>
> 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`](https://docs.renesis.fi/lp-reporting/api/configuration#the-unified-configuration-shape) under `strategies[]`:

| Field | Meaning |
| - | - |
| `position_group_id` | The group backing this strategy |
| `position_group_nav_usd` | The group's NAV, imported at configuration time |
| `strategy_type` | One of the `/strategy-types` ids |
| `mandate` | The group's mandate text |

The strategy's returns, allocation slice and per-strategy detail on the statement then derive from the group's legs.

> [!WARNING]
>
> **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.


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