Handling missing market data is the step teams skip most often, and the one that surfaces in production. Engineering effort usually goes into authentication, reconnection, and rate limits, while every response field is assumed to carry a value. Then one symbol returns null or 0 for one window, and the metrics, alerts, and backtests all drift at once. The root cause is that "no value" has more than one meaning. TIQEX quote responses split it into two explicit booleans, delayed and unavailable, while 0 stays a legitimate number. Conflating the three is where most data quality incidents begin. This article walks through the field semantics and provides runnable handling code.
A familiar incident: null treated as zero
The setup is typical. A monitor polls /api/v1/market/quote every five seconds and compares last against a threshold. One day the upstream feed blips and the API returns HTTP 502 with UPSTREAM_UNAVAILABLE. The collector reads resp.json()["data"], finds nothing, and writes 0. Every symbol shows a price of zero within the same minute, hundreds of alerts fire, and the on-call engineer spends forty minutes confirming a data problem rather than a market one.
Two independent mistakes sit in that chain. First, a failed request was downgraded into a numeric zero, discarding error distinguishability. Second, missing and legitimate zero were collapsed into one representation. Once 0 means both "price is zero" and "no data", no downstream logic can recover on its own.
The fix is not more fallback branches. It is modelling state at the boundary: a quote record has at least three states — valued, delayed fallback, and unavailable — plus an error state. Once distinct, downstream code asks whether a record may drive a real-time decision instead of guessing what a zero means.
Three states: valued, delayed, unavailable
TIQEX quote responses return a set of fields per symbol inside data. The ones that speak to data availability are source, delayed, unavailable, and timestampMs. The documentation describes delayed and unavailable as "delayed fallback or unavailable markers", and they are distinct booleans: delayed=true means the symbol has data, delivered at the delayed tier permitted by your entitlement; unavailable=true means no usable data exists right now.
Note that the shape of data varies. Requesting several codes at once (codes, comma-separated, up to 50) returns an array; a single code may return an object. The handler below normalises both.
| State | Test | Correct handling |
|---|---|---|
| Valued (live) | succ=true, neither flag set, last present | Enters the real-time path; usable for alerts and dashboards |
| Delayed fallback | delayed=true, fields complete | Usable for analysis, backtests, display; never for real-time triggers |
| Unavailable | unavailable=true | Write a tagged null; exclude from all computation |
| Request failed | HTTP not 200, or succ=false with a code | Classify by error code, retry or alert; do not write to the data table |
Rows three and four carry the point: unavailable is a business state, failed is a transport state. The former may be persisted with a marker; the latter must take the error path. Blur them and you get silent data loss: dashboards look healthy while no real quote has landed all day.
Why zero is a legitimate value
In candlestick data, zero is even more clearly legitimate. /api/v1/market/equity/kline returns bars[] with open / high / low / close / volume, plus auxiliary fields such as vwap, trades, and session. If a symbol prints no trades in a given window, volume=0 is the fact, not a gap. Filtering it out deletes a real low-liquidity stretch; backfilling it with the previous bar invents trades that never happened.
The only reliable test for a missing field is whether it is null or absent — never whether it equals zero. This matters most for fields that are naturally zero, such as volume and trade count. Full field semantics live in the glossary entry K-line OHLCV field reference, so they are not repeated here.
By contrast, a null in high or low is an explicit signal: the bar carries no valid quote at all. The whole bar should be treated as unusable rather than patched to zero and fed into indicators.
Five sources of gaps and how to handle each
Treating missing data as one concept makes retry policy impossible to design. Split by source and the handling diverges sharply:
- Closed sessions. Market hours differ by venue, while crypto runs near 7×24 — see the crypto market page. No ticks outside a session is normal and should be filtered by schedule, not flagged as failure.
- No trades in the window. Produces a legitimate zero: the field is present and the number is zero. Persist normally.
- Upstream feed temporarily down. HTTP 502
UPSTREAM_UNAVAILABLE. Retryable — back off exponentially and retry, never writing zeros meanwhile. - Entitlement not covering the market. HTTP 403 codes such as
MARKET_NOT_ENABLEDorAPI_KEY_SCOPE_DENIED. A configuration problem where retrying is pointless and alerting is the only sane response. - Quota and rate limits. HTTP 429 codes including
DAILY_LIMIT_EXCEEDEDandRATE_LIMIT_EXCEEDED. Reduce frequency or move to streaming. Note that RESTquote/klinecalls and the WebSocket handshake both count toward the daily quota and RPM, while pushed ticks are not counted again.
Only category three rewards waiting. Running three and four through the same retry loop typically burns quota for nothing while burying the real configuration error in retry noise.
Implementation: encode the states
The function below normalises one quote request into four states and preserves the raw error code. It is the single entry point for everything downstream and its output can be persisted directly.
import requests
API = "https://api.tiqex.io/api/v1/market/quote"
HEADERS = {"X-API-Key": "YOUR_API_KEY"}
def fetch_quotes(codes):
"""Returns (state, payload); state is live / delayed / unavailable / error."""
resp = requests.get(API, params={"codes": ",".join(codes)},
headers=HEADERS, timeout=10)
try:
body = resp.json()
except ValueError:
return "error", {"code": "BAD_JSON", "msg": resp.text[:200]}
if resp.status_code != 200 or not body.get("succ"):
# Transport failure: keep the machine-readable code, never downgrade to zero
return "error", {"code": body.get("code"), "msg": body.get("msg")}
data = body["data"]
rows = data if isinstance(data, list) else [data]
out = []
for q in rows:
if q.get("unavailable"):
state = "unavailable"
elif q.get("delayed"):
state = "delayed"
elif q.get("last") is None:
state = "unavailable"
else:
state = "live"
out.append({"symbol": q.get("code") or q.get("symbol"),
"state": state, "last": q.get("last"),
"at": q.get("timestampMs")})
return "ok", out
if __name__ == "__main__":
print(fetch_quotes(["EURUSD", "XAUUSD"]))
The last is None branch handles a successful call whose symbol has no latest price. Classifying it as unavailable rather than inventing a zero keeps threshold logic quiet.
The candlestick side follows the same rule: check presence, not magnitude.
import requests
KLINE = "https://api.tiqex.io/api/v1/market/equity/kline"
HEADERS = {"X-API-Key": "YOUR_API_KEY"}
def fetch_bars(symbol="AAPL", interval="1d", limit=30):
resp = requests.get(KLINE, params={"symbol": symbol,
"interval": interval,
"limit": limit},
headers=HEADERS, timeout=10)
body = resp.json()
if not body.get("succ"):
raise RuntimeError(body.get("code"), body.get("msg"))
return body["data"]["bars"]
def usable(bar):
"""Complete fields mean usable; volume of zero is a valid state, not a gap."""
return all(bar.get(k) is not None
for k in ("open", "high", "low", "close", "volume"))
if __name__ == "__main__":
bars = fetch_bars()
good = [b for b in bars if usable(b)]
print("bars:", len(bars), "complete:", len(good))
print("zero-volume bars:", sum(1 for b in good if b["volume"] == 0))
print("sample:", good[-1])
On the streaming path the same principle applies. Frames from wss://api.tiqex.io/api/v1/market/stream are separated by type: subscribed confirms a subscription, tick carries data, and error carries msg plus code — for example WS_CONN_LIMIT when connections exceed the plan limit. Parsing an error frame as data is another route to silent loss. Subscription and heartbeat details are in the WebSocket live quotes guide.
When delayed data is acceptable
Once delayed is separated out, the question is whether it can be used. The test is not the delay length but the decision the record drives.
- Acceptable: historical research, backtest dataset construction, charting, daily and weekly reporting, non-real-time risk sweeps. These are insensitive to freshness, so delayed data changes no conclusion.
- Not acceptable: real-time alerting, automated order triggers, quote-driven order placement or cancellation, and anything premised on "current state". Delayed data describes a past moment.
Delay is an entitlement matter rather than an API matter: the free tier covers delayed data for crypto, US equities, and metals; the standard tier delivers crypto and metals in real time with the remaining markets delayed; only the professional tier is real-time across all markets. Coverage and quotas per tier are listed on the pricing page. The advice is to propagate the delayed flag to the consumer instead of dropping it at the boundary; otherwise you cannot tell how many records in last week's false trigger were delayed.
Frequently asked questions
Is a returned 0 always a missing value?
No. Zero is legitimate: volume and trade count should be zero in windows with no trades. Only null or an absent field counts as missing. Write the test as is None, never as not value — the latter also flags zero and empty strings.
Can delayed and unavailable be judged together?
No. delayed=true means data exists at the delayed tier and may be used for analysis; unavailable=true means no usable data exists and the record must not enter any computation. Combining them means one code path handles two different problems, either blocking usable data or admitting unusable data into the live path.
Should a failed request write 0 or leave the field empty?
Neither. Failure is a transport state, not a data state. Record the error code separately and skip the quote table for that cycle. Only a business state of unavailable should produce a tagged null, which keeps "no data" distinct from "no successful request".
How do I tell which kind of gap I hit?
Start with the HTTP status and the code in the response envelope. HTTP 502 UPSTREAM_UNAVAILABLE is a retryable upstream issue; 403 codes such as MARKET_NOT_ENABLED and API_KEY_SCOPE_DENIED are entitlement problems; 429 codes are quota problems. Each needs different handling, so the boundary must preserve the original code instead of flattening everything into "fetch failed".
Next step
Encoding three states costs little: one normalisation function, one is None test, one path that carries error codes through. The payoff is that every downstream metric, alert, and backtest rests on data whose state is known. A free key is available on signup, so validation logic can be built against delayed data first and the tier upgraded when the use case demands it; no credit card is required.
Get a free API key and start building →
API fields, error codes, quotas, and plan coverage cited here come from the official tiqex.io documentation (updated 2026-09-12) and pricing page, accessed 2026-09-20. Figures illustrate engineering practice only and constitute no investment advice or forecast of future prices.