Skip to content

Market Data TTL

Eugene Palchukovsky edited this page Jun 27, 2026 · 3 revisions

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.

Quote Freshness

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

TTL Cascade

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".

Related Pages

Clone this wiki locally