-
Notifications
You must be signed in to change notification settings - Fork 1
Account Blocking
The accounts handle exposed by the engine carries an admin API for
blocking and unblocking accounts and account groups from outside any
policy callback. This is distinct from the in-policy kill-switch
facility (see Policies - Account Blocking by Engine),
which triggers a block automatically when a policy returns a
scope = account reject or when apply execution report emits an
AccountBlock value.
The same engine-owned path covers a recovered panic from a Go policy's
ApplyExecutionReport callback. The binding converts it to a
SystemUnavailable account block before the engine records it; see
When the engine records a block.
A third engine-owned trigger is a failed mutation finalizer. That one is documented in full below, in Mutation Finalizer Contract, because its reach is not always one account.
The admin API is reached through engine.accounts(). The same handle
also owns the account group membership registry.
An accounts block gates pre-trade order evaluation only. The
engine short-circuits the entire policy chain for every incoming order
on the blocked account - no policy runs, and the caller receives an
AccountBlocked reject immediately. Account adjustments (non-trade
operations on balances and positions) continue to work regardless of
block state; the block is strictly a pre-trade signal.
Drop copy is the intentional exception. It applies an order that already happened, so existing account and account-group blocks do not gate it. Policies still run and may request another account block after recording the historical effects. The accepted drop-copy operation distinguishes the first block requested by that call from the effective blocked state when apply returned. That state is a snapshot taken before the operation was returned, and the block itself is outside the finalization boundary: rolling the operation back does not undo it.
The exception applies only after the engine has read the order account. That
account is its routing and account-control key, so an unreadable account_id
causes a fatal MissingRequiredField reject before any policy runs or state is
changed. This does not create a global account block.
block(account, reason) adds the account to the engine's blocked set.
unblock(account) removes it - clearing the block regardless of its origin:
it lifts both an admin block set through this API and a kill-switch block
recorded by the engine from a policy (for example a PnL limit). If the
account is already blocked, calling block again is a no-op - the first
reason is kept. Use replace block reason to change the reason of an
existing block.
block group(group, reason) installs a group-level block. The engine
evaluates group membership live on every pre-trade request: any account
that is currently a member of a blocked group is rejected, including
accounts that join the group after the block was set. Conversely, an
account that leaves a blocked group is no longer group-blocked.
unblock group(group) removes the group-level block.
The default account group cannot be passed to block group,
unblock group, or replace group block reason - these calls return
an error for the reserved default.
An engine-wide block stops every pre-trade request on every account, whatever
its account group. No admin call raises it - block and block group are
always scoped. The engine raises it itself, in two cases:
- an execution report signals a kill switch but exposes no readable
account id, so exposure that already exists cannot be attributed to one account; - a mutation finalizer registered by a custom policy fails - see Mutation Finalizer Contract.
The engine-wide block carries the cause the engine recorded for it, and it is
clearable without rebuilding the engine. unblock all lifts it and lifts
nothing else: accounts and account groups blocked individually stay blocked, and
their own recorded cause still wins over the engine-wide one, so clear those
with unblock and unblock group. Calling unblock all while no engine-wide
block is active is a no-op.
| Language | Clear the engine-wide block |
|---|---|
| Go |
accounts.UnblockAll(); on the async engine asyncAccounts.UnblockAll(ctx)
|
| Python | accounts.unblock_all() |
| JavaScript | accounts.unblockAll() |
| C++ | accounts.UnblockAll() |
| Rust | accounts.unblock_all() |
| C | openpit_engine_unblock_all_accounts(engine) |
An engine-wide block is the engine reporting that its own bookkeeping may no longer be trustworthy. Clear it once the inconsistency behind it has been investigated, not as a way to get order flow moving again.
A mutation finalizer - the commit or the rollback callback a policy registers
with a mutation - has no right to fail. By the time it runs, the decision is
already made and the state it finalizes was applied eagerly, so there is nothing
left to compensate and no caller left to answer. commit and rollback are
void on every surface, and they stay void when a finalizer reports failure.
A reported failure is never ignored either. The engine's own bookkeeping is then in an unknown state, so the engine arms a kill switch under its own cause:
| Field | Value |
|---|---|
policy |
Engine |
code |
SystemUnavailable |
reason |
mutation finalizer failed |
details |
a mutation commit or rollback callback failed; engine state may be inconsistent |
The reach of that kill switch follows the failed mutation's provenance:
- a mutation registered by an engine-owned built-in policy has a bounded reach, so the account the pipeline ran for is blocked;
- a mutation registered by a custom policy has an unbounded reach - such a policy may write engine-wide barriers such as a broker-level rate limit or a P&L bound - so every account is blocked.
Every mutation registered through a binding is a custom-policy mutation, so the engine-wide reach is what Go, Python, JavaScript, C++, and C policies get.
The block is engine-owned and carries no account or account-group identifier.
Nothing reports it to the caller that was finalizing; that caller learns the
ordinary way, when the next pre-trade request is rejected with
SystemUnavailable. Clear it with
the engine-wide unblock.
The contract holds on every pipeline - pre-trade reservation finalization,
drop-copy operation finalization, compensation of a fatal drop-copy evaluation
exit, and the account-adjustment batch - and on implicit finalization, where a
handle released without commit or rollback rolls back and the block is the
only channel left.
A custom policy that cannot guarantee its finalizer will succeed is choosing an engine-wide outage as its failure mode. Everything that can fail belongs before the mutation is registered: apply the tentative state first, then leave the pair with nothing but bookkeeping that cannot fail - no allocation that may throw, no I/O, no lock that may be poisoned, no call back into the engine. A finalizer that only adjusts a counter or restores a value captured at registration time has no failure mode to report.
Arming the kill switch does not replace a binding's own callback-error channel. Both effects happen for the same failure:
- Go recovers a panicking callback at the SDK boundary and reports it to the
core as exactly such a failure.
Commit,Rollback, andClosestay void, so the kill switch is the whole of what the caller can observe. - Python re-raises the original exception, with its original type and message,
from
commit()orrollback()once every remaining callback of the batch has run. - JavaScript throws
PolicyCallbackErrorwith the original value ascause. - C++ rethrows the original exception from
Commit()orRollback(), likewise after the batch finishes. A destructor cannot rethrow, so on implicit rollback the kill switch is the only channel. - Rust adds no containment around policy or mutation callbacks, so a native Rust finalizer must not panic.
On the one path that has a reject channel of its own - compensation of a fatal
drop-copy evaluation exit - the SystemUnavailable reject is appended after
the fatal policy rejects, so rejects[0] still answers why the operation
failed rather than how the cleanup failed.
The reason is an operator-supplied free-text cause string. It may be empty;
the SDK does not enforce non-emptiness. The reason is replayed in the
AccountBlocked reject that the engine returns to the caller on every
blocked pre-trade attempt. Enforcing a non-empty reason for operator
accountability is the caller's responsibility.
The examples build an engine, block account 99224416 with a reason, unblock it, and show a one-line group block/unblock.
Go
engine, err := openpit.NewEngineBuilder().
FullSync().
Builtin(policies.BuildOrderValidation()).
Build()
if err != nil {
log.Fatal(err)
}
defer engine.Stop()
accounts := engine.Accounts()
// Block account 99224416 - all subsequent pre-trade orders are rejected.
accounts.Block(param.NewAccountIDFromUint64(99224416), "compliance hold")
// Unblock account 99224416 - pre-trade orders are allowed again.
accounts.Unblock(param.NewAccountIDFromUint64(99224416))
// Block every current and future member of a group in one call.
desk, err := param.NewAccountGroupIDFromUint32(7)
if err != nil {
log.Fatal(err)
}
if err := accounts.BlockGroup(desk, "desk suspended"); err != nil {
log.Fatal(err)
}
if err := accounts.UnblockGroup(desk); err != nil {
log.Fatal(err)
}Python
engine = (
openpit.Engine.builder()
.no_sync()
.builtin(openpit.pretrade.policies.build_order_validation())
.build()
)
accounts = engine.accounts()
# Block account 99224416 - all subsequent pre-trade orders are rejected.
accounts.block(openpit.param.AccountId.from_int(99224416), "compliance hold")
# Unblock account 99224416 - pre-trade orders are allowed again.
accounts.unblock(openpit.param.AccountId.from_int(99224416))
# Block every current and future member of a group in one call.
desk = openpit.param.AccountGroupId.from_int(7)
accounts.block_group(desk, "desk suspended")
accounts.unblock_group(desk)JavaScript
import { Engine } from "@openpit/engine";
import { buildOrderValidation } from "@openpit/engine/pretrade/policies";
const engine = Engine.builder()
.builtin(buildOrderValidation())
.build();
const accounts = engine.accounts();
// Block account 99224416 - all subsequent pre-trade orders are rejected.
accounts.block(99224416, "compliance hold");
// Unblock account 99224416 - pre-trade orders are allowed again.
accounts.unblock(99224416);
// Block every current and future member of a group in one call.
const desk = 7;
accounts.blockGroup(desk, "desk suspended");
accounts.unblockGroup(desk);C++
#include "openpit/accounts/accounts.hpp"
#include "openpit/engine.hpp"
#include "openpit/param/account_id.hpp"
#include "openpit/pretrade/policies.hpp"
openpit::EngineBuilder builder(openpit::SyncPolicy::Full);
builder.Add(openpit::pretrade::policies::OrderValidationPolicy{});
openpit::Engine engine = builder.Build();
openpit::accounts::Accounts accounts = engine.Accounts();
// Block account 99224416 - all subsequent pre-trade orders are rejected.
accounts.Block(openpit::param::AccountId::FromUint64(99224416),
"compliance hold");
// Unblock account 99224416 - pre-trade orders are allowed again.
accounts.Unblock(openpit::param::AccountId::FromUint64(99224416));
// Block every current and future member of a group in one call.
openpit::param::AccountGroupId desk =
openpit::param::AccountGroupId::FromUint32(7);
if (auto err = accounts.BlockGroup(desk, "desk suspended")) {
// handle err->message
}
if (auto err = accounts.UnblockGroup(desk)) {
// handle err->message
}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()?;
let accounts = engine.accounts();
// Block account 99224416 - all subsequent pre-trade orders are rejected.
accounts.block(AccountId::from_u64(99224416), "compliance hold".to_string());
// Unblock account 99224416 - pre-trade orders are allowed again.
accounts.unblock(AccountId::from_u64(99224416));
// Block every current and future member of a group in one call.
let desk = AccountGroupId::from_u32(7)?;
accounts.block_group(desk, "desk suspended".to_string())?;
accounts.unblock_group(desk)?;- Policies - Account Blocking by Engine: the in-policy kill-switch that blocks accounts automatically from policy callbacks
-
Account Groups: account group membership registry
and the
account group idtype - Pre-trade Pipeline: request and reservation semantics, including how blocked accounts surface in results
- Policy API: custom policy hooks and the mutation pair a policy registers
-
Reject Codes: standard business reject codes,
including
AccountBlockedandSystemUnavailable