Skip to content

The guide

This guide teaches the DEBYKO Query Language (DQL) by example. The generated API reference (/v2 in Scalar) lists every endpoint field; the normative language definition is the specification. This document is the tutorial and the cookbook.

Every response in this guide was produced by the engine. They are rendered from a small fixture (DocumentationFixture) whose venue codes and symbols are the ones the platform really collects and whose prices are invented, and they are regenerated on every build — so the shape here is the shape you will get, field for field. What is made up is the numbers, never the form, and no page claims a price. The whole answers, including the canonical query each one echoes back, are in examples.json beside this file.


DEBYKO stores what perpetual-futures venues published, when they published it, and when we received it. DQL is how you ask questions about that store:

  • Screening — “which instruments satisfy this condition right now?” (POST /v2/screen)
  • Screening in the past — the same question, answered with only what was known at a moment you name (as of)
  • Alerts — “call my webhook when this condition becomes true for an instrument”, with a tag you choose in the payload (POST /v2/rules)

A DQL query is a condition: a yes/no statement about data, written the way you would write a WHERE clause. Indicators use the names you already know (ema, rsi, atr, crossover). There are no variables, loops or scripts — a query is one expression, and the same expression has an equivalent JSON form for programs.

Three things make DQL different from other screeners, and they come from how DEBYKO treats data:

  1. Venues stay separate. There is no “BTC price”; there is a BTC price on each venue. Every condition about a venue value says which venues it is about: any(...), all(...), count(...) or max/min/sum(...).
  2. Missing data stays missing. A venue that has not sent a funding rate has no funding rate — not zero, not yesterday’s. A condition on it is neither true nor false; it is unknown, and the language has rules for that (§6).
  3. Everything has an age and a source. Every value in every response tells you which venue sent it, when we received it and how old it was when the query used it.

Question: which instruments have a positive funding rate on at least one venue?

any(venues where funding > 0)

Read it as: for this instrument, is there any venue where funding is greater than zero? funding on its own would not be a valid query — the language insists you say which venues.

Send it:

POST /v2/screen
Content-Type: application/json
{ "query": "any(venues where funding > 0)", "limit": 2 }

Response, on the documentation fixture — real venue codes, invented numbers, the query echo dropped:

{
"dql": "1",
"evaluated_at": "2026-09-23T12:00:05Z",
"as_of": null,
"settled": false,
"freshness": {
"ticker": "30s",
"mark": "30s",
"oi": "30s",
"book": "30s",
"stats": "120s",
"funding": "300s"
},
"instruments": [
{
"instrument": "BTC-PERP",
"result": "true",
"values": {},
"venues": [
{
"venue": "BYBIT-PERP",
"symbol": "BTCUSDT",
"quote": "USDT",
"values": {
"funding": {
"value": 0.00002,
"unit": "fraction",
"venue": "BYBIT-PERP",
"layer": "funding",
"venue_ts": null,
"received_at": "2026-09-23T11:52:00Z",
"age_ms": 485000,
"status": "stale"
}
}
},
{
"venue": "HYPERLIQUID",
"symbol": "BTC",
"quote": "USDC",
"values": {
"funding": {
"value": 0.000013,
"unit": "fraction",
"venue": "HYPERLIQUID",
"layer": "funding",
"venue_ts": null,
"received_at": "2026-09-23T12:00:04Z",
"age_ms": 1000,
"status": "present"
}
}
},
{
"venue": "KRAKEN-FUTURES",
"symbol": "PF_XBTUSD",
"quote": "USD",
"values": {
"funding": {
"value": 0.00001,
"unit": "fraction",
"venue": "KRAKEN-FUTURES",
"layer": "funding",
"venue_ts": null,
"received_at": "2026-09-23T12:00:04Z",
"age_ms": 1000,
"status": "present"
}
}
},
{
"venue": "OKX-PERP",
"symbol": "BTC-USDT-SWAP",
"quote": "USDT",
"values": {
"funding": {
"value": 0.000054,
"unit": "fraction",
"venue": "OKX-PERP",
"layer": "funding",
"venue_ts": null,
"received_at": "2026-09-23T12:00:04Z",
"age_ms": 1000,
"status": "present"
}
}
}
]
}
],
"page": {
"limit": 1,
"returned": 1,
"next_cursor": "eyJ0IjoiMjAyNi0wOS0yM1QxMjowMDowNVoiLCJhZnRlciI6IkJUQy1QRVJQIiwicSI6IjdlZjMxMGZmYTMyOTI2ZDEifQ"
}
}

Things to notice:

  • result is "true" for each returned instrument. Instruments where the answer is unknown are not returned unless you ask ("include_unknown": true).
  • Under each instrument, every listed venue appears, even the one whose value is missing (OKX-PERP above). That is how you see why any was true: three venues said yes, one said nothing.
  • Every value carries venue, received_at, age_ms and status. There is no bare number anywhere in a DEBYKO response.
  • next_cursor continues the same evaluation instant, so page two is consistent with page one even though the market moved.

{ "value": 0.0001, "venue": "BYBIT-PERP", "layer": "funding",
"venue_ts": "2026-09-21T09:30:03.517Z", "received_at": "2026-09-21T09:30:03.702Z",
"age_ms": 1410, "status": "present" }
Key Meaning
value the number the query used; null when there was nothing to use
venue which venue published it
layer the data layer it came from (funding, mark, book, candles, …); fields in one layer share a timestamp
venue_ts the venue’s own timestamp, when the venue sends one; null when it does not
received_at when DEBYKO received it
age_ms evaluated_at − received_at (or as_of − received_at)
status present, stale, missing, not_published or off — see §6
reason only with a null value: no_value, not_retained, gap, insufficient_history, venue_capability, policy_off

Indicator values add timeframe, bars (how many candles the number was computed from) and candle_end (the close time of the newest candle used). Cross-venue aggregates (max, min, sum) replace the single venue with a from list naming every venue that contributed.

Units, once:

  • Prices are plain numbers in the venue’s quote currency (quote), never converted.
  • Rates and spreads are fractions: 0.0001 is one basis point, 0.01 is one percent. You may write them with a suffix — 1bps, 0.01% — and the language turns the suffix into the fraction. spread < 5bps and spread < 0.0005 are the same condition.
  • funding is the rate per funding interval as the venue publishes it; intervals differ (1h, 4h, 8h), so funding_8h is provided as the same rate scaled to eight hours.
  • Durations are 10s, 2m, 1h, 1d (one unit, no 1h30m).

A condition says what must be true. A where clause says which listings to ask at all, and it is the same clause on every endpoint — screen, snapshots, history, candles, books, stream. It runs first, against the listing’s own identity, and never against a measurement:

venues_answered >= 1 where base = BTC
venues_answered >= 1 where instrument in (BTC-PERP, ETH-PERP)
venues_answered >= 1 where venue = KRAKEN-FUTURES
venues_answered >= 1 where quote = USDT
venues_answered >= 1 where symbol = "BTCUSDT"

Five things can be named there:

In the clause What it is Written as
instrument the market code DEBYKO normalised the listing to a code: BTC-PERP
base the base asset of that market a code: BTC
venue the venue’s published code a code: KRAKEN-FUTURES, from GET /v2/venues
quote the listing’s quote currency a code: USDT
symbol the venue’s own name for the listing a quoted string: "BTCUSDT"

A selection may stand alone: where base = BTC with no condition means “every listing of BTC”, which is what the data endpoints send. A venue code that no longer exists, or an instrument that never did, is an error naming what is known — never an empty answer that looks like “nothing matched”.

Venue codes come from GET /v2/venues, which needs no key:

{ "evaluated_at": "2026-09-23T08:51:50.944Z",
"items": [ { "venue": "KRAKEN-FUTURES", "name": "Kraken Futures", "kind": "order_book",
"listings": 296, "listings_collected": 5, "since": "2026-09-20T15:11:19.524Z" } ] }

listings is what the venue lists; listings_collected is what DEBYKO collects today. The two differ on purpose, and the difference is not hidden.

3b. Units, and what a value says about itself

Section titled “3b. Units, and what a value says about itself”

A venue publishes what it publishes: one exchange counts open interest in contracts, another in the base asset. DQL does not guess. You ask for the units you want, and every value tells you what it is.

  • "units": "published" (the default) — exactly what the venue published.
  • "units": "base" — converted to base-asset units with the listing’s multiplier and contract size.

Every value in every response carries unit: quote, base, contracts, fraction, count or duration. Nothing has to be assumed from a field name.

Two consequences worth knowing before they surprise you:

  • Summing a quantity across venues (sum(venues, oi)) is refused under units: published — the sum of Bybit contracts and Kraken contracts is not a number anybody should print. Ask for units: base and it is allowed.
  • Under units: base, a listing whose multiplier or contract size DEBYKO does not know answers null with reason unit_unknown rather than an unconverted number that looks right.

Three response options, while you are here:

  • "number_format": "string" returns every number as the venue’s own digits in a JSON string, so a price that does not fit a double survives the round trip. "json" (the default) returns numbers.
  • "include_raw": true adds the venue’s untouched payload for each layer beside the parsed values.
  • items_sha256 comes back on every data answer: the hash of the items as serialised. Two answers with the same hash are the same answer, which is cheaper to check than a diff.

Each entry gives the intent, the string form, the JSON form, a request and a rendered response. Responses are shortened to the instruments and venues that make the point; a real response lists every venue of every returned instrument.

Intent: instruments with a positive funding rate on at least one venue.

any(venues where funding > 0)

Covered in §2, including the JSON form and the full response.

4.2 Fresh positive funding on at least three venues

Section titled “4.2 Fresh positive funding on at least three venues”

Intent: the funding rate, normalised to eight hours, is above one basis point on three or more venues, and each of those readings is less than two minutes old.

count(venues where funding_8h > 1bps and age(funding) < 2m) >= 3

JSON form:

{ "dql": "1", "condition": { "type": "cmp", "op": ">=",
"left": { "type": "count", "where": { "type": "and", "args": [
{ "type": "cmp", "op": ">", "left": { "type": "field", "name": "funding_8h" }, "right": { "type": "number", "value": 0.0001 } },
{ "type": "cmp", "op": "<", "left": { "type": "call", "name": "age", "args": [ { "type": "field", "name": "funding" } ] },
"right": { "type": "duration", "value": "2m" } } ] } },
"right": { "type": "number", "value": 3 } } }

Request:

{ "query": "count(venues where funding_8h > 1bps and age(funding) < 2m) >= 3", "limit": 50 }

Response, on the documentation fixture — real venue codes, invented numbers, the query echo dropped:

{
"dql": "1",
"evaluated_at": "2026-09-23T12:00:05Z",
"as_of": null,
"settled": false,
"freshness": {
"ticker": "30s",
"mark": "30s",
"oi": "30s",
"book": "30s",
"stats": "120s",
"funding": "300s"
},
"instruments": [],
"page": {
"limit": 1,
"returned": 0,
"next_cursor": null
}
}

DYDX-PERP is listed but did not count: its rate is below the threshold. Three venues did, so the condition holds. Note the canonical string in query.string: 1bps became 0.0001.

Intent: the highest and lowest mark price across venues differ by more than five basis points of the lowest, and at least three venues are answering.

(max(venues, mark) - min(venues, mark)) / min(venues, mark) > 5bps and venues_answered >= 3

JSON form:

{ "dql": "1", "condition": { "type": "and", "args": [
{ "type": "cmp", "op": ">",
"left": { "type": "arith", "op": "/",
"left": { "type": "arith", "op": "-", "left": { "type": "agg", "fn": "max", "expr": { "type": "field", "name": "mark" } },
"right": { "type": "agg", "fn": "min", "expr": { "type": "field", "name": "mark" } } },
"right": { "type": "agg", "fn": "min", "expr": { "type": "field", "name": "mark" } } },
"right": { "type": "number", "value": 0.0005 } },
{ "type": "cmp", "op": ">=", "left": { "type": "field", "name": "venues_answered" }, "right": { "type": "number", "value": 3 } } ] } }

Request:

{ "query": "(max(venues, mark) - min(venues, mark)) / min(venues, mark) > 5bps and venues_answered >= 3" }

Response, on the documentation fixture — real venue codes, invented numbers, the query echo dropped:

{
"dql": "1",
"evaluated_at": "2026-09-23T12:00:05Z",
"as_of": null,
"settled": false,
"freshness": {
"ticker": "30s",
"mark": "30s",
"oi": "30s",
"book": "30s",
"stats": "120s",
"funding": "300s"
},
"instruments": [
{
"instrument": "ETH-PERP",
"result": "true",
"values": {
"max(venues, mark)": {
"value": 2751,
"unit": "quote",
"from": [
{
"venue": "BYBIT-PERP",
"value": 2751,
"received_at": "2026-09-23T12:00:04Z",
"age_ms": 1000,
"status": "present"
}
]
},
"min(venues, mark)": {
"value": 2749,
"unit": "quote",
"from": [
{
"venue": "OKX-PERP",
"value": 2749,
"received_at": "2026-09-23T12:00:04Z",
"age_ms": 1000,
"status": "present"
}
]
},
"venues_answered": {
"value": 3,
"from": "reference",
"as_of": "2026-09-23T12:00:05Z"
}
},
"venues": [
{
"venue": "BYBIT-PERP",
"symbol": "ETHUSDT",
"quote": "USDT",
"values": {
"mark": {
"value": 2751,
"unit": "quote",
"venue": "BYBIT-PERP",
"layer": "mark",
"venue_ts": null,
"received_at": "2026-09-23T12:00:04Z",
"age_ms": 1000,
"status": "present"
}
}
},
{
"venue": "KRAKEN-FUTURES",
"symbol": "PF_ETHUSD",
"quote": "USD",
"values": {
"mark": {
"value": 2750,
"unit": "quote",
"venue": "KRAKEN-FUTURES",
"layer": "mark",
"venue_ts": null,
"received_at": "2026-09-23T12:00:04Z",
"age_ms": 1000,
"status": "present"
}
}
},
{
"venue": "OKX-PERP",
"symbol": "ETH-USDT-SWAP",
"quote": "USDT",
"values": {
"mark": {
"value": 2749,
"unit": "quote",
"venue": "OKX-PERP",
"layer": "mark",
"venue_ts": null,
"received_at": "2026-09-23T12:00:04Z",
"age_ms": 1000,
"status": "present"
}
}
}
]
}
],
"page": {
"limit": 1,
"returned": 1,
"next_cursor": null
}
}

Dispersion is (0.8134 − 0.8119) / 0.8119 ≈ 18.5 bps. KRAKEN-FUTURES had a mark, but it was 84 seconds old — older than the 30-second freshness bound — so it was stale, excluded from max/min, and not counted in venues_answered. The response shows the value’s age; it does not pretend the value is current. Also note that max/min provenance names the winning venue.

Intent: the mark price on KRAKEN-FUTURES and BYBIT-PERP differs by more than 10 (quote units).

max(venues where venue in (KRAKEN-FUTURES, BYBIT-PERP), mark) - min(venues where venue in (KRAKEN-FUTURES, BYBIT-PERP), mark) > 10

JSON form:

{ "dql": "1", "condition": { "type": "cmp", "op": ">",
"left": { "type": "arith", "op": "-",
"left": { "type": "agg", "fn": "max", "expr": { "type": "field", "name": "mark" },
"where": { "type": "in", "left": { "type": "field", "name": "venue" }, "values": [ { "type": "code", "value": "KRAKEN-FUTURES" }, { "type": "code", "value": "BYBIT-PERP" } ] } },
"right": { "type": "agg", "fn": "min", "expr": { "type": "field", "name": "mark" },
"where": { "type": "in", "left": { "type": "field", "name": "venue" }, "values": [ { "type": "code", "value": "KRAKEN-FUTURES" }, { "type": "code", "value": "BYBIT-PERP" } ] } } },
"right": { "type": "number", "value": 10 } } }

Request:

{ "query": "max(venues where venue in (KRAKEN-FUTURES, BYBIT-PERP), mark) - min(venues where venue in (KRAKEN-FUTURES, BYBIT-PERP), mark) > 10" }

Response, on the documentation fixture — real venue codes, invented numbers, the query echo dropped:

{
"dql": "1",
"evaluated_at": "2026-09-23T12:00:05Z",
"as_of": null,
"settled": false,
"freshness": {
"ticker": "30s",
"mark": "30s",
"oi": "30s",
"book": "30s",
"stats": "120s",
"funding": "300s"
},
"instruments": [
{
"instrument": "SOL-PERP",
"result": "unknown",
"values": {
"max(venues where venue in (KRAKEN-FUTURES, BYBIT-PERP), mark)": {
"value": null,
"unit": "quote",
"from": [],
"reason": "no_value"
},
"min(venues where venue in (KRAKEN-FUTURES, BYBIT-PERP), mark)": {
"value": null,
"unit": "quote",
"from": [],
"reason": "no_value"
}
},
"venues": [
{
"venue": "HYPERLIQUID",
"symbol": "SOL",
"quote": "USDC",
"values": {
"venue": {
"value": "HYPERLIQUID",
"venue": "HYPERLIQUID",
"layer": "reference",
"venue_ts": null,
"received_at": "2026-09-23T00:00:00Z",
"age_ms": 43205000,
"status": "present"
},
"mark": {
"value": 118,
"unit": "quote",
"venue": "HYPERLIQUID",
"layer": "mark",
"venue_ts": null,
"received_at": "2026-09-23T12:00:04Z",
"age_ms": 1000,
"status": "present"
}
}
}
]
}
],
"page": {
"limit": 1,
"returned": 1,
"next_cursor": null
}
}

The where inside max(...) restricts the venues the aggregate looks at; other venues still appear in the response because they are part of the instrument, but they did not contribute.

Intent: every venue that publishes an order book has a spread below five basis points. Venues that publish no book (oracle-priced venues) must not make the condition fail.

all(venues where status(bid) = not_published or spread < 5bps)

JSON form:

{ "dql": "1", "condition": { "type": "all", "where": { "type": "or", "args": [
{ "type": "cmp", "op": "=", "left": { "type": "call", "name": "status", "args": [ { "type": "field", "name": "bid" } ] }, "right": { "type": "status", "value": "not_published" } },
{ "type": "cmp", "op": "<", "left": { "type": "field", "name": "spread" }, "right": { "type": "number", "value": 0.0005 } } ] } } }

Request:

{ "query": "all(venues where status(bid) = not_published or spread < 5bps)" }

Response, on the documentation fixture — real venue codes, invented numbers, the query echo dropped:

{
"dql": "1",
"evaluated_at": "2026-09-23T12:00:05Z",
"as_of": null,
"settled": false,
"freshness": {
"ticker": "30s",
"mark": "30s",
"oi": "30s",
"book": "30s",
"stats": "120s",
"funding": "300s"
},
"instruments": [
{
"instrument": "BTC-PERP",
"result": "true",
"values": {},
"venues": [
{
"venue": "BYBIT-PERP",
"symbol": "BTCUSDT",
"quote": "USDT",
"values": {
"status(bid)": {
"value": "present",
"venue": "BYBIT-PERP",
"layer": "book",
"venue_ts": null,
"received_at": "2026-09-23T12:00:04Z",
"age_ms": 1000,
"status": "present"
},
"spread": {
"value": 1.1495574203931486e-05,
"unit": "fraction",
"venue": "BYBIT-PERP",
"layer": "book",
"venue_ts": null,
"received_at": "2026-09-23T12:00:04Z",
"age_ms": 1000,
"status": "present"
}
}
},
{
"venue": "HYPERLIQUID",
"symbol": "BTC",
"quote": "USDC",
"values": {
"status(bid)": {
"value": "present",
"venue": "HYPERLIQUID",
"layer": "book",
"venue_ts": null,
"received_at": "2026-09-23T12:00:04Z",
"age_ms": 1000,
"status": "present"
},
"spread": {
"value": 2.2985863693828294e-05,
"unit": "fraction",
"venue": "HYPERLIQUID",
"layer": "book",
"venue_ts": null,
"received_at": "2026-09-23T12:00:04Z",
"age_ms": 1000,
"status": "present"
}
}
},
{
"venue": "KRAKEN-FUTURES",
"symbol": "PF_XBTUSD",
"quote": "USD",
"values": {
"status(bid)": {
"value": "present",
"venue": "KRAKEN-FUTURES",
"layer": "book",
"venue_ts": null,
"received_at": "2026-09-23T12:00:04Z",
"age_ms": 1000,
"status": "present"
},
"spread": {
"value": 2.2988505747126437e-05,
"unit": "fraction",
"venue": "KRAKEN-FUTURES",
"layer": "book",
"venue_ts": null,
"received_at": "2026-09-23T12:00:04Z",
"age_ms": 1000,
"status": "present"
}
}
},
{
"venue": "OKX-PERP",
"symbol": "BTC-USDT-SWAP",
"quote": "USDT",
"values": {
"status(bid)": {
"value": "present",
"venue": "OKX-PERP",
"layer": "book",
"venue_ts": null,
"received_at": "2026-09-23T12:00:04Z",
"age_ms": 1000,
"status": "present"
},
"spread": {
"value": 2.298982700289e-06,
"unit": "fraction",
"venue": "OKX-PERP",
"layer": "book",
"venue_ts": null,
"received_at": "2026-09-23T12:00:04Z",
"age_ms": 1000,
"status": "present"
}
}
}
]
}
],
"page": {
"limit": 1,
"returned": 1,
"next_cursor": "eyJ0IjoiMjAyNi0wOS0yM1QxMjowMDowNVoiLCJhZnRlciI6IkJUQy1QRVJQIiwicSI6ImI5ZTY0YzFjZWJmOTkzZmQifQ"
}
}

Without the status(bid) = not_published or part, a venue that publishes no book would make the all unknown (they have no spread, so spread < 5bps is neither true nor false) and ETH would not be returned. §6 explains why.

Intent: on at least one venue, the 20-candle EMA of hourly closes is above the 50-candle EMA.

any(venues where ema(close, 20) > ema(close, 50)) timeframe 1h

JSON form:

{ "dql": "1", "timeframe": "1h", "condition": { "type": "any", "where": { "type": "cmp", "op": ">",
"left": { "type": "call", "name": "ema", "args": [ { "type": "source", "name": "close" }, { "type": "number", "value": 20 } ] },
"right": { "type": "call", "name": "ema", "args": [ { "type": "source", "name": "close" }, { "type": "number", "value": 50 } ] } } } }

Request:

{ "query": "any(venues where ema(close, 20) > ema(close, 50)) timeframe 1h", "limit": 20 }

Response, on the documentation fixture — real venue codes, invented numbers, the query echo dropped:

{
"dql": "1",
"evaluated_at": "2026-09-23T12:00:05Z",
"as_of": null,
"settled": false,
"timeframe": "1h",
"freshness": {
"ticker": "30s",
"mark": "30s",
"oi": "30s",
"book": "30s",
"stats": "120s",
"funding": "300s"
},
"instruments": [
{
"instrument": "BTC-PERP",
"result": "unknown",
"values": {},
"venues": [
{
"venue": "BYBIT-PERP",
"symbol": "BTCUSDT",
"quote": "USDT",
"values": {
"ema(close, 20)": {
"value": null,
"unit": "quote",
"venue": "BYBIT-PERP",
"layer": "candles",
"timeframe": "1h",
"bars": 80,
"candle_end": null,
"venue_ts": null,
"received_at": null,
"age_ms": null,
"status": "missing",
"reason": "no_value"
},
"ema(close, 50)": {
"value": null,
"unit": "quote",
"venue": "BYBIT-PERP",
"layer": "candles",
"timeframe": "1h",
"bars": 200,
"candle_end": null,
"venue_ts": null,
"received_at": null,
"age_ms": null,
"status": "missing",
"reason": "no_value"
}
}
},
{
"venue": "HYPERLIQUID",
"symbol": "BTC",
"quote": "USDC",
"values": {
"ema(close, 20)": {
"value": null,
"unit": "quote",
"venue": "HYPERLIQUID",
"layer": "candles",
"timeframe": "1h",
"bars": 80,
"candle_end": null,
"venue_ts": null,
"received_at": null,
"age_ms": null,
"status": "missing",
"reason": "no_value"
},
"ema(close, 50)": {
"value": null,
"unit": "quote",
"venue": "HYPERLIQUID",
"layer": "candles",
"timeframe": "1h",
"bars": 200,
"candle_end": null,
"venue_ts": null,
"received_at": null,
"age_ms": null,
"status": "missing",
"reason": "no_value"
}
}
},
{
"venue": "KRAKEN-FUTURES",
"symbol": "PF_XBTUSD",
"quote": "USD",
"values": {
"ema(close, 20)": {
"value": null,
"unit": "quote",
"venue": "KRAKEN-FUTURES",
"layer": "candles",
"timeframe": "1h",
"bars": 80,
"candle_end": null,
"venue_ts": null,
"received_at": null,
"age_ms": null,
"status": "missing",
"reason": "no_value"
},
"ema(close, 50)": {
"value": null,
"unit": "quote",
"venue": "KRAKEN-FUTURES",
"layer": "candles",
"timeframe": "1h",
"bars": 200,
"candle_end": null,
"venue_ts": null,
"received_at": null,
"age_ms": null,
"status": "missing",
"reason": "no_value"
}
}
},
{
"venue": "OKX-PERP",
"symbol": "BTC-USDT-SWAP",
"quote": "USDT",
"values": {
"ema(close, 20)": {
"value": null,
"unit": "quote",
"venue": "OKX-PERP",
"layer": "candles",
"timeframe": "1h",
"bars": 80,
"candle_end": null,
"venue_ts": null,
"received_at": null,
"age_ms": null,
"status": "missing",
"reason": "no_value"
},
"ema(close, 50)": {
"value": null,
"unit": "quote",
"venue": "OKX-PERP",
"layer": "candles",
"timeframe": "1h",
"bars": 200,
"candle_end": null,
"venue_ts": null,
"received_at": null,
"age_ms": null,
"status": "missing",
"reason": "no_value"
}
}
}
]
}
],
"page": {
"limit": 1,
"returned": 1,
"next_cursor": "eyJ0IjoiMjAyNi0wOS0yM1QxMjowMDowNVoiLCJhZnRlciI6IkJUQy1QRVJQIiwicSI6ImRkOWI3MzhhMWQzNzY1OTQifQ"
}
}

Two details. First, the candle used is the last completed hourly candle (candle_end 09:00); the candle that is still forming is never used, so a condition cannot become true and then untrue within the same hour as the price wobbles. Second, ema(close, 20) uses exactly 80 candles (bars), and ema(close, 50) exactly 200 — a fixed window, defined in the specification, so you can recompute the number yourself. WEEX-FUTURES had a hole somewhere in its last 200 hourly candles, so both EMAs are null there (reason: gap); DEBYKO does not fill holes, and the other two venues carried the condition.

4.7 A cross on the latest closed 15-minute candle, one venue, clean history

Section titled “4.7 A cross on the latest closed 15-minute candle, one venue, clean history”

Intent: on KRAKEN-FUTURES, the 20-EMA crossed above the 50-EMA on the most recent completed 15-minute candle, and the last 200 candles have no gaps.

any(venues where venue = KRAKEN-FUTURES and crossover(ema(close, 20), ema(close, 50)) and gaps(200) = 0) timeframe 15m

JSON form:

{ "dql": "1", "timeframe": "15m", "condition": { "type": "any", "where": { "type": "and", "args": [
{ "type": "cmp", "op": "=", "left": { "type": "field", "name": "venue" }, "right": { "type": "code", "value": "KRAKEN-FUTURES" } },
{ "type": "call", "name": "crossover", "args": [
{ "type": "call", "name": "ema", "args": [ { "type": "source", "name": "close" }, { "type": "number", "value": 20 } ] },
{ "type": "call", "name": "ema", "args": [ { "type": "source", "name": "close" }, { "type": "number", "value": 50 } ] } ] },
{ "type": "cmp", "op": "=", "left": { "type": "call", "name": "gaps", "args": [ { "type": "number", "value": 200 } ] }, "right": { "type": "number", "value": 0 } } ] } } }

Request:

{ "query": "any(venues where venue = KRAKEN-FUTURES and crossover(ema(close, 20), ema(close, 50)) and gaps(200) = 0) timeframe 15m" }

Illustrative response (only the named venue is shown; the others are listed in a real response with venue = KRAKEN-FUTURES evaluating to false):

{
"dql": "1",
"query": { "string": "any(venues where venue = KRAKEN-FUTURES and crossover(ema(close, 20), ema(close, 50)) and gaps(200) = 0) timeframe 15m", "json": { "…": "…" } },
"evaluated_at": "2026-09-21T09:30:05.112Z", "as_of": null, "settled": false, "timeframe": "15m",
"freshness": { "ticker": "30s", "mark": "30s", "oi": "30s", "book": "30s", "stats": "120s", "funding": "300s" },
"instruments": [
{ "instrument": "DOGE", "result": "true", "values": {},
"venues": [
{ "venue": "KRAKEN-FUTURES", "symbol": "DOGEUSDT", "quote": "USDT", "values": {
"venue": { "value": "KRAKEN-FUTURES", "venue": "KRAKEN-FUTURES", "layer": "reference", "venue_ts": null, "received_at": "2026-08-30T00:00:12.000Z", "age_ms": 1934393112, "status": "present" },
"crossover(ema(close, 20), ema(close, 50))": { "value": true, "venue": "KRAKEN-FUTURES", "layer": "candles", "timeframe": "15m", "bars": 201, "candle_end": "2026-09-21T09:30:00Z", "venue_ts": "2026-09-21T09:30:00Z", "received_at": "2026-09-21T09:30:00.455Z", "age_ms": 4657, "status": "present" },
"gaps(200)": { "value": 0, "venue": "KRAKEN-FUTURES", "layer": "candles", "timeframe": "15m", "bars": 200, "candle_end": "2026-09-21T09:30:00Z", "venue_ts": "2026-09-21T09:30:00Z", "received_at": "2026-09-21T09:30:00.455Z", "age_ms": 4657, "status": "present" } } }
] }
],
"page": { "limit": 100, "returned": 1, "next_cursor": null }
}

crossover(a, b) means: on the newest completed candle a > b, and on the one before it a <= b. It is a statement about two candles, not a forecast.

Intent: hourly trend up (EMA 20 above EMA 50 on 1h candles) while the 15-minute RSI is low.

any(venues where ema(close, 20, 1h) > ema(close, 50, 1h) and rsi(close, 14, 15m) < 35)

JSON form:

{ "dql": "1", "condition": { "type": "any", "where": { "type": "and", "args": [
{ "type": "cmp", "op": ">",
"left": { "type": "call", "name": "ema", "args": [ { "type": "source", "name": "close" }, { "type": "number", "value": 20 } ], "timeframe": "1h" },
"right": { "type": "call", "name": "ema", "args": [ { "type": "source", "name": "close" }, { "type": "number", "value": 50 } ], "timeframe": "1h" } },
{ "type": "cmp", "op": "<",
"left": { "type": "call", "name": "rsi", "args": [ { "type": "source", "name": "close" }, { "type": "number", "value": 14 } ], "timeframe": "15m" },
"right": { "type": "number", "value": 35 } } ] } } }

Request:

{ "query": "any(venues where ema(close, 20, 1h) > ema(close, 50, 1h) and rsi(close, 14, 15m) < 35)" }

Response, on the documentation fixture — real venue codes, invented numbers, the query echo dropped:

{
"dql": "1",
"evaluated_at": "2026-09-23T12:00:05Z",
"as_of": null,
"settled": false,
"freshness": {
"ticker": "30s",
"mark": "30s",
"oi": "30s",
"book": "30s",
"stats": "120s",
"funding": "300s"
},
"instruments": [
{
"instrument": "BTC-PERP",
"result": "unknown",
"values": {},
"venues": [
{
"venue": "BYBIT-PERP",
"symbol": "BTCUSDT",
"quote": "USDT",
"values": {
"ema(close, 20, 1h)": {
"value": null,
"unit": "quote",
"venue": "BYBIT-PERP",
"layer": "candles",
"timeframe": "1h",
"bars": 80,
"candle_end": null,
"venue_ts": null,
"received_at": null,
"age_ms": null,
"status": "missing",
"reason": "no_value"
},
"ema(close, 50, 1h)": {
"value": null,
"unit": "quote",
"venue": "BYBIT-PERP",
"layer": "candles",
"timeframe": "1h",
"bars": 200,
"candle_end": null,
"venue_ts": null,
"received_at": null,
"age_ms": null,
"status": "missing",
"reason": "no_value"
},
"rsi(close, 14, 15m)": {
"value": null,
"venue": "BYBIT-PERP",
"layer": "candles",
"timeframe": "15m",
"bars": 57,
"candle_end": "2026-09-23T12:00:00Z",
"venue_ts": null,
"received_at": "2026-09-23T12:00:00.300Z",
"age_ms": 4700,
"status": "present",
"reason": "gap"
}
}
},
{
"venue": "HYPERLIQUID",
"symbol": "BTC",
"quote": "USDC",
"values": {
"ema(close, 20, 1h)": {
"value": null,
"unit": "quote",
"venue": "HYPERLIQUID",
"layer": "candles",
"timeframe": "1h",
"bars": 80,
"candle_end": null,
"venue_ts": null,
"received_at": null,
"age_ms": null,
"status": "missing",
"reason": "no_value"
},
"ema(close, 50, 1h)": {
"value": null,
"unit": "quote",
"venue": "HYPERLIQUID",
"layer": "candles",
"timeframe": "1h",
"bars": 200,
"candle_end": null,
"venue_ts": null,
"received_at": null,
"age_ms": null,
"status": "missing",
"reason": "no_value"
},
"rsi(close, 14, 15m)": {
"value": 88.19612724877551,
"venue": "HYPERLIQUID",
"layer": "candles",
"timeframe": "15m",
"bars": 57,
"candle_end": "2026-09-23T12:00:00Z",
"venue_ts": null,
"received_at": "2026-09-23T12:00:00.300Z",
"age_ms": 4700,
"status": "present"
}
}
},
{
"venue": "KRAKEN-FUTURES",
"symbol": "PF_XBTUSD",
"quote": "USD",
"values": {
"ema(close, 20, 1h)": {
"value": null,
"unit": "quote",
"venue": "KRAKEN-FUTURES",
"layer": "candles",
"timeframe": "1h",
"bars": 80,
"candle_end": null,
"venue_ts": null,
"received_at": null,
"age_ms": null,
"status": "missing",
"reason": "no_value"
},
"ema(close, 50, 1h)": {
"value": null,
"unit": "quote",
"venue": "KRAKEN-FUTURES",
"layer": "candles",
"timeframe": "1h",
"bars": 200,
"candle_end": null,
"venue_ts": null,
"received_at": null,
"age_ms": null,
"status": "missing",
"reason": "no_value"
},
"rsi(close, 14, 15m)": {
"value": 88.19612724877551,
"venue": "KRAKEN-FUTURES",
"layer": "candles",
"timeframe": "15m",
"bars": 57,
"candle_end": "2026-09-23T12:00:00Z",
"venue_ts": null,
"received_at": "2026-09-23T12:00:00.300Z",
"age_ms": 4700,
"status": "present"
}
}
},
{
"venue": "OKX-PERP",
"symbol": "BTC-USDT-SWAP",
"quote": "USDT",
"values": {
"ema(close, 20, 1h)": {
"value": null,
"unit": "quote",
"venue": "OKX-PERP",
"layer": "candles",
"timeframe": "1h",
"bars": 80,
"candle_end": null,
"venue_ts": null,
"received_at": null,
"age_ms": null,
"status": "missing",
"reason": "no_value"
},
"ema(close, 50, 1h)": {
"value": null,
"unit": "quote",
"venue": "OKX-PERP",
"layer": "candles",
"timeframe": "1h",
"bars": 200,
"candle_end": null,
"venue_ts": null,
"received_at": null,
"age_ms": null,
"status": "missing",
"reason": "no_value"
},
"rsi(close, 14, 15m)": {
"value": null,
"venue": "OKX-PERP",
"layer": "candles",
"timeframe": "15m",
"bars": 57,
"candle_end": null,
"venue_ts": null,
"received_at": null,
"age_ms": null,
"status": "missing",
"reason": "no_value"
}
}
}
]
}
],
"page": {
"limit": 1,
"returned": 1,
"next_cursor": "eyJ0IjoiMjAyNi0wOS0yM1QxMjowMDowNVoiLCJhZnRlciI6IkJUQy1QRVJQIiwicSI6IjMxOTVhZGUxMDU2YTFhOWMifQ"
}
}

When every series function carries its own timeframe, the timeframe clause is not needed. Mixing is fine as long as each function’s series is consistent: crossover(ema(close, 20, 1h), ema(close, 50, 4h)) is rejected because the two series have different timeframes.

4.9 Which instruments have a venue that has gone quiet

Section titled “4.9 Which instruments have a venue that has gone quiet”

Intent: instruments where at least one listed venue is not answering (no fresh mark price).

venues_answered < venues_listed

JSON form:

{ "dql": "1", "condition": { "type": "cmp", "op": "<", "left": { "type": "field", "name": "venues_answered" }, "right": { "type": "field", "name": "venues_listed" } } }

Request:

{ "query": "venues_answered < venues_listed", "limit": 3 }

Response, on the documentation fixture — real venue codes, invented numbers, the query echo dropped:

{
"dql": "1",
"evaluated_at": "2026-09-23T12:00:05Z",
"as_of": null,
"settled": false,
"freshness": {
"ticker": "30s",
"mark": "30s",
"oi": "30s",
"book": "30s",
"stats": "120s",
"funding": "300s"
},
"instruments": [],
"page": {
"limit": 1,
"returned": 0,
"next_cursor": null
}
}

This query references no venue field, so per-venue values are empty; add status(mark) inside a quantifier if you want to see which venue is quiet (next entry).

4.10 Stale, missing and not published are three different things

Section titled “4.10 Stale, missing and not published are three different things”

Intent: instruments where at least one venue’s funding rate is stale — we have a value, but it is older than the freshness bound.

count(venues where status(funding) = stale) >= 1

JSON form:

{ "dql": "1", "condition": { "type": "cmp", "op": ">=",
"left": { "type": "count", "where": { "type": "cmp", "op": "=", "left": { "type": "call", "name": "status", "args": [ { "type": "field", "name": "funding" } ] }, "right": { "type": "status", "value": "stale" } } },
"right": { "type": "number", "value": 1 } } }

Request:

{ "query": "count(venues where status(funding) = stale) >= 1", "limit": 1 }

Response, on the documentation fixture — real venue codes, invented numbers, the query echo dropped:

{
"dql": "1",
"evaluated_at": "2026-09-23T12:00:05Z",
"as_of": null,
"settled": false,
"freshness": {
"ticker": "30s",
"mark": "30s",
"oi": "30s",
"book": "30s",
"stats": "120s",
"funding": "300s"
},
"instruments": [
{
"instrument": "BTC-PERP",
"result": "true",
"values": {},
"venues": [
{
"venue": "BYBIT-PERP",
"symbol": "BTCUSDT",
"quote": "USDT",
"values": {
"status(funding)": {
"value": "stale",
"venue": "BYBIT-PERP",
"layer": "funding",
"venue_ts": null,
"received_at": "2026-09-23T11:52:00Z",
"age_ms": 485000,
"status": "stale"
}
}
},
{
"venue": "HYPERLIQUID",
"symbol": "BTC",
"quote": "USDC",
"values": {
"status(funding)": {
"value": "present",
"venue": "HYPERLIQUID",
"layer": "funding",
"venue_ts": null,
"received_at": "2026-09-23T12:00:04Z",
"age_ms": 1000,
"status": "present"
}
}
},
{
"venue": "KRAKEN-FUTURES",
"symbol": "PF_XBTUSD",
"quote": "USD",
"values": {
"status(funding)": {
"value": "present",
"venue": "KRAKEN-FUTURES",
"layer": "funding",
"venue_ts": null,
"received_at": "2026-09-23T12:00:04Z",
"age_ms": 1000,
"status": "present"
}
}
},
{
"venue": "OKX-PERP",
"symbol": "BTC-USDT-SWAP",
"quote": "USDT",
"values": {
"status(funding)": {
"value": "present",
"venue": "OKX-PERP",
"layer": "funding",
"venue_ts": null,
"received_at": "2026-09-23T12:00:04Z",
"age_ms": 1000,
"status": "present"
}
}
}
]
}
],
"page": {
"limit": 1,
"returned": 1,
"next_cursor": null
}
}

Four situations, one per venue: a reading that arrived a second ago is present; one that arrived eight minutes ago, past the layer’s freshness bound, is stale — the value comes back with its true age, and a comparison treats it as unknown; a layer DEBYKO collects for which nothing has arrived for this listing is missing; a field the venue does not publish at all — Hyperliquid’s mark candles, Kraken’s funding interval — is not_published. A fifth status, off, means DEBYKO does not collect that layer for that instrument’s group by policy. Only the first counts as data in a comparison.

Intent: which instruments had positive funding on three or more venues at 08:00 UTC today, using only what DEBYKO had received by then.

count(venues where funding > 0) >= 3 as of 2026-09-21T08:00:00Z

JSON form:

{ "dql": "1", "as_of": "2026-09-21T08:00:00Z", "condition": { "type": "cmp", "op": ">=",
"left": { "type": "count", "where": { "type": "cmp", "op": ">", "left": { "type": "field", "name": "funding" }, "right": { "type": "number", "value": 0 } } },
"right": { "type": "number", "value": 3 } } }

Request:

{ "query": "count(venues where funding > 0) >= 3 as of 2026-09-21T08:00:00Z", "limit": 1 }

Response, on the documentation fixture — real venue codes, invented numbers, the query echo dropped:

{
"dql": "1",
"evaluated_at": "2026-09-23T12:00:05Z",
"as_of": "2026-09-21T08:00:00Z",
"settled": true,
"freshness": {
"ticker": "30s",
"mark": "30s",
"oi": "30s",
"book": "30s",
"stats": "120s",
"funding": "300s"
},
"instruments": [],
"page": {
"limit": 1,
"returned": 0,
"next_cursor": null
}
}

Notice the ages: they are measured against 08:00:00, not against now, and they are up to a minute because funding history is retained once per minute. settled: true says the answer will not change if you ask again — the moment is older than the platform’s settle window, so nothing is still in flight. Every received_at is before 08:00:00; a value received at 08:00:00.100 would not be visible to this query even if the venue stamped it 07:59:59.

4.12 Relaxed freshness for a historical question

Section titled “4.12 Relaxed freshness for a historical question”

Intent: a week ago at noon, did any venue show open interest at all — accepting values up to ten minutes old.

any(venues where oi > 0) freshness 10m as of 2026-09-14T12:00:00Z

JSON form:

{ "dql": "1", "freshness": "10m", "as_of": "2026-09-14T12:00:00Z",
"condition": { "type": "any", "where": { "type": "cmp", "op": ">", "left": { "type": "field", "name": "oi" }, "right": { "type": "number", "value": 0 } } } }

Request:

{ "query": "any(venues where oi > 0) freshness 10m as of 2026-09-14T12:00:00Z", "limit": 1 }

Response, on the documentation fixture — real venue codes, invented numbers, the query echo dropped:

{
"dql": "1",
"evaluated_at": "2026-09-23T12:00:05Z",
"as_of": "2026-09-14T12:00:00Z",
"settled": true,
"freshness": {
"ticker": "10m",
"mark": "10m",
"oi": "10m",
"book": "10m",
"stats": "10m",
"funding": "10m"
},
"instruments": [
{
"instrument": "BTC-PERP",
"result": "unknown",
"values": {},
"venues": [
{
"venue": "BYBIT-PERP",
"symbol": "BTCUSDT",
"quote": "USDT",
"values": {
"oi": {
"value": null,
"unit": "base",
"venue": "BYBIT-PERP",
"layer": "oi",
"venue_ts": null,
"received_at": null,
"age_ms": null,
"status": "missing",
"reason": "no_value"
}
}
},
{
"venue": "HYPERLIQUID",
"symbol": "BTC",
"quote": "USDC",
"values": {
"oi": {
"value": null,
"unit": "base",
"venue": "HYPERLIQUID",
"layer": "oi",
"venue_ts": null,
"received_at": null,
"age_ms": null,
"status": "missing",
"reason": "no_value"
}
}
},
{
"venue": "KRAKEN-FUTURES",
"symbol": "PF_XBTUSD",
"quote": "USD",
"values": {
"oi": {
"value": null,
"unit": "base",
"venue": "KRAKEN-FUTURES",
"layer": "oi",
"venue_ts": null,
"received_at": null,
"age_ms": null,
"status": "missing",
"reason": "no_value"
}
}
},
{
"venue": "OKX-PERP",
"symbol": "BTC-USDT-SWAP",
"quote": "USDT",
"values": {
"oi": {
"value": null,
"unit": "base",
"venue": "OKX-PERP",
"layer": "oi",
"venue_ts": null,
"received_at": null,
"age_ms": null,
"status": "missing",
"reason": "no_value"
}
}
}
]
}
],
"page": {
"limit": 1,
"returned": 1,
"next_cursor": "eyJ0IjoiMjAyNi0wOS0xNFQxMjowMDowMFoiLCJhZnRlciI6IkJUQy1QRVJQIiwicSI6Ijg5MDVkMmI1ZmRjNWVhNzEifQ"
}
}

Bybit’s reading above was almost eight minutes old; under the default 30-second bound it would have been stale, under freshness 10m it is present — and its true age comes back either way. A venue whose history is kept for a shorter period answers missing with reason not_retained instead: nothing from that day exists any more. The query still runs; it simply cannot see that venue.

Intent: dispersion of mark prices above ten basis points, comparing only listings quoted in a USD stablecoin or USD.

max(venues where quote in (USDT, USDC, USD), mark) / min(venues where quote in (USDT, USDC, USD), mark) - 1 > 10bps

JSON form:

{ "dql": "1", "condition": { "type": "cmp", "op": ">",
"left": { "type": "arith", "op": "-",
"left": { "type": "arith", "op": "/",
"left": { "type": "agg", "fn": "max", "expr": { "type": "field", "name": "mark" }, "where": { "type": "in", "left": { "type": "field", "name": "quote" }, "values": [ { "type": "code", "value": "USDT" }, { "type": "code", "value": "USDC" }, { "type": "code", "value": "USD" } ] } },
"right": { "type": "agg", "fn": "min", "expr": { "type": "field", "name": "mark" }, "where": { "type": "in", "left": { "type": "field", "name": "quote" }, "values": [ { "type": "code", "value": "USDT" }, { "type": "code", "value": "USDC" }, { "type": "code", "value": "USD" } ] } } },
"right": { "type": "number", "value": 1 } },
"right": { "type": "number", "value": 0.001 } } }

Request:

{ "query": "max(venues where quote in (USDT, USDC, USD), mark) / min(venues where quote in (USDT, USDC, USD), mark) - 1 > 10bps", "limit": 1 }

Response, on the documentation fixture — real venue codes, invented numbers, the query echo dropped:

{
"dql": "1",
"evaluated_at": "2026-09-23T12:00:05Z",
"as_of": null,
"settled": false,
"freshness": {
"ticker": "30s",
"mark": "30s",
"oi": "30s",
"book": "30s",
"stats": "120s",
"funding": "300s"
},
"instruments": [],
"page": {
"limit": 1,
"returned": 0,
"next_cursor": null
}
}

DQL never converts currencies. USDT, USDC and USD are three different values of quote; if you want to treat them as one group, say so in the query, as here. (The comparison page groups them for display; the language does not.)

Intent: instruments answering on at least two venues, highest eight-hour funding first.

venues_answered >= 2 order by max(venues, funding_8h) desc

JSON form:

{ "dql": "1",
"condition": { "type": "cmp", "op": ">=", "left": { "type": "field", "name": "venues_answered" }, "right": { "type": "number", "value": 2 } },
"order_by": { "expr": { "type": "agg", "fn": "max", "expr": { "type": "field", "name": "funding_8h" } }, "direction": "desc" } }

Request:

{ "query": "venues_answered >= 2 order by max(venues, funding_8h) desc", "limit": 3 }

Response, on the documentation fixture — real venue codes, invented numbers, the query echo dropped:

{
"dql": "1",
"evaluated_at": "2026-09-23T12:00:05Z",
"as_of": null,
"settled": false,
"freshness": {
"ticker": "30s",
"mark": "30s",
"oi": "30s",
"book": "30s",
"stats": "120s",
"funding": "300s"
},
"instruments": [
{
"instrument": "BTC-PERP",
"result": "true",
"values": {
"venues_answered": {
"value": 4,
"from": "reference",
"as_of": "2026-09-23T12:00:05Z"
},
"max(venues, funding_8h)": {
"value": 0.000104,
"unit": "fraction",
"from": [
{
"venue": "HYPERLIQUID",
"value": 0.000104,
"received_at": "2026-09-23T12:00:04Z",
"age_ms": 1000,
"status": "present"
}
]
}
},
"venues": [
{
"venue": "BYBIT-PERP",
"symbol": "BTCUSDT",
"quote": "USDT",
"values": {
"funding_8h": {
"value": 2e-05,
"unit": "fraction",
"venue": "BYBIT-PERP",
"layer": "funding",
"venue_ts": null,
"received_at": "2026-09-23T11:52:00Z",
"age_ms": 485000,
"status": "stale"
}
}
},
{
"venue": "HYPERLIQUID",
"symbol": "BTC",
"quote": "USDC",
"values": {
"funding_8h": {
"value": 0.000104,
"unit": "fraction",
"venue": "HYPERLIQUID",
"layer": "funding",
"venue_ts": null,
"received_at": "2026-09-23T12:00:04Z",
"age_ms": 1000,
"status": "present"
}
}
},
{
"venue": "KRAKEN-FUTURES",
"symbol": "PF_XBTUSD",
"quote": "USD",
"values": {
"funding_8h": {
"value": 8e-05,
"unit": "fraction",
"venue": "KRAKEN-FUTURES",
"layer": "funding",
"venue_ts": null,
"received_at": "2026-09-23T12:00:04Z",
"age_ms": 1000,
"status": "present"
}
}
},
{
"venue": "OKX-PERP",
"symbol": "BTC-USDT-SWAP",
"quote": "USDT",
"values": {
"funding_8h": {
"value": 5.4e-05,
"unit": "fraction",
"venue": "OKX-PERP",
"layer": "funding",
"venue_ts": null,
"received_at": "2026-09-23T12:00:04Z",
"age_ms": 1000,
"status": "present"
}
}
}
]
}
],
"page": {
"limit": 1,
"returned": 1,
"next_cursor": "eyJ0IjoiMjAyNi0wOS0yM1QxMjowMDowNVoiLCJhZnRlciI6IkJUQy1QRVJQIiwicSI6IjE0MTk1NWNjMGNhMzliODEifQ"
}
}

The ordering key appears in values so you can see what the sort used (BTC’s venue list is shortened here; a real response lists all fourteen). Instruments whose key is null (no venue had a usable funding_8h) sort last, in either direction.

Intent: be told when, for any instrument, the 20/50 EMA cross happens on hourly candles on a venue with a clean 200-candle history.

The condition is a query like any other:

any(venues where crossover(ema(close, 20), ema(close, 50)) and gaps(200) = 0) timeframe 1h

The API does not hold alerts. It answers questions; it does not remember them, does not run them on a schedule and does not deliver anything to a webhook. That belongs to Agent: Agent holds the query, the cadence, the destination and your own tag, calls this API on that cadence, and decides what a firing means.

The reason is not architectural tidiness. A saved rule with a webhook makes the data API responsible for delivery, for retries, and — once a rule can be public — for showing somebody’s words next to a number, which is how a data platform ends up looking like it recommends a trade. The language has no vocabulary for an action or an opinion (spec §7.5), and now it has no tag to store either.

Until Agent opens, the same thing is four lines of your own code: run the query on your cadence, compare with the previous answer, act on the difference. Everything the query needs — the canonical form to compare against, items_sha256 to see that nothing changed, settled to know the answer may still move — is in the response.

Intent: the mark price is more than half a per cent above where it stood five minutes ago, on any venue. mark is a snapshot field, not a candle, and DQL samples it at the effective timeframe.

any(venues where roc(mark, 5) > 0.5%) timeframe 1m

The samples are the platform’s own per-second readings collapsed to the timeframe (spec §7.2a); a period with nothing retained is a gap, and a gap makes the answer null rather than a smaller number.

4.17 A spread that is unusually wide for this venue

Section titled “4.17 A spread that is unusually wide for this venue”

Intent: the current spread sits above the 90th percentile of its own last hour — “wide for this venue”, not “wide compared with a number I guessed”.

any(venues where spread > percentile(spread, 60, 90%)) timeframe 1m

percentile takes the nearest rank by default; add linear as a fourth argument for interpolation between the two neighbouring readings.

Intent: at least one venue has more than two million quote units resting within ten basis points of the mid on the bid side.

any(venues where depth_bid_value(10bps) > 2000000)

The bands are the platform’s, not yours: ask GET /v2/dql/catalogue for the set. A book that did not reach the band’s edge answers null with reason band_truncated — never the sum of the levels that happened to arrive.

4.19 Choosing the columns, and ordering by two keys

Section titled “4.19 Choosing the columns, and ordering by two keys”

Intent: the instruments where at least two venues answer, the widest funding first and, where that ties, the tighter spread first — and bring back only the three numbers I want.

venues_answered >= 2 select max(venues, funding_8h), min(venues, spread), venues_answered order by max(venues, funding_8h) desc, min(venues, spread) asc

select decides what comes back; without it the answer carries every atom the condition touched.

4.20 The venue’s mark candle, not its trade candle

Section titled “4.20 The venue’s mark candle, not its trade candle”

Intent: compare the last closed hourly mark candle with the live mark price.

any(venues where mark > mark_close) timeframe 1h

A venue that publishes no mark candles answers not_published for mark_close — Hyperliquid today — and the comparison is unknown for that venue rather than false.

Intent: a question about yesterday, where a reading a few minutes old is perfectly good.

any(venues where oi > 1000) freshness 30m as of 2026-09-20T12:00:00Z

freshness replaces every layer’s default bound at once. It does not make stale data fresh: it changes where the line between present and stale is drawn for this question, and every value still comes back with its true age.

Intent: the listing a venue calls BTCUSDT, whatever DEBYKO calls the instrument.

any(venues where mark > 0) where symbol = "BTCUSDT"

symbol is the venue’s own string and is quoted; it may appear only in the where selection. A bare code there (symbol = BTCUSDT) is DQL_TYPE, because a symbol is not a code.

Every query has a string form and a JSON form, and they are interchangeable: send either to POST /v2/screen, and the response contains both, normalised. POST /v2/dql/parse converts and validates without evaluating — useful for editors and for programs that generate queries.

{ "query": "count(venues where funding_8h > 1bps) >= 3", "context": "screen" }

returns the canonical string (1bps becomes 0.0001), the JSON tree, the list of fields and functions the query touches, and its complexity score — or a precise error with a position and a hint. Errors are structured:

{ "errors": [ { "code": "DQL_SCOPE", "message": "'funding' is venue-scoped at 1:1",
"position": { "line": 1, "column": 1, "offset": 0, "length": 7 },
"hint": "wrap it in any(venues where …), all(…), count(…) or an aggregate" } ] }

GET /v2/dql/catalogue lists every field (with type, unit, layer and which venues do not publish it), every function, every venue, currency and timeframe, the freshness defaults and the limits. If you generate queries programmatically, read it first.


A DQL comparison can come out three ways: true, false, or unknown. Unknown means “there was no value to compare”, and it happens more often than in a stock screener because DEBYKO refuses to invent values:

  • a venue has not sent a field for a while (stale): the last value is shown with its age, but the language will not compare it as if it were current;
  • a venue has never sent the field for this instrument, or its history was not kept that long (missing);
  • the venue does not have the field at all — an oracle-priced venue has no order book (not_published);
  • DEBYKO does not collect that layer for that instrument group (off);
  • an indicator needs 80 candles and one is absent (gap), or only 30 exist (insufficient history).

The combining rules are the ones SQL uses for NULL:

  • unknown and false is false (one false is enough), unknown and true is unknown;
  • unknown or true is true (one true is enough), unknown or false is unknown;
  • not unknown is unknown;
  • any(...) is true if some venue is true, otherwise unknown if some venue is unknown, otherwise false;
  • all(...) is false if some venue is false, otherwise unknown if some venue is unknown, otherwise true;
  • count(...) counts only venues that are true, so it is never unknown.

A screen returns instruments where the condition is true. A rule fires only on true. Unknown is silent by default; ask for it with "include_unknown": true when you are debugging a condition.

Writing conditions that handle it:

You want Write
ignore venues that do not publish the field all(venues where status(field) = not_published or <condition>)
only count venues with fresh data count(venues where status(field) = present and <condition>)
require a minimum number of answering venues ... and venues_answered >= 3
treat “no data” as a failure of the screen leave the condition as it is — unknown is already excluded
find the instruments where data quality is the problem count(venues where status(field) != present) >= 1
accept older readings on purpose ... freshness 10m

A useful habit: when a screen returns fewer instruments than you expect, run it once with include_unknown: true and look at the status and reason of the values. The answer is always in the response.


The two reasons that are about DEBYKO, not about the venue

Section titled “The two reasons that are about DEBYKO, not about the venue”

Seven of the nine reasons say something about the venue or the moment. Two say something about us:

  • unit_unknown — you asked for units: base and we do not know this listing’s contract size, so we will not convert. Ask for units: published and do the conversion yourself, or tell us the contract rules are missing.
  • unmapped — the listing is collected but belongs to no market yet, so it has no instrument. It still answers by venue and symbol; it just cannot be compared with anything.

6a. Four mistakes, and what the API says about them

Section titled “6a. Four mistakes, and what the API says about them”

Each of these is rejected before anything is evaluated, with the position and a suggestion. They are worth reading once: three of the four are the difference between DQL and the query languages it resembles.

A venue-scoped field on its own. Which venue’s funding? The language will not guess.

funding > 0

'funding' is venue-scoped; wrap it in any(venues where …), all(…), count(…) or an aggregate

A series without a period. ema over what candles?

any(venues where ema(close, 20) > 0)

add timeframe 1h, or write the period into the call: ema(close, 20, 1h)

A period the platform does not keep.

any(venues where ema(close, 20) > 0) timeframe 7m

timeframe '7m' is not supported; supported: 1m, 2m, 3m, 5m, 15m, 30m, 1h, 4h, 12h, 1d

A venue’s symbol written as a code. symbol is the venue’s own string, so it is quoted.

any(venues where mark > 0) where symbol = BTCUSDT

symbol takes a quoted string

DQL borrows indicator names, nothing else. Pine is a scripting language that runs bar by bar over a chart; DQL is a single condition evaluated over many instruments and venues at one instant.

In Pine you write In DQL you write Note
ta.ema(close, 20) ema(close, 20) no ta. prefix; fixed 80-candle window with SMA seed (see spec §7.3)
close close the last completed candle; the forming candle is never used
close[1] prev(close, 1) no [] operator
ta.crossover(a, b) crossover(a, b) same meaning: a > b now and a <= b one candle earlier
ta.rsi(close, 14) rsi(close, 14) Wilder smoothing, fixed 56-change window
ta.atr(14) atr(14) same
chart timeframe timeframe 15m clause, or ema(close, 20, 15m) no implicit timeframe
request.security(...) trailing timeframe argument per function several timeframes in one condition
and or not and or not same words; not binds below comparisons (not a > b is not (a > b))
== != = != (== and <> accepted)
na(x) status(x) != present five statuses instead of one na
nz(x, 0) deliberately absent; missing is never zero
ta.percentile_nearest_rank(S, n, p) percentile(S, n, p) and percentile(S, n, p, linear) for interpolation
math.abs(x) abs(x) element-wise over a series
math.sum(S, n) rolling_sum(S, n)
a script per symbol, switched by hand where base = BTC, where venue = KRAKEN-FUTURES, where symbol = "BTCUSDT" selection is part of the query
input, var, :=, if, for, plot, strategy.*, alertcondition not a scripting language
one symbol per script any(venues where ...), all(...), count(...), max(venues, ...) every venue value needs a venue scope

Numbers will not match TradingView exactly. TradingView’s EMA depends on how much chart history is loaded; DQL’s uses a fixed window so that anyone can recompute it from public candles.

Think of a table with one row per (instrument, venue). A DQL condition is a WHERE clause over that table, grouped by instrument.

In SQL you write In DQL you write Note
WHERE funding > 0 any(venues where funding > 0) the group-by is built in; say which rows
HAVING COUNT(*) FILTER (WHERE funding > 0) >= 3 count(venues where funding > 0) >= 3
HAVING MAX(mark) - MIN(mark) > 5 max(venues, mark) - min(venues, mark) > 5
MAX(mark) FILTER (WHERE quote = 'USDT') max(venues where quote = USDT, mark) codes are unquoted, uppercase
venue IN ('KRAKEN-FUTURES', 'BYBIT-PERP') venue in (KRAKEN-FUTURES, BYBIT-PERP)
funding IS NULL status(funding) != present and the status tells you why
NULL semantics in AND/OR/NOT identical three-valued logic
AVG(mark) deliberately absent: venues are never averaged
SUM(oi) sum(venues, oi) only additive quantities can be summed
AS OF SYSTEM TIME '...' as of 2026-09-21T08:00:00Z visibility by received_at, not by venue timestamp
ORDER BY x DESC LIMIT 50 order by x desc in the query, "limit": 50 in the request
WHERE base = 'BTC' (choosing rows before aggregating) the where clause: … where base = BTC selection and condition are separate; the clause runs first
SELECT a, b, c select max(venues, funding_8h), min(venues, spread) chooses what comes back, not what is compared
ORDER BY a DESC, b ASC order by max(venues, funding_8h) desc, min(venues, spread) asc several keys, C1-06
WHERE symbol = 'BTCUSDT' where symbol = "BTCUSDT" the venue’s own symbol is a quoted string, not a code
SUM(oi) across venues with mixed contract sizes sum(venues, oi) with units: base refused under units: published, because contracts do not add up
LIKE, joins, subqueries, CASE not in the language

Keywords are lowercase (and, not AND); uppercase words are venue, currency and asset codes.


  • It does not express actions or opinions. There is no buy, sell, long, short, signal, entry, exit, strength or confidence anywhere in the language, and there will not be. ema(close, 20) > ema(close, 50) is a statement about two numbers; what you do with it is not DEBYKO’s business. The alert tag is text you choose; DEBYKO stores it, sends it back, and never reads it. This keeps DEBYKO a data service, and it keeps your rules yours.
  • It does not blend venues. No average price, no consensus price, no “best” price. You can ask for the maximum, the minimum, the count, the sum, or any single venue.
  • It does not fill gaps. A missing candle stays missing and an indicator over it is null. If you want indicators that tolerate holes, you have all the candles via /v1/candles and can compute your own.
  • It does not convert currencies. A price is a number in the venue’s quote currency.
  • It does not run code. No variables, loops, user-defined functions or state. A query is one condition. If you need more, the data endpoints are there.
  • It does not let you see the future. as of uses only what had been received by that moment, including candles that had not yet arrived.

Nothing in a response is a forecast, a rating or advice. Every number is something a venue published, or an arithmetic function of such numbers defined in the specification, with the venue, the receipt time and the age attached.


One runnable query per function, the function’s name visible in the query itself. Every line here is run by GuideExampleTests against the conformance fixture, so an example that stops working fails the build instead of sitting here misleading you.

Series functions — all need a period, from the timeframe clause or a trailing argument:

any(venues where close > prev(close, 1)) timeframe 15m
any(venues where close > sma(close, 20)) timeframe 15m
any(venues where ema(close, 20) > ema(close, 50)) timeframe 1h
any(venues where rsi(close, 14) < 30) timeframe 1h
any(venues where atr(14) > 100) timeframe 15m
any(venues where stdev(close, 20) > 50) timeframe 15m
any(venues where close >= highest(close, 50)) timeframe 15m
any(venues where close <= lowest(close, 50)) timeframe 15m
any(venues where change(close, 10) > 0) timeframe 15m
any(venues where roc(close, 12) > 1%) timeframe 5m
any(venues where crossover(close, sma(close, 3))) timeframe 15m
any(venues where crossunder(sma(close, 3), close)) timeframe 15m
any(venues where gaps(100) = 0) timeframe 15m
any(venues where abs(change(close, 5)) > 100) timeframe 15m
any(venues where rolling_sum(volume, 12) > 1000) timeframe 5m
any(venues where close > percentile(close, 100, 95%)) timeframe 15m
any(venues where close > percentile(close, 100, 95%, linear)) timeframe 15m
any(venues where percentile_rank(close, 100) > 0.8) timeframe 15m

Data quality — these two are the reason the language exists:

any(venues where age(mark) < 5s)
any(venues where status(funding) = stale)

Book depth — the bands are the platform’s, from GET /v2/dql/catalogue:

any(venues where depth_bid(10bps) > 5)
any(venues where depth_ask(10bps) > 5)
any(venues where depth_bid_value(10bps) > 250000)
any(venues where depth_ask_value(10bps) > 250000)

The instrument, and the two counts that say how many venues answered:

venues_answered >= 2 where instrument = BTC-PERP
venues_answered >= 2 where base = BTC
venues_listed >= 3
venues_answered < venues_listed

The listing — what a venue calls it and what its contract rules are:

any(venues where venue = KRAKEN-FUTURES and mark > 0)
any(venues where quote = USDT and mark > 0)
venues_answered >= 1 where symbol = "BTCUSDT"
any(venues where tick_size <= 0.1)
any(venues where lot_size <= 0.001)
any(venues where min_qty <= 0.001)

What the venue publishes, layer by layer:

any(venues where last > 0)
max(venues, mark) - min(venues, mark) > 5bps
any(venues where index > 0)
any(venues where basis > 10bps)
any(venues where funding > 0)
any(venues where funding_interval = 8h)
any(venues where funding_8h > 1bps)
any(venues where time_to_funding < 30m)
sum(venues, oi) > 10000
any(venues where ask - bid > 0)
any(venues where mid > 0)
all(venues where status(bid) = not_published or spread < 5bps)
any(venues where bid_size > 1 and ask_size > 1)
any(venues where volume_24h > 10000)
any(venues where turnover_24h > 100000000)
any(venues where change_24h < -1%)

Candles. Trade candles are open high low close volume; the venue’s mark and index candles carry the same five names with a prefix. A venue that publishes no mark candle answers not_published for every mark_* source rather than repeating its trade candle:

any(venues where high - low > 0 and close > open) timeframe 1h
any(venues where volume > 0) timeframe 1h
any(venues where mark_high - mark_low > 0 and mark_close > mark_open) timeframe 1h
any(venues where mark_volume >= 0) timeframe 1h
any(venues where index_high - index_low > 0 and index_close > index_open) timeframe 1h
any(venues where index_volume >= 0) timeframe 1h