Inside OTCStreaming Data Queries: Ask Once, Stay Up to Date
Release
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.
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:
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.
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.
{
"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:
{
"securityQueries": [ { "type": "cdsQuery", "id": "mex5y",
"query": { "cdsCurve": "MEXICO", "maturity": "5Y" } } ],
"directions": ["Mid"],
"priceStyles": ["IsdaSpread"],
"greeks": ["CS01"]
}{
"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:
| Ask | Pipeline call | Data query, first request | Data query, after that | Pipeline ÷ data query |
|---|---|---|---|---|
| 10 names × 5Y (10 CDS) | 105 ms | 295 ms | 43 ms | 2.4× |
| 10 names × 1Y to 10Y (100 CDS) | 562 ms | 717 ms | 76 ms | 7.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.
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:
{ "sinceEventId": 1042, "eventEpoch": "5b1e…" }{
"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.
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:
{
"modelQueries": [ { "type": "cdsCurve", "id": "run" } ],
"securityGrid": [ { "path": "$.terminationDate",
"values": ["1Y", "3Y", "5Y", "7Y", "10Y"] } ],
"directions": ["Mid"],
"priceStyles": ["IsdaSpread"]
}{
"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
]
}symbolspicks the models. Left out, as here, the query covers every CDS curve. Add"symbols": ["MEXICO", "BRAZIL"]to follow only those.securityGridpicks 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:
Each date with the 5Y contract current then, following the rolls.
Today's 5Y contract priced on every past date, in one pass.
For instance, the last three months in Historical mode:
{ …same query as step 1…,
"from": "2026-08-10", "to": "2026-11-06",
"histoTag": "London Close", "mode": "Historical" }"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.
| Endpoint | What it does |
|---|---|
POST security-valuation | Creates or reuses an ODQ on named securities (securityQueries) and returns its data. |
POST grid-valuation | Same, on the grids of models (modelQueries, securityGrid). |
POST odq/{odqId}/valuation | Reads an existing ODQ by its id, in full or as a delta. |
GET odqs/events | Server-sent events for up to 100 ODQs: subscribed, changed, closed, and a heartbeat every 15 s. |
POST odqs/changes | Long 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. |
| Option | Meaning |
|---|---|
directions | Required: Bid, Mid, Offer, Level. |
priceStyles, greeks | What to compute, e.g. IsdaSpread, CS01. At least one of the two. Only what you ask for comes back. |
scenarios | Scenario symbols, e.g. CS01, 110, CRD1Y. |
from, to, histoTag | A date range and the stored snapshot to read, e.g. London Close. No dates: live. from only: history up to the live level. |
mode | Replay (default): today's security on every past date. Historical: the security current on each date, following the rolls. |
quotingStyle | The quoting of option grids: delta, moneyness, isdaSpread, price. |
proxy, proxySymbol | Stand-ins for models missing on past dates, e.g. "proxySymbol": "OnTheRun". |
securityFeatures | Fields of the security record to copy onto each row, e.g. {"terminationDate": "$.content.terminationDate"}. |
description, creator | Free text shown on the ODQ monitor. Part of the ODQ identity: keep them fixed to reuse the same ODQ. |
sinceEventId, eventEpoch | Live 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.
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:
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.
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.
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.
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.
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.
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.