Skip to content

Market Data

Eugene Palchukovsky edited this page Aug 11, 2026 · 8 revisions

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 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:

For the full threading model see Threading Contract.

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.
  • Per-group bucket - a quote targeted at an entire account group 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:

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

account then group then default is the widest mode and is what the spot funds 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).

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")
}
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
JavaScript
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");
}
C++
#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);
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));

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; 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).

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")
}
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")
JavaScript
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");
}
C++
#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);
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")?));

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.

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")
}
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
JavaScript
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");
}
C++
#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());
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);

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.

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")
}
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
)
JavaScript
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");
}
C++
#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());
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")?));

Related Pages

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

Clone this wiki locally