From 1a1a1c651711845323a1097f45c308a0b339c26a Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Tue, 5 Nov 2024 19:57:47 -0800 Subject: [PATCH] [ECO-2372] Fix candlestick chart issues (#330) Run format Fix lints --- .../frontend/src/app/market/[market]/page.tsx | 1 - .../src/app/pools/api/getPoolDataQuery.ts | 1 + .../src/components/charts/PrivateChart.tsx | 31 ++++++++++++++----- .../frontend/src/lib/chart-utils/index.ts | 6 ++++ .../src/lib/store/event/candlestick-bars.ts | 10 ++++-- .../src/lib/store/event/event-store.ts | 10 ++++++ 6 files changed, 48 insertions(+), 11 deletions(-) diff --git a/src/typescript/frontend/src/app/market/[market]/page.tsx b/src/typescript/frontend/src/app/market/[market]/page.tsx index 8d5c1bc7bf..db773aeb18 100644 --- a/src/typescript/frontend/src/app/market/[market]/page.tsx +++ b/src/typescript/frontend/src/app/market/[market]/page.tsx @@ -70,7 +70,6 @@ const EmojicoinPage = async (params: EmojicoinPageProps) => { }); const state = await fetchMarketState({ searchEmojis: emojis }); - if (state) { const { marketID } = state.market; const marketAddress = deriveEmojicoinPublisherAddress({ emojis }); diff --git a/src/typescript/frontend/src/app/pools/api/getPoolDataQuery.ts b/src/typescript/frontend/src/app/pools/api/getPoolDataQuery.ts index db9b2a0ea0..a061b90620 100644 --- a/src/typescript/frontend/src/app/pools/api/getPoolDataQuery.ts +++ b/src/typescript/frontend/src/app/pools/api/getPoolDataQuery.ts @@ -12,6 +12,7 @@ export async function getPoolData( searchEmojis?: string[], provider?: string ) { + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ let ret: any; if (provider) { ret = fetchUserLiquidityPools({ diff --git a/src/typescript/frontend/src/components/charts/PrivateChart.tsx b/src/typescript/frontend/src/components/charts/PrivateChart.tsx index 467957cfb6..61fc592793 100644 --- a/src/typescript/frontend/src/components/charts/PrivateChart.tsx +++ b/src/typescript/frontend/src/components/charts/PrivateChart.tsx @@ -20,7 +20,7 @@ import { type Timezone, widget, } from "@static/charting_library"; -import { getClientTimezone } from "lib/chart-utils"; +import { getClientTimezone, hasTradingActivity } from "lib/chart-utils"; import { type ChartContainerProps } from "./types"; import { useRouter } from "next/navigation"; import { ROUTES } from "router/routes"; @@ -188,12 +188,13 @@ export const Chart = (props: ChartContainerProps) => { // Convert the market view data to `latestBar[]` and set the latest bars in our EventStore to those values. const latestBars = marketToLatestBars(marketResource); const marketEmojiData = toMarketEmojiData(marketResource.metadata.emojiBytes); + const symbolEmojis = marketEmojiData.emojis.map((e) => e.emoji); const marketMetadata: MarketMetadataModel = { marketID: marketResource.metadata.marketID, time: 0n, marketNonce: marketResource.sequenceInfo.nonce, trigger: Trigger.PackagePublication, // Make up some bunk trigger, since it should be clear it's made up. - symbolEmojis: marketEmojiData.emojis.map((e) => e.emoji), + symbolEmojis, marketAddress: marketResource.metadata.marketAddress, ...marketEmojiData, }; @@ -218,9 +219,11 @@ export const Chart = (props: ChartContainerProps) => { // some visual inconsistencies in the chart. const bars: Bar[] = data.reduce((acc: Bar[], event) => { const bar = toBar(event); - if (bar.time >= from * 1000 && bar.time <= to * 1000) { - if (acc.at(-1)) { - bar.open = acc.at(-1)!.close; + const inTimeRange = bar.time >= from * 1000 && bar.time <= to * 1000; + if (inTimeRange && hasTradingActivity(bar)) { + const prev = acc.at(-1); + if (prev) { + bar.open = prev.close; } acc.push(bar); } @@ -229,9 +232,23 @@ export const Chart = (props: ChartContainerProps) => { // Push the latest bar to the bars array if it exists and update its `open` value to be the previous bar's // `close` if it's not the first/only bar. + // This logic mirrors what we use in `createBarFrom[Swap|PeriodicState]` but we need it here because we + // update the latest bar based on the market view every time we fetch with `getBars`, not just when a new + // event comes in. if (latestBar) { - if (bars.at(-1)) { - latestBar.open = bars.at(-1)!.close; + const secondLatestBar = bars.at(-1); + if (secondLatestBar) { + // If the latest bar has no trading activity, set all of its fields to the previous bar's close. + if (!hasTradingActivity(latestBar)) { + latestBar.high = secondLatestBar.close; + latestBar.low = secondLatestBar.close; + latestBar.close = secondLatestBar.close; + } + if (secondLatestBar.close !== 0) { + latestBar.open = secondLatestBar.close; + } else { + latestBar.open = latestBar.close; + } } bars.push(latestBar); } diff --git a/src/typescript/frontend/src/lib/chart-utils/index.ts b/src/typescript/frontend/src/lib/chart-utils/index.ts index bbe56ca113..ea7883b7ed 100644 --- a/src/typescript/frontend/src/lib/chart-utils/index.ts +++ b/src/typescript/frontend/src/lib/chart-utils/index.ts @@ -1,5 +1,8 @@ // cspell:word Kolkata // cspell:word Fakaofo + +import { type Bar } from "@static/charting_library/datafeed-api"; + /** * Retrieves the client's timezone based on the current system time offset. * @@ -70,3 +73,6 @@ export function getClientTimezone() { } return "Etc/UTC"; } + +export const hasTradingActivity = (bar: Bar) => + [bar.open, bar.high, bar.low, bar.close].some((price) => price !== 0); diff --git a/src/typescript/frontend/src/lib/store/event/candlestick-bars.ts b/src/typescript/frontend/src/lib/store/event/candlestick-bars.ts index 3d1b2d58a7..4c8b1e1651 100644 --- a/src/typescript/frontend/src/lib/store/event/candlestick-bars.ts +++ b/src/typescript/frontend/src/lib/store/event/candlestick-bars.ts @@ -59,7 +59,7 @@ export const toBar = (event: PeriodicStateEventModel): Bar => ({ high: q64ToBig(event.periodicState.highPriceQ64).toNumber(), low: q64ToBig(event.periodicState.lowPriceQ64).toNumber(), close: q64ToBig(event.periodicState.closePriceQ64).toNumber(), - volume: Number(event.periodicState.volumeQuote), + volume: Number(event.periodicState.volumeBase), }); export const toBars = (events: PeriodicStateEventModel | PeriodicStateEventModel[]) => @@ -75,7 +75,9 @@ export const createBarFromSwap = ( const periodStartTime = getPeriodStartTimeFromTime(market.time, period); return { time: Number(periodStartTime / 1000n), - open: previousClose ?? price, + // Only use previousClose if it's a truthy value, otherwise, new bars that follow bars with no + // trading activity will appear as a huge green candlestick because their open price is `0`. + open: previousClose ? previousClose : price, high: price, low: price, close: price, @@ -94,7 +96,9 @@ export const createBarFromPeriodicState = ( const price = q64ToBig(periodicState.closePriceQ64).toNumber(); return { time: periodEnumToRawDuration(period) / 1000, - open: previousClose ?? price, + // Only use previousClose if it's a truthy value, otherwise, new bars that follow bars with no + // trading activity will appear as a huge green candlestick because their open price is `0`. + open: previousClose ? previousClose : price, high: price, low: price, close: price, diff --git a/src/typescript/frontend/src/lib/store/event/event-store.ts b/src/typescript/frontend/src/lib/store/event/event-store.ts index a68f8e3196..cf0e62a298 100644 --- a/src/typescript/frontend/src/lib/store/event/event-store.ts +++ b/src/typescript/frontend/src/lib/store/event/event-store.ts @@ -113,6 +113,16 @@ export const createEventStore = () => { const market = state.markets.get(symbol)!; latestBars.forEach((bar) => { const period = bar.period; + // A bar's open should never be zero, so use the previous bar if it exists and isn't 0, + // otherwise, use the existing current bar's close. + if (bar.open === 0) { + const prevLatestBarClose = market[period].latestBar?.close; + if (prevLatestBarClose) { + bar.open = prevLatestBarClose; + } else { + bar.open = bar.close; + } + } market[period].latestBar = bar; }); });