---
title: "Pre-Trade Lock"
description: "funds for an order, it also records how that hold must later be settled - the exact price it reserved at. That record is the lock. It travels with the."
---

<!-- markdownlint-disable MD010 MD033 -->
# Pre-Trade Lock

**The pre-trade lock is the receipt for a reservation.** When a policy holds
funds for an order, it also records *how* that hold must later be settled - the
exact price it reserved at. That record is the lock. It travels with the order
from the moment it passes pre-trade to the final execution report, and it is
what lets the engine release or consume the reserved funds using the very same
assumptions it accepted the order under.

Without it, post-trade reconciliation is guesswork. With it, every fill,
partial, and cancel reconciles to the cent against what was actually reserved -
deterministically, across process restarts, across machines.

## Why It Matters

- **Reconciliation that can't drift.** The amount released on a cancel or
  consumed on a fill is computed from the price the order was *reserved* at, not
  re-derived later from a live quote that has since moved. Held funds always net
  back to zero.
- **Crash-safe by design.** The lock is a small, self-contained value with a
  compact wire format. Persist it next to the order and a restart loses nothing:
  the engine reconciles the order exactly as the pre-restart process would have.
- **Your storage, your format.** Built-in JSON, MessagePack, and CBOR codecs
  ship in the box; if you keep your own schema, walk the lock's entries and
  store them however you like. The lock round-trips byte-for-byte either way.
- **Tiny on the wire.** The format emits no field names, no map keys, no struct
  tags - a default-only lock with one price is nine bytes of JSON, six of
  MessagePack. It costs almost nothing to store one per working order.
- **One contract, wherever it's decoded.** The lock's lifecycle and wire format
  are fixed in the core, not by how the SDK is consumed, so a lock persisted by
  one process reconciles deterministically when restored by another - on
  restart, or on a different machine.

## What the Lock Carries

A lock is a set of `(policy_group_id, price)` records. Each pre-trade policy that
needs post-trade context writes its prices under its own group identifier, so
several policies can share one lock without colliding. Most orders carry a single
price under the default group.

For [Spot Funds](Spot-Funds.md), the recorded price is the **effective settlement
price** the order was reserved at:

- A **buy** records its lock price. The policy held `price × quantity` of the
  settlement asset, and it must release or consume that exact amount as the
  order fills. It also reserves the base asset quantity as `incoming`, which
  the fill reduces by the filled quantity.
- A **sell** also records its lock price. Every accepted sell has a resolvable
  price (a sell with no order price and no market-data price is rejected at
  pre-trade). The policy reserves the underlying asset as `held` and records
  `price × quantity` as `incoming` on the settlement asset (the expected
  proceeds). The fill and cancel steps consume and release that `incoming`
  using the lock price.

## Why Spot Funds Needs It

The lock is not optional bookkeeping for Spot Funds - it is **required input** to
process any execution report. When a fill or cancel arrives, the policy reads the
lock price for its group and reconciles both the outflow and the incoming legs
against it:

- On a **fill**, for a non-negative lock price:
  - Outflow leg: buys consume `lock_price × filled_quantity` from settlement
    `held` and return the price improvement
    `(lock_price - fill_price) × filled_quantity` to `available`, negative
    when the fill was worse than the lock; sells consume `filled_quantity`
    from underlying `held`.
  - Incoming leg: buys credit base `available` with `filled_quantity` and
    reduce base `incoming` by the same; sells credit settlement `available`
    with `fill_price × filled_quantity` and reduce settlement `incoming` by
    `lock_price × filled_quantity`.

  With a negative lock price, a sell also consumes settlement `held` instead of
  settlement `incoming`, while a buy consumes no settlement `held`.

- On any report that is final and carries a non-zero
  `remaining_reserved_quantity`, a release runs in addition to any fill on the
  same report. For a non-negative lock price:
  - Outflow leg: buys release
    `lock_price × remaining_reserved_quantity` from settlement `held` back to
    `available`; sells release `remaining_reserved_quantity` from underlying
    `held` back to `available`.
  - Incoming leg: the acquiring asset's `incoming` is reduced by the reserved
    remainder (buys: `remaining_reserved_quantity` in base units; sells:
    `lock_price × remaining_reserved_quantity` in settlement units).

  With a negative lock price, a sell also releases settlement `held` instead of
  settlement `incoming`, while a buy releases no settlement `held`.

Like the lock, `remaining_reserved_quantity` is required input on every
execution report. The caller calculates and supplies
`remaining_reserved_quantity`; the engine releases exactly that quantity from
the reservation on finalization. It is not the venue-reported remaining order
quantity (FIX `LeavesQty`).
It is always a quantity of the instrument's underlying asset, never a
settlement or money amount, including for volume orders sized at the lock
price during reservation. Supply the greater of zero and the reserved quantity
minus all fill quantities recorded for the order so far, including the fill on
this report. See [Spot Funds](Spot-Funds.md#holdings-lifecycle) for the partial-fill
and cancel cases.

An execution report - fill or cancel - that arrives **without** its lock price
blocks the account with `MissingRequiredField`, for both buys and sells. The
lock is required input: without it the settlement legs cannot be reconciled and
the policy refuses to guess.

That is why the lock must survive the whole order lifecycle for every order.
See [Spot Funds](Spot-Funds.md) for the holdings model the lock reconciles against.

## The Order Lifecycle

1. **Reserve.** `execute_pre_trade` accepts the order and produces a
   reservation. The lock is attached to it.
2. **Persist.** Read the lock off the reservation and store it next to the
   order - serialized, so it outlives the process.
3. **Commit.** Finalize the reservation once the venue has accepted the order.
4. **Attach.** Every execution report for that order carries the stored lock
   back into `apply_execution_report`.
5. **Release.** Keep the stored lock until the **final** report (filled,
   cancelled, or rejected) has been processed. Only then is it safe to drop.

## Persisting and Restoring a Lock

The example below reserves a buy, serializes its lock, then - as if after a
restart - restores the lock and feeds it back on the final fill so the held
funds reconcile cleanly.

<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
// Limit-only spot funds: the lock price is required to reconcile fills.
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")
aapl, _ := param.NewAsset("AAPL")

// Seed 10000 USD so the buy can be reserved.
total, _ := param.NewPositionSizeFromString("10000")
seed, _ := 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)),
        }),
    ),
})
if _, err := engine.ApplyAccountAdjustment(
    accountID, []model.AccountAdjustment{seed},
); err != nil {
    panic(err)
}

// Buy 10 AAPL @ 200 holds 2000 USD and records the lock price (200).
order := model.NewOrder()
op := order.EnsureOperationView()
op.SetInstrument(param.NewInstrument(aapl, usd))
op.SetAccountID(accountID)
op.SetSide(param.SideBuy)
qty, _ := param.NewQuantityFromString("10")
price, _ := param.NewPriceFromString("200")
op.SetTradeAmount(param.NewQuantityTradeAmount(qty))
op.SetPrice(price)

reservation, execRejects, err := engine.ExecutePreTrade(order)
if err != nil {
    panic(err)
}
if execRejects != nil {
    panic("unexpected post-trade rejects")
}

// Persist the lock with its built-in JSON serialization before committing.
lock, err := reservation.Lock()
if err != nil {
    panic(err)
}
payload, err := json.Marshal(lock)
if err != nil {
    panic(err)
}
reservation.CommitAndClose()

// --- After a process restart, rebuild the lock from your store. ---
var restored pretrade.Lock
if err := json.Unmarshal(payload, &restored); err != nil {
    panic(err)
}

// The final fill must carry the restored lock so the policy reconciles the
// 2000 USD it held against the real fill instead of blocking the account.
report := model.NewExecutionReport()
reportOp := model.NewExecutionReportOperation()
reportOp.SetInstrument(param.NewInstrument(aapl, usd))
reportOp.SetAccountID(accountID)
reportOp.SetSide(param.SideBuy)
report.SetOperation(reportOp)

filledQty, _ := param.NewQuantityFromString("10")
remainingReservedQty, _ := param.NewQuantityFromString("0")
fill := report.EnsureFillView()
fill.SetLastTrade(model.NewExecutionReportTrade(price, filledQty))
fill.SetRemainingReservedQuantity(remainingReservedQty)
fill.SetLock(restored.Bytes())
fill.SetIsFinal(true)

result, err := engine.ApplyExecutionReport(report)
if err != nil {
    panic(err)
}
if len(result.AccountBlocks) > 0 {
    panic("unexpected account blocks")
}
```

</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)

# Seed 10000 USD so the buy can be reserved.
seed = openpit.AccountAdjustment(
    operation=openpit.AccountAdjustmentBalanceOperation(asset="USD"),
    amount=openpit.AccountAdjustmentAmount(
        balance=openpit.param.AdjustmentAmount.absolute(
            openpit.param.PositionSize(10000)
        )
    ),
)
engine.apply_account_adjustment(account_id=account_id, adjustments=[seed])

# Buy 10 AAPL @ 200 holds 2000 USD and records the lock price (200).
order = openpit.Order(
    operation=openpit.OrderOperation(
        instrument=openpit.Instrument("AAPL", "USD"),
        account_id=account_id,
        side=openpit.param.Side.BUY,
        trade_amount=openpit.param.TradeAmount.quantity("10"),
        price=openpit.param.Price("200"),
    ),
)
result = engine.execute_pre_trade(order=order)

# Persist the lock with its built-in JSON serialization before committing.
payload = result.reservation.lock.to_json()
result.reservation.commit()

# --- After a process restart, rebuild the lock from your store. ---
restored = openpit.pretrade.Lock.from_json(payload)

# The final fill must carry the restored lock so the policy reconciles the
# 2000 USD it held against the real fill instead of blocking the account.
report = openpit.ExecutionReport(
    operation=openpit.ExecutionReportOperation(
        instrument=openpit.Instrument("AAPL", "USD"),
        account_id=account_id,
        side=openpit.param.Side.BUY,
    ),
    fill=openpit.ExecutionReportFillDetails(
        last_trade=openpit.param.Trade(
            price=openpit.param.Price("200"),
            quantity=openpit.param.Quantity("10"),
        ),
        remaining_reserved_quantity=openpit.param.Quantity("0"),
        lock=restored,
        is_final=True,
    ),
)
post = engine.apply_execution_report(report=report)
assert not post.account_blocks
```

</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 { AdjustmentAmount, TradeAmount } from "@openpit/engine/param";
import { Lock } from "@openpit/engine/pretrade";
import { buildSpotFunds } from "@openpit/engine/pretrade/policies";

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

// Seed 10000 USD so the buy can be reserved.
engine.applyAccountAdjustment(accountId, [
  {
    operation: { asset: "USD" },
    amount: { balance: AdjustmentAmount.absolute("10000") },
  },
]);

// Buy 10 AAPL @ 200 holds 2000 USD and records the lock price (200).
const result = engine.executePreTrade({
  operation: {
    underlyingAsset: "AAPL",
    settlementAsset: "USD",
    accountId,
    side: "BUY",
    tradeAmount: TradeAmount.quantity("10"),
    price: "200",
  },
});

// Persist the lock with its built-in JSON serialization before committing.
if (!result.ok) {
  throw new Error("unexpected rejects");
}
const reservation = result.reservation;
if (reservation === undefined) {
  throw new Error("accepted execute result is missing its reservation");
}
const payload = reservation.lock().toJson();
reservation.commit();

// --- After a process restart, rebuild the lock from your store. ---
const restored = Lock.fromJson(payload);

// The final fill must carry the restored lock so the policy reconciles the
// 2000 USD it held against the real fill instead of blocking the account.
const post = engine.applyExecutionReport({
  operation: {
    underlyingAsset: "AAPL",
    settlementAsset: "USD",
    accountId,
    side: "BUY",
  },
  fill: {
    lastTrade: { price: "200", quantity: "10" },
    remainingReservedQuantity: "0",
    lock: restored,
    isFinal: true,
  },
});
// post.accountBlocks is empty: the restored lock let the policy reconcile.
```

</details>

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

At the C++ type level, `openpit::model::Fill::lock` is
`std::optional<openpit::pretrade::PreTradeLock>`. An absent lock is a normal,
supported state that maps to the C ABI's null pointer; it is distinct from a
lock that is present but empty. This does not relax a configured policy's
requirements: whether a lock is required for an execution report is a policy
property, and Spot Funds still requires the lock input described above.

Every C++ `openpit::pretrade::PreTradeLock` operation that uses native state
requires a live native handle. Using a moved-from lock, or a lock copied from a
moved-from lock, throws `openpit::Error`. `Len()` and `IsEmpty()` are therefore
not `noexcept`; use `operator bool()` as the non-throwing probe for whether a
lock still holds its native handle.

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

#include <cassert>
#include <string>
#include <utility>
#include <vector>

// Limit-only spot funds: the lock price is required to reconcile fills.
openpit::EngineBuilder builder(openpit::SyncPolicy::None);
openpit::pretrade::policies::SpotFundsPolicy{}.AddTo(builder);
openpit::Engine engine = builder.Build();

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

// Seed 10000 USD so the buy can be reserved.
openpit::accountadjustment::AccountAdjustment seed;
openpit::accountadjustment::BalanceOperation balanceOp;
balanceOp.asset = ::openpit::param::Asset("USD");
seed.operation =
    openpit::accountadjustment::Operation::OfBalance(std::move(balanceOp));
openpit::accountadjustment::Amount seedAmount;
seedAmount.balance = openpit::param::AdjustmentAmount::Absolute(
    openpit::param::PositionSize::FromString("10000"));
seed.amount = std::move(seedAmount);

const openpit::AdjustmentResult seedResult = engine.ApplyAccountAdjustment(
    accountId,
    std::vector<openpit::accountadjustment::AccountAdjustment>{seed});
assert(seedResult.Passed());

// Buy 10 AAPL @ 200 holds 2000 USD and records the lock price (200).
openpit::model::Order order = openpit::model::Order::Limit(
    openpit::model::Instrument(::openpit::param::Asset("AAPL"),
                               ::openpit::param::Asset("USD")),
    accountId, openpit::model::Side::Buy,
    openpit::model::TradeAmount::OfQuantity(
        openpit::param::Quantity::FromString("10")),
    openpit::param::Price::FromString("200"));

openpit::pretrade::ExecuteResult result = engine.ExecutePreTrade(order);
assert(result.Passed());

// Persist the lock with its built-in JSON serialization before committing.
const openpit::pretrade::PreTradeLock lock = result.reservation->Lock();
const std::string payload = lock.ToJson();
assert(!payload.empty());
result.reservation->Commit();

// --- After a process restart, rebuild the lock from your store. ---
openpit::pretrade::PreTradeLock restored =
    openpit::pretrade::PreTradeLock::FromJson(payload);
assert(!restored.IsEmpty());

// The final fill must carry the restored lock so the policy reconciles the
// 2000 USD it held against the real fill instead of blocking the account.
openpit::model::ExecutionReportOperation operation;
operation.instrument = openpit::model::Instrument(
    ::openpit::param::Asset("AAPL"), ::openpit::param::Asset("USD"));
operation.accountId = accountId;
operation.side = openpit::model::Side::Buy;

openpit::model::Fill fill;
fill.lastTrade =
    openpit::model::Trade(openpit::param::Price::FromString("200"),
                          openpit::param::Quantity::FromString("10"));
fill.remainingReservedQuantity = openpit::param::Quantity::FromString("0");
fill.isFinal = true;
fill.lock = std::move(restored);

openpit::model::ExecutionReport report;
report.operation = std::move(operation);
report.fill = std::move(fill);
const openpit::PostTradeResult postTradeResult =
    engine.ApplyExecutionReport(report);
assert(postTradeResult.accountBlocks.empty());
```

</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, Price, Quantity, Side,
    Trade, TradeAmount,
};
use openpit::pretrade::policies::{SpotFundsPolicy, SpotFundsSettings};
use openpit::pretrade::PreTradeLock;
use openpit::{
    AccountAdjustmentAmount, AccountAdjustmentBalanceOperation, AccountAdjustmentBounds,
    Engine, ExecutionReportFillDetails, ExecutionReportOperation, FullSync, Instrument,
    OrderOperation, PolicyGroupId, SpotFundsMarketData, SpotFundsPricingSource,
    WithAccountAdjustmentAmount, WithAccountAdjustmentBalanceOperation,
    WithAccountAdjustmentBounds, WithExecutionReportFillDetails, WithExecutionReportOperation,
};

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 instrument = Instrument::new(Asset::new("AAPL")?, Asset::new("USD")?);

// Seed 10000 USD so the buy can be reserved.
let seed = 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("10000")?)),
        held: None,
        incoming: None,
    },
};
engine.apply_account_adjustment(account, &[seed])?;

// Buy 10 AAPL @ 200 holds 2000 USD and records the lock price (200).
let order = OrderOperation {
    instrument: instrument.clone(),
    account_id: account,
    side: Side::Buy,
    trade_amount: TradeAmount::Quantity(Quantity::from_str("10")?),
    price: Some(Price::from_str("200")?),
};
let mut reservation = engine.execute_pre_trade(order)?;

// Persist the lock in whatever format your store prefers. Here we walk the
// entries and keep `(group, price-as-string)` pairs; the built-in serde
// (`serde_json::to_string(reservation.lock())`) is an alternative.
let persisted: Vec<(u16, String)> = reservation
    .lock()
    .entries()
    .map(|(group, price)| (group.value(), price.to_string()))
    .collect();

reservation.commit();

// --- After a process restart, rebuild the lock from your store. ---
let restored = persisted
    .iter()
    .map(|(group, price)| {
        Ok::<_, Box<dyn std::error::Error>>((PolicyGroupId::new(*group), Price::from_str(price)?))
    })
    .collect::<Result<PreTradeLock, _>>()?;

// The final fill must carry the restored lock so the policy reconciles the
// 2000 USD it held against the real fill instead of blocking the account.
let report = WithExecutionReportOperation {
    inner: WithExecutionReportFillDetails {
        inner: (),
        fill: ExecutionReportFillDetails {
            last_trade: Some(Trade {
                price: Price::from_str("200")?,
                quantity: Quantity::from_str("10")?,
            }),
            fee: None,
            remaining_reserved_quantity: Quantity::from_str("0")?,
            lock: restored,
            is_final: true,
        },
    },
    operation: ExecutionReportOperation {
        instrument,
        account_id: account,
        side: Side::Buy,
    },
};
let result = engine.apply_execution_report(&report);
assert!(result.account_blocks.is_empty());
```

</details>

## Serialization Formats

The lock has serialization built in, and the wire format is deliberately
minimal: a sequence of sublists with no field names or tags. The first sublist
is the default group's prices; each following sublist is one non-default group
(its identifier followed by its prices). The same lock in three formats:

```text
// default-only lock with a single price 185
JSON         [["185"]]                 // 9 bytes
MessagePack  91 91 a3 31 38 35         // 6 bytes (hex)
CBOR         81 81 63 31 38 35         // 6 bytes (hex)
```

Because the encoding only uses sequences, every self-describing serde format
works - JSON (the canonical FFI exchange format), MessagePack, CBOR, and others.

**Built-in codecs.** Each binding exposes ready-made encoders and decoders:

- **Go** - `Lock` implements `json.Marshaler`/`Unmarshaler`,
  `msgpack.Marshaler`, and `cbor.Marshaler`, so `json.Marshal(lock)` and friends
  just work; or call `lock.MarshalJSON` / `pretrade.NewLockFromJSON`
  (`MarshalMsgpack` / `NewLockFromMsgPack`, `MarshalCBOR` / `NewLockFromCBOR`)
  directly. `lock.Bytes()` / `pretrade.NewLockFromBytes` round-trip the
  in-process representation used on execution reports.
- **Python** - `lock.to_json()` / `Lock.from_json(text)`, `lock.to_msgpack()` /
  `Lock.from_msgpack(data)`, `lock.to_cbor()` / `Lock.from_cbor(data)`.
- **C++** - `lock.ToJson()` / `PreTradeLock::FromJson(text)`,
  `lock.ToMsgpack()` / `PreTradeLock::FromMsgpack(bytes)`,
  `lock.ToCbor()` / `PreTradeLock::FromCbor(bytes)`.
- **Rust** - with the `serde` feature, the lock implements `Serialize` and
  `Deserialize`, so `serde_json::to_string(&lock)` (or any serde format) works.

**Bring your own format.** If you persist into a schema of your own, you do not
need the built-in codecs at all. Iterate the lock's `(policy_group_id, price)`
entries, store them in your columns/rows/protobuf, and rebuild the lock from
those entries later:

- **Go** - `lock.Entries()` returns `[]Entry{PolicyGroupID, Price}`;
  `pretrade.NewLockFromEntries(entries)` rebuilds it.
- **Python** - `lock.entries()` returns `(policy_group_id, price)` tuples;
  `openpit.pretrade.Lock(entries)` rebuilds it.
- **C++** - `lock.Entries()` returns `std::vector<LockEntry>` where each entry
  carries `policyGroupId` and `price`; `PreTradeLock::PushMany(entries)`
  rebuilds a lock from those entries.
- **Rust** - `lock.entries()` yields `(PolicyGroupId, Price)`;
  `PreTradeLock::from_entries(...)` (or `.collect()`) rebuilds it. This is the
  path shown in the Rust example above, and it needs no feature flag.

A lock rebuilt from entries is identical to one decoded from JSON - the engine
treats them the same on the execution report.

## Surviving a Restart

Spot Funds keeps all state in memory. After a restart the engine starts empty,
so **you** are responsible for restoring it before resuming trading. Two things
must be reloaded, and missing either one corrupts reconciliation:

1. **Every balance bucket, for every `(account, asset)`.** Replay your balances
   through the [account-adjustment](Account-Adjustments.md) pipeline - and not just
   `available`. You must also restore `held` (funds reserved against orders that
   were still working at shutdown) and `incoming` (expected inflows that had not
   yet settled). An `AccountAdjustmentAmount` carries all three fields; seed them
   together so the engine's view matches reality. Restoring only `available`
   silently understates committed exposure and lets the account over-commit.

2. **The lock of every non-finalized order.** For each order that had not reached
   a final execution report (open, partially filled, pending cancel), reload its
   persisted lock and keep it until that order's final report is processed. An
   order whose lock is lost cannot have its settlement reconciled. Any report -
   buy or sell - that arrives without its lock blocks the account with
   `MissingRequiredField`.

In short: persist `(available, held, incoming)` per holding **and** the lock per
working order, restore both on startup, and the engine resumes exactly where it
left off. See [Balance Reconciliation](Balance-Reconciliation.md) for keeping those
balances in step while the process runs.

## Rejects

<!-- markdownlint-disable MD013 MD060 -->
| Code | Scope | When |
|------|-------|------|
| `MissingRequiredField` | `account` | An execution report (fill or cancel) arrives without the lock price needed to reconcile its settlement and `incoming` legs. Applies to both buys and sells (lock dropped or never persisted). |
<!-- markdownlint-enable MD013 MD060 -->

## Related Pages

- [Spot Funds](Spot-Funds.md) - the policy that produces and consumes the lock.
- [Account Adjustments](Account-Adjustments.md) - restore balances, including
  `held` and `incoming`, after a restart.
- [Balance Reconciliation](Balance-Reconciliation.md) - keep your books in step
  with engine outcomes.
- [Pre-trade Pipeline](Pre-trade-Pipeline.md) - where reservations and execution
  reports flow through the engine.
- [Policies](Policies.md) - the full built-in policy catalog.
- [Dynamic Policy Reconfiguration](Dynamic-Policy-Reconfiguration.md) - retune
  policies.
