---
title: "Market Data"
description: "The market-data service is a shared, in-memory quote cache. Quote producers push the latest snapshot for each instrument; policies pull the current."
---

<!-- markdownlint-disable MD010 MD033 -->
# Market Data

The market-data service is a shared, in-memory quote cache. Quote producers push
the latest snapshot for each instrument; policies pull the current snapshot when
they need a price. It is the data source the [spot funds](Spot-Funds.md) policy uses
to price market orders, and it can be passed to any policy that needs live
quotes.

The service is pull-based: it never calls back into producer or policy code. A
producer writes the newest snapshot whenever it has one, and a reader gets
whatever is currently stored. There are no callbacks, queues, or background
threads.

This page covers registration, synchronization, quote buckets, and the read/write
API. Two related topics live on their own pages:

- [Market Data TTL](Market-Data-TTL.md) - quote freshness and the eight-tier TTL
  cascade.
- [Market Data Pricing](Market-Data-Pricing.md) - how the spot funds policy prices
  market orders from the cache.

For the full threading model see [Threading Contract](Threading-Contract.md).

## Registering Instruments

Every instrument gets a stable identifier the first time it is registered. All
subsequent reads and writes use that identifier, which is cheaper than resolving
the instrument by name on every quote. Registration is one-time; a second attempt
to register the same instrument fails.

## Synchronization

The market-data builder comes from the engine builder via the `market data` builder
method, so its synchronization mode is derived from the engine's - there is no
silent default:

- A **no-sync** engine yields a no-sync, single-threaded service whose internal
  locks are a genuine no-op (free). Push and read it from the same thread the
  engine runs on. Call `full sync` on the market-data builder to upgrade it to
  a fully-synchronized service when a background producer must publish quotes
  concurrently with the engine.
- A **full-sync** or **account-sync** engine yields a fully-synchronized service
  (real locks); it cannot be downgraded.

Across every SDK the engine-builder path is the standard way to obtain a service;
prefer it in all normal code. Where an SDK also exposes a lower-level constructor
for the service, treat it as an advanced escape hatch rather than the default.

## Quote Buckets

Quotes are stored per instrument in three conceptual buckets:

- **Per-account bucket** - a quote targeted at one specific account via [`push for`](#targeted-fan-out-push-for).
- **Per-group bucket** - a quote targeted at an entire [account group](Account-Groups.md)
  via `push for`.
- **Default bucket** - the "everyone-else" quote written by the plain `push`
  call; internally it is the bucket of the reserved `default account group`
  (id `0`).

A `push for` call targets any combination of individual accounts and groups in
one shot, all sharing the same source age. Passing the `default account group`
in the group list targets the default bucket directly.

## Pushing and Reading Quotes

A `quote` carries an optional `mark`, `bid`, and `ask`. Every publication
replaces the target snapshot in full: a field left unset does not exist in the
new snapshot, and never retains its old value.

Every `push` and `push for` also receives the quote's **source age**: the time
elapsed between the source observing the price and this call. The publisher is
responsible for computing it and for combining partial observations before it
publishes one complete snapshot. The cache measures freshness from the original
observation, not from publication. A producer that observed a quote 20 ms ago
publishes it with a 20 ms source age; under a 50 ms effective TTL, it has about
30 ms of freshness left when it arrives. A negative age is clamped to zero; a
non-finite JavaScript age raises `RangeError`. The bindings take the age as Go
`time.Duration`, Python `datetime.timedelta`, JavaScript `sourceAgeMs`, C++
`std::chrono::nanoseconds`, Rust `std::time::Duration`, or C seconds plus
nanoseconds.

A zero source age explicitly means the quote was observed at the moment of
publication.

A read supplies the **reading account**, an **`account info`** object (which
provides the account group lazily on demand), and a **resolution mode** that
controls which buckets are consulted, in order:

<!-- markdownlint-disable MD013 MD060 -->
| Mode | Buckets consulted |
|------|-------------------|
| `account only` | Per-account only |
| `account then group` | Per-account, then the account's group |
| `account then group then default` | Per-account, then group, then default |
<!-- markdownlint-enable MD013 MD060 -->

`account then group then default` is the widest mode and is what the
[spot funds](Spot-Funds.md) policy uses internally.

Use `get optional` when absence is normal and `get` when you want to
distinguish an unknown instrument from a quote that exists but is unusable
(the latter raises an error on an expired quote that carries the stale value).

<details>
<summary>Go</summary>

<!-- markdownlint-disable-next-line MD013 -->
<!-- Test mirror: https://github.com/openpitkit/pit/blob/main/bindings/go/examples_market_data_test.go -->
```go
// noGroupInfo is a minimal AccountInfo whose reading account has no group.
type noGroupInfo struct{}

func (noGroupInfo) AccountGroup() optional.Option[param.AccountGroupID] {
    return optional.None[param.AccountGroupID]()
}

service, err := openpit.NewEngineBuilder().
    FullSync().
    MarketData(marketdata.InfiniteTTL()).
    Build()
if err != nil {
    panic(err)
}
defer service.Close()

aapl, _ := param.NewAsset("AAPL")
usd, _ := param.NewAsset("USD")
instrument := param.NewInstrument(aapl, usd)

aaplID, err := service.Register(instrument)
if err != nil {
    panic(err)
}

// Publish a full snapshot into the default ("everyone-else") bucket.
mark, _ := param.NewPriceFromString("150")
bid, _ := param.NewPriceFromString("149.5")
ask, _ := param.NewPriceFromString("150.5")
// Caller computes feed-observation-to-call age; the SDK cannot know it.
quoteSourceAge := 20 * time.Millisecond
if err := service.Push(
    aaplID,
    marketdata.NewQuote().WithMark(mark).WithBid(bid).WithAsk(ask),
    quoteSourceAge,
); err != nil {
    panic(err)
}

// Read for an account with no group: the lookup falls through to the
// default bucket. Pass any marketdata.AccountInfo; in policy code this is
// usually the pretrade.Context. The test mirror uses a no-group stub.
accountID := param.NewAccountIDFromUint64(1)
quote, ok := service.GetOptional(
    aaplID,
    accountID,
    noGroupInfo{},
    marketdata.QuoteResolutionAccountThenGroupThenDefault,
).Get()
if !ok {
    panic("quote must be present")
}
gotMark, _ := quote.Mark().Get()
if !gotMark.Equal(mark) {
    panic("unexpected mark")
}
gotBid, _ := quote.Bid().Get()
if !gotBid.Equal(bid) {
    panic("unexpected bid")
}

// Resolve recovers the id from the instrument name.
resolved, ok := service.Resolve(instrument)
if !ok || resolved.String() != aaplID.String() {
    panic("unexpected resolve result")
}
```

</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_market_data.py -->
```python
import types
from datetime import timedelta

import openpit
import openpit.marketdata

# no_sync: the engine spawns no OS threads; each call runs on the caller's
# thread. See Threading-Contract for the full model.
service = (
    openpit.Engine.builder()
    .no_sync()
    .market_data(openpit.marketdata.QuoteTtl.infinite())
    .build()
)

aapl = openpit.Instrument("AAPL", "USD")
aapl_id = service.register(aapl)

# Publish a full snapshot into the default ("everyone-else") bucket.
# Caller computes feed-observation-to-call age; the SDK cannot know it.
quote_source_age = timedelta(milliseconds=20)
service.push(
    aapl_id,
    openpit.marketdata.Quote(mark="150", bid="149.5", ask="150.5"),
    quote_source_age,
)

# Read for an account with no group: the lookup falls through to the
# default bucket. account_info exposes .account_group returning
# AccountGroupId | None. In policy code this is usually the pre-trade
# context; here we use a simple stand-in.
account_id = openpit.param.AccountId.from_int(1)
account_info = types.SimpleNamespace(account_group=None)
quote = service.get(
    aapl_id,
    account_id,
    account_info,
    openpit.marketdata.QuoteResolution.ACCOUNT_THEN_GROUP_THEN_DEFAULT,
)
assert quote.mark == openpit.param.Price("150")
assert quote.bid == openpit.param.Price("149.5")

# resolve recovers the id from the instrument name.
assert service.resolve(aapl) == aapl_id
```

</details>

<details>
<summary>JavaScript</summary>

<!-- markdownlint-disable-next-line MD013 -->
<!-- Test mirror: https://github.com/openpitkit/pit/blob/main/bindings/js/tests/examples_market_data.test.ts -->
```ts
import { Engine } from "@openpit/engine";
import { Quote, QuoteTtl } from "@openpit/engine/marketdata";
import { Instrument, Price } from "@openpit/engine/param";

// The engine spawns no threads; each call runs on the caller's thread.
// See Threading-Contract for the full model.
const service = Engine.builder().marketData(QuoteTtl.infinite()).build();

// register returns an InstrumentId; read its numeric value so the same id can
// be reused across calls (the id reads on every push and get below).
const aaplId = service.register(new Instrument("AAPL", "USD")).value;

// Publish a full snapshot into the default ("everyone-else") bucket.
// Caller computes feed-observation-to-call age; the SDK cannot know it.
const quoteSourceAgeMs = 20;
service.push(
  aaplId,
  new Quote({ mark: "150", bid: "149.5", ask: "150.5" }),
  quoteSourceAgeMs,
);

// Read for an account with no group: the lookup falls through to the default
// bucket. accountInfo is any object exposing an `accountGroup` getter; in
// policy code this is usually the pre-trade context. Here we use a plain
// stand-in. The resolution accepts a wire string.
const accountId = 1;
const accountInfo = { accountGroup: null };
const quote = service.get(
  aaplId,
  accountId,
  accountInfo,
  "ACCOUNT_THEN_GROUP_THEN_DEFAULT",
);
if (quote === undefined) {
  throw new Error("quote must be present");
}
if (!quote.mark!.equals(Price.fromString("150"))) {
  throw new Error("unexpected mark");
}
if (!quote.bid!.equals(Price.fromString("149.5"))) {
  throw new Error("unexpected bid");
}

// resolve recovers the id from the instrument name.
if (service.resolve(new Instrument("AAPL", "USD"))!.value !== aaplId) {
  throw new Error("unexpected resolve result");
}
```

</details>

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

<!-- markdownlint-disable-next-line MD013 -->
<!-- Test mirror: https://github.com/openpitkit/pit/blob/main/bindings/cpp/test/wiki/market_data_test.cpp -->
```cpp
#include <cassert>
#include <chrono>

namespace md = openpit::marketdata;
using openpit::param::AccountGroupId;
using openpit::param::AccountId;
using openpit::param::Price;

// A reading account that belongs to no group: resolution falls through to the
// default bucket. Service::Get accepts any object exposing
// `std::optional<AccountGroupId> AccountGroup() const`; in policy code this is
// usually the pre-trade context. Here we use a no-group stub.
#include <optional>

struct NoGroupInfo {
  std::optional<AccountGroupId> AccountGroup() const { return std::nullopt; }
};

// The engine builder fixes the sync mode; the market-data service derives it
// via the engine-builder path. A no-sync engine yields a no-sync,
// single-threaded service whose locks are a free no-op.
md::Service service =
    md::Builder::FromEngineSyncPolicy(md::QuoteTtl::Infinite(),
                                      openpit::SyncPolicy::None)
        .Build();

const openpit::model::Instrument aapl(::openpit::param::Asset("AAPL"),
                                      ::openpit::param::Asset("USD"));
const md::RegisterResult registration = service.Register(aapl);
assert(registration.status == md::RegisterStatus::Ok);
assert(registration.instrumentId.has_value());
const md::InstrumentId aaplId = registration.instrumentId.value();

// Publish a full snapshot into the default ("everyone-else") bucket.
const Price mark = Price::FromString("150");
const Price bid = Price::FromString("149.5");
const Price ask = Price::FromString("150.5");
// Caller computes feed-observation-to-call age; the SDK cannot know it.
const std::chrono::milliseconds quoteSourceAge(20);
assert(service.Push(aaplId,
                    md::Quote().WithMark(mark).WithBid(bid).WithAsk(ask),
                    quoteSourceAge) ==
       md::RegisterStatus::Ok);

// Read for an account with no group: the lookup falls through to the default
// bucket. Pass any AccountInfo; in policy code this is usually the pre-trade
// context. Here we use a no-group stub.
const AccountId accountId = AccountId::FromUint64(1);
const std::optional<md::Quote> quote =
    service.Find(aaplId, accountId, NoGroupInfo{},
                 md::QuoteResolution::AccountThenGroupThenDefault);
assert(quote.has_value());
assert(quote->Mark() == mark && quote->Bid() == bid);

// Resolve recovers the id from the instrument name.
assert(service.Resolve(aapl) == aaplId);
```

</details>

<details>
<summary>Rust</summary>

<!-- markdownlint-disable-next-line MD013 -->
<!-- Test mirror: https://github.com/openpitkit/pit/blob/main/crates/openpit/tests/examples_market_data.rs -->
```rust
use std::time::Duration;

use openpit::param::{AccountId, AccountGroupId, Asset, Price};
use openpit::{Engine, Instrument, Quote, QuoteResolution, QuoteTtl};

let service = Engine::builder::<(), (), ()>().no_sync().market_data(QuoteTtl::Infinite).build();

let aapl = Instrument::new(Asset::new("AAPL")?, Asset::new("USD")?);
let aapl_id = service.register(aapl.clone())?;

// Publish a full snapshot into the default ("everyone-else") bucket.
// Caller computes feed-observation-to-call age; the SDK cannot know it.
let quote_source_age = Duration::from_millis(20);
service.push(
    aapl_id,
    Quote::new()
        .with_mark(Price::from_str("150")?)
        .with_bid(Price::from_str("149.5")?)
        .with_ask(Price::from_str("150.5")?),
    quote_source_age,
)?;

// Read for an account with no group: the lookup falls through to the
// default bucket.
let account = AccountId::from_u64(1);
let quote = service
    .get(
        aapl_id,
        account,
        &None::<AccountGroupId>,
        QuoteResolution::AccountThenGroupThenDefault,
    )
    .expect("quote must be present");
assert_eq!(quote.mark, Some(Price::from_str("150")?));
assert_eq!(quote.bid, Some(Price::from_str("149.5")?));

// resolve recovers the id from the instrument name.
assert_eq!(service.resolve(&aapl), Some(aapl_id));
```

</details>

### Resolving the Account Group

`account info` is consulted lazily: the group is read only when the per-account
bucket misses and the selected mode still has a group tier to try. The answer is
three-valued, not two-valued:

- **a group** - the group bucket (and the group TTL tier) is consulted;
- **no group** - the account genuinely has none, so the lookup falls through to
  the default bucket;
- **could not be determined** - the accessor failed.

The third case fails the whole read. It is never folded into "no group",
because that would silently move the read onto the default bucket and skip
every group-scoped rule and TTL override that should have applied.

How the failure is reported follows each language:

- Go: `Service.Get` returns `ErrAccountGroupResolution`, an
  `*AccountGroupResolutionError` carrying the recovered panic value, or, when
  `AccountGroup` answers with an uninitialized `param.AccountGroupID`, an `Err`
  of `param.ErrUninitializedAccountGroupID`; `Service.GetOptional` yields
  nothing.
- Python: the exception raised by the `account_group` attribute is re-raised by
  the lookup that asked for it.
- JavaScript: a throwing `accountGroup` getter propagates out of the read, and
  an `accountGroup` that is neither an `AccountGroupId` nor `null` is a
  `TypeError`.
- C++: an exception thrown by `AccountGroup()` is rethrown from `Service::Get`
  with its original type once the native frame has unwound.
- Rust: the `AccountInfo` trait answers with `Option<AccountGroupId>` and has no
  failure channel, so this case does not arise in the pure-Rust SDK.

## Targeted Fan-Out: `push for`

`push for` fans a single complete quote out to a list of accounts and a list of
groups in one call. All targets share the same source age. Passing the `default
account group` in the group list writes the default bucket. Calling `push for`
with both lists empty is a caller error (`no target`).

<details>
<summary>Go</summary>

<!-- markdownlint-disable-next-line MD013 -->
<!-- Test mirror: https://github.com/openpitkit/pit/blob/main/bindings/go/examples_market_data_test.go -->
```go
groupID, _ := param.NewAccountGroupIDFromUint32(7)

// Fan out to two accounts and one group simultaneously.
if err := service.PushFor(
    aaplID,
    marketdata.NewQuote().WithMark(mark),
    0,
    []param.AccountID{
        param.NewAccountIDFromUint64(10),
        param.NewAccountIDFromUint64(11),
    },
    []param.AccountGroupID{groupID},
); err != nil {
    panic(err)
}

// Read back for account 10 under AccountOnly - hits the per-account bucket.
quote, ok := service.GetOptional(
    aaplID,
    param.NewAccountIDFromUint64(10),
    noGroupInfo{},
    marketdata.QuoteResolutionAccountOnly,
).Get()
if !ok {
    panic("quote must be present for account 10")
}
gotMark, _ := quote.Mark().Get()
if !gotMark.Equal(mark) {
    panic("unexpected mark for account 10")
}
```

</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_market_data.py -->
```python
group_id = openpit.param.AccountGroupId.from_int(7)

# Fan out to two accounts and one group simultaneously.
service.push_for(
    aapl_id,
    openpit.marketdata.Quote(mark="150"),
    timedelta(),
    [
        openpit.param.AccountId.from_int(10),
        openpit.param.AccountId.from_int(11),
    ],
    [group_id],
)

# Read back for account 10 under AccountOnly - hits the per-account bucket.
account_info = types.SimpleNamespace(account_group=None)
quote = service.get(
    aapl_id,
    openpit.param.AccountId.from_int(10),
    account_info,
    openpit.marketdata.QuoteResolution.ACCOUNT_ONLY,
)
assert quote.mark == openpit.param.Price("150")
```

</details>

<details>
<summary>JavaScript</summary>

<!-- markdownlint-disable-next-line MD013 -->
<!-- Test mirror: https://github.com/openpitkit/pit/blob/main/bindings/js/tests/examples_market_data.test.ts -->
```ts
import { Engine } from "@openpit/engine";
import { Quote, QuoteTtl } from "@openpit/engine/marketdata";
import { Instrument, Price } from "@openpit/engine/param";

const service = Engine.builder().marketData(QuoteTtl.infinite()).build();
const aaplId = service.register(new Instrument("AAPL", "USD")).value;

const groupId = 7;

// Fan out to two accounts and one group simultaneously.
service.pushFor(aaplId, new Quote({ mark: "150" }), 0, [10, 11], [groupId]);

// Read back for account 10 under ACCOUNT_ONLY - hits the per-account bucket.
const accountInfo = { accountGroup: null };
const quote = service.get(aaplId, 10, accountInfo, "ACCOUNT_ONLY");
if (quote === undefined) {
  throw new Error("quote must be present for account 10");
}
if (!quote.mark!.equals(Price.fromString("150"))) {
  throw new Error("unexpected mark for account 10");
}
```

</details>

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

<!-- markdownlint-disable-next-line MD013 -->
<!-- Test mirror: https://github.com/openpitkit/pit/blob/main/bindings/cpp/test/wiki/market_data_test.cpp -->
```cpp
#include <cassert>
#include <chrono>

namespace md = openpit::marketdata;
using openpit::param::AccountGroupId;
using openpit::param::AccountId;
using openpit::param::Price;

md::Service service =
    md::Builder::FromEngineSyncPolicy(md::QuoteTtl::Infinite(),
                                      openpit::SyncPolicy::None)
        .Build();
const md::RegisterResult registration =
    service.Register(openpit::model::Instrument(
        ::openpit::param::Asset("AAPL"), ::openpit::param::Asset("USD")));
assert(registration.status == md::RegisterStatus::Ok);
assert(registration.instrumentId.has_value());
const md::InstrumentId aaplId = registration.instrumentId.value();

const Price mark = Price::FromString("150");
const AccountGroupId groupId = AccountGroupId::FromUint32(7);

#include <optional>

struct NoGroupInfo {
  std::optional<AccountGroupId> AccountGroup() const { return std::nullopt; }
};

// Fan out to two accounts and one group simultaneously.
assert(service.PushFor(
           aaplId, md::Quote().WithMark(mark),
           std::chrono::nanoseconds::zero(),
           {AccountId::FromUint64(10), AccountId::FromUint64(11)},
           {groupId}) == md::RegisterStatus::Ok);

// Read back for account 10 under AccountOnly - hits the per-account bucket.
const std::optional<md::Quote> quote =
    service.Find(aaplId, AccountId::FromUint64(10), NoGroupInfo{},
                 md::QuoteResolution::AccountOnly);
assert(quote.has_value() && quote->Mark() == mark);
```

</details>

<details>
<summary>Rust</summary>

<!-- markdownlint-disable-next-line MD013 -->
<!-- Test mirror: https://github.com/openpitkit/pit/blob/main/crates/openpit/tests/examples_market_data.rs -->
```rust
use std::time::Duration;

use openpit::param::{AccountGroupId, AccountId, Asset, Price};
use openpit::{Engine, Instrument, Quote, QuoteResolution, QuoteTtl};

let service = Engine::builder::<(), (), ()>().no_sync().market_data(QuoteTtl::Infinite).build();
let aapl_id = service.register(Instrument::new(Asset::new("AAPL")?, Asset::new("USD")?))?;

let group_id = AccountGroupId::from_u32(7)?;

// Fan out to two accounts and one group simultaneously.
service.push_for(
    aapl_id,
    Quote::new().with_mark(Price::from_str("150")?),
    Duration::ZERO,
    &[AccountId::from_u64(10), AccountId::from_u64(11)],
    &[group_id],
)?;

// Read back for account 10 under AccountOnly - hits the per-account bucket.
let quote = service
    .get(
        aapl_id,
        AccountId::from_u64(10),
        &None::<AccountGroupId>,
        QuoteResolution::AccountOnly,
    )
    .expect("quote must be present");
assert_eq!(quote.mark, Some(Price::from_str("150")?));
```

</details>

## Replace Quote Snapshots

Each publication is a complete observation. A mark-only observation removes the
stored `bid` and `ask`; it does not retain them from an earlier observation. If
a source delivers fields separately, its publisher must combine only the fields
that belong to the same observation and publish one snapshot with that
observation's source age.

<details>
<summary>Go</summary>

<!-- markdownlint-disable-next-line MD013 -->
<!-- Test mirror: https://github.com/openpitkit/pit/blob/main/bindings/go/examples_market_data_test.go -->
```go
service, err := NewEngineBuilder().
    FullSync().
    MarketData(marketdata.InfiniteTTL()).
    Build()
if err != nil {
    panic(err)
}
defer service.Close()

aapl, _ := param.NewAsset("AAPL")
usd, _ := param.NewAsset("USD")
aaplID, err := service.Register(param.NewInstrument(aapl, usd))
if err != nil {
    panic(err)
}

mark, _ := param.NewPriceFromString("100")
bid, _ := param.NewPriceFromString("99")
ask, _ := param.NewPriceFromString("101")
if err := service.Push(
    aaplID,
    marketdata.NewQuote().WithMark(mark).WithBid(bid).WithAsk(ask),
    0,
); err != nil {
    panic(err)
}

// A mark-only observation clears bid and ask.
newMark, _ := param.NewPriceFromString("105")
if err := service.Push(
    aaplID,
    marketdata.NewQuote().WithMark(newMark),
    0,
); err != nil {
    panic(err)
}

accountID := param.NewAccountIDFromUint64(1)
quote, ok := service.GetOptional(
    aaplID,
    accountID,
    noGroupInfo{},
    marketdata.QuoteResolutionAccountThenGroupThenDefault,
).Get()
if !ok {
    panic("quote must be present")
}
gotMark, _ := quote.Mark().Get()
if !gotMark.Equal(newMark) {
    panic("unexpected mark after replacement")
}
if quote.Bid().IsSet() {
    panic("bid must be unset after replacement")
}
if quote.Ask().IsSet() {
    panic("ask must be unset after replacement")
}
```

</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_market_data.py -->
```python
import types
from datetime import timedelta

import openpit
import openpit.marketdata

service = (
    openpit.Engine.builder()
    .no_sync()
    .market_data(openpit.marketdata.QuoteTtl.infinite())
    .build()
)
aapl_id = service.register(openpit.Instrument("AAPL", "USD"))

service.push(
    aapl_id,
    openpit.marketdata.Quote(mark="100", bid="99", ask="101"),
    timedelta(),
)

# A mark-only observation clears bid and ask.
service.push(aapl_id, openpit.marketdata.Quote(mark="105"), timedelta())

account_id = openpit.param.AccountId.from_int(1)
account_info = types.SimpleNamespace(account_group=None)
quote = service.get(
    aapl_id,
    account_id,
    account_info,
    openpit.marketdata.QuoteResolution.ACCOUNT_THEN_GROUP_THEN_DEFAULT,
)
assert quote.mark == openpit.param.Price("105")
assert quote.bid is None
assert quote.ask is None
```

</details>

<details>
<summary>JavaScript</summary>

<!-- markdownlint-disable-next-line MD013 -->
<!-- Test mirror: https://github.com/openpitkit/pit/blob/main/bindings/js/tests/examples_market_data.test.ts -->
```ts
import { Engine } from "@openpit/engine";
import { Quote, QuoteTtl } from "@openpit/engine/marketdata";
import { Instrument, Price } from "@openpit/engine/param";

const service = Engine.builder().marketData(QuoteTtl.infinite()).build();
const aaplId = service.register(new Instrument("AAPL", "USD")).value;

service.push(aaplId, new Quote({ mark: "100", bid: "99", ask: "101" }), 0);

// A mark-only observation clears bid and ask.
service.push(aaplId, new Quote({ mark: "105" }), 0);

const accountInfo = { accountGroup: null };
const quote = service.get(
  aaplId,
  1,
  accountInfo,
  "ACCOUNT_THEN_GROUP_THEN_DEFAULT",
)!;
if (!quote.mark!.equals(Price.fromString("105"))) {
  throw new Error("unexpected mark after replacement");
}
if (quote.bid !== undefined) {
  throw new Error("bid must be unset after replacement");
}
if (quote.ask !== undefined) {
  throw new Error("ask must be unset after replacement");
}
```

</details>

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

<!-- markdownlint-disable-next-line MD013 -->
<!-- Test mirror: https://github.com/openpitkit/pit/blob/main/bindings/cpp/test/wiki/market_data_test.cpp -->
```cpp
#include <cassert>
#include <chrono>

namespace md = openpit::marketdata;
using openpit::param::AccountId;
using openpit::param::Price;

md::Service service =
    md::Builder::FromEngineSyncPolicy(md::QuoteTtl::Infinite(),
                                      openpit::SyncPolicy::None)
        .Build();
const md::RegisterResult registration =
    service.Register(openpit::model::Instrument(
        ::openpit::param::Asset("AAPL"), ::openpit::param::Asset("USD")));
assert(registration.status == md::RegisterStatus::Ok);
assert(registration.instrumentId.has_value());
const md::InstrumentId aaplId = registration.instrumentId.value();

const Price bid = Price::FromString("99");
const Price ask = Price::FromString("101");
assert(service.Push(aaplId, md::Quote()
                                 .WithMark(Price::FromString("100"))
                                 .WithBid(bid)
                                 .WithAsk(ask),
                    std::chrono::nanoseconds::zero()) ==
       md::RegisterStatus::Ok);

// A mark-only observation clears bid and ask.
const Price newMark = Price::FromString("105");
assert(service.Push(aaplId, md::Quote().WithMark(newMark),
                    std::chrono::nanoseconds::zero()) ==
       md::RegisterStatus::Ok);

#include <optional>

struct NoGroupInfo {
  std::optional<openpit::param::AccountGroupId> AccountGroup() const {
    return std::nullopt;
  }
};

const AccountId accountId = AccountId::FromUint64(1);
const std::optional<md::Quote> quote =
    service.Find(aaplId, accountId, NoGroupInfo{},
                 md::QuoteResolution::AccountThenGroupThenDefault);
assert(quote.has_value());
assert(quote->Mark() == newMark);
assert(!quote->Bid().has_value());
assert(!quote->Ask().has_value());
```

</details>

<details>
<summary>Rust</summary>

<!-- markdownlint-disable-next-line MD013 -->
<!-- Test mirror: https://github.com/openpitkit/pit/blob/main/crates/openpit/tests/examples_market_data.rs -->
```rust
use std::time::Duration;

use openpit::param::{AccountId, AccountGroupId, Asset, Price};
use openpit::{Engine, Instrument, Quote, QuoteResolution, QuoteTtl};

let service = Engine::builder::<(), (), ()>().no_sync().market_data(QuoteTtl::Infinite).build();
let aapl_id = service.register(Instrument::new(Asset::new("AAPL")?, Asset::new("USD")?))?;

service.push(
    aapl_id,
    Quote::new()
        .with_mark(Price::from_str("100")?)
        .with_bid(Price::from_str("99")?)
        .with_ask(Price::from_str("101")?),
    Duration::ZERO,
)?;

// A mark-only observation clears bid and ask.
service.push(
    aapl_id,
    Quote::new().with_mark(Price::from_str("105")?),
    Duration::ZERO,
)?;

let account = AccountId::from_u64(1);
let quote = service
    .get(
        aapl_id,
        account,
        &None::<AccountGroupId>,
        QuoteResolution::AccountThenGroupThenDefault,
    )
    .expect("quote must be present");
assert_eq!(quote.mark, Some(Price::from_str("105")?));
assert_eq!(quote.bid, None);
assert_eq!(quote.ask, None);
```

</details>

## Clearing a Quote

`clear` hides the current quote for an instrument across all three buckets without
unregistering it. A subsequent read reports the quote as absent, just as if it
had never been pushed, while the instrument keeps its identifier. Pushing again
restores visibility. Clearing an unknown instrument is a no-op.

<details>
<summary>Go</summary>

<!-- markdownlint-disable-next-line MD013 -->
<!-- Test mirror: https://github.com/openpitkit/pit/blob/main/bindings/go/examples_market_data_test.go -->
```go
service, err := NewEngineBuilder().
    FullSync().
    MarketData(marketdata.InfiniteTTL()).
    Build()
if err != nil {
    panic(err)
}
defer service.Close()

aapl, _ := param.NewAsset("AAPL")
usd, _ := param.NewAsset("USD")
aaplID, err := service.Register(param.NewInstrument(aapl, usd))
if err != nil {
    panic(err)
}

mark, _ := param.NewPriceFromString("200")
if err := service.Push(
    aaplID,
    marketdata.NewQuote().WithMark(mark),
    0,
); err != nil {
    panic(err)
}

// Clear hides the quote but keeps the instrument registered.
service.Clear(aaplID)
accountID := param.NewAccountIDFromUint64(1)
if _, ok := service.GetOptional(
    aaplID, accountID, noGroupInfo{},
    marketdata.QuoteResolutionAccountThenGroupThenDefault,
).Get(); ok {
    panic("quote must be absent after clear")
}

// Pushing again restores a quote for the same id.
recovered, _ := param.NewPriceFromString("210")
if err := service.Push(
    aaplID,
    marketdata.NewQuote().WithMark(recovered),
    0,
); err != nil {
    panic(err)
}
quote, ok := service.GetOptional(
    aaplID, accountID, noGroupInfo{},
    marketdata.QuoteResolutionAccountThenGroupThenDefault,
).Get()
if !ok {
    panic("quote must be present after recovery push")
}
if got, _ := quote.Mark().Get(); !got.Equal(recovered) {
    panic("unexpected mark after recovery push")
}
```

</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_market_data.py -->
```python
import types
from datetime import timedelta

import openpit
import openpit.marketdata

service = (
    openpit.Engine.builder()
    .no_sync()
    .market_data(openpit.marketdata.QuoteTtl.infinite())
    .build()
)
aapl_id = service.register(openpit.Instrument("AAPL", "USD"))

account_id = openpit.param.AccountId.from_int(1)
account_info = types.SimpleNamespace(account_group=None)

service.push(aapl_id, openpit.marketdata.Quote(mark="200"), timedelta())

# clear hides the quote but keeps the instrument registered.
service.clear(aapl_id)
assert (
    service.get_optional(
        aapl_id,
        account_id,
        account_info,
        openpit.marketdata.QuoteResolution.ACCOUNT_THEN_GROUP_THEN_DEFAULT,
    )
    is None
)

# Pushing again restores a quote for the same id.
service.push(aapl_id, openpit.marketdata.Quote(mark="210"), timedelta())
assert (
    service.get_optional(
        aapl_id,
        account_id,
        account_info,
        openpit.marketdata.QuoteResolution.ACCOUNT_THEN_GROUP_THEN_DEFAULT,
    )
    is not None
)
```

</details>

<details>
<summary>JavaScript</summary>

<!-- markdownlint-disable-next-line MD013 -->
<!-- Test mirror: https://github.com/openpitkit/pit/blob/main/bindings/js/tests/examples_market_data.test.ts -->
```ts
import { Engine } from "@openpit/engine";
import { Quote, QuoteTtl } from "@openpit/engine/marketdata";
import { Instrument } from "@openpit/engine/param";

const service = Engine.builder().marketData(QuoteTtl.infinite()).build();
const aaplId = service.register(new Instrument("AAPL", "USD")).value;

const accountInfo = { accountGroup: null };
const read = () => service.get(aaplId, 1, accountInfo, "ACCOUNT_THEN_GROUP_THEN_DEFAULT");

service.push(aaplId, new Quote({ mark: "200" }), 0);

// clear hides the quote but keeps the instrument registered.
service.clear(aaplId);
if (read() !== undefined) {
  throw new Error("quote must be absent after clear");
}

// Pushing again restores a quote for the same id.
service.push(aaplId, new Quote({ mark: "210" }), 0);
if (read() === undefined) {
  throw new Error("quote must be present after recovery push");
}
```

</details>

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

<!-- markdownlint-disable-next-line MD013 -->
<!-- Test mirror: https://github.com/openpitkit/pit/blob/main/bindings/cpp/test/wiki/market_data_test.cpp -->
```cpp
#include <cassert>
#include <chrono>

namespace md = openpit::marketdata;
using openpit::param::AccountId;
using openpit::param::Price;

md::Service service =
    md::Builder::FromEngineSyncPolicy(md::QuoteTtl::Infinite(),
                                      openpit::SyncPolicy::None)
        .Build();
const md::RegisterResult registration =
    service.Register(openpit::model::Instrument(
        ::openpit::param::Asset("AAPL"), ::openpit::param::Asset("USD")));
assert(registration.status == md::RegisterStatus::Ok);
assert(registration.instrumentId.has_value());
const md::InstrumentId aaplId = registration.instrumentId.value();

#include <optional>

struct NoGroupInfo {
  std::optional<openpit::param::AccountGroupId> AccountGroup() const {
    return std::nullopt;
  }
};

const AccountId accountId = AccountId::FromUint64(1);
assert(service.Push(aaplId, md::Quote().WithMark(Price::FromString("200")),
                    std::chrono::nanoseconds::zero()) ==
       md::RegisterStatus::Ok);

// Clear hides the quote but keeps the instrument registered.
service.Clear(aaplId);
assert(!service
            .Find(aaplId, accountId, NoGroupInfo{},
                  md::QuoteResolution::AccountThenGroupThenDefault)
            .has_value());

// Pushing again restores a quote for the same id.
assert(service.Push(aaplId, md::Quote().WithMark(Price::FromString("210")),
                    std::chrono::nanoseconds::zero()) ==
       md::RegisterStatus::Ok);
assert(service
           .Find(aaplId, accountId, NoGroupInfo{},
                 md::QuoteResolution::AccountThenGroupThenDefault)
           .has_value());
```

</details>

<details>
<summary>Rust</summary>

<!-- markdownlint-disable-next-line MD013 -->
<!-- Test mirror: https://github.com/openpitkit/pit/blob/main/crates/openpit/tests/examples_market_data.rs -->
```rust
use std::time::Duration;

use openpit::param::{AccountId, AccountGroupId, Asset, Price};
use openpit::{Engine, Instrument, Quote, QuoteResolution, QuoteTtl};

let service = Engine::builder::<(), (), ()>().no_sync().market_data(QuoteTtl::Infinite).build();
let aapl_id = service.register(Instrument::new(Asset::new("AAPL")?, Asset::new("USD")?))?;

let account = AccountId::from_u64(1);
let read = |id| {
    service
        .get(
            id,
            account,
            &None::<AccountGroupId>,
            QuoteResolution::AccountThenGroupThenDefault,
        )
        .ok()
};

service.push(
    aapl_id,
    Quote::new().with_mark(Price::from_str("200")?),
    Duration::ZERO,
)?;
assert!(read(aapl_id).is_some());

// clear hides the quote but keeps the instrument registered.
service.clear(aapl_id);
assert!(read(aapl_id).is_none());

// Pushing again restores a quote for the same id.
service.push(
    aapl_id,
    Quote::new().with_mark(Price::from_str("210")?),
    Duration::ZERO,
)?;
let quote = read(aapl_id).expect("quote must be present");
assert_eq!(quote.mark, Some(Price::from_str("210")?));
```

</details>

## Related Pages

- [Market Data TTL](Market-Data-TTL.md) - quote freshness and the eight-tier TTL
  cascade.
- [Market Data Pricing](Market-Data-Pricing.md) - market-order pricing from the
  quote cache.
- [Spot Funds](Spot-Funds.md) - uses market data to price market orders.
- [Account Groups](Account-Groups.md) - account group identifiers; membership drives
  `account then group` and `account then group then default` resolution and the
  TTL cascade group tiers.
- [Policies](Policies.md) - the full built-in policy catalog.
- [Domain Types](Domain-Types.md) - the `price` and `instrument` value types used
  here.
