-
Notifications
You must be signed in to change notification settings - Fork 1
Market Data TTL
The market-data service attaches a time-to-live to every quote and resolves the effective lifetime of a read through a multi-axis cascade. This page covers quote freshness and the full TTL cascade. For registration, quote buckets, and the read/write API see Market Data. For the threading model see Threading Contract.
Each quote has a time-to-live. The service default is set at build time. Once a quote is older than its effective time-to-live a read reports it as absent, even though the instrument stays registered. Pushing a new quote restores visibility. An infinite time-to-live keeps the last quote visible until it is replaced or cleared.
The effective TTL for a read is resolved per (instrument, account, group) by a
cascade of eight tiers - see TTL Cascade for the full order.
This matters for policy pricing: a policy that asks for a price gets nothing when the quote has gone stale, which is why the spot funds policy rejects a market order whose pricing quote is unavailable rather than trading on a stale price.
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]()
}
// A 50 ms service-wide lifetime: quotes older than that read as absent.
service, err := openpit.NewEngineBuilder().
FullSync().
MarketData(marketdata.WithinTTL(50 * time.Millisecond)).
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),
); err != nil {
panic(err)
}
accountID := param.NewAccountIDFromUint64(1)
if _, ok := service.GetOptional(
aaplID, accountID, noGroupInfo{},
marketdata.QuoteResolutionAccountThenGroupThenDefault,
).Get(); !ok {
panic("quote must be present right after push")
}
// After the lifetime elapses the quote reads as absent.
time.Sleep(80 * time.Millisecond)
if _, ok := service.GetOptional(
aaplID, accountID, noGroupInfo{},
marketdata.QuoteResolutionAccountThenGroupThenDefault,
).Get(); ok {
panic("quote must be absent after TTL expires")
}
// A fresh push restores visibility.
fresh, _ := param.NewPriceFromString("205")
if err := service.Push(
aaplID,
marketdata.NewQuote().WithMark(fresh),
); err != nil {
panic(err)
}
quote, ok := service.GetOptional(
aaplID, accountID, noGroupInfo{},
marketdata.QuoteResolutionAccountThenGroupThenDefault,
).Get()
if !ok {
panic("quote must be present after fresh push")
}
if got, _ := quote.Mark().Get(); !got.Equal(fresh) {
panic("unexpected mark after fresh push")
}Python
import time
import types
from datetime import timedelta
import openpit
import openpit.marketdata
# A 50 ms service-wide lifetime: quotes older than that read as absent.
service = (
openpit.Engine.builder()
.no_sync()
.market_data(openpit.marketdata.QuoteTtl.within(timedelta(milliseconds=50)))
.build()
)
aapl_id = service.register(openpit.Instrument("AAPL", "USD"))
account_id = openpit.param.AccountId.from_int(1)
account_info = types.SimpleNamespace(account_group=None)
def read():
return service.get_optional(
aapl_id,
account_id,
account_info,
openpit.marketdata.QuoteResolution.ACCOUNT_THEN_GROUP_THEN_DEFAULT,
)
service.push(aapl_id, openpit.marketdata.Quote(mark="200"))
assert read() is not None
# After the lifetime elapses the quote reads as absent.
time.sleep(0.08)
assert read() is None
# A fresh push restores visibility.
service.push(aapl_id, openpit.marketdata.Quote(mark="205"))
assert read() is not NoneC++
#include "openpit/account_id.hpp"
#include "openpit/marketdata.hpp"
#include "openpit/model.hpp"
#include "openpit/param.hpp"
#include <cassert>
#include <chrono>
#include <optional>
#include <thread>
namespace md = openpit::marketdata;
using openpit::param::AccountGroupId;
using openpit::param::AccountId;
using openpit::param::Price;
// AccountInfo for a reading account that belongs to no group.
struct NoGroupInfo {
[[nodiscard]] std::optional<AccountGroupId> AccountGroup() const {
return std::nullopt;
}
};
// A 50 ms service-wide lifetime: quotes older than that read as absent.
md::Service service =
md::Builder(md::QuoteTtl::Within(std::chrono::milliseconds(50)))
.NoSync()
.Build();
const md::RegisterResult registration =
service.Register(openpit::model::Instrument("AAPL", "USD"));
assert(registration.status == md::RegisterStatus::Ok);
assert(registration.instrumentId.has_value());
const md::InstrumentId aaplId = registration.instrumentId.value();
const AccountId accountId = AccountId::FromUint64(1);
auto read = [&]() -> std::optional<md::Quote> {
return service.Find(aaplId, accountId, NoGroupInfo{},
md::QuoteResolution::AccountThenGroupThenDefault);
};
assert(service.Push(aaplId, md::Quote().WithMark(Price::FromString("200"))) ==
md::RegisterStatus::Ok);
assert(read().has_value());
// After the lifetime elapses the quote reads as absent.
std::this_thread::sleep_for(std::chrono::milliseconds(80));
assert(!read().has_value());
// A fresh push restores visibility.
assert(service.Push(aaplId, md::Quote().WithMark(Price::FromString("205"))) ==
md::RegisterStatus::Ok);
assert(read().has_value());Rust
use std::time::Duration;
use openpit::param::{AccountId, AccountGroupId, Asset, Price};
use openpit::{Engine, Instrument, Quote, QuoteResolution, QuoteTtl};
// A 50 ms service-wide lifetime: quotes older than that read as absent.
let service = Engine::builder::<(), (), ()>()
.no_sync()
.market_data(QuoteTtl::Within(Duration::from_millis(50)))
.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")?))?;
assert!(read(aapl_id).is_some());
// After the lifetime elapses the quote is hidden until the next push.
std::thread::sleep(Duration::from_millis(80));
assert!(read(aapl_id).is_none());
// A fresh push restores visibility.
service.push(aapl_id, Quote::new().with_mark(Price::from_str("205")?))?;
let quote = read(aapl_id).expect("quote must be present");
assert_eq!(quote.mark, Some(Price::from_str("205")?));The effective lifetime for a read of (instrument, account, group) is
resolved by walking the following tiers in order and stopping at the
first set value:
| Tier | Cell |
|---|---|
| 1 | instrument x account |
| 2 | instrument x group (skipped when the account has no group) |
| 3 | instrument x default-group |
| 4 | service-level account |
| 5 | service-level group (skipped when the account has no group) |
| 6 | service-level default-group |
| 7 | instrument-level (the registration TTL) |
| 8 | service-wide default from the builder |
A set Infinite TTL stops the cascade immediately and means "no expiry". The
TTL is resolved for the requested (account, group), independent of which
bucket (account, group, or default) the quote was actually found in.
TTL cells are set and cleared via the set * ttl / clear * ttl family:
| Setter | Tier |
|---|---|
set instrument account ttl(id, account, ttl) |
1 |
set instrument account group ttl(id, group, ttl) |
2-3 |
set account ttl(account, ttl) |
4 |
set account group ttl(group, ttl) |
5-6 |
set instrument ttl(id, ttl) |
7 |
| builder default TTL | 8 |
Pass the default account group to set instrument account group ttl or
set account group ttl to address tiers 3 and 6 respectively. Each setter has
a matching clear * ttl counterpart that reverts the cell to "inherit".
- Market Data - registration, quote buckets, and the read/write API.
- Market Data Pricing - market-order pricing from the quote cache.
- Account Groups - group membership drives the group tiers of the cascade.
- Spot Funds - rejects market orders priced from a stale quote.