-
Notifications
You must be signed in to change notification settings - Fork 1
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.
The account group and the policy group are unrelated concepts; do not
confuse the two:
- An
account group(theaccount 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(thepolicy 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.
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.
Both register group and unregister group are all-or-nothing over the
accounts passed in a single call:
-
register groupfails 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 groupfails 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 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
ndistinct group strings the probability of at least one collision is approximatelyn² / (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.
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(aliasAccountGroupId::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.
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 reporthook 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.
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 NoneRust
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 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 fortargeting 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
groupslist ofpush for; to publish to the default bucket explicitly, pass thedefault account groupin the same list.
See Market Data for the full resolution mode table and Market Data TTL for the eight-tier TTL cascade.
-
Account Blocking: block or unblock an entire account
group in one call via the admin API on the
accountshandle -
Market Data: quote buckets, resolution modes, and the
push fortargeted push - Market Data TTL: the eight-tier TTL cascade, including the group tiers
-
Policy API: policy hooks and the lazy
account groupaccessor 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 idand per-policy lock records - the unrelated policy-group concept