Skip to content

Account Groups

Eugene Palchukovsky edited this page Jun 8, 2026 · 2 revisions

Account Groups

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, 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) 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.

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.

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.
  • 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), 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_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.

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.

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.

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)
}
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
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);

Account Groups and Market Data

Account group membership has a direct effect on how the market-data service 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 / push patch 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 for the full resolution mode table and Market Data TTL for the eight-tier TTL cascade.

Related Pages

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

Clone this wiki locally