-
Notifications
You must be signed in to change notification settings - Fork 1
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:
- Market Data TTL - quote freshness and the eight-tier TTL cascade.
- Market Data Pricing - how the spot funds policy prices market orders from the cache.
For the full threading model see Threading Contract.
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.
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 syncon 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.
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
pushcall; internally it is the bucket of the reserveddefault account group(id0).
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.
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_idJavaScript
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));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.GetreturnsErrAccountGroupResolution, an*AccountGroupResolutionErrorcarrying the recovered panic value;Service.GetOptionalyields nothing. - Python: the exception raised by the
account_groupattribute is re-raised by the lookup that asked for it. - JavaScript: a throwing
accountGroupgetter propagates out of the read, and anaccountGroupthat is neither anAccountGroupIdnornullis aTypeError. - C++: an exception thrown by
AccountGroup()is rethrown fromService::Getwith its original type once the native frame has unwound. - Rust: the
AccountInfotrait answers withOption<AccountGroupId>and has no failure channel, so this case does not arise in the pure-Rust SDK.
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")?));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 NoneJavaScript
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);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")?));- 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 groupandaccount then group then defaultresolution and the TTL cascade group tiers. - Policies - the full built-in policy catalog.
-
Domain Types - the
priceandinstrumentvalue types used here.