DTF pricing API
Prices every DTF on the platform from live constituent marks, on every trade rather than on every epoch, and keeps a five minute candle history of the result.
https://prices.indexes.fi
No authentication. Every endpoint is a GET returning JSON, except the WebSocket. Responses
are no-store: the whole point is that they change.
Currently pricing
ixda-dtf-btc-eth-50-50.
Every example on this page uses ixda-dtf-btc-eth-50-50 and the base URL above, so
they run as written.
This is indicative. It is not attested, and it is not the price a
redemption settles against. That number is the DTF's own
pricePerShareWad(), which only moves when a lifecycle run checkpoints it. This
service answers the different question of what the basket is worth right now.
Concepts
Index level
Every price is an index level that is 1.0 at the DTF's creation, so the number reads directly as a price per share rather than as an abstract index. History reaches back before creation, where the same basket is carried backwards; those points are indicative in a second sense and are what a chart should label as pre-launch.
Fresh legs against total legs
Every price carries pricedLegs and totalLegs. A price built from
one of two legs is not wrong, but it is not the index either. Marks are never forgotten, so
a leg that goes quiet keeps its last price and the index keeps moving on the other legs;
pricedLegs is how you tell that apart from a market that stopped moving.
Observed against reconstructed
Bars are tagged live or backfill. Reconstructed bars come from
upstream aggregates and their high and low are their own endpoints, because the legs'
extremes did not occur at the same instant and combining them would quote a level the index
never reached. A flat run of each means a different thing, so the two are never merged.
Resolution
Bars are stored every five minutes. A long window is rolled up rather than truncated, and
the response states the resolution it used. Ask for resolutionMinutes=5 to
force the stored granularity on a short window.
Endpoints
Every sample below runs against the base URL above. Run it calls this service for real, so the response shown is the live one.
GET /health
Whether the service is running, and what it is watching.
Read feed and marks, not just status. A
service whose upstream authenticated and then subscribed to nothing answers
ok on any check that only looks at the process.
Response
| Field | Type | Meaning |
|---|---|---|
feed | string | live, connecting, authenticating, rejected (bad key, terminal) or stopped. |
dtfs | number | DTFs currently being priced. |
instruments | number | Distinct upstream instruments subscribed. |
marks | number | Instruments with at least one mark held. |
uptimeSeconds | number | Since the process started. |
GET /health
import requests
BASE = "https://prices.indexes.fi"
health = requests.get(f"{BASE}/health", timeout=10).json()
print(health["feed"]) # live | connecting | rejected | stopped
print(health["dtfs"]) # DTFs being priced
print(health["instruments"]) # distinct upstream instruments subscribed
print(health["marks"]) # instruments with at least one mark
# "ok" alone is not enough. A feed that authenticated and subscribed to
# nothing also answers ok, so check that it is actually watching something.
assert health["feed"] == "live" and health["marks"] > 0const BASE = 'https://prices.indexes.fi'
const health = await fetch(`${BASE}/health`).then((r) => r.json())
console.log(health.feed) // live | connecting | rejected | stopped
console.log(health.dtfs) // DTFs being priced
console.log(health.instruments) // distinct upstream instruments subscribed
console.log(health.marks) // instruments with at least one mark
// "ok" alone is not enough. A feed that authenticated and subscribed to
// nothing also answers ok, so check that it is actually watching something.
if (health.feed !== 'live' || health.marks === 0) {
throw new Error('Pricing service is up but not watching anything.')
}import { useEffect, useState } from 'react'
const BASE = 'https://prices.indexes.fi'
export function usePricingHealth(intervalMs = 30_000) {
const [health, setHealth] = useState(null)
useEffect(() => {
let cancelled = false
const check = async () => {
try {
const res = await fetch(`${BASE}/health`, { cache: 'no-store' })
const body = await res.json()
if (!cancelled) setHealth(body)
} catch {
if (!cancelled) setHealth({ feed: 'offline' })
}
}
void check()
const timer = setInterval(check, intervalMs)
return () => {
cancelled = true
clearInterval(timer)
}
}, [intervalMs])
return health
}
GET /v1/dtfs
Every DTF being priced, with its legs and its current price.
An array of DTFs. The set matches the platform's public directory, so a DTF appears here exactly when it appears on the site, and it refreshes once a minute.
Response
| Field | Type | Meaning |
|---|---|---|
[].id | uuid | The DTF id. |
[].slug | string | The public path segment, e.g. ixda-dtf-btc-eth-50-50. |
[].shareSymbol | string | The share token symbol. |
[].createdAt | iso8601 | Where the index is defined to be 1.0. |
[].rebalanceCadenceHours | number | null | Null means a DTF nobody rebalances, so the basket is held from inception. |
[].legs[] | array | One entry per constituent, carrying both what it is and what it is worth: symbol, feedSymbol (the upstream instrument), a signed weightPct (negative is short, above 100 is levered), the current price, its markAgeMs, and stale. |
[].unpriceableSymbols | string[] | Legs the upstream cannot mark, named rather than dropped. Non-empty means the index is missing part of itself. |
[].price | object | null | The index price: price, returnBps and observedAt. Null until every leg has printed. The constituent marks are not repeated here; they are on legs above. |
GET /v1/dtfs
import requests
BASE = "https://prices.indexes.fi"
# The collection is an array. There is no envelope to reach through.
dtfs = requests.get(f"{BASE}/v1/dtfs", timeout=10).json()
for dtf in dtfs:
# Each leg carries its weight and its current mark together.
legs = ", ".join(f"{leg['symbol']} {leg['weightPct']}% @ {leg['price']}" for leg in dtf["legs"])
price = dtf["price"]["price"] if dtf["price"] else None
print(f"{dtf['shareSymbol']:8} {dtf['slug']:32} {legs:24} {price}")
# A leg the service cannot mark is named, not silently dropped.
if dtf["unpriceableSymbols"]:
print(f" not priced: {dtf['unpriceableSymbols']}")const BASE = 'https://prices.indexes.fi'
// The collection is an array. There is no envelope to reach through.
const dtfs = await fetch(`${BASE}/v1/dtfs`).then((r) => r.json())
for (const dtf of dtfs) {
// Each leg carries its weight and its current mark together.
const legs = dtf.legs.map((leg) => `${leg.symbol} ${leg.weightPct}% @ ${leg.price}`).join(', ')
console.log(dtf.shareSymbol, dtf.slug, legs, dtf.price?.price ?? 'awaiting marks')
// A leg the service cannot mark is named, not silently dropped.
if (dtf.unpriceableSymbols.length > 0) {
console.warn(`${dtf.shareSymbol} excludes ${dtf.unpriceableSymbols.join(', ')}`)
}
}import { useEffect, useState } from 'react'
const BASE = 'https://prices.indexes.fi'
export function DtfList() {
const [dtfs, setDtfs] = useState([])
useEffect(() => {
let cancelled = false
fetch(`${BASE}/v1/dtfs`)
.then((r) => r.json())
.then((list) => {
if (!cancelled) setDtfs(list)
})
return () => {
cancelled = true
}
}, [])
return (
<ul>
{dtfs.map((dtf) => (
<li key={dtf.id}>
<a href={`/${dtf.slug}`}>{dtf.name}</a>{' '}
{dtf.price ? dtf.price.price.toFixed(4) : 'awaiting marks'} USDC
</li>
))}
</ul>
)
}
GET /v1/dtfs/:key
One DTF. The key is its slug, its share symbol, or its DTF id.
All three resolve to the same record, so a caller holding any of them does not
need a lookup first. An unknown key answers 404 with
{"code":"DTF_NOT_FOUND"}.
The DTF is the response body, with its current price under
price. Fields are the same as one element of the collection above.
GET /v1/dtfs/ixda-dtf-btc-eth-50-50
import requests
BASE = "https://prices.indexes.fi"
KEY = "ixda-dtf-btc-eth-50-50" # slug, share symbol, or DTF id
res = requests.get(f"{BASE}/v1/dtfs/{KEY}", timeout=10)
if res.status_code == 404:
raise SystemExit(f"No DTF matches {KEY}")
# The resource is the body.
dtf = res.json()
print(dtf["name"], dtf["shareSymbol"])
print("created", dtf["createdAt"])
print("rebalances every", dtf["rebalanceCadenceHours"], "hours")
for leg in dtf["legs"]:
flag = " STALE" if leg["stale"] else ""
print(" ", leg["symbol"], leg["feedSymbol"], leg["weightPct"], "%", leg["price"], flag)
# The current price comes embedded, so one request is enough.
print("price", dtf["price"]["price"] if dtf["price"] else "awaiting marks")const BASE = 'https://prices.indexes.fi'
const KEY = 'ixda-dtf-btc-eth-50-50' // slug, share symbol, or DTF id
const res = await fetch(`${BASE}/v1/dtfs/${encodeURIComponent(KEY)}`)
if (res.status === 404) throw new Error(`No DTF matches ${KEY}`)
// The resource is the body, with its current price embedded.
const dtf = await res.json()
console.log(dtf.name, dtf.shareSymbol)
console.log('created', dtf.createdAt)
console.log('rebalances every', dtf.rebalanceCadenceHours, 'hours')
for (const leg of dtf.legs) {
console.log(' ', leg.symbol, leg.feedSymbol, leg.weightPct + '%', leg.price, leg.stale ? 'STALE' : '')
}
console.log('price', dtf.price?.price ?? 'awaiting marks')import { useEffect, useState } from 'react'
const BASE = 'https://prices.indexes.fi'
export function useDtf(key) {
const [state, setState] = useState({ dtf: null, status: 'loading' })
useEffect(() => {
let cancelled = false
fetch(`${BASE}/v1/dtfs/${encodeURIComponent(key)}`)
.then((r) => (r.ok ? r.json() : Promise.reject(new Error(String(r.status)))))
.then((dtf) => {
if (!cancelled) setState({ dtf, status: 'ready' })
})
.catch(() => {
if (!cancelled) setState({ dtf: null, status: 'unavailable' })
})
return () => {
cancelled = true
}
}, [key])
return state
}
GET /v1/dtfs/:key/price
The index price as of the last trade, and the marks behind it.
The path already names the DTF, so the body is the price and the marks it was built from, with nothing that restates which DTF that is.
A DTF with no price yet answers 200 with price: null
and a reason, not an error. The service is up and the DTF exists; it has simply
not seen every leg print. Retrying will not change that, only a trade will.
Response
| Field | Type | Meaning |
|---|---|---|
price | number | null | Index level. 1.0 at the DTF’s creation, so it reads as a price per share. Null when there is no price yet. |
returnBps | number | Return since creation, in basis points. |
observedAt | iso8601 | When the venue timestamped the trade, not when it arrived here. Parse it for a number. |
legs[].stale | boolean | Whether the mark is too old to count as live. Read it: a stale leg and a flat market draw the same line. Naming the leg beats a count, and the service knows the threshold a caller would otherwise have to invent. |
legs[].symbol | string | The constituent, matching the DTF’s legs. |
legs[].price | number | The mark it was priced from. Weights are not repeated here; they belong to the DTF. |
legs[].markAgeMs | number | null | How old that mark was at the tick. Null means never marked. |
reason | string | Present only when price is null. Currently AWAITING_MARKS. |
GET /v1/dtfs/ixda-dtf-btc-eth-50-50/price
import requests
BASE = "https://prices.indexes.fi"
KEY = "ixda-dtf-btc-eth-50-50"
# The path already named the DTF, so the body is the price on its own.
price = requests.get(f"{BASE}/v1/dtfs/{KEY}/price", timeout=10).json()
# price=None means the service is up and this DTF has not had every leg
# print yet. It is not an error and retrying will not change it; only a
# trade will.
if price["price"] is None:
raise SystemExit(price.get("reason", "AWAITING_MARKS"))
print(price["price"]) # index level, 1.0 at the DTF's creation
print(price["returnBps"]) # return since creation, basis points
print(price["observedAt"]) # when the venue timestamped the trade
# Always read this. A price built on a stale leg is not wrong, but it is not
# the index either, and a stale leg looks identical to a flat market.
stale = [leg["symbol"] for leg in price["legs"] if leg["stale"]]
if stale:
print("stale legs:", stale)
for leg in price["legs"]:
print(" ", leg["symbol"], leg["price"], "age(ms)", leg["markAgeMs"])const BASE = 'https://prices.indexes.fi'
const KEY = 'ixda-dtf-btc-eth-50-50'
// The path already named the DTF, so the body is the price on its own.
const price = await fetch(
`${BASE}/v1/dtfs/${encodeURIComponent(KEY)}/price`,
).then((r) => r.json())
// price=null means the service is up and this DTF has not had every leg
// print yet. It is not an error and retrying will not change it; only a
// trade will.
if (price.price === null) throw new Error(price.reason ?? 'AWAITING_MARKS')
console.log(price.price) // index level, 1.0 at the DTF's creation
console.log(price.returnBps) // return since creation, basis points
console.log(price.observedAt) // when the venue timestamped the trade
// Always read this. A price built on a stale leg is not wrong, but it is not
// the index either, and a stale leg looks identical to a flat market.
const stale = price.legs.filter((leg) => leg.stale).map((leg) => leg.symbol)
if (stale.length > 0) console.warn('stale legs:', stale.join(', '))import { useEffect, useState } from 'react'
const BASE = 'https://prices.indexes.fi'
/** Polling fallback. Prefer the WebSocket below; this is for when one is not
* available, or for a page that only needs an occasional number. */
export function usePolledDtfPrice(key, intervalMs = 15_000) {
const [price, setPrice] = useState(null)
useEffect(() => {
let cancelled = false
const read = async () => {
const body = await fetch(
`${BASE}/v1/dtfs/${encodeURIComponent(key)}/price`,
{ cache: 'no-store' },
).then((r) => r.json())
if (!cancelled) setPrice(body.price === null ? null : body)
}
void read()
const timer = setInterval(read, intervalMs)
return () => {
cancelled = true
clearInterval(timer)
}
}, [key, intervalMs])
const stale = (price?.legs ?? []).filter((leg) => leg.stale).map((leg) => leg.symbol)
return { price, stale }
}
GET /v1/dtfs/:key/candles
The index history over a trailing window, plus the bar still taking ticks.
Bars are stored every five minutes. Long windows are rolled up, not
truncated: ninety days of five-minute bars is 25,920 rows, so the response comes
back at a coarser resolution and says which one in resolutionMinutes. The
series always spans the days you asked for.
The open bar is merged into the series, so a caller opening mid-bucket ends at the current price rather than up to a bucket behind it. That is also why no separate current price is sent: the last bar’s close already is it.
Query parameters
| Field | Type | Meaning |
|---|---|---|
days | number | Window length. Default 90, capped at 400. |
resolutionMinutes | number | Force a granularity instead of letting the service choose. Floored at 5, the granularity bars are stored at. |
Response
| Field | Type | Meaning |
|---|---|---|
resolutionMinutes | number | The granularity this response came back at. |
candles[].t | number | Bucket open, Unix seconds. |
candles[].o / h / l / c | number | Open, high, low, close, as index levels. |
candles[].n | number | Ticks observed in the bar. Zero for a reconstructed one. |
candles[].source | string | live was observed tick by tick here. backfill was reconstructed from upstream aggregates, and its high and low are its own endpoints, since the legs’ extremes did not occur at the same instant. |
GET /v1/dtfs/ixda-dtf-btc-eth-50-50/candles?days=1&resolutionMinutes=5
import requests
from datetime import datetime, timezone
BASE = "https://prices.indexes.fi"
KEY = "ixda-dtf-btc-eth-50-50"
body = requests.get(
f"{BASE}/v1/dtfs/{KEY}/candles",
params={"days": 90},
timeout=30,
).json()
# Read this rather than assuming five minutes. The service coarsens long
# windows so the series always spans the days you asked for; what varies
# is how finely.
print("resolution", body["resolutionMinutes"], "minutes")
print("bars", len(body["candles"]))
for bar in body["candles"][-5:]:
when = datetime.fromtimestamp(bar["t"], tz=timezone.utc)
print(when.isoformat(), bar["o"], bar["h"], bar["l"], bar["c"], bar["source"])
# source distinguishes bars observed tick by tick from bars reconstructed
# out of upstream aggregates. A flat run of each means different things.
observed = [b for b in body["candles"] if b["source"] == "live"]
print(f"{len(observed)} of {len(body['candles'])} bars observed here")
# Force the stored granularity for a short window.
fine = requests.get(
f"{BASE}/v1/dtfs/{KEY}/candles",
params={"days": 1, "resolutionMinutes": 5},
timeout=30,
).json()
print("fine bars", len(fine["candles"]))const BASE = 'https://prices.indexes.fi'
const KEY = 'ixda-dtf-btc-eth-50-50'
const url = new URL(`${BASE}/v1/dtfs/${encodeURIComponent(KEY)}/candles`)
url.searchParams.set('days', '90')
const body = await fetch(url).then((r) => r.json())
// Read this rather than assuming five minutes. The service coarsens long
// windows so the series always spans the days you asked for; what varies
// is how finely.
console.log('resolution', body.resolutionMinutes, 'minutes')
console.log('bars', body.candles.length)
for (const bar of body.candles.slice(-5)) {
console.log(new Date(bar.t * 1000).toISOString(), bar.o, bar.h, bar.l, bar.c, bar.source)
}
// source distinguishes bars observed tick by tick from bars reconstructed
// out of upstream aggregates. A flat run of each means different things.
const observed = body.candles.filter((bar) => bar.source === 'live')
console.log(`${observed.length} of ${body.candles.length} bars observed here`)import { useEffect, useMemo, useState } from 'react'
const BASE = 'https://prices.indexes.fi'
export function useDtfCandles(key, days) {
const [series, setSeries] = useState(null)
useEffect(() => {
let cancelled = false
const url = new URL(`${BASE}/v1/dtfs/${encodeURIComponent(key)}/candles`)
url.searchParams.set('days', String(days))
fetch(url, { cache: 'no-store' })
.then((r) => r.json())
.then((body) => {
if (!cancelled) setSeries(body)
})
// Refetch so newly closed bars land. The open bar is already included in
// every response, so the chart is never more than a tick behind.
const timer = setInterval(() => {
fetch(url, { cache: 'no-store' })
.then((r) => r.json())
.then((body) => {
if (!cancelled) setSeries(body)
})
}, 150_000)
return () => {
cancelled = true
clearInterval(timer)
}
}, [key, days])
// Re-base to the window's first point so each range starts at zero. The
// series is absolute levels, so plotting them raw makes every range switch
// look like a jump.
const points = useMemo(() => {
const candles = series?.candles ?? []
const base = candles[0]?.c
if (base === undefined || base === 0) return []
return candles.map((bar) => ({
t: bar.t * 1000,
returnPct: (bar.c / base - 1) * 100,
observed: bar.source === 'live',
}))
}, [series])
return { points, resolutionMinutes: series?.resolutionMinutes ?? null }
}
GET /v1/marks
The latest upstream mark per instrument.
An array of marks: the inputs every index price is built from, so a quoted price can be checked against them rather than trusted.
Response
| Field | Type | Meaning |
|---|---|---|
[].feedSymbol | string | The upstream instrument, e.g. X:BTC-USD. |
[].price | number | The last trade price. |
[].observedAt | iso8601 | When the venue timestamped it. |
GET /v1/marks
import requests
BASE = "https://prices.indexes.fi"
marks = requests.get(f"{BASE}/v1/marks", timeout=10).json()
# The inputs every index price above was built from, so a quoted price can
# be checked rather than trusted.
for mark in marks:
print(mark["feedSymbol"], mark["price"], mark["observedAt"])const BASE = 'https://prices.indexes.fi'
const marks = await fetch(`${BASE}/v1/marks`).then((r) => r.json())
// The inputs every index price above was built from, so a quoted price can
// be checked rather than trusted.
for (const mark of marks) {
console.log(mark.feedSymbol, mark.price, mark.observedAt)
}import { useEffect, useState } from 'react'
const BASE = 'https://prices.indexes.fi'
export function ConstituentMarks() {
const [marks, setMarks] = useState([])
useEffect(() => {
const read = () =>
fetch(`${BASE}/v1/marks`, { cache: 'no-store' })
.then((r) => r.json())
.then(setMarks)
void read()
const timer = setInterval(read, 10_000)
return () => clearInterval(timer)
}, [])
return (
<table>
<tbody>
{marks.map((mark) => (
<tr key={mark.feedSymbol}>
<td>{mark.feedSymbol}</td>
<td>{mark.price}</td>
<td>{mark.observedAt}</td>
</tr>
))}
</tbody>
</table>
)
}
Realtime
WS /v1/stream?dtf=:key
Live index prices, one message per tick, for the DTFs you name.
Name DTFs in the query string as ?dtf=IXDA&dtf=IXDB or
?dtf=IXDA,IXDB, and change the set at any time by sending
{"action":"subscribe","dtf":["IXDB"]} or the matching
unsubscribe. {"action":"ping"} answers
{"type":"pong"}.
Messages received
| Type | When | Carries |
|---|---|---|
hello | On connect | subscribed: the ids your keys resolved to. What is available is GET /v1/dtfs, not repeated here. |
tick | On connect, then per tick | dtfId, then the price endpoint’s fields. The id travels here because one socket carries several DTFs; the slug does not, because one routing key is enough and a slug can change. |
subscribed | After a subscribe or unsubscribe | The full resolved set, so a client never has to track it. |
pong | After a ping | Nothing. |
The first tick arrives immediately. A client that connects mid-bucket gets the current price without waiting for the next trade, so a page never opens on an empty number.
Prices are computed on every trade. Broadcasts are coalesced to at most one per 250ms per DTF, because a browser redrawing twelve times a second gains nothing. The five-minute bar is the durable record either way.
import json
import asyncio
import websockets # pip install websockets
WS = "https://prices.indexes.fi".replace("https://", "wss://").replace("http://", "ws://")
KEY = "ixda-dtf-btc-eth-50-50"
async def main():
# Reconnecting generator: the socket will drop, and a client that does
# not reconnect goes quiet without reporting anything.
async for socket in websockets.connect(f"{WS}/v1/stream?dtf={KEY}"):
try:
async for raw in socket:
message = json.loads(raw)
if message["type"] == "hello":
print("subscribed to", message["subscribed"])
continue
if message["type"] == "tick":
stale = [l["symbol"] for l in message["legs"] if l["stale"]]
print(message["dtfId"], round(message["price"], 6), "stale:", stale)
except websockets.ConnectionClosed:
continue # the generator reconnects with backoff
asyncio.run(main())// Node 22 has a global WebSocket, so there is no dependency to add.
const WS = 'https://prices.indexes.fi'.replace(/^http/, 'ws')
const KEY = 'ixda-dtf-btc-eth-50-50'
const BACKOFF_MS = [500, 1_000, 2_000, 5_000, 10_000, 30_000]
let attempt = 0
function connect() {
const socket = new WebSocket(`${WS}/v1/stream?dtf=${encodeURIComponent(KEY)}`)
socket.onopen = () => {
attempt = 0
}
socket.onmessage = (event) => {
const message = JSON.parse(event.data)
if (message.type === 'hello') {
console.log('subscribed to', message.subscribed)
return
}
if (message.type === 'tick') {
const stale = message.legs.filter((leg) => leg.stale).map((leg) => leg.symbol)
console.log(message.dtfId, message.price.toFixed(6), 'stale:', stale.join(', ') || 'none')
}
}
// The socket will drop. A client that does not reconnect goes quiet
// without reporting anything, which reads as a flat market.
socket.onclose = () => {
const delay = BACKOFF_MS[Math.min(attempt, BACKOFF_MS.length - 1)]
attempt += 1
setTimeout(connect, delay)
}
}
connect()
// Subscribe to more DTFs on the same socket at any time:
// socket.send(JSON.stringify({ action: 'subscribe', dtf: ['IXDA', 'IXDB'] }))
// socket.send(JSON.stringify({ action: 'unsubscribe', dtf: 'IXDB' }))import { useEffect, useState } from 'react'
const BASE = 'https://prices.indexes.fi'
const BACKOFF_MS = [500, 1_000, 2_000, 5_000, 10_000, 30_000]
export function useDtfLivePrice(key) {
const [tick, setTick] = useState(null)
const [status, setStatus] = useState('connecting')
useEffect(() => {
let socket = null
let retry = null
let attempt = 0
let closed = false
const connect = () => {
if (closed) return
setStatus('connecting')
socket = new WebSocket(
`${BASE.replace(/^http/, 'ws')}/v1/stream?dtf=${encodeURIComponent(key)}`,
)
socket.onopen = () => {
attempt = 0
setStatus('live')
}
socket.onmessage = (event) => {
const message = JSON.parse(event.data)
if (message.type === 'tick') setTick(message)
}
socket.onclose = () => {
socket = null
if (closed) return
setStatus('offline')
const delay = BACKOFF_MS[Math.min(attempt, BACKOFF_MS.length - 1)]
attempt += 1
retry = setTimeout(connect, delay)
}
}
connect()
return () => {
closed = true
if (retry !== null) clearTimeout(retry)
if (socket !== null) {
// Cleared first, or the handler reads this deliberate close as an
// outage and reconnects a subscription nobody is listening to.
socket.onclose = null
socket.close()
}
}
}, [key])
const stale = (tick?.legs ?? []).some((leg) => leg.stale)
return { tick, status, stale }
}
export function LivePrice({ dtfKey }) {
const { tick, status, stale } = useDtfLivePrice(dtfKey)
if (tick === null) return <span>awaiting price</span>
return (
<span data-live={status === 'live' && !stale}>
{tick.price.toFixed(4)} USDC
{stale ? ' (stale mark)' : ''}
</span>
)
}
Errors
| Status | Body | Meaning |
|---|---|---|
200 | {"latest": null, "reason": "AWAITING_MARKS"} | The DTF exists and has no price yet. Not an error, and retrying will not change it. |
404 | {"code": "DTF_NOT_FOUND"} | No DTF matches that slug, symbol or id. |
404 | {"error": "No route for ..."} | Unknown path. |
405 | {"error": "Only GET is served here."} | The read surface is GET only. |
403 | {"error": "Origin not allowed."} | The browser origin is not in PRICING_ALLOWED_ORIGINS. |
500 | {"error": "The pricing service failed to answer."} | Unhandled fault. The detail is in the service log, never in the response. |
How a DTF is priced
A DTF holds a basket, so the index holds units and cash, not weights:
Units are fixed between rebalances, so weights drift with the market exactly as the DTF's real holdings do. Pricing the index as a fixed-weight blend of leg returns assumes the opposite, continuous and free rebalancing on every tick, which over ninety days is not a rounding difference: it harvests volatility the DTF never traded.
Cash is what lets shorts and leverage share one code path. It is zero for a long only book, negative when levered because the notional is borrowed, and positive collateral for a market neutral one.
Rebalances are applied on the DTF's own cadence, anchored to its creation, so a reconstruction and the live engine agree on where the boundaries fall. A DTF with no cadence holds the basket it opened with.
Where the numbers come from
- Constituent marks are trades from the upstream crypto feed, per instrument.
- History before the service was watching is reconstructed from upstream five minute aggregates, then divided through so the index reads exactly 1.0 at creation.
- The live basket opens at the level and the marks the reconstruction ended on, so there is no step where history meets live.