Skip to content

Commit f59363e

Browse files
fix: use reference price for market baseline (kuestcom#1333)
Upstream: fc2852b
1 parent f2ff8c7 commit f59363e

11 files changed

Lines changed: 312 additions & 54 deletions

File tree

docs/api-reference/schemas/openapi-price-reference.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,8 +218,9 @@
218218
"Source": {
219219
"type": "string",
220220
"enum": [
221+
"binance",
221222
"chainlink",
222-
"massive"
223+
"pyth"
223224
]
224225
},
225226
"LastFinal": {

src/app/[locale]/(platform)/event/[slug]/_components/EventLiveSeriesChart.tsx

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,15 @@ import { useLiveSeriesPriceSnapshot } from '../_hooks/useLiveSeriesPriceSnapshot
1313
import { useLiveSeriesWebSocket } from '../_hooks/useLiveSeriesWebSocket'
1414
import {
1515
buildAxis,
16+
classifyLiveSeriesReference,
1617
findLiveSeriesEvent,
1718
formatDateAtTimezone,
1819
formatTimeAtTimezone,
1920
formatUsd,
2021
getVisibleCountdownUnits,
2122
hexToRgba,
2223
inferIntervalMsFromSeriesSlug,
24+
isCanonicalBinanceDailySnapshot,
2325
isUsEquityMarketOpen,
2426
LIVE_CHART_HEIGHT,
2527
LIVE_CHART_MARGIN_BOTTOM,
@@ -37,6 +39,7 @@ import {
3739
normalizeLiveChartPrice,
3840
normalizeSubscriptionSymbol,
3941
parseUtcDate,
42+
requiresCanonicalBinanceDailyClose,
4043
resolveEventEndTimestamp,
4144
resolveLiveSeriesDisplayPrice,
4245
SERIES_KEY,
@@ -191,8 +194,7 @@ function EventLiveSeriesChartContent({
191194

192195
const {
193196
referenceSnapshot,
194-
baselinePrice,
195-
setBaselinePrice,
197+
referenceSnapshotStatus,
196198
persistedFallbackPrice: snapshotFallbackPrice,
197199
} = useLiveSeriesPriceSnapshot({
198200
config,
@@ -209,14 +211,20 @@ function EventLiveSeriesChartContent({
209211
const chartNowMs = isEventClosed ? endTimestamp : nowMs
210212

211213
const persistedFallbackPrice = snapshotFallbackPrice
214+
const seriesReferenceClassification = useMemo(
215+
() => classifyLiveSeriesReference({
216+
topic: config.topic,
217+
activeWindowMinutes: config.active_window_minutes,
218+
}),
219+
[config.active_window_minutes, config.topic],
220+
)
212221

213222
const { data, status } = useLiveSeriesWebSocket({
214223
topic: config.topic,
215224
eventType: config.event_type,
216225
eventEndTimestamp: explicitEndTimestamp,
217226
subscriptionSymbol,
218227
isLiveView: isLiveView && !isEventClosed,
219-
setBaselinePrice,
220228
})
221229

222230
const isMarketClosed = useMemo(() => {
@@ -283,8 +291,16 @@ function EventLiveSeriesChartContent({
283291

284292
return persistedFallbackPrice.price
285293
}, [endTimestamp, persistedFallbackPrice])
294+
const hasCanonicalBinanceDailySnapshot = isCanonicalBinanceDailySnapshot(referenceSnapshot)
295+
const requiresCanonicalBinanceClose = requiresCanonicalBinanceDailyClose({
296+
snapshot: referenceSnapshot,
297+
snapshotStatus: referenceSnapshotStatus,
298+
seriesClassification: seriesReferenceClassification,
299+
})
286300
const finalPrice = isEventClosed
287-
? referenceClosingPrice ?? latestReferencePriceBeforeEnd ?? persistedFallbackPriceBeforeEnd
301+
? requiresCanonicalBinanceClose
302+
? hasCanonicalBinanceDailySnapshot ? referenceClosingPrice : null
303+
: referenceClosingPrice ?? latestReferencePriceBeforeEnd ?? persistedFallbackPriceBeforeEnd
288304
: null
289305

290306
const fallbackCurrentPrice = useMemo(() => {
@@ -364,7 +380,7 @@ function EventLiveSeriesChartContent({
364380
}
365381

366382
if (!isFinitePositivePrice(finalPrice)) {
367-
return preCloseData
383+
return requiresCanonicalBinanceClose ? [] : preCloseData
368384
}
369385

370386
return [
@@ -374,7 +390,7 @@ function EventLiveSeriesChartContent({
374390
[SERIES_KEY]: finalPrice,
375391
},
376392
].slice(-MAX_POINTS)
377-
}, [closedFallbackData, data, endTimestamp, finalPrice, isEventClosed])
393+
}, [closedFallbackData, data, endTimestamp, finalPrice, isEventClosed, requiresCanonicalBinanceClose])
378394

379395
const renderData = useMemo(() => {
380396
if (!dataSource.length) {
@@ -441,11 +457,10 @@ function EventLiveSeriesChartContent({
441457
finalPrice,
442458
renderedPrice,
443459
fallbackCurrentPrice,
460+
requiresCanonicalClose: requiresCanonicalBinanceClose,
444461
})
445462
const axisSourceData = renderData
446-
const resolvedBaselinePrice = isEventClosed
447-
? referenceOpeningPrice
448-
: baselinePrice ?? referenceOpeningPrice
463+
const resolvedBaselinePrice = referenceOpeningPrice
449464
const precisionReferencePrice = currentPrice
450465
?? resolvedBaselinePrice
451466
?? referenceSnapshot?.latest_price

src/app/[locale]/(platform)/event/[slug]/_components/EventLiveSeriesChartHeader.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -185,8 +185,11 @@ export default function EventLiveSeriesChartHeader({
185185
<div className={cn('h-10 w-px bg-border', liveMarketHref ? 'block' : 'hidden sm:block')} />
186186
<div>
187187
<div
188-
className="flex items-center gap-0.5 text-xs font-semibold whitespace-nowrap min-[360px]:gap-1 sm:gap-2"
189-
style={{ color: liveColor }}
188+
className={cn(
189+
'flex items-center gap-0.5 text-xs font-semibold whitespace-nowrap min-[360px]:gap-1 sm:gap-2',
190+
isEventClosed && 'text-foreground',
191+
)}
192+
style={isEventClosed ? undefined : { color: liveColor }}
190193
>
191194
<span>{priceStatusLabel}</span>
192195
{delta != null && (
@@ -210,8 +213,9 @@ export default function EventLiveSeriesChartHeader({
210213
sm:text-[22px]
211214
`,
212215
liveMarketHref ? 'min-[360px]:text-[20px]' : 'min-[360px]:text-[18px]',
216+
isEventClosed && 'text-foreground',
213217
)}
214-
style={{ color: liveColor }}
218+
style={isEventClosed ? undefined : { color: liveColor }}
215219
>
216220
{currentPrice != null
217221
? <RollingValue value={formatUsd(currentPrice, headerPriceDisplayDigits)} />

src/app/[locale]/(platform)/event/[slug]/_hooks/useLiveSeriesPriceSnapshot.ts

Lines changed: 67 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1-
import type { LiveSeriesPriceSnapshot, PersistedLivePrice } from '../_utils/eventLiveSeriesChartUtils'
1+
import type {
2+
LiveSeriesPriceSnapshot,
3+
LiveSeriesPriceSnapshotStatus,
4+
PersistedLivePrice,
5+
} from '../_utils/eventLiveSeriesChartUtils'
26
import type { EventLiveChartConfig } from '@/types'
3-
import { useCallback, useMemo, useState, useSyncExternalStore } from 'react'
7+
import { useCallback, useMemo, useSyncExternalStore } from 'react'
48
import {
59
LIVE_DATA_RETENTION_MS,
610
normalizeLiveChartPrice,
@@ -17,13 +21,13 @@ interface UseLiveSeriesPriceSnapshotOptions {
1721

1822
export interface LiveSeriesPriceSnapshotResult {
1923
referenceSnapshot: LiveSeriesPriceSnapshot | null
20-
baselinePrice: number | null
21-
setBaselinePrice: React.Dispatch<React.SetStateAction<number | null>>
24+
referenceSnapshotStatus: LiveSeriesPriceSnapshotStatus
2225
persistedFallbackPrice: PersistedLivePrice | null
2326
}
2427

2528
interface LiveSeriesPriceSnapshotStoreSnapshot {
2629
referenceSnapshot: LiveSeriesPriceSnapshot | null
30+
referenceSnapshotStatus: LiveSeriesPriceSnapshotStatus
2731
persistedFallbackPrice: PersistedLivePrice | null
2832
}
2933

@@ -47,8 +51,12 @@ interface LiveSeriesPriceSnapshotRequest {
4751
const liveSeriesPriceSnapshotStores = new Map<string, LiveSeriesPriceSnapshotStoreEntry>()
4852
const liveSeriesPriceStorageKeyPrefix = 'kuest-live-last-price'
4953
const LIVE_SERIES_PRICE_SNAPSHOT_STORE_TTL_MS = 10 * 60 * 1000
54+
const BINANCE_CLOSE_FIRST_REFRESH_DELAY_MS = 65 * 1000
55+
const BINANCE_CLOSE_REFRESH_INTERVAL_MS = 10 * 1000
56+
const BINANCE_CLOSE_REFRESH_WINDOW_MS = 5 * 60 * 1000
5057
const EMPTY_LIVE_SERIES_PRICE_SNAPSHOT: LiveSeriesPriceSnapshotStoreSnapshot = {
5158
referenceSnapshot: null,
59+
referenceSnapshotStatus: 'loading',
5260
persistedFallbackPrice: null,
5361
}
5462

@@ -199,6 +207,13 @@ async function fetchLiveSeriesPriceSnapshot(
199207
const requestToken = entry.fetchToken + 1
200208
entry.fetchToken = requestToken
201209
entry.abortController = controller
210+
if (entry.snapshot.referenceSnapshot == null) {
211+
entry.snapshot = {
212+
...entry.snapshot,
213+
referenceSnapshotStatus: 'loading',
214+
}
215+
notifyLiveSeriesPriceSnapshotStore(storeKey)
216+
}
202217
entry.inflightFetch = (async function runLiveSeriesPriceSnapshotFetch() {
203218
try {
204219
const response = await fetch(`/api/price-reference/live-series?${buildLiveSeriesPriceSnapshotQuery(request).toString()}`, {
@@ -207,13 +222,18 @@ async function fetchLiveSeriesPriceSnapshot(
207222
})
208223

209224
if (!response.ok) {
225+
entry.snapshot = {
226+
...entry.snapshot,
227+
referenceSnapshotStatus: 'unavailable',
228+
}
210229
return
211230
}
212231

213232
const payload = await response.json() as LiveSeriesPriceSnapshot
214233
entry.snapshot = {
215234
...entry.snapshot,
216235
referenceSnapshot: payload,
236+
referenceSnapshotStatus: 'ready',
217237
}
218238

219239
const fallbackPrice = normalizeLiveChartPrice(
@@ -236,6 +256,10 @@ async function fetchLiveSeriesPriceSnapshot(
236256
}
237257
}
238258
catch {
259+
entry.snapshot = {
260+
...entry.snapshot,
261+
referenceSnapshotStatus: 'unavailable',
262+
}
239263
}
240264
finally {
241265
if (entry.fetchToken === requestToken) {
@@ -260,12 +284,46 @@ function subscribeToLiveSeriesPriceSnapshot(
260284
const storeKey = buildLiveSeriesPriceSnapshotStoreKey(request)
261285
const entry = getLiveSeriesPriceSnapshotStoreEntry(storeKey)
262286
entry.listeners.add(onStoreChange)
287+
let binanceCloseRefreshTimer: ReturnType<typeof setTimeout> | null = null
288+
let isSubscribed = true
263289

264290
if (syncPersistedLivePriceSnapshot(storeKey, request)) {
265291
onStoreChange()
266292
}
267293
void fetchLiveSeriesPriceSnapshot(storeKey, request)
268294

295+
async function refreshBinanceCloseUntilAvailable() {
296+
if (!isSubscribed) {
297+
return
298+
}
299+
300+
await fetchLiveSeriesPriceSnapshot(storeKey, request)
301+
const snapshot = entry.snapshot.referenceSnapshot
302+
const eventEndTimestamp = request.explicitEndTimestamp
303+
if (!isSubscribed
304+
|| (snapshot != null && snapshot.source !== 'binance')
305+
|| snapshot?.closing_price != null
306+
|| eventEndTimestamp == null
307+
|| Date.now() >= eventEndTimestamp + BINANCE_CLOSE_REFRESH_WINDOW_MS) {
308+
return
309+
}
310+
311+
binanceCloseRefreshTimer = setTimeout(
312+
refreshBinanceCloseUntilAvailable,
313+
BINANCE_CLOSE_REFRESH_INTERVAL_MS,
314+
)
315+
}
316+
317+
if (request.explicitEndTimestamp != null) {
318+
const firstRefreshAtMs = request.explicitEndTimestamp + BINANCE_CLOSE_FIRST_REFRESH_DELAY_MS
319+
if (Date.now() < request.explicitEndTimestamp + BINANCE_CLOSE_REFRESH_WINDOW_MS) {
320+
binanceCloseRefreshTimer = setTimeout(
321+
refreshBinanceCloseUntilAvailable,
322+
Math.max(0, firstRefreshAtMs - Date.now()),
323+
)
324+
}
325+
}
326+
269327
function refreshSnapshotAfterResume() {
270328
if (document.hidden) {
271329
return
@@ -307,6 +365,10 @@ function subscribeToLiveSeriesPriceSnapshot(
307365
document.addEventListener('visibilitychange', handleVisibilityChange)
308366

309367
return function unsubscribeFromLiveSeriesPriceSnapshot() {
368+
isSubscribed = false
369+
if (binanceCloseRefreshTimer) {
370+
clearTimeout(binanceCloseRefreshTimer)
371+
}
310372
entry.listeners.delete(onStoreChange)
311373
if (entry.listeners.size === 0 && entry.abortController) {
312374
entry.fetchToken += 1
@@ -385,20 +447,9 @@ export function useLiveSeriesPriceSnapshot({
385447
getServerSnapshot,
386448
)
387449

388-
const [baselinePrice, setBaselinePrice] = useState<number | null>(null)
389-
390-
const effectiveBaselinePrice = baselinePrice ?? (
391-
typeof referenceSnapshot.referenceSnapshot?.opening_price === 'number'
392-
&& Number.isFinite(referenceSnapshot.referenceSnapshot.opening_price)
393-
&& referenceSnapshot.referenceSnapshot.opening_price > 0
394-
? referenceSnapshot.referenceSnapshot.opening_price
395-
: null
396-
)
397-
398450
return {
399451
referenceSnapshot: referenceSnapshot.referenceSnapshot,
400-
baselinePrice: effectiveBaselinePrice,
401-
setBaselinePrice,
452+
referenceSnapshotStatus: referenceSnapshot.referenceSnapshotStatus,
402453
persistedFallbackPrice: referenceSnapshot.persistedFallbackPrice,
403454
}
404455
}

src/app/[locale]/(platform)/event/[slug]/_hooks/useLiveSeriesWebSocket.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ interface UseLiveSeriesWebSocketOptions {
2121
eventEndTimestamp: number | null
2222
subscriptionSymbol: string
2323
isLiveView: boolean
24-
setBaselinePrice: React.Dispatch<React.SetStateAction<number | null>>
2524
}
2625

2726
export function useLiveSeriesWebSocket({
@@ -30,7 +29,6 @@ export function useLiveSeriesWebSocket({
3029
eventEndTimestamp,
3130
subscriptionSymbol,
3231
isLiveView,
33-
setBaselinePrice,
3432
}: UseLiveSeriesWebSocketOptions) {
3533
const { wsLiveDataUrl } = usePublicRuntimeConfig()
3634
const wsUrl = wsLiveDataUrl
@@ -185,8 +183,6 @@ export function useLiveSeriesWebSocket({
185183
transitionDurationMs,
186184
)
187185
})
188-
189-
setBaselinePrice(current => current ?? wsUpdatesForRender[0]?.price ?? null)
190186
}
191187

192188
function handleError() {
@@ -259,7 +255,7 @@ export function useLiveSeriesWebSocket({
259255
})
260256
}
261257
}
262-
}, [eventEndTimestamp, eventType, topic, isLiveView, wsUrl, subscriptionSymbol, setBaselinePrice])
258+
}, [eventEndTimestamp, eventType, topic, isLiveView, wsUrl, subscriptionSymbol])
263259

264260
return { data, status }
265261
}

0 commit comments

Comments
 (0)