---
title: "Account Groups"
description: "An account group is a compact identifier (the account group id, a 32-bit value) that names a set of accounts. It lets the host apply an abstract."
---

<!-- markdownlint-disable MD033 -->
# Account Groups

An `account group` is a compact identifier (the [`account group id`](#the-account-group-id-type),
a 32-bit value) that names a set of accounts. It lets the host apply an abstract,
group-wide action - or read a per-account decision by group - without
enumerating individual accounts on every call.

Typical uses:

- tag accounts that share a hedging book, a desk, or a strategy, then branch
  policy logic on the group instead of a hard-coded account list;
- route an abstract action ("treat this group differently") across a set of
  accounts identified by one id rather than per account;
- keep a policy's per-request lookup cheap: the bound account's group is read
  once and cached for the rest of the call.

## Account Groups vs Policy Groups

The `account group` and the `policy group` are unrelated concepts; do not
confuse the two:

- An `account group` (the [`account group id`](#the-account-group-id-type))
  partitions **accounts**. It is owned by the engine's membership registry and
  answers "which group does this account belong to?".
- A `policy group` (the `policy group id`) partitions **policies**. It scopes
  pre-trade lock records and related per-policy state; see
  [Pre-Trade Lock](Pre-Trade-Lock.md).

They share neither identifier space nor registry.

## Membership Registry

The engine owns a single membership registry. An account belongs to **at most
one** group at a time. Reach the registry through the `accounts` handle the
engine exposes; the handle is cloneable and inherits the engine's
synchronization mode.

The registry exposes three operations:

- `register group` - register every listed account into one group.
- `unregister group` - remove every listed account from a group.
- `group of` - read the account's current group, or nothing when it is
  ungrouped.

[Account retirement](Account-Retirement.md) removes the retired account's group
membership. It does not remove the group or its block.

### Atomic Batch Semantics

Both `register group` and `unregister group` are **all-or-nothing** over the
accounts passed in a single call:

- `register group` fails if **any** listed account is already a member of a
  group - including the same target group. On failure no account is registered
  and the error names the offending account and its current group.
- `unregister group` fails if **any** listed account is not currently a member
  of the requested group (ungrouped, or in another group). On failure no
  account is removed and the error names the offending account.

Atomicity holds under every synchronization mode, including the multi-threaded
one: a failed multi-account call leaves the registry exactly as it was.

## The `account group id` Type

The `account group id` is a type-safe 32-bit identifier with two **fallible**
constructors:

- build it from an integer - zero cost, zero collision risk. Prefer this
  whenever group IDs are already numeric. Fails when the value is `0`, the
  reserved [default account group](#default-account-group).
- build it from a string - convenience constructor that hashes the string with
  FNV-1a 32-bit. Fails on an empty (or whitespace-only) string. Collisions are
  theoretically possible; for `n` distinct group strings the probability of at
  least one collision is approximately `n² / (2 × 2³²)`. A string whose hash
  would land on the reserved default is remapped, so a non-empty input never
  names the default group.

Construction reflects the failure in each language: Rust returns a `Result`
(`AccountGroupId::from_u32(7)?`), Go returns `(AccountGroupID, error)`,
C++ throws `openpit::Error` (`AccountGroupId::FromUint32(7)`), and
Python raises on the reserved value.

Use exactly one constructor family per runtime state: mixing a hashed
string-derived id with a direct numeric id can collapse two distinct groups into
one key.

## Default Account Group

Every account belongs to the **default account group**
(the `default account group`, value `0`) until it is explicitly assigned to
another group. The constant is exposed per language:

- Rust: `openpit::param::DEFAULT_ACCOUNT_GROUP` (alias `AccountGroupId::DEFAULT`);
- Go: `param.DefaultAccountGroup`;
- Python: `openpit.param.DEFAULT_ACCOUNT_GROUP`;
- C++: `openpit::param::DefaultAccountGroup`;
- C: `OPENPIT_DEFAULT_ACCOUNT_GROUP`.

The default is a real, addressable id (callers may key per-group settings on it),
but it is **reserved**: no constructor can produce it (building from an integer
rejects `0`, and building from a string never hashes to it), and it cannot be
passed to `register group` or `unregister group`. The only way to name it is the
constant. Because no external input can forge it, a future cascading lookup can
safely fall back from a specific account to its group and finally to this
default group.

An account group id left unset is not the default group. The account-group
currency, market-data TTL and quote-publishing operations accept the default
group as a target, so the engine cannot tell an unset id from a deliberate one;
the bindings make sure an unset id never reaches it. In Go, the zero value
`param.AccountGroupID{}` is uninitialized, and those operations reject it with
`param.ErrUninitializedAccountGroupID`, as does a market-data read whose account
info answers with it. In C++, `AccountGroupId` has no default constructor. Rust,
Python and JavaScript have no unset value to begin with. In C the identifier is
a plain integer, so a zero-initialized one is the default group: set it
explicitly.

## Reading the Group from a Policy

The engine contexts expose a lazy `account group` accessor for the bound
account, so a policy can learn the account's group cheaply:

- pre-trade context (start and main stage) - the order's account; returns
  nothing when the order carries no account;
- account-adjustment context - the account being adjusted;
- post-trade context - the execution report's account. The
  `apply execution report` hook receives this context as its first argument; see
  [Policy API](Policy-API.md#post-trade-context).

The lookup runs once on first access and is cached for the lifetime of that
context, so repeated reads within one call are free.

## Examples

The examples register two accounts into one group, read membership back by id,
and unregister the group.

<details>
<summary>Go</summary>

<!-- markdownlint-disable-next-line MD013 -->
<!-- Test mirror: https://github.com/openpitkit/pit/blob/main/bindings/go/examples_wiki_test.go -->
```go
engine, err := openpit.NewEngineBuilder().
 FullSync().
 Builtin(policies.BuildOrderValidation()).
 Build()
if err != nil {
 log.Fatal(err)
}
defer engine.Stop()

// Group two accounts under one compact identifier.
accounts := engine.Accounts()
hedgeBook, err := param.NewAccountGroupIDFromUint32(7)
if err != nil {
 log.Fatal(err)
}
members := []param.AccountID{
 param.NewAccountIDFromUint64(10),
 param.NewAccountIDFromUint64(11),
}
if err := accounts.RegisterGroup(members, hedgeBook); err != nil {
 log.Fatal(err)
}

// Membership is readable by id, without enumerating the accounts.
group, ok := accounts.GroupOf(param.NewAccountIDFromUint64(10)).Get()
fmt.Println(ok, group)         // true 7
_, ok = accounts.GroupOf(param.NewAccountIDFromUint64(99)).Get()
fmt.Println(ok)                // false

// Removing the group is atomic too: every listed account must be a member.
if err := accounts.UnregisterGroup(members, hedgeBook); err != nil {
 log.Fatal(err)
}
```

</details>

<details>
<summary>Python</summary>

<!-- markdownlint-disable-next-line MD013 -->
<!-- Test mirror: https://github.com/openpitkit/pit/blob/main/bindings/python/tests/integration/test_examples_wiki.py -->
```python
engine = (
    openpit.Engine.builder()
    .no_sync()
    .builtin(openpit.pretrade.policies.build_order_validation())
    .build()
)

# Group two accounts under one compact identifier.
hedge_book = openpit.param.AccountGroupId.from_int(7)
accounts = [
    openpit.param.AccountId.from_int(10),
    openpit.param.AccountId.from_int(11),
]
engine.accounts().register_group(accounts, hedge_book)

# Membership is readable by id, without enumerating the accounts.
assert (
    engine.accounts().group_of(openpit.param.AccountId.from_int(10)) == hedge_book
)
assert engine.accounts().group_of(openpit.param.AccountId.from_int(99)) is None

# Removing the group is atomic too: every listed account must be a member.
engine.accounts().unregister_group(accounts, hedge_book)
assert engine.accounts().group_of(openpit.param.AccountId.from_int(10)) is None
```

</details>

<details>
<summary>JavaScript</summary>

<!-- markdownlint-disable-next-line MD013 -->
<!-- Test mirror: https://github.com/openpitkit/pit/blob/main/bindings/js/tests/examples_wiki.test.ts -->
```ts
import { Engine } from "@openpit/engine";
import { buildOrderValidation } from "@openpit/engine/pretrade/policies";

const engine = Engine.builder()
  .builtin(buildOrderValidation())
  .build();

// Group two accounts under one compact identifier. Account and group ids cross
// the boundary as plain numbers.
const accounts = engine.accounts();
const hedgeBook = 7;
accounts.registerGroup([10, 11], hedgeBook);

// Membership is readable by id, without enumerating the accounts.
console.log(accounts.groupOf(10)?.value); // => 7 (hedgeBook)
console.log(accounts.groupOf(99)); // => undefined (no group)

// Removing the group is atomic too: every listed account must be a member.
accounts.unregisterGroup([10, 11], hedgeBook);
console.log(accounts.groupOf(10)); // => undefined
```

</details>

<details>
<summary>C++</summary>

<!-- markdownlint-disable-next-line MD013 -->
<!-- Test mirror: https://github.com/openpitkit/pit/blob/main/bindings/cpp/test/wiki/account_groups_test.cpp -->
```cpp
#include <openpit/accounts/accounts.hpp>
#include <openpit/engine.hpp>
#include <openpit/param/account_id.hpp>
#include <openpit/pretrade/policies.hpp>

#include <vector>

openpit::EngineBuilder builder(openpit::SyncPolicy::Full);
builder.Add(openpit::pretrade::policies::OrderValidationPolicy{});
openpit::Engine engine = builder.Build();

// Group two accounts under one compact identifier.
openpit::accounts::Accounts accounts = engine.Accounts();
const openpit::param::AccountGroupId hedgeBook =
    openpit::param::AccountGroupId::FromUint32(7);
const std::vector<openpit::param::AccountId> members = {
    openpit::param::AccountId::FromUint64(10),
    openpit::param::AccountId::FromUint64(11),
};
if (auto err = accounts.RegisterGroup(members, hedgeBook)) {
  // handle err->message on conflict
}

// Membership is readable by id, without enumerating the accounts.
std::optional<openpit::param::AccountGroupId> group =
    accounts.GroupOf(openpit::param::AccountId::FromUint64(10));
assert(group.has_value() && *group == hedgeBook);  // true 7
group = accounts.GroupOf(openpit::param::AccountId::FromUint64(99));
assert(!group.has_value());                        // false

// Removing the group is atomic too: every listed account must be a member.
if (auto err = accounts.UnregisterGroup(members, hedgeBook)) {
  // handle err->message on conflict
}
```

</details>

<details>
<summary>Rust</summary>

<!-- markdownlint-disable-next-line MD013 -->
<!-- Test mirror: https://github.com/openpitkit/pit/blob/main/crates/openpit/tests/examples_wiki.rs -->
```rust
use openpit::param::{AccountGroupId, AccountId};
use openpit::pretrade::policies::OrderValidationPolicy;
use openpit::{Engine, OrderOperation};

let engine: openpit::LocalEngine<OrderOperation> = Engine::builder()
    .no_sync()
    .pre_trade(OrderValidationPolicy::new())
    .build()?;

// Group two accounts under one compact identifier.
let accounts = engine.accounts();
let hedge_book = AccountGroupId::from_u32(7)?;
accounts.register_group(&[AccountId::from_u64(10), AccountId::from_u64(11)], hedge_book)?;

// Membership is readable by id, without enumerating the accounts.
assert_eq!(accounts.group_of(AccountId::from_u64(10)), Some(hedge_book));
assert_eq!(accounts.group_of(AccountId::from_u64(99)), None);

// Removing the group is atomic too: every listed account must be a member.
accounts.unregister_group(&[AccountId::from_u64(10), AccountId::from_u64(11)], hedge_book)?;
assert_eq!(accounts.group_of(AccountId::from_u64(10)), None);
```

</details>

## Account Groups and Market Data

Account group membership has a direct effect on how the [market-data service](Market-Data.md)
resolves quotes and TTLs.

The default account group (id `0`) doubles as the market-data **"everyone-else"
bucket**: a quote pushed with the plain `push` call is stored in this bucket and
is visible to every account that has no more specific per-account or per-group
quote under the chosen resolution mode.

When a reader uses `account then group` or `account then group then default` resolution,
the service calls the `account info` object passed to `get` / `get or err` to
resolve the reading account's group. In the engine integration a pre-trade context
already satisfies this interface by looking up the membership registry; in
standalone usage any object exposing the required `group` property works. If the
account belongs to a group that has a per-group bucket for the instrument, that
bucket is tried before the default bucket. The same group is consulted when the
TTL cascade reaches the group tiers (tiers 2, 3, 5, and 6 of the eight-tier
cascade).

Key implications:

- Registering an account into a group immediately makes that account eligible for
  per-group quotes pushed via `push for` targeting that group.
- Accounts that remain in no group (or whose group has no per-group quote) fall
  through to the default bucket when the resolution mode permits it.
- To publish a different quote to a specific account group, pass the group id in
  the `groups` list of `push for`; to publish to the default bucket explicitly,
  pass the `default account group` in the same list.

See [Market Data](Market-Data.md) for the full resolution mode table and
[Market Data TTL](Market-Data-TTL.md) for the eight-tier TTL cascade.

## Related Pages

- [Account Blocking](Account-Blocking.md): block or unblock an entire account
  group in one call via the admin API on the `accounts` handle
- [Market Data](Market-Data.md): quote buckets, resolution modes, and the
  `push for` targeted push
- [Market Data TTL](Market-Data-TTL.md): the eight-tier TTL cascade, including the
  group tiers
- [Policy API](Policy-API.md): policy hooks and the lazy `account group` accessor
  on the pre-trade, account-adjustment, and post-trade contexts
- [Account Adjustments](Account-Adjustments.md): non-trade adjustment batches and
  their policy hook
- [Pre-Trade Lock](Pre-Trade-Lock.md): the `policy group id` and per-policy lock
  records - the unrelated policy-group concept
