---
title: "Balance Reconciliation"
description: "When funds move, the engine reports each change twice: as a signed delta and as an absolute snapshot. The delta is the authoritative number for."
---

<!-- markdownlint-disable MD010 MD033 -->
# Balance Reconciliation

When funds move, the engine reports each change twice: as a signed `delta` and
as an `absolute` snapshot. The `delta` is the authoritative number for
bookkeeping; the `absolute` is a convenience reading of the field at the moment
the policy returned. A caller that maintains its own ledger - its own view of
how much each account may spend - must reconcile from `delta` and treat
`absolute` as diagnostic only.

This matters because the engine is an embedded, in-process component, not the
system of record. Your application still owns the durable account ledger. These
outcomes are how the two stay in agreement.

## Where Outcomes Come From

Three engine surfaces return position outcomes, all carrying the same
`AccountAdjustmentOutcome` shape:

- `apply account adjustment` returns a batch result whose `outcomes` list has one
  entry per asset each adjustment touched.
- `apply execution report` returns a post-trade result whose
  `account adjustments` list carries the same outcome shape, produced as fills settle.
- the pre-trade path returns a `PreTradeReservation` whose `account adjustments`
  list reports the **reserve** a passing order applied - typically `available`
  down and `held` up for the reserved amount. Read it with `account adjustments`
  from `reservation`.

Each outcome entry names one `asset` and exposes an optional `balance`, `held`,
and `incoming` amount. A field is present only when it changed. Every present
amount carries both `delta` and `absolute`.

### The reservation outcome is provisional

The reserve reported by `PreTradeReservation` is real engine state - it is what
makes the held funds unavailable to the next order - but it is *provisional*, and
that changes how you reconcile it:

- **`commit` and `rollback` report no balances.** The outcome is delivered
  once, on the reservation. `commit` only makes the already-applied reserve
  durable; `rollback` releases it. Neither returns a fresh outcome list. Both
  calls are void, which is not the same as consequence-free: a mutation
  finalizer that fails during either one arms the engine kill switch, so
  reconcile against the last delivered outcome and expect the next pre-trade
  call to be rejected. See
  [Account Blocking - Mutation Finalizer Contract](Account-Blocking.md#mutation-finalizer-contract).
- **Reservations emit an `incoming` leg.** A buy reservation produces two
  outcome entries: one for the settlement asset (`available` down, `held` up)
  and one for the base asset (`incoming` up). A priced sell reservation produces
  an entry for the underlying asset (`available` down, `held` up) and an entry
  for the settlement asset (`incoming` up, representing the expected proceeds).
  Reconcilers that mirror the `incoming` bucket must apply these deltas from the
  reservation outcome, not only from fills.
- **The matching fill nets the reserve out through the execution report.** When
  the order fills, `apply execution report` reports `held` coming back down (and
  the acquired asset crediting `available`); it also reports `incoming` coming
  back down on the acquiring leg. A final report releases the reservation
  remainder the caller supplies on it, and reports that the same way. Those are
  separate outcomes you reconcile normally.
- **`incoming` never gates funds.** It does not factor into spendable capacity
  (`available + min(held, 0)`), so it never affects a funds decision. It gates
  orders only through a configured
  [position limit](Spot-Funds.md#position-limit), which counts open positive
  `incoming` on the long side. Track it for position-awareness, but do not use
  it to compute available-to-spend.

If your durable ledger tracks only *settled* balances and leaves the
`held`/`available` split to the engine, you can ignore the reservation outcome
for bookkeeping and reconcile solely from the account-adjustment and
execution-report paths - the reserve is netted out before anything settles. If
your ledger mirrors `held`/`available`, treat the reservation outcome as a first
class delta source.

The pre-trade path also produces one result that is not a balance at all: it can
**latch an account block** (a kill switch). It carries no `delta`, so it sits
outside the outcome model - but it is in-memory engine state your application
must track. See [Account Blocks Are State Too](#account-blocks-are-state-too).

## Delta Versus Absolute

- **`delta`** - the signed change this one operation applied. Sum your own deltas
  to keep your ledger aligned with the engine, regardless of how operations
  interleave across accounts or chains.
- **`absolute`** - the field value at the instant the policy returned. It is a
  read-only snapshot. Under `full sync` or `account sync`, another chain may move
  the same field a moment later, so the `absolute` you received can already be
  out of date by the time you act on it. Never persist `absolute` as your source
  of truth.

The example below seeds an account twice. The first seed sets available funds to
`10000`, so both `delta` and `absolute` read `10000`. The second seed raises the
target to `15000`: `delta` is `5000` (the change) while `absolute` is `15000`
(the new level). Bookkeeping that added `absolute` instead of `delta` would
double-count.

<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.BuildSpotFunds()).
    Build()
if err != nil {
    panic(err)
}
defer engine.Stop()

accountID := param.NewAccountIDFromUint64(99224416)
usd, _ := param.NewAsset("USD")

seed := func(amount string) model.AccountAdjustment {
    total, _ := param.NewPositionSizeFromString(amount)
    adj, _ := model.NewAccountAdjustmentFromValues(model.AccountAdjustmentValues{
        BalanceOperation: optional.Some(
            model.NewAccountAdjustmentBalanceOperationFromValues(
                model.AccountAdjustmentBalanceOperationValues{Asset: optional.Some(usd)},
            ),
        ),
        Amount: optional.Some(
            model.NewAccountAdjustmentAmountFromValues(model.AccountAdjustmentAmountValues{
                Balance: optional.Some(param.NewAbsoluteAdjustmentAmount(total)),
            }),
        ),
    })
    return adj
}

// First seed: available USD goes from 0 to 10000.
firstResult, err := engine.ApplyAccountAdjustment(
    accountID, []model.AccountAdjustment{seed("10000")},
)
if err != nil {
    panic(err)
}
firstUSD, _ := firstResult.Outcomes[0].Entry.Balance.Get()
want10000, _ := param.NewPositionSizeFromString("10000")
if !firstUSD.Delta.Equal(want10000) {
    panic("unexpected delta")
}
if !firstUSD.Absolute.Equal(want10000) {
    panic("unexpected absolute")
}

// Second seed: available USD goes from 10000 to 15000.
secondResult, err := engine.ApplyAccountAdjustment(
    accountID, []model.AccountAdjustment{seed("15000")},
)
if err != nil {
    panic(err)
}
secondUSD, _ := secondResult.Outcomes[0].Entry.Balance.Get()
// delta is the change to add to your own ledger; absolute is just a snapshot.
want5000, _ := param.NewPositionSizeFromString("5000")
want15000, _ := param.NewPositionSizeFromString("15000")
if !secondUSD.Delta.Equal(want5000) {
    panic("unexpected delta")
}
if !secondUSD.Absolute.Equal(want15000) {
    panic("unexpected absolute")
}
```

</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
import openpit
import openpit.pretrade.policies

engine = (
    openpit.Engine.builder()
    .no_sync()
    .builtin(openpit.pretrade.policies.build_spot_funds())
    .build()
)
account_id = openpit.param.AccountId.from_int(99224416)


def seed(amount: int) -> openpit.AccountAdjustment:
    return openpit.AccountAdjustment(
        operation=openpit.AccountAdjustmentBalanceOperation(asset="USD"),
        amount=openpit.AccountAdjustmentAmount(
            balance=openpit.param.AdjustmentAmount.absolute(
                openpit.param.PositionSize(amount)
            )
        ),
    )


# First seed: available USD goes from 0 to 10000.
first = engine.apply_account_adjustment(account_id=account_id, adjustments=[seed(10000)])
assert first.ok
usd = first.outcomes[0].entry.balance
assert usd.delta == openpit.param.PositionSize(10000)
assert usd.absolute == openpit.param.PositionSize(10000)

# Second seed: available USD goes from 10000 to 15000.
second = engine.apply_account_adjustment(account_id=account_id, adjustments=[seed(15000)])
assert second.ok
usd = second.outcomes[0].entry.balance
# delta is the change to add to your own ledger; absolute is just a snapshot.
assert usd.delta == openpit.param.PositionSize(5000)
assert usd.absolute == openpit.param.PositionSize(15000)
```

</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 { type AccountAdjustmentInit } from "@openpit/engine/model";
import { AdjustmentAmount } from "@openpit/engine/param";
import { buildSpotFunds } from "@openpit/engine/pretrade/policies";

const engine = Engine.builder().builtin(buildSpotFunds()).build();
const accountId = 99224416;

// An absolute adjustment sets the available USD to a target level. The amount
// crosses the boundary as a decimal string.
const seed = (amount: string): AccountAdjustmentInit => ({
  operation: { asset: "USD" },
  amount: { balance: AdjustmentAmount.absolute(amount) },
});

// First seed: available USD goes from 0 to 10000.
const first = engine.applyAccountAdjustment(accountId, [seed("10000")]);
let usd = first.outcomes[0]!.entry.balance!;
console.log(usd.delta.toString(), usd.absolute.toString()); // => "10000" "10000"

// Second seed: available USD goes from 10000 to 15000.
const second = engine.applyAccountAdjustment(accountId, [seed("15000")]);
usd = second.outcomes[0]!.entry.balance!;
// delta is the change to add to your own ledger; absolute is just a snapshot.
console.log(usd.delta.toString(), usd.absolute.toString()); // => "5000" "15000"
```

</details>

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

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

#include <cassert>
#include <vector>

namespace aa = openpit::accountadjustment;
namespace param = openpit::param;
namespace policies = openpit::pretrade::policies;

openpit::EngineBuilder builder(openpit::SyncPolicy::None);
builder.Add(policies::SpotFundsPolicy{});
openpit::Engine engine = builder.Build();

const param::AccountId accountId = param::AccountId::FromUint64(99224416);

auto seed = [](const char* amount) {
  aa::AccountAdjustment adj;
  aa::BalanceOperation op;
  op.asset = ::openpit::param::Asset("USD");
  adj.operation = aa::Operation::OfBalance(op);
  aa::Amount amountGroup;
  amountGroup.balance = param::AdjustmentAmount::Absolute(
      param::PositionSize::FromString(amount));
  adj.amount = amountGroup;
  return adj;
};

// First seed: available USD goes from 0 to 10000.
const openpit::AdjustmentResult firstResult = engine.ApplyAccountAdjustment(
    accountId, std::vector<aa::AccountAdjustment>{seed("10000")});
assert(firstResult.Passed());
assert(firstResult.accountAdjustmentOutcomes.size() == 1);
assert(firstResult.accountAdjustmentOutcomes[0].entry.balance);
const aa::OutcomeAmount& firstUSD =
    *firstResult.accountAdjustmentOutcomes[0].entry.balance;
assert(firstUSD.delta == param::PositionSize::FromString("10000"));
assert(firstUSD.absolute == param::PositionSize::FromString("10000"));

// Second seed: available USD goes from 10000 to 15000.
const openpit::AdjustmentResult secondResult = engine.ApplyAccountAdjustment(
    accountId, std::vector<aa::AccountAdjustment>{seed("15000")});
assert(secondResult.Passed());
assert(secondResult.accountAdjustmentOutcomes.size() == 1);
assert(secondResult.accountAdjustmentOutcomes[0].entry.balance);
const aa::OutcomeAmount& secondUSD =
    *secondResult.accountAdjustmentOutcomes[0].entry.balance;
// delta is the change to add to your own ledger; absolute is just a snapshot.
assert(secondUSD.delta == param::PositionSize::FromString("5000"));
assert(secondUSD.absolute == param::PositionSize::FromString("15000"));
```

</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::{AccountId, AdjustmentAmount, Asset, PositionSize};
use openpit::pretrade::policies::{SpotFundsPolicy, SpotFundsSettings};
use openpit::{
    AccountAdjustmentAmount, AccountAdjustmentBalanceOperation, AccountAdjustmentBounds,
    Engine, FullSync, OrderOperation, SpotFundsMarketData, SpotFundsPricingSource,
    WithAccountAdjustmentAmount, WithAccountAdjustmentBalanceOperation,
    WithAccountAdjustmentBounds, WithExecutionReportFillDetails, WithExecutionReportOperation,
};

// Report and account-adjustment shapes composed from public SDK wrappers.
type SpotReport = WithExecutionReportOperation<WithExecutionReportFillDetails<()>>;
type SpotAdjustment = WithAccountAdjustmentAmount<
    WithAccountAdjustmentBounds<
        WithAccountAdjustmentBalanceOperation<openpit::AccountAdjustmentAmount>,
    >,
>;

let builder = Engine::builder::<OrderOperation, SpotReport, SpotAdjustment>().full_sync();
let policy = SpotFundsPolicy::<FullSync, FullSync>::new(
    SpotFundsSettings::new(0, SpotFundsPricingSource::Mark, [])?,
    None::<SpotFundsMarketData<FullSync>>,
    builder.storage_builder(),
);
let engine = builder.pre_trade(policy).build()?;

let account = AccountId::from_u64(99224416);

let seed = |amount: &str| -> Result<
    SpotAdjustment,
    Box<dyn std::error::Error>,
> {
    Ok(WithAccountAdjustmentAmount {
        inner: WithAccountAdjustmentBounds {
            inner: WithAccountAdjustmentBalanceOperation {
                inner: AccountAdjustmentAmount::default(),
                operation: AccountAdjustmentBalanceOperation {
                    asset: Asset::new("USD")?,
                    average_entry_price: None,
                },
            },
            bounds: AccountAdjustmentBounds::default(),
        },
        amount: AccountAdjustmentAmount {
            balance: Some(AdjustmentAmount::Absolute(PositionSize::from_str(amount)?)),
            held: None,
            incoming: None,
        },
    })
};

// First seed: available USD goes from 0 to 10000.
let first = engine.apply_account_adjustment(account, &[seed("10000")?])?;
let usd = first.outcomes[0].entry.balance.expect("balance changed");
assert_eq!(usd.delta, PositionSize::from_str("10000")?);
assert_eq!(usd.absolute, PositionSize::from_str("10000")?);

// Second seed: available USD goes from 10000 to 15000.
let second = engine.apply_account_adjustment(account, &[seed("15000")?])?;
let usd = second.outcomes[0].entry.balance.expect("balance changed");
// delta is the change to add to your own ledger; absolute is just a snapshot.
assert_eq!(usd.delta, PositionSize::from_str("5000")?);
assert_eq!(usd.absolute, PositionSize::from_str("15000")?);
```

</details>

## Tracking Limits in Your Own Store

The engine holds spendable funds only in memory; restarting it loses that state.
Keep the durable copy in your own database and reconcile it with deltas:

1. **On startup, replay your ledger into the engine.** Read each account's
   persisted balance from your database and apply it through the
   [account adjustment](Account-Adjustments.md) pipeline. The engine and your store
   start aligned. Replay **all** of `available`, `held`, and `incoming`, not
   just spendable funds - and for any order still working at shutdown, also
   reload its persisted [pre-trade lock](Pre-Trade-Lock.md) so its fills still
   reconcile. See [Pre-Trade Lock → Surviving a Restart](Pre-Trade-Lock.md#surviving-a-restart).
2. **On every settled outcome, apply the delta.** For each
   `apply account adjustment` and `apply execution report`, read each outcome's
   `delta` and add the same signed value to your store - in the same transaction
   that records the operation, so the two never drift. If your store also mirrors
   `held`/`available`, fold in the provisional `PreTradeReservation` outcomes per
   [The reservation outcome is provisional](#the-reservation-outcome-is-provisional);
   if it tracks only settled balances, skip them.
3. **Treat `absolute` as diagnostic.** Log it, alert on a mismatch against your
   own running total, but never write it back as the authoritative balance.
4. **Repeat.** Because reconciliation is delta-based, it is order-independent:
   concurrent chains can interleave freely and your store still converges to the
   engine's view.

A mismatch between your delta-summed total and a reported `absolute` is a signal
worth investigating - it usually means an operation was applied to one side but
not the other, not that `absolute` is wrong.

## Account Blocks Are State Too

Balances are not the only in-memory state the engine holds. When a policy
emits a reject with `scope = account` from any pre-trade stage, or when
`apply execution report` returns a `PostTradeResult` with non-empty account
blocks (a kill switch), the engine **latches a block** for that account in its
internal blocked-accounts registry. Every later request for that account is then
short-circuited before any policy runs. See
[Account Blocking by Engine](Policies.md#account-blocking-by-engine) for the full
mechanics.

This block is a reconcilable result of the same kind as a balance - it lives only
in memory and is lost on restart - but it behaves differently and must be handled
differently. Record the cause that later rejects on that account report and,
after restart, restore it with
[`block with cause`](Account-Blocking.md#restore-a-persisted-cause) on a newly
built engine before any operation can record a block for that account:

- **It is not a `delta`.** A block is latching, binary state, not a signed
  number. There is nothing to sum. Reconcile it by recording the fact of the
  block, not by accumulating amounts.
- **The pre-trade path surfaces it as a reject, not an outcome.** A start- or
  main-stage block arrives as `Err`/rejects with `scope = account`; the engine
  records it as a side effect. Subsequent rejects on that account carry
  `code = AccountBlocked`. The post-trade path surfaces it explicitly in
  [`PostTradeResult`](Pre-trade-Pipeline.md) `account blocks`.

## Related Pages

- [Spot Funds](Spot-Funds.md) - the policy that produces these outcomes as funds
  move.
- [Account Adjustments](Account-Adjustments.md) - the pipeline that emits adjustment
  outcomes and the place to replay your ledger on startup.
- [Pre-Trade Lock](Pre-Trade-Lock.md) - persist the reservation lock per working
  order so fills reconcile after a restart.
- [Policies](Policies.md#account-blocking-by-engine) - how the engine latches and
  enforces account blocks, the second kind of in-memory state to reconcile.
- [Pre-trade Pipeline](Pre-trade-Pipeline.md) - where pre-trade rejects and
  `PostTradeResult` account blocks surface to the caller.
- [Policies](Policies.md) - the full built-in policy catalog.
