<< Back to articles list

Inside OTCStreaming Data Queries: Ask Once, Stay Up to Date

A new API to get OTC valuations without writing a pipeline call: live, historical or replayed, kept up to date, with a notification when they change. Part 1 shows how to use it, on one example. Part 2 gives a little of the architecture behind the service.

Why data queries

Until now, getting numbers out of OTCStreaming meant building a pipeline call. That gives full control: your own quotes, patches, models and scenarios. Most of the time, though, you only want standard data: today's spread on a list of CDS, a London Close series for the last six months, or a model's live curve on a screen.

An OTCStreaming Data Query (ODQ) is a query you describe once. OTCStreaming resolves it and prices it in the background with the pipeline's default parameters. Above all, OTCStreaming optimises the computation for you: it prices only what the market moved, computes each move once for every security it touches, spreads the computation over multiple processors, shares that work between all the users of the instance who ask for it, and keeps the latest state in memory. You read it whenever you need it, by its odqId, without sending the query again, and asking again costs no new computation, so it is much faster: a few milliseconds, against tens of milliseconds for the equivalent pipeline call. You are also notified as soon as anything in it changes. No more relaunching the full computation blindly at your own frequency, without knowing whether anything changed: high enough to stay current, but not so high that it overloads the server.

Part 1

Data on a plate: from MEXICO 5Y to the whole credit universe

One example, followed from the first request to live updates, then scaled up to every CDS curve. Each step is one of the new features.

1. Ask once

Describe what you want, not how to compute it. Take the latest value of a MEXICO CDS:

Before: a pipeline call

You describe how to compute it: the stage, the MEXICO contract, the model and the pricing. To get the latest value again, you send the whole call again, and each call is a new computation.

Now: a data query

You describe what you want: MEXICO 5Y, its spread and CS01. OTCStreaming resolves the contract, optimises the computation and keeps the latest value ready, and tells you when it changes.

POST/api/v1/pipelinebefore: how to compute it
{
  "stage": "valuation",
  "correlation": {
    "portfolioQueries": [ { "type": "cdsQuery", "id": "mex5y",
                            "query": { "cdsCurve": "MEXICO", "maturity": "5Y" } } ],
    "ioRefPatches": { "gridPatches": [ { "types": ["cdsGrid"], "modifications": {
        "$.content.priceStyle":     ["IsdaSpread"],
        "$.content.priceDirection": ["Mid"],
        "$.content.greekStyle":     ["CS01"] } } ] }
  },
  "responseContent": "StageOutput"
}

Each call computes from scratch, and you call again to know if anything moved. The same ask as a data query:

POST/api/v1/data-query/security-valuation
{
  "securityQueries": [ { "type": "cdsQuery", "id": "mex5y",
                         "query": { "cdsCurve": "MEXICO", "maturity": "5Y" } } ],
  "directions":  ["Mid"],
  "priceStyles": ["IsdaSpread"],
  "greeks":      ["CS01"]
}
Response tick trimmed, values illustrative
{
  "status": "ok",
  "odqId": "9F3A…",
  "eventId": 1042,
  "eventEpoch": "5b1e…",
  "isFullResync": true,
  "data": [ {
    "securityQueryId": "mex5y",
    "realTime": true,
    "date": "2026-11-09T10:14:03",
    "eventId": 1037,
    "tick": {
      "identification": { "symbol": "MEXICO-3112-100" },
      "attributes": { "context": { "absoluteDirection": "Mid" } },
      "prices": { "computed": { "IsdaSpread": 118.4 } },
      "greeks": { "computed": { "CS01": 4.52 } }
    }
  } ]
}

Two ids: "id": "mex5y" is yours, a label you choose to identify each security you send. odqId is the server's id for the whole ODQ; you use it to read the ODQ again or follow it. The 5Y resolved to today's contract (MEXICO-3112-100), and the spread is in basis points. Three values are worth keeping: the odqId of this query, the eventId you are up to (1042, the top-level one) and its eventEpoch. Anyone on the instance sending the same request gets the same ODQ, already computed. The first answer may say "status": "refreshing" while the first price is computed; ok means complete.

How fast is a repeat read? Indicative timings on one instance, median of 10 calls, same results both ways:

AskPipeline callData query, first requestData query, after thatPipeline ÷ data query
10 names × 5Y (10 CDS)105 ms295 ms43 ms2.4×
10 names × 1Y to 10Y (100 CDS)562 ms717 ms76 ms7.4×

Each time is a full round trip, measured from a client: the request, the work on the server and the answer back. It includes a flat network cost of about 40 ms per call, estimated from an empty request to the same server, which every column pays alike. Without it, a repeat data-query read takes about 10 ms on the server for 10 CDS and under 40 ms for 100, while the pipeline call takes over half a second for 100.

The pipeline prices every security on every call, so its time grows with the size of the ask. A repeat data-query read takes the latest state from memory, whatever the size and whoever asks. The first request pays once to set up the query, for every user of the instance.

2. Be told when it moves

No polling. Open one stream for your ODQs. It stays quiet until the MEXICO curve recalibrates, then sends a single event.

GET/api/v1/data-query/odqs/events?odqId=9F3A…
Response text/event-stream
event: subscribed
data: {"odqId":"9F3A…","eventId":1042,"eventEpoch":"5b1e…"}

event: changed
data: {"odqId":"9F3A…","eventId":1051,"eventEpoch":"5b1e…"}

If MEXICO moves three times while your client is busy, you still get one event, with the latest id. Conflation happens on both sides. On the server, a move that arrives while a refresh is waiting merges into it. On your side, you read when you are ready: one read with the id you hold returns the latest state of everything that moved since, not each intermediate move.

Clients that can't keep a stream open use POST odqs/changes, a long poll that does the same.

3. Fetch only what changed

Move only the delta. Read the ODQ by its id, with the eventId you hold:

POST/api/v1/data-query/odq/9F3A…/valuation
{ "sinceEventId": 1042, "eventEpoch": "5b1e…" }
Response only the rows that moved
{
  "status": "ok",
  "eventId": 1055,
  "eventEpoch": "5b1e…",
  "isFullResync": false,
  "data": [ {
    "securityQueryId": "mex5y",
    "realTime": true,
    "date": "2026-11-09T10:31:47",
    "eventId": 1051,
    "tick": {
      "identification": { "symbol": "MEXICO-3112-100" },
      "prices": { "computed": { "IsdaSpread": 119.1 } },
      "greeks": { "computed": { "CS01": 4.51 } }
    }
  } ]
}

isFullResync: false means this is a delta: replace the MEXICO 5Y row in what you hold, keep the top-level 1055 for the next read, and wait for the next event. If nothing moved, data is empty. After a server restart the answer comes back in full (isFullResync: true), so the client recovers on its own.

Event ids and epochs

Event ids. Every commit on the instance takes the next id from one shared counter, so the ids of your ODQ go up, with gaps left by other models. Keep the highest id you have read; a read with sinceEventId returns the latest value of everything that moved after it, however many moves happened in between.

Epoch. An epoch is one generation of that counter, named by eventEpoch. The epoch is set by the server: starting a new one, after a restart or a reset of the data queries, is the way it forces every client to synchronise. A new epoch means a full refresh: the counter starts again, ids from the old epoch mean nothing, and the next read returns the whole ODQ with isFullResync: true. Replace what you hold and carry on with the new epoch.

512513514 515516517 518519 client holds 512 MEXICOother MEXICOother MEXICOotherother one "changed" event, eventId 517 513, 515 and 517 arrived while the event was waiting to be sent valuation sinceEventId 512 → MEXICO rows at 517, nothing else
Filled dots are commits that touch the client's ODQ (here, the MEXICO curve); empty dots are other models on the same instance. A slow client is never flooded: it gets one event and fetches the latest state.
EPOCH 5b1e… EPOCH a7c2… 517518519 client holds 519 restart 123 read sinceEventId 519, eventEpoch 5b1e… → full refresh, isFullResync: true
A new epoch means a full refresh. Ids from the old epoch cannot be compared with the new ones, so the server answers in full. The client replaces what it holds and keeps the new epoch and id.

4. The whole credit universe: grids

Get the curves of everything quoted in OTCStreaming, then receive only the ones that moved. Every model in OTCStreaming carries a grid: the set of standard securities it prices. For a CDS curve, the grid is the CDS at each standard tenor. For an index option surface, it is the options at each expiry and strike. A grid is how a model becomes a curve or a surface on a screen.

grid-valuation asks for grids instead of named securities. Here, every CDS curve at the tenors you show:

POST/api/v1/data-query/grid-valuation
{
  "modelQueries": [ { "type": "cdsCurve", "id": "run" } ],
  "securityGrid": [ { "path": "$.terminationDate",
                      "values": ["1Y", "3Y", "5Y", "7Y", "10Y"] } ],
  "directions":  ["Mid"],
  "priceStyles": ["IsdaSpread"]
}
Response delta: MEXICO and BRAZIL moved
{
  "status": "ok",
  "eventId": 1093,
  "eventEpoch": "5b1e…",
  "isFullResync": false,
  "data": [
    { "modelQueryId": "run", "realTime": true, "eventId": 1088,
      "tick": { "identification": { "symbol": "MEXICO-3112-100" },
                "prices": { "computed": { "IsdaSpread": 119.6 } } } },
    { "modelQueryId": "run", "realTime": true, "eventId": 1091,
      "tick": { "identification": { "symbol": "BRAZIL-3112-100" },
                "prices": { "computed": { "IsdaSpread": 154.0 } } } },
    …  the other tenors of MEXICO and BRAZIL
  ]
}
  • symbols picks the models. Left out, as here, the query covers every CDS curve. Add "symbols": ["MEXICO", "BRAZIL"] to follow only those.
  • securityGrid picks the points. Left out, you get each model's own stored grid, every point of it.

The first answer is the full run, one row per curve and tenor, all under your "id": "run". After that, a read with your sinceEventId returns only the curves that recalibrated since. If MEXICO and BRAZIL moved, you get their points and nothing else, as in the answer above.

The size of a delta follows what moved, not what you watch: if only MEXICO moved, the answer for the whole credit universe is the same size as the answer for MEXICO alone. A curve that is not in the answer has not changed: keep the rows you hold. All the points of a curve are priced in one pass and arrive together, so a screen never shows half a curve old and half new. The stream of step 2 sends changed events for the run as a whole, not one per curve. The same works with security-valuation and a list of named securities.

5. The same request, back in time

Live and history from one API. Add a range and a stored snapshot, "from", "to", "histoTag": "London Close", and pick a mode:

Historical

Each date with the 5Y contract current then, following the rolls.

Replay

Today's 5Y contract priced on every past date, in one pass.

For instance, the last three months in Historical mode:

POST/api/v1/data-query/security-valuation
{ …same query as step 1…,
  "from": "2026-08-10", "to": "2026-11-06",
  "histoTag": "London Close", "mode": "Historical" }
Response one row per date
"data": [
  { "securityQueryId": "mex5y", "mode": "Historical", "date": "2026-09-18",
    "tick": { "identification": { "symbol": "MEXICO-3106-100" },
              "prices": { "computed": { "IsdaSpread": 112.9 } } } },
  { "securityQueryId": "mex5y", "mode": "Historical", "date": "2026-09-21",
    "tick": { "identification": { "symbol": "MEXICO-3112-100" },
              "prices": { "computed": { "IsdaSpread": 117.3 } } } },
  …
]

You get one row per date, and the contract changes at the September roll: MEXICO-3106-100 before it, MEXICO-3112-100 after it.

In Replay mode, the series comes back inside a single row, as a list of { replayDate, prices, greeks } in tick.rScenarios.absolute. Leave out to and the series runs up to the live level. A history fill takes a while: poll until status is ok.

Recap: endpoints and options

All endpoints sit under /api/v1/data-query on your instance and take your own bearer token: Authorization: Bearer <token>, one per user, from your OTCStreaming login.

EndpointWhat it does
POST security-valuationCreates or reuses an ODQ on named securities (securityQueries) and returns its data.
POST grid-valuationSame, on the grids of models (modelQueries, securityGrid).
POST odq/{odqId}/valuationReads an existing ODQ by its id, in full or as a delta.
GET odqs/eventsServer-sent events for up to 100 ODQs: subscribed, changed, closed, and a heartbeat every 15 s.
POST odqs/changesLong poll for clients that can't keep a stream open. Body: eventEpoch, odqs (id → last eventId), waitSeconds up to 55. Returns the ODQs changed or refused.
OptionMeaning
directionsRequired: Bid, Mid, Offer, Level.
priceStyles, greeksWhat to compute, e.g. IsdaSpread, CS01. At least one of the two. Only what you ask for comes back.
scenariosScenario symbols, e.g. CS01, 110, CRD1Y.
from, to, histoTagA date range and the stored snapshot to read, e.g. London Close. No dates: live. from only: history up to the live level.
modeReplay (default): today's security on every past date. Historical: the security current on each date, following the rolls.
quotingStyleThe quoting of option grids: delta, moneyness, isdaSpread, price.
proxy, proxySymbolStand-ins for models missing on past dates, e.g. "proxySymbol": "OnTheRun".
securityFeaturesFields of the security record to copy onto each row, e.g. {"terminationDate": "$.content.terminationDate"}.
description, creatorFree text shown on the ODQ monitor. Part of the ODQ identity: keep them fixed to reuse the same ODQ.
sinceEventId, eventEpochLive queries only: return a delta, the rows changed since that event.

Full reference: the OTCStreaming data-query skill. It documents every endpoint, option and response shape, and is written so that your AI agents can load it and call the API directly. For your own quotes, patches or bespoke shocks, use the pipeline, which has its own skill.

Part 2

Under the hood: ODQ and ADQ

The problems and their fixes, how it fits together, then one market move followed from end to end.

Four problems, four fixes

Serving standard data, live, to every user and every AI agent of an instance:

Cache

Everyone asks the same thing

Users and agents send the same questions again and again.

Fix: one shared state per security, kept in memory. Asking again costs no new pricing.

Correlation

One move touches a whole curve

A recalibration moves every tenor, its scenarios, the options on it.

Fix: what moves together is priced together, in one vectorised call.

Conflation

The market outruns heavy analytics

Moves can arrive faster than a heavy tensor can be priced.

Fix: moves that arrive while a refresh waits merge into it, so the latest state is priced next. Quotes are never sampled: every quote is stored.

Channels

Light and heavy work compete

A slow option surface must not hold up a CDS curve.

Fix: one channel per type of work, each with its own processors and its own pace of conflation.

From your query to priced securities

Take two users: a desk asks for MEXICO 5Y, an AI agent for every CDS curve. Each request is an ODQ (OTCStreaming Data Query), and the same request always gives the same ODQ, whoever sends it: the odqId is a signature of the request, a hash of what you ask.

Each ODQ is split into ADQs (Atomic Data Queries), one per security. An ADQ is a small state machine holding the latest price, greeks and scenarios of its security in memory. ADQs are shared: both requests include MEXICO 5Y, so they use the same ADQ, priced once.

When MEXICO recalibrates, the ODQ orchestrator reprices only the MEXICO ADQs, with the same pipeline you already call directly, using its default parameters. Each ADQ then tells the ODQs that hold it, and they notify their clients.

ODQ · WHAT YOU ASK ADQ · WHAT GETS PRICED ODQ 9F3A…MEXICO 5Y · desk and risk ODQ 7C10…every CDS curve · AI agent each ODQ notifies its clients over SSE links both ways: the ODQ knows its ADQs, each ADQ knows its ODQs MEXICO 1Y 3Y 5Y 7Y 10Y BRAZIL 1Y 3Y 5Y 7Y 10Y ITXEB545 … … tens of thousands of state machines shared model recalibratesMEXICO, BRAZIL… ODQ orchestrator resolves requests into ADQs · groups them by real-time key (correlation) · channels (conflation) · stamps an event id prices and commits
Both ODQs hold the MEXICO 5Y ADQ: it is priced once, and when it changes it tells both.

Example: one market move, end to end

The diagram follows one move through. Each row is a channel; each processor group reads its own channel first, then helps the lighter ones, never a heavier one.

1 · MODEL 2 · REAL-TIME KEY correlation 3 · CHANNEL QUEUE conflation 4 · PROCESSORS channels 5 · COMMIT AND NOTIFY cache MEXICOCDS curve MEXICO · Mid 1Y 3Y 5Y 7Y 10Y all ADQs in one call default · the rest little conflation up to 12/12 same MEXICO move MEXICO · scenarios CS01 110 CRD1Y all ADQs in one call scenario more conflation up to 4/12 ITXEB545index option surface ITXEB545 · delta K×T K×T K×T K×T K×T all ADQs in one call heavy more conflation up to 8/12 BRAZILCDS curve BRAZIL · i-scenario 3Y 5Y 10Y all ADQs in one call iscenario most conflation up to 2/12 Commit eventId 517 stamped on every ADQ of the key at once Cache latest state in memory, shared by every user of the instance Notify SSE: changed client reads only the delta real-time key = model × quoting style × scenario kind processor groups:i-scenarioscenarioheavydefault· each reads its own channel first, then helps lighter channels
One MEXICO move feeds two keys, default and scenario; every channel ends in one commit, one cached state and one changed event. The example is a server with 12 processors allocated to ADQ pricing.

Available now

OTCStreaming Data Queries are an addition to the current offer, at no additional charge, including the bundle offer. They are part of the OTCStreaming API (/api/v1/data-query). Ask your OTCStreaming contact for a walkthrough.

Built by the OTCStreaming team: Yunfan Zhou, Benjamin d'Hayer, Thomas Blayac, Guillaume Racineux and Philippe Donnat.

<< Back to articles list