From bf626cc8ef5f5ab7dcadaac4a6f1746f4e22e944 Mon Sep 17 00:00:00 2001 From: saml33 Date: Wed, 18 Jan 2023 16:06:00 +1100 Subject: [PATCH 01/27] add recent trade volume alert --- components/forms/Label.tsx | 12 +- components/modals/TradeVolumeAlertModal.tsx | 123 ++++++++++ components/settings/SoundSettings.tsx | 4 +- components/trade/RecentTrades.tsx | 259 ++++++++++---------- public/locales/en/trade.json | 10 +- public/locales/es/trade.json | 10 +- public/locales/ru/trade.json | 10 +- public/locales/zh/trade.json | 10 +- public/locales/zh_tw/trade.json | 10 +- utils/constants.ts | 2 + 10 files changed, 312 insertions(+), 138 deletions(-) create mode 100644 components/modals/TradeVolumeAlertModal.tsx diff --git a/components/forms/Label.tsx b/components/forms/Label.tsx index 819fe044..1e529fa1 100644 --- a/components/forms/Label.tsx +++ b/components/forms/Label.tsx @@ -1,5 +1,13 @@ -const Label = ({ text, optional }: { text: string; optional?: boolean }) => ( -

+const Label = ({ + text, + optional, + className, +}: { + text: string + optional?: boolean + className?: string +}) => ( +

{text}{' '} {optional ? ( (Optional) diff --git a/components/modals/TradeVolumeAlertModal.tsx b/components/modals/TradeVolumeAlertModal.tsx new file mode 100644 index 00000000..097547b2 --- /dev/null +++ b/components/modals/TradeVolumeAlertModal.tsx @@ -0,0 +1,123 @@ +import { ModalProps } from '../../types/modal' +import Modal from '../shared/Modal' +import Button, { LinkButton } from '../shared/Button' +import { useTranslation } from 'next-i18next' +import useLocalStorageState from 'hooks/useLocalStorageState' +import { SOUND_SETTINGS_KEY, TRADE_VOLUME_ALERT_KEY } from 'utils/constants' +import Label from '@components/forms/Label' +import { useState } from 'react' +import Switch from '@components/forms/Switch' +import { INITIAL_SOUND_SETTINGS } from '@components/settings/SoundSettings' +import NumberFormat, { NumberFormatValues } from 'react-number-format' +import { Howl } from 'howler' +import { PlayIcon } from '@heroicons/react/20/solid' + +const volumeAlertSound = new Howl({ + src: ['/sounds/trade-buy.mp3'], + volume: 0.8, +}) + +export const DEFAULT_VOLUME_ALERT_SETTINGS = { seconds: 30, value: 10000 } + +const INPUT_CLASSES = + 'h-12 w-full rounded-md border border-th-input-border bg-th-input-bkg px-3 font-mono text-base text-th-fgd-1 focus:border-th-input-border-hover focus:outline-none md:hover:border-th-input-border-hover' + +const TradeVolumeAlertModal = ({ isOpen, onClose }: ModalProps) => { + const { t } = useTranslation(['common', 'trade']) + const [soundSettings, setSoundSettings] = useLocalStorageState( + SOUND_SETTINGS_KEY, + INITIAL_SOUND_SETTINGS + ) + const [alertSettings, setAlertSettings] = useLocalStorageState( + TRADE_VOLUME_ALERT_KEY, + DEFAULT_VOLUME_ALERT_SETTINGS + ) + const [formValues, setFormValues] = useState(alertSettings) + + const handleSave = () => { + setAlertSettings(formValues) + onClose() + } + + return ( + +

{t('trade:volume-alert')}

+

{t('trade:volume-alert-desc')}

+ volumeAlertSound.play()} + > + + {t('trade:preview-sound')} + +
+

{t('trade:activate-volume-alert')}

+ + setSoundSettings({ + ...soundSettings, + 'recent-trades': !soundSettings['recent-trades'], + }) + } + /> +
+ {soundSettings['recent-trades'] ? ( + <> +
+
+
+
+ + + ) : null} + + ) +} + +export default TradeVolumeAlertModal diff --git a/components/settings/SoundSettings.tsx b/components/settings/SoundSettings.tsx index ef28e964..b4a04482 100644 --- a/components/settings/SoundSettings.tsx +++ b/components/settings/SoundSettings.tsx @@ -41,13 +41,13 @@ const SoundSettings = () => { onChange={() => handleToggleSoundSetting('all')} /> -
+ {/*

{t('settings:recent-trades')}

handleToggleSoundSetting('recent-trades')} /> -
+
*/}

{t('settings:swap-success')}

{ const { t } = useTranslation(['common', 'trade']) const fills = mangoStore((s) => s.selectedMarket.fills) - const [soundSettings, setSoundSettings] = useLocalStorageState( + const [latestFillId, setLatestFillId] = useState('') + const [soundSettings] = useLocalStorageState( SOUND_SETTINGS_KEY, INITIAL_SOUND_SETTINGS ) - const previousFills = usePrevious(fills) - - useEffect(() => { - if (!soundSettings['recent-trades']) return - if (fills.length && previousFills && previousFills.length) { - const latestFill: ChartTradeType = fills[0] - const previousFill: ChartTradeType = previousFills[0] - if (previousFill.orderId.toString() !== latestFill.orderId.toString()) { - const side = - latestFill.side || (latestFill.takerSide === 1 ? 'bid' : 'ask') - if (['buy', 'bid'].includes(side)) { - buySound.play() - } else { - sellSound.play() - } - } - } - }, [fills, previousFills, soundSettings]) + const [alertSettings] = useLocalStorageState( + TRADE_VOLUME_ALERT_KEY, + DEFAULT_VOLUME_ALERT_SETTINGS + ) + const [showVolumeAlertModal, setShowVolumeAlertModal] = useState(false) const { selectedMarket, serumOrPerpMarket: market, baseSymbol, + quoteBank, quoteSymbol, } = useSelectedMarket() + useEffect(() => { + if (!fills.length) return + if (!latestFillId) { + setLatestFillId(fills[0].orderId.toString()) + } + }, [fills]) + + useInterval(() => { + if (!soundSettings['recent-trades'] || !quoteBank) return + setLatestFillId(fills[0].orderId.toString()) + const fillsLimitIndex = fills.findIndex( + (f) => f.orderId.toString() === latestFillId + ) + const newFillsVolumeValue = fills + .slice(0, fillsLimitIndex) + .reduce((a, c) => a + c.size * c.price, 0) + if (newFillsVolumeValue * quoteBank.uiPrice > Number(alertSettings.value)) { + volumeAlertSound.play() + } + }, alertSettings.seconds * 1000) + // const fetchRecentTrades = useCallback(async () => { // if (!market) return @@ -113,107 +121,110 @@ const RecentTrades = () => { }, [fills]) return ( -
-
- - - setSoundSettings({ - ...soundSettings, - 'recent-trades': !soundSettings['recent-trades'], - }) - } - size="small" - hideBg - > - {soundSettings['recent-trades'] ? ( - - ) : ( - - )} - - - - {t('trade:buys')}:{' '} - - {(buyRatio * 100).toFixed(1)}% + <> +
+
+ + setShowVolumeAlertModal(true)} + size="small" + hideBg + > + {soundSettings['recent-trades'] ? ( + + ) : ( + + )} + + + + {t('trade:buys')}:{' '} + + {(buyRatio * 100).toFixed(1)}% + + | + {t('trade:sells')}:{' '} + + {(sellRatio * 100).toFixed(1)}% + - | - {t('trade:sells')}:{' '} - - {(sellRatio * 100).toFixed(1)}% - - -
-
- - - - - - - - - - {!!fills.length && - fills.map((trade: ChartTradeType, i: number) => { - const side = - trade.side || (trade.takerSide === 1 ? 'bid' : 'ask') + +
+
{`${t( - 'price' - )} (${quoteSymbol})`} - {t('trade:size')} ({baseSymbol}) - {t('time')}
+ + + + + + + + + {!!fills.length && + fills.map((trade: ChartTradeType, i: number) => { + const side = + trade.side || (trade.takerSide === 1 ? 'bid' : 'ask') - // const price = - // typeof trade.price === 'number' - // ? trade.price - // : trade.price.toNumber() - const formattedPrice = market?.tickSize - ? floorToDecimal( - trade.price, - getDecimalCount(market.tickSize) - ) - : new Decimal(trade?.price || 0) - - // const size = trade?.quantity?.toNumber() || trade?.size - const formattedSize = - market?.minOrderSize && trade.size + // const price = + // typeof trade.price === 'number' + // ? trade.price + // : trade.price.toNumber() + const formattedPrice = market?.tickSize ? floorToDecimal( - trade.size, - getDecimalCount(market.minOrderSize) + trade.price, + getDecimalCount(market.tickSize) ) - : new Decimal(trade.size || 0) + : new Decimal(trade?.price || 0) - return ( - - - - - - ) - })} - -
{`${t( + 'price' + )} (${quoteSymbol})`} + {t('trade:size')} ({baseSymbol}) + {t('time')}
- {formattedPrice.toFixed()} - - {formattedSize.toFixed()} - - {trade.time - ? new Date(trade.time).toLocaleTimeString() - : trade.timestamp - ? new Date( - trade.timestamp.toNumber() - ).toLocaleTimeString() - : '-'} -
+ // const size = trade?.quantity?.toNumber() || trade?.size + const formattedSize = + market?.minOrderSize && trade.size + ? floorToDecimal( + trade.size, + getDecimalCount(market.minOrderSize) + ) + : new Decimal(trade.size || 0) + + return ( + + + {formattedPrice.toFixed()} + + + {formattedSize.toFixed()} + + + {trade.time + ? new Date(trade.time).toLocaleTimeString() + : trade.timestamp + ? new Date( + trade.timestamp.toNumber() + ).toLocaleTimeString() + : '-'} + + + ) + })} + + +
-
+ {showVolumeAlertModal ? ( + setShowVolumeAlertModal(false)} + /> + ) : null} + ) } diff --git a/public/locales/en/trade.json b/public/locales/en/trade.json index fed60d19..ccc3e4e1 100644 --- a/public/locales/en/trade.json +++ b/public/locales/en/trade.json @@ -1,4 +1,5 @@ { + "activate-volume-alert": "Activate Volume Alert", "amount": "Amount", "base": "Base", "book": "Book", @@ -16,6 +17,7 @@ "hourly-funding": "Hourly Funding", "in-orders": "In Orders", "instantaneous-funding": "Instantaneous Funding", + "interval-seconds": "Interval (seconds)", "limit-price": "Limit Price", "margin": "Margin", "no-balances": "No balances", @@ -23,6 +25,7 @@ "no-positions": "No perp positions", "no-unsettled": "No unsettled funds", "notional": "Notional", + "notional-volume": "Notional Volume ($)", "open-interest": "Open Interest", "oracle-price": "Oracle Price", "order-error": "Failed to place order", @@ -32,6 +35,7 @@ "post": "Post", "place-order": "Place {{side}} Order", "placing-order": "Placing Order", + "preview-sound": "Preview Sound", "quote": "Quote", "sells": "Sells", "settle-funds": "Settle Funds", @@ -45,7 +49,9 @@ "tooltip-ioc": "Immediate-Or-Cancel (IOC) orders are guaranteed to be the taker and must be executed immediately. Any portion of the order that can't be filled immediately will be cancelled", "tooltip-post": "Post orders are guaranteed to be the maker or they will be canceled", "tooltip-slippage": "An estimate of the difference between the current price and the price your trade will be executed at", - "trade-sounds-tooltip": "Play a sound alert for every new trade", + "tooltip-volume-alert": "Volume Alert Settings", "trades": "Trades", - "unsettled": "Unsettled" + "unsettled": "Unsettled", + "volume-alert": "Volume Alert", + "volume-alert-desc": "Play a sound whenever volume exceeds your alert threshold" } \ No newline at end of file diff --git a/public/locales/es/trade.json b/public/locales/es/trade.json index fed60d19..ccc3e4e1 100644 --- a/public/locales/es/trade.json +++ b/public/locales/es/trade.json @@ -1,4 +1,5 @@ { + "activate-volume-alert": "Activate Volume Alert", "amount": "Amount", "base": "Base", "book": "Book", @@ -16,6 +17,7 @@ "hourly-funding": "Hourly Funding", "in-orders": "In Orders", "instantaneous-funding": "Instantaneous Funding", + "interval-seconds": "Interval (seconds)", "limit-price": "Limit Price", "margin": "Margin", "no-balances": "No balances", @@ -23,6 +25,7 @@ "no-positions": "No perp positions", "no-unsettled": "No unsettled funds", "notional": "Notional", + "notional-volume": "Notional Volume ($)", "open-interest": "Open Interest", "oracle-price": "Oracle Price", "order-error": "Failed to place order", @@ -32,6 +35,7 @@ "post": "Post", "place-order": "Place {{side}} Order", "placing-order": "Placing Order", + "preview-sound": "Preview Sound", "quote": "Quote", "sells": "Sells", "settle-funds": "Settle Funds", @@ -45,7 +49,9 @@ "tooltip-ioc": "Immediate-Or-Cancel (IOC) orders are guaranteed to be the taker and must be executed immediately. Any portion of the order that can't be filled immediately will be cancelled", "tooltip-post": "Post orders are guaranteed to be the maker or they will be canceled", "tooltip-slippage": "An estimate of the difference between the current price and the price your trade will be executed at", - "trade-sounds-tooltip": "Play a sound alert for every new trade", + "tooltip-volume-alert": "Volume Alert Settings", "trades": "Trades", - "unsettled": "Unsettled" + "unsettled": "Unsettled", + "volume-alert": "Volume Alert", + "volume-alert-desc": "Play a sound whenever volume exceeds your alert threshold" } \ No newline at end of file diff --git a/public/locales/ru/trade.json b/public/locales/ru/trade.json index fed60d19..ccc3e4e1 100644 --- a/public/locales/ru/trade.json +++ b/public/locales/ru/trade.json @@ -1,4 +1,5 @@ { + "activate-volume-alert": "Activate Volume Alert", "amount": "Amount", "base": "Base", "book": "Book", @@ -16,6 +17,7 @@ "hourly-funding": "Hourly Funding", "in-orders": "In Orders", "instantaneous-funding": "Instantaneous Funding", + "interval-seconds": "Interval (seconds)", "limit-price": "Limit Price", "margin": "Margin", "no-balances": "No balances", @@ -23,6 +25,7 @@ "no-positions": "No perp positions", "no-unsettled": "No unsettled funds", "notional": "Notional", + "notional-volume": "Notional Volume ($)", "open-interest": "Open Interest", "oracle-price": "Oracle Price", "order-error": "Failed to place order", @@ -32,6 +35,7 @@ "post": "Post", "place-order": "Place {{side}} Order", "placing-order": "Placing Order", + "preview-sound": "Preview Sound", "quote": "Quote", "sells": "Sells", "settle-funds": "Settle Funds", @@ -45,7 +49,9 @@ "tooltip-ioc": "Immediate-Or-Cancel (IOC) orders are guaranteed to be the taker and must be executed immediately. Any portion of the order that can't be filled immediately will be cancelled", "tooltip-post": "Post orders are guaranteed to be the maker or they will be canceled", "tooltip-slippage": "An estimate of the difference between the current price and the price your trade will be executed at", - "trade-sounds-tooltip": "Play a sound alert for every new trade", + "tooltip-volume-alert": "Volume Alert Settings", "trades": "Trades", - "unsettled": "Unsettled" + "unsettled": "Unsettled", + "volume-alert": "Volume Alert", + "volume-alert-desc": "Play a sound whenever volume exceeds your alert threshold" } \ No newline at end of file diff --git a/public/locales/zh/trade.json b/public/locales/zh/trade.json index fed60d19..ccc3e4e1 100644 --- a/public/locales/zh/trade.json +++ b/public/locales/zh/trade.json @@ -1,4 +1,5 @@ { + "activate-volume-alert": "Activate Volume Alert", "amount": "Amount", "base": "Base", "book": "Book", @@ -16,6 +17,7 @@ "hourly-funding": "Hourly Funding", "in-orders": "In Orders", "instantaneous-funding": "Instantaneous Funding", + "interval-seconds": "Interval (seconds)", "limit-price": "Limit Price", "margin": "Margin", "no-balances": "No balances", @@ -23,6 +25,7 @@ "no-positions": "No perp positions", "no-unsettled": "No unsettled funds", "notional": "Notional", + "notional-volume": "Notional Volume ($)", "open-interest": "Open Interest", "oracle-price": "Oracle Price", "order-error": "Failed to place order", @@ -32,6 +35,7 @@ "post": "Post", "place-order": "Place {{side}} Order", "placing-order": "Placing Order", + "preview-sound": "Preview Sound", "quote": "Quote", "sells": "Sells", "settle-funds": "Settle Funds", @@ -45,7 +49,9 @@ "tooltip-ioc": "Immediate-Or-Cancel (IOC) orders are guaranteed to be the taker and must be executed immediately. Any portion of the order that can't be filled immediately will be cancelled", "tooltip-post": "Post orders are guaranteed to be the maker or they will be canceled", "tooltip-slippage": "An estimate of the difference between the current price and the price your trade will be executed at", - "trade-sounds-tooltip": "Play a sound alert for every new trade", + "tooltip-volume-alert": "Volume Alert Settings", "trades": "Trades", - "unsettled": "Unsettled" + "unsettled": "Unsettled", + "volume-alert": "Volume Alert", + "volume-alert-desc": "Play a sound whenever volume exceeds your alert threshold" } \ No newline at end of file diff --git a/public/locales/zh_tw/trade.json b/public/locales/zh_tw/trade.json index fed60d19..ccc3e4e1 100644 --- a/public/locales/zh_tw/trade.json +++ b/public/locales/zh_tw/trade.json @@ -1,4 +1,5 @@ { + "activate-volume-alert": "Activate Volume Alert", "amount": "Amount", "base": "Base", "book": "Book", @@ -16,6 +17,7 @@ "hourly-funding": "Hourly Funding", "in-orders": "In Orders", "instantaneous-funding": "Instantaneous Funding", + "interval-seconds": "Interval (seconds)", "limit-price": "Limit Price", "margin": "Margin", "no-balances": "No balances", @@ -23,6 +25,7 @@ "no-positions": "No perp positions", "no-unsettled": "No unsettled funds", "notional": "Notional", + "notional-volume": "Notional Volume ($)", "open-interest": "Open Interest", "oracle-price": "Oracle Price", "order-error": "Failed to place order", @@ -32,6 +35,7 @@ "post": "Post", "place-order": "Place {{side}} Order", "placing-order": "Placing Order", + "preview-sound": "Preview Sound", "quote": "Quote", "sells": "Sells", "settle-funds": "Settle Funds", @@ -45,7 +49,9 @@ "tooltip-ioc": "Immediate-Or-Cancel (IOC) orders are guaranteed to be the taker and must be executed immediately. Any portion of the order that can't be filled immediately will be cancelled", "tooltip-post": "Post orders are guaranteed to be the maker or they will be canceled", "tooltip-slippage": "An estimate of the difference between the current price and the price your trade will be executed at", - "trade-sounds-tooltip": "Play a sound alert for every new trade", + "tooltip-volume-alert": "Volume Alert Settings", "trades": "Trades", - "unsettled": "Unsettled" + "unsettled": "Unsettled", + "volume-alert": "Volume Alert", + "volume-alert-desc": "Play a sound whenever volume exceeds your alert threshold" } \ No newline at end of file diff --git a/utils/constants.ts b/utils/constants.ts index 36bf95da..9e854ad6 100644 --- a/utils/constants.ts +++ b/utils/constants.ts @@ -65,3 +65,5 @@ export const MIN_SOL_BALANCE = 0.001 export const ACCOUNT_ACTION_MODAL_HEIGHT = '506px' export const ACCOUNT_ACTION_MODAL_INNER_HEIGHT = '444px' + +export const TRADE_VOLUME_ALERT_KEY = 'tradeVolumeAlert-0.1' From 541a0114c780cfa9991452caadbeeeaa06db9ebb Mon Sep 17 00:00:00 2001 From: Riordan Panayides Date: Wed, 1 Feb 2023 20:22:09 +0000 Subject: [PATCH 02/27] Add basic perp ohlcv feed --- apis/mngo/datafeed.ts | 245 ++++++++++++++++++++++++++ apis/mngo/helpers.ts | 55 ++++++ apis/mngo/streaming.ts | 80 +++++++++ components/trade/TradingViewChart.tsx | 14 +- 4 files changed, 384 insertions(+), 10 deletions(-) create mode 100644 apis/mngo/datafeed.ts create mode 100644 apis/mngo/helpers.ts create mode 100644 apis/mngo/streaming.ts diff --git a/apis/mngo/datafeed.ts b/apis/mngo/datafeed.ts new file mode 100644 index 00000000..3db70a3e --- /dev/null +++ b/apis/mngo/datafeed.ts @@ -0,0 +1,245 @@ +import { makeApiRequest, parseResolution } from './helpers' +import { subscribeOnStream, unsubscribeFromStream } from './streaming' +import mangoStore from '@store/mangoStore' +import { + DatafeedConfiguration, + LibrarySymbolInfo, + ResolutionString, + SearchSymbolResultItem, +} from '@public/charting_library' + +export const SUPPORTED_RESOLUTIONS = [ + '1', + '3', + '5', + '15', + '30', + '60', + '120', + '240', + '1D', +] as const + +type BaseBar = { + low: number + high: number + open: number + close: number +} + +type KlineBar = BaseBar & { + volume: number + timestamp: number +} + +type TradingViewBar = BaseBar & { + time: number +} + +type Bar = KlineBar & TradingViewBar + +type SymbolInfo = LibrarySymbolInfo & { + address: string +} + +const lastBarsCache = new Map() + +const configurationData = { + supported_resolutions: SUPPORTED_RESOLUTIONS, + intraday_multipliers: [ + '1', + '3', + '5', + '15', + '30', + '45', + '60', + '120', + '240', + '1440', + ], + exchanges: [], +} + +// async function getAllSymbols() { +// const data = await makeApiRequest( +// 'public/tokenlist?sort_by=v24hUSD&sort_type=desc&offset=0&limit=-1' +// ) + +// return data.data.tokens +// } + +export const queryBars = async ( + tokenAddress: string, + resolution: typeof SUPPORTED_RESOLUTIONS[number], + periodParams: { + firstDataRequest: boolean + from: number + to: number + } +): Promise => { + const { from, to } = periodParams + const urlParameters = { + 'perp-market': tokenAddress, + resolution: parseResolution(resolution), + start_datetime: new Date(from * 1000).toISOString(), + end_datetime: new Date(to * 1000).toISOString(), + } + const query = Object.keys(urlParameters) + .map((name: string) => `${name}=${(urlParameters as any)[name]}`) + .join('&') + + const data = await makeApiRequest(`/stats/candles-perp?${query}`) + if (!data || !data.length) { + return [] + } + let bars: Bar[] = [] + for (const bar of data) { + const timestamp = new Date(bar.candle_start).getTime() + if (timestamp >= from * 1000 && timestamp < to * 1000) { + bars = [ + ...bars, + { + time: timestamp, + low: bar.low, + high: bar.high, + open: bar.open, + close: bar.close, + volume: bar.volume, + timestamp, + }, + ] + } + } + return bars +} + +export default { + onReady: (callback: (configuration: DatafeedConfiguration) => void) => { + setTimeout(() => callback(configurationData as any)) + }, + + searchSymbols: async ( + _userInput: string, + _exchange: string, + _symbolType: string, + _onResultReadyCallback: (items: SearchSymbolResultItem[]) => void + ) => { + return + }, + + resolveSymbol: async ( + symbolAddress: string, + onSymbolResolvedCallback: (symbolInfo: SymbolInfo) => void + // _onResolveErrorCallback: any, + // _extension: any + ) => { + let symbolItem: + | { + address: string + type: string + symbol: string + } + | undefined + + if (!symbolItem) { + symbolItem = { + address: symbolAddress, + type: 'pair', + symbol: '', + } + } + const ticker = mangoStore.getState().selectedMarket.name + + const symbolInfo: SymbolInfo = { + address: symbolItem.address, + ticker: symbolItem.address, + name: symbolItem.symbol || symbolItem.address, + description: ticker || symbolItem.address, + type: symbolItem.type, + session: '24x7', + timezone: 'Etc/UTC', + minmov: 1, + pricescale: 100, + has_intraday: true, + has_weekly_and_monthly: false, + supported_resolutions: configurationData.supported_resolutions as any, + intraday_multipliers: configurationData.intraday_multipliers, + volume_precision: 2, + data_status: 'streaming', + full_name: '', + exchange: '', + listed_exchange: '', + format: 'price', + } + + onSymbolResolvedCallback(symbolInfo) + }, + getBars: async ( + symbolInfo: SymbolInfo, + resolution: ResolutionString, + periodParams: { + countBack: number + firstDataRequest: boolean + from: number + to: number + }, + onHistoryCallback: ( + bars: Bar[], + t: { + noData: boolean + } + ) => void, + onErrorCallback: (e: any) => void + ) => { + try { + const { firstDataRequest } = periodParams + const bars = await queryBars( + symbolInfo.address, + resolution as any, + periodParams + ) + if (!bars || bars.length === 0) { + // "noData" should be set if there is no data in the requested period. + onHistoryCallback([], { + noData: true, + }) + return + } + + if (firstDataRequest) { + lastBarsCache.set(symbolInfo.address, { + ...bars[bars.length - 1], + }) + } + onHistoryCallback(bars, { + noData: false, + }) + } catch (error) { + console.warn('[getBars]: Get error', error) + onErrorCallback(error) + } + }, + + subscribeBars: ( + symbolInfo: SymbolInfo, + resolution: string, + onRealtimeCallback: (data: any) => void, + subscriberUID: string, + onResetCacheNeededCallback: () => void + ) => { + subscribeOnStream( + symbolInfo, + resolution, + onRealtimeCallback, + subscriberUID, + onResetCacheNeededCallback, + lastBarsCache.get(symbolInfo.address) + ) + }, + + unsubscribeBars: () => { + console.warn('[unsubscribeBars]') + unsubscribeFromStream() + }, +} diff --git a/apis/mngo/helpers.ts b/apis/mngo/helpers.ts new file mode 100644 index 00000000..f0937786 --- /dev/null +++ b/apis/mngo/helpers.ts @@ -0,0 +1,55 @@ +import { MANGO_DATA_API_URL } from 'utils/constants' + +// Make requests to mngo.cloud API +export async function makeApiRequest(path: string) { + try { + const response = await fetch(`${MANGO_DATA_API_URL}${path}`) + return response.json() + } catch (error: any) { + throw new Error(`mngo.cloud request error: ${error.status}`) + } +} + +const RESOLUTION_MAPPING: Record = { + '1': '1', + '3': '3', + '5': '5', + '15': '15', + '30': '30', + '45': '45', + '60': '60', + '120': '120', + '240': '240', + '1D': '1440', + '1W': '10080', +} + +export function parseResolution(resolution: string) { + if (!resolution || !RESOLUTION_MAPPING[resolution]) + return RESOLUTION_MAPPING[0] + + return RESOLUTION_MAPPING[resolution] +} + +export function getNextBarTime(lastBar: any, resolution = '1D') { + if (!lastBar) return + + const lastCharacter = resolution.slice(-1) + let nextBarTime + + switch (true) { + case lastCharacter === 'W': + nextBarTime = 7 * 24 * 60 * 60 * 1000 + lastBar.time + break + + case lastCharacter === 'D': + nextBarTime = 1 * 24 * 60 * 60 * 1000 + lastBar.time + break + + default: + nextBarTime = 1 * 60 * 1000 + lastBar.time + break + } + + return nextBarTime +} diff --git a/apis/mngo/streaming.ts b/apis/mngo/streaming.ts new file mode 100644 index 00000000..3778cc3a --- /dev/null +++ b/apis/mngo/streaming.ts @@ -0,0 +1,80 @@ +import { parseResolution, getNextBarTime } from './helpers' + +let subscriptionItem: any = {} + +// Create WebSocket connection. +const socket = new WebSocket(`wss://api.mngo.cloud/fills/v1/`) + +// Connection opened +socket.addEventListener('open', (_event) => { + console.log('[socket] Connected') +}) + +// Listen for messages +socket.addEventListener('message', (msg) => { + const data = JSON.parse(msg.data) + + if (!data.event) return console.warn(data) + if (data.event.maker) return + + const currTime = new Date(data.event.timestamp).getTime() + const lastBar = subscriptionItem.lastBar + const resolution = subscriptionItem.resolution + const nextBarTime = getNextBarTime(lastBar, resolution) + const price = data.event.price + const size = data.event.quantity + let bar + + if (currTime >= nextBarTime) { + bar = { + time: nextBarTime, + open: price, + high: price, + low: price, + close: price, + volume: size, + } + } else { + bar = { + ...lastBar, + high: Math.max(lastBar.high, price), + low: Math.min(lastBar.low, price), + close: price, + volume: lastBar.volume + size, + } + } + + subscriptionItem.lastBar = bar + subscriptionItem.callback(bar) +}) + +export function subscribeOnStream( + symbolInfo: any, + resolution: any, + onRealtimeCallback: any, + subscriberUID: any, + onResetCacheNeededCallback: any, + lastBar: any +) { + subscriptionItem = { + resolution, + lastBar, + callback: onRealtimeCallback, + } + + const msg = { + command: 'subscribe', + marketId: 'HwhVGkfsSQ9JSQeQYu2CbkRCLvsh3qRZxG6m4oMVwZpN', + } + + socket.send(JSON.stringify(msg)) +} + +export function unsubscribeFromStream() { + const msg = { + command: 'unsubscribe', + marketId: 'HwhVGkfsSQ9JSQeQYu2CbkRCLvsh3qRZxG6m4oMVwZpN', + } + + socket.send(JSON.stringify(msg)) +} diff --git a/components/trade/TradingViewChart.tsx b/components/trade/TradingViewChart.tsx index fea260c3..e9120911 100644 --- a/components/trade/TradingViewChart.tsx +++ b/components/trade/TradingViewChart.tsx @@ -11,7 +11,8 @@ import { useViewport } from 'hooks/useViewport' import { CHART_DATA_FEED, DEFAULT_MARKET_NAME } from 'utils/constants' import { breakpoints } from 'utils/theme' import { COLORS } from 'styles/colors' -import Datafeed from 'apis/birdeye/datafeed' +import SpotDatafeed from 'apis/birdeye/datafeed' +import PerpDatafeed from 'apis/mngo/datafeed' export interface ChartContainerProps { container: ChartingLibraryWidgetOptions['container'] @@ -134,22 +135,15 @@ const TradingViewChart = () => { useEffect(() => { if (window) { - // const tempBtcDatafeedUrl = 'https://dex-pyth-price-mainnet.zeta.markets/tv/history?symbol=BTC-USDC&resolution=5&from=1674427748&to=1674430748&countback=2' - const tempBtcDatafeedUrl = - 'https://redirect-origin.mangomarkets.workers.dev' - const btcDatafeed = new (window as any).Datafeeds.UDFCompatibleDatafeed( - tempBtcDatafeedUrl - ) - const widgetOptions: ChartingLibraryWidgetOptions = { // debug: true, symbol: spotOrPerp === 'spot' ? '8BnEgHoWFysVcuFFX7QztDmzuH8r5ZFvyP3sYwn1XTh6' - : 'BTC-USDC', + : 'HwhVGkfsSQ9JSQeQYu2CbkRCLvsh3qRZxG6m4oMVwZpN', // BEWARE: no trailing slash is expected in feed URL // tslint:disable-next-line:no-any - datafeed: spotOrPerp === 'spot' ? Datafeed : btcDatafeed, + datafeed: spotOrPerp === 'spot' ? SpotDatafeed : PerpDatafeed, interval: defaultProps.interval as ChartingLibraryWidgetOptions['interval'], container: From 7126f00c445e413717c46845178c87faa96497b6 Mon Sep 17 00:00:00 2001 From: tjs Date: Wed, 1 Feb 2023 18:05:01 -0500 Subject: [PATCH 03/27] add a memo for selected market --- components/trade/TradingViewChart.tsx | 33 +++++++++++++++------------ 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/components/trade/TradingViewChart.tsx b/components/trade/TradingViewChart.tsx index e9120911..00edf00f 100644 --- a/components/trade/TradingViewChart.tsx +++ b/components/trade/TradingViewChart.tsx @@ -94,20 +94,26 @@ const TradingViewChart = () => { } }) + const selectedMarket = useMemo(() => { + const group = mangoStore.getState().group + if (!group || !selectedMarketName) + return '8BnEgHoWFysVcuFFX7QztDmzuH8r5ZFvyP3sYwn1XTh6' + + if (!selectedMarketName.toLowerCase().includes('perp')) { + return group + .getSerum3MarketByName(selectedMarketName) + .serumMarketExternal.toString() + } else { + return group.getPerpMarketByName(selectedMarketName).publicKey.toString() + } + }, [selectedMarketName]) + useEffect(() => { const group = mangoStore.getState().group - if (tvWidgetRef.current && chartReady && selectedMarketName && group) { + if (tvWidgetRef.current && chartReady && selectedMarket && group) { try { - let symbolName - if (!selectedMarketName.toLowerCase().includes('PERP')) { - symbolName = group - .getSerum3MarketByName(selectedMarketName) - .serumMarketExternal.toString() - } else { - symbolName = selectedMarketName - } tvWidgetRef.current.setSymbol( - symbolName, + selectedMarket, tvWidgetRef.current.activeChart().resolution(), () => { return @@ -117,7 +123,7 @@ const TradingViewChart = () => { console.warn('Trading View change symbol error: ', e) } } - }, [selectedMarketName, chartReady]) + }, [selectedMarket, chartReady]) useEffect(() => { if ( @@ -137,10 +143,7 @@ const TradingViewChart = () => { if (window) { const widgetOptions: ChartingLibraryWidgetOptions = { // debug: true, - symbol: - spotOrPerp === 'spot' - ? '8BnEgHoWFysVcuFFX7QztDmzuH8r5ZFvyP3sYwn1XTh6' - : 'HwhVGkfsSQ9JSQeQYu2CbkRCLvsh3qRZxG6m4oMVwZpN', + symbol: selectedMarket, // BEWARE: no trailing slash is expected in feed URL // tslint:disable-next-line:no-any datafeed: spotOrPerp === 'spot' ? SpotDatafeed : PerpDatafeed, From 30ca20aa90f6b657ca81b9244deeea5e507d0387 Mon Sep 17 00:00:00 2001 From: tjs Date: Wed, 1 Feb 2023 21:56:07 -0500 Subject: [PATCH 04/27] show empty candles if no data --- apis/birdeye/datafeed.ts | 1 + apis/mngo/datafeed.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/apis/birdeye/datafeed.ts b/apis/birdeye/datafeed.ts index 7d7ae735..37892a03 100644 --- a/apis/birdeye/datafeed.ts +++ b/apis/birdeye/datafeed.ts @@ -156,6 +156,7 @@ export default { pricescale: 100, has_intraday: true, has_weekly_and_monthly: false, + has_empty_bars: true, supported_resolutions: configurationData.supported_resolutions as any, intraday_multipliers: configurationData.intraday_multipliers, volume_precision: 2, diff --git a/apis/mngo/datafeed.ts b/apis/mngo/datafeed.ts index 3db70a3e..5f5527f5 100644 --- a/apis/mngo/datafeed.ts +++ b/apis/mngo/datafeed.ts @@ -163,6 +163,7 @@ export default { pricescale: 100, has_intraday: true, has_weekly_and_monthly: false, + has_empty_bars: true, supported_resolutions: configurationData.supported_resolutions as any, intraday_multipliers: configurationData.intraday_multipliers, volume_precision: 2, From d67648baddfefe14ed165a87b1c0541f0273d64f Mon Sep 17 00:00:00 2001 From: saml33 Date: Wed, 8 Feb 2023 21:47:31 +1100 Subject: [PATCH 05/27] fix activity translation values --- components/account/ActivityFeedTable.tsx | 4 +--- components/account/HistoryTabs.tsx | 8 ++++---- public/locales/en/activity.json | 4 ++-- public/locales/es/activity.json | 4 ++-- public/locales/ru/activity.json | 4 ++-- public/locales/zh/activity.json | 4 ++-- public/locales/zh_tw/activity.json | 4 ++-- 7 files changed, 15 insertions(+), 17 deletions(-) diff --git a/components/account/ActivityFeedTable.tsx b/components/account/ActivityFeedTable.tsx index 75dcfabd..0cf4d31c 100644 --- a/components/account/ActivityFeedTable.tsx +++ b/components/account/ActivityFeedTable.tsx @@ -209,9 +209,7 @@ const ActivityFeedTable = ({ {t('fee')} - - {t('activity:activity-value')} - + {t('value')} {t('explorer')} diff --git a/components/account/HistoryTabs.tsx b/components/account/HistoryTabs.tsx index c4583184..b9e424d2 100644 --- a/components/account/HistoryTabs.tsx +++ b/components/account/HistoryTabs.tsx @@ -7,11 +7,11 @@ import ActivityFilters from './ActivityFilters' import mangoStore from '@store/mangoStore' import useMangoAccount from 'hooks/useMangoAccount' -const TABS = ['activity:activity', 'activity:swaps', 'activity:trades'] +const TABS = ['activity:activity-feed', 'activity:swaps', 'activity:trades'] const HistoryTabs = () => { const { t } = useTranslation(['common', 'activity']) - const [activeTab, setActiveTab] = useState('activity:activity') + const [activeTab, setActiveTab] = useState('activity:activity-feed') const actions = mangoStore((s) => s.actions) const { mangoAccountAddress } = useMangoAccount() @@ -39,7 +39,7 @@ const HistoryTabs = () => { ))}
- {activeTab === 'activity:activity' ? : null} + {activeTab === 'activity:activity-feed' ? : null}
@@ -48,7 +48,7 @@ const HistoryTabs = () => { const TabContent = ({ activeTab }: { activeTab: string }) => { switch (activeTab) { - case 'activity:activity': + case 'activity:activity-feed': return case 'activity:swaps': return diff --git a/public/locales/en/activity.json b/public/locales/en/activity.json index 86866c93..0efb0541 100644 --- a/public/locales/en/activity.json +++ b/public/locales/en/activity.json @@ -1,7 +1,7 @@ { - "activity": "Activity Feed", + "activity": "Activity", + "activity-feed": "Activity Feed", "activity-type": "Activity Type", - "activity-value": "Activity Value", "advanced-filters": "Advanced Filters", "asset-liquidated": "Asset Liquidated", "asset-returned": "Asset Returned", diff --git a/public/locales/es/activity.json b/public/locales/es/activity.json index 86866c93..0efb0541 100644 --- a/public/locales/es/activity.json +++ b/public/locales/es/activity.json @@ -1,7 +1,7 @@ { - "activity": "Activity Feed", + "activity": "Activity", + "activity-feed": "Activity Feed", "activity-type": "Activity Type", - "activity-value": "Activity Value", "advanced-filters": "Advanced Filters", "asset-liquidated": "Asset Liquidated", "asset-returned": "Asset Returned", diff --git a/public/locales/ru/activity.json b/public/locales/ru/activity.json index 86866c93..0efb0541 100644 --- a/public/locales/ru/activity.json +++ b/public/locales/ru/activity.json @@ -1,7 +1,7 @@ { - "activity": "Activity Feed", + "activity": "Activity", + "activity-feed": "Activity Feed", "activity-type": "Activity Type", - "activity-value": "Activity Value", "advanced-filters": "Advanced Filters", "asset-liquidated": "Asset Liquidated", "asset-returned": "Asset Returned", diff --git a/public/locales/zh/activity.json b/public/locales/zh/activity.json index 86866c93..0efb0541 100644 --- a/public/locales/zh/activity.json +++ b/public/locales/zh/activity.json @@ -1,7 +1,7 @@ { - "activity": "Activity Feed", + "activity": "Activity", + "activity-feed": "Activity Feed", "activity-type": "Activity Type", - "activity-value": "Activity Value", "advanced-filters": "Advanced Filters", "asset-liquidated": "Asset Liquidated", "asset-returned": "Asset Returned", diff --git a/public/locales/zh_tw/activity.json b/public/locales/zh_tw/activity.json index 86866c93..0efb0541 100644 --- a/public/locales/zh_tw/activity.json +++ b/public/locales/zh_tw/activity.json @@ -1,7 +1,7 @@ { - "activity": "Activity Feed", + "activity": "Activity", + "activity-feed": "Activity Feed", "activity-type": "Activity Type", - "activity-value": "Activity Value", "advanced-filters": "Advanced Filters", "asset-liquidated": "Asset Liquidated", "asset-returned": "Asset Returned", From 2d9e73d207be2651ba55a22e616fc2ef8ab39042 Mon Sep 17 00:00:00 2001 From: saml33 Date: Wed, 8 Feb 2023 22:29:03 +1100 Subject: [PATCH 06/27] fix perp position table size click --- components/trade/PerpPositions.tsx | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/components/trade/PerpPositions.tsx b/components/trade/PerpPositions.tsx index 8ec85087..75ad8b12 100644 --- a/components/trade/PerpPositions.tsx +++ b/components/trade/PerpPositions.tsx @@ -29,7 +29,7 @@ const PerpPositions = () => { const { connected } = useWallet() const { mangoAccountAddress } = useMangoAccount() - const handlePositionClick = (positionSize: number) => { + const handlePositionClick = (positionSize: number, market: PerpMarket) => { const tradeForm = mangoStore.getState().tradeForm const set = mangoStore.getState().set @@ -43,15 +43,15 @@ const PerpPositions = () => { ) } const newSide = positionSize > 0 ? 'sell' : 'buy' + const quoteSize = floorToDecimal( + positionSize * price, + getDecimalCount(market.tickSize) + ) set((s) => { s.tradeForm.side = newSide s.tradeForm.baseSize = positionSize.toString() - if (newSide === 'buy') { - s.tradeForm.quoteSize = (positionSize * price).toString() - } else { - s.tradeForm.quoteSize = (positionSize / price).toString() - } + s.tradeForm.quoteSize = quoteSize.toString() }) } @@ -122,7 +122,9 @@ const PerpPositions = () => {

{isSelectedMarket ? ( handlePositionClick(floorBasePosition)} + onClick={() => + handlePositionClick(floorBasePosition, market) + } > Date: Wed, 8 Feb 2023 12:34:46 +0100 Subject: [PATCH 07/27] update kline api key --- apis/birdeye/helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apis/birdeye/helpers.ts b/apis/birdeye/helpers.ts index 425964e7..24c2ff5c 100644 --- a/apis/birdeye/helpers.ts +++ b/apis/birdeye/helpers.ts @@ -1,6 +1,6 @@ export const NEXT_PUBLIC_BIRDEYE_API_KEY = process.env.NEXT_PUBLIC_BIRDEYE_API_KEY || - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2Njc1NTI4MzV9.FpbBT3M6GN_TKSJ8CarGeOMU5U7ZUvgZOIy8789m1bk' + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2NzM0NTE4MDF9.KTEqB1hrmZTMzk19rZNx9aesh2bIHj98Cb8sg5Ikz-Y' export const API_URL = 'https://public-api.birdeye.so/' From 5388035d336aa93a24a75aade7cb147bfd08511c Mon Sep 17 00:00:00 2001 From: saml33 Date: Thu, 9 Feb 2023 11:31:53 +1100 Subject: [PATCH 08/27] handle swap reduce only --- components/swap/SwapForm.tsx | 28 +++++++++++++++--- components/swap/SwapFormTokenList.tsx | 25 +++++++++++++---- components/swap/useTokenMax.tsx | 39 ++++++++++++++++---------- components/trade/AdvancedTradeForm.tsx | 2 +- public/locales/en/swap.json | 2 ++ public/locales/en/trade.json | 1 + public/locales/es/swap.json | 2 ++ public/locales/es/trade.json | 1 + public/locales/ru/swap.json | 2 ++ public/locales/ru/trade.json | 1 + public/locales/zh/swap.json | 2 ++ public/locales/zh_tw/swap.json | 2 ++ 12 files changed, 82 insertions(+), 25 deletions(-) diff --git a/components/swap/SwapForm.tsx b/components/swap/SwapForm.tsx index 8470972f..9900fdad 100644 --- a/components/swap/SwapForm.tsx +++ b/components/swap/SwapForm.tsx @@ -63,7 +63,7 @@ const SwapForm = () => { //initial state is undefined null is returned on error const [selectedRoute, setSelectedRoute] = useState() const [animateSwitchArrow, setAnimateSwitchArrow] = useState(0) - const [showTokenSelect, setShowTokenSelect] = useState('') + const [showTokenSelect, setShowTokenSelect] = useState(undefined) const [showSettings, setShowSettings] = useState(false) const [showConfirm, setShowConfirm] = useState(false) const { group } = useMangoGroup() @@ -193,7 +193,7 @@ const SwapForm = () => { s.swap.inputBank = bank }) } - setShowTokenSelect('') + setShowTokenSelect(undefined) }, []) const handleTokenOutSelect = useCallback((mintAddress: string) => { @@ -204,7 +204,7 @@ const SwapForm = () => { s.swap.outputBank = bank }) } - setShowTokenSelect('') + setShowTokenSelect(undefined) }, []) const handleSwitchTokens = useCallback(() => { @@ -299,7 +299,7 @@ const SwapForm = () => { show={!!showTokenSelect} > setShowTokenSelect('')} + onClose={() => setShowTokenSelect(undefined)} onTokenSelect={ showTokenSelect === 'input' ? handleTokenInSelect @@ -451,6 +451,26 @@ const SwapForm = () => { {group && inputBank ? ( ) : null} + {inputBank && inputBank.reduceOnly ? ( +

+ +
+ ) : null} + {outputBank && outputBank.reduceOnly ? ( +
+ +
+ ) : null}
diff --git a/components/swap/SwapFormTokenList.tsx b/components/swap/SwapFormTokenList.tsx index 94383112..5a672ee8 100644 --- a/components/swap/SwapFormTokenList.tsx +++ b/components/swap/SwapFormTokenList.tsx @@ -47,10 +47,17 @@ const TokenItem = ({ token: Token onSubmit: (x: string) => void useMargin: boolean - type: string + type: 'input' | 'output' | undefined }) => { + const { t } = useTranslation('trade') const { address, symbol, logoURI, name } = token + const bank = useMemo(() => { + const group = mangoStore.getState().group + if (!group) return + return group.getFirstBankByMint(new PublicKey(address)) + }, [address]) + return (
{type === 'input' && @@ -103,7 +118,7 @@ const SwapFormTokenList = ({ }: { onClose: () => void onTokenSelect: (x: string) => void - type: string + type: 'input' | 'output' | undefined useMargin: boolean }) => { const { t } = useTranslation(['common', 'swap']) diff --git a/components/swap/useTokenMax.tsx b/components/swap/useTokenMax.tsx index 5b676433..5aed2e09 100644 --- a/components/swap/useTokenMax.tsx +++ b/components/swap/useTokenMax.tsx @@ -49,19 +49,29 @@ export const getTokenInMax = ( mangoAccount.getTokenBalanceUi(inputBank) ) - const maxAmountWithoutMargin = inputTokenBalance.gt(0) - ? inputTokenBalance - : new Decimal(0) - - const maxUiAmountWithBorrow = floorToDecimal( - mangoAccount.getMaxSourceUiForTokenSwap( - group, - inputBank.mint, - outputBank.mint - ), - inputBank.mintDecimals + const outputTokenBalance = new Decimal( + mangoAccount.getTokenBalanceUi(outputBank) ) + const maxAmountWithoutMargin = + (inputTokenBalance.gt(0) && !outputBank.reduceOnly) || + (outputBank.reduceOnly && outputTokenBalance.lt(0)) + ? inputTokenBalance + : new Decimal(0) + + const maxUiAmountWithBorrow = + outputBank.reduceOnly && + (outputTokenBalance.gt(0) || outputTokenBalance.eq(0)) + ? new Decimal(0) + : floorToDecimal( + mangoAccount.getMaxSourceUiForTokenSwap( + group, + inputBank.mint, + outputBank.mint + ), + inputBank.mintDecimals + ) + const inputBankVaultBalance = floorToDecimal( group .getTokenVaultBalanceByMintUi(inputBank.mint) @@ -81,10 +91,9 @@ export const getTokenInMax = ( maxUiAmountWithBorrow ) - const maxAmountWithBorrow = Decimal.min( - maxUiAmountWithBorrow, - inputBankVaultBalance - ) + const maxAmountWithBorrow = inputBank.reduceOnly + ? Decimal.min(maxAmountWithoutMargin, inputBankVaultBalance) + : Decimal.min(maxUiAmountWithBorrow, inputBankVaultBalance) return { amount: maxAmount, diff --git a/components/trade/AdvancedTradeForm.tsx b/components/trade/AdvancedTradeForm.tsx index 2aee0a38..c8670819 100644 --- a/components/trade/AdvancedTradeForm.tsx +++ b/components/trade/AdvancedTradeForm.tsx @@ -561,7 +561,7 @@ const AdvancedTradeForm = () => { checked={tradeForm.reduceOnly} onChange={(e) => handleReduceOnlyChange(e.target.checked)} > - Reduce Only + {t('trade:reduce-only')}
diff --git a/public/locales/en/swap.json b/public/locales/en/swap.json index a918f3e2..4cf427ff 100644 --- a/public/locales/en/swap.json +++ b/public/locales/en/swap.json @@ -5,12 +5,14 @@ "fees-paid-to": "Fees Paid to {{route}}", "health-impact": "Health Impact", "hide-fees": "Hide Fees", + "input-reduce-only-warning": "{{symbol}} is in reduce only mode. You can swap your balance to another token", "insufficient-balance": "Insufficient {{symbol}} Balance", "insufficient-collateral": "Insufficient Collateral", "max-slippage": "Max Slippage", "maximum-cost": "Maximum Cost", "minimum-received": "Minimum Received", "no-history": "No swap history", + "output-reduce-only-warning": "{{symbol}} is in reduce only mode. You can swap to close borrows only", "paid": "Paid", "pay": "You Pay", "preset": "Preset", diff --git a/public/locales/en/trade.json b/public/locales/en/trade.json index 6a1e53a6..08162e4b 100644 --- a/public/locales/en/trade.json +++ b/public/locales/en/trade.json @@ -38,6 +38,7 @@ "post": "Post", "price-expect": "The price you receive may be worse than you expect and full execution is not guaranteed. Max slippage is 2.5% for your safety. The part of your position with slippage beyond 2.5% will not be closed.", "quote": "Quote", + "reduce-only": "Reduce Only", "sells": "Sells", "settle-funds": "Settle Funds", "settle-funds-error": "Failed to settle funds", diff --git a/public/locales/es/swap.json b/public/locales/es/swap.json index a918f3e2..4cf427ff 100644 --- a/public/locales/es/swap.json +++ b/public/locales/es/swap.json @@ -5,12 +5,14 @@ "fees-paid-to": "Fees Paid to {{route}}", "health-impact": "Health Impact", "hide-fees": "Hide Fees", + "input-reduce-only-warning": "{{symbol}} is in reduce only mode. You can swap your balance to another token", "insufficient-balance": "Insufficient {{symbol}} Balance", "insufficient-collateral": "Insufficient Collateral", "max-slippage": "Max Slippage", "maximum-cost": "Maximum Cost", "minimum-received": "Minimum Received", "no-history": "No swap history", + "output-reduce-only-warning": "{{symbol}} is in reduce only mode. You can swap to close borrows only", "paid": "Paid", "pay": "You Pay", "preset": "Preset", diff --git a/public/locales/es/trade.json b/public/locales/es/trade.json index 6a1e53a6..08162e4b 100644 --- a/public/locales/es/trade.json +++ b/public/locales/es/trade.json @@ -38,6 +38,7 @@ "post": "Post", "price-expect": "The price you receive may be worse than you expect and full execution is not guaranteed. Max slippage is 2.5% for your safety. The part of your position with slippage beyond 2.5% will not be closed.", "quote": "Quote", + "reduce-only": "Reduce Only", "sells": "Sells", "settle-funds": "Settle Funds", "settle-funds-error": "Failed to settle funds", diff --git a/public/locales/ru/swap.json b/public/locales/ru/swap.json index a918f3e2..4cf427ff 100644 --- a/public/locales/ru/swap.json +++ b/public/locales/ru/swap.json @@ -5,12 +5,14 @@ "fees-paid-to": "Fees Paid to {{route}}", "health-impact": "Health Impact", "hide-fees": "Hide Fees", + "input-reduce-only-warning": "{{symbol}} is in reduce only mode. You can swap your balance to another token", "insufficient-balance": "Insufficient {{symbol}} Balance", "insufficient-collateral": "Insufficient Collateral", "max-slippage": "Max Slippage", "maximum-cost": "Maximum Cost", "minimum-received": "Minimum Received", "no-history": "No swap history", + "output-reduce-only-warning": "{{symbol}} is in reduce only mode. You can swap to close borrows only", "paid": "Paid", "pay": "You Pay", "preset": "Preset", diff --git a/public/locales/ru/trade.json b/public/locales/ru/trade.json index 6a1e53a6..08162e4b 100644 --- a/public/locales/ru/trade.json +++ b/public/locales/ru/trade.json @@ -38,6 +38,7 @@ "post": "Post", "price-expect": "The price you receive may be worse than you expect and full execution is not guaranteed. Max slippage is 2.5% for your safety. The part of your position with slippage beyond 2.5% will not be closed.", "quote": "Quote", + "reduce-only": "Reduce Only", "sells": "Sells", "settle-funds": "Settle Funds", "settle-funds-error": "Failed to settle funds", diff --git a/public/locales/zh/swap.json b/public/locales/zh/swap.json index a918f3e2..4cf427ff 100644 --- a/public/locales/zh/swap.json +++ b/public/locales/zh/swap.json @@ -5,12 +5,14 @@ "fees-paid-to": "Fees Paid to {{route}}", "health-impact": "Health Impact", "hide-fees": "Hide Fees", + "input-reduce-only-warning": "{{symbol}} is in reduce only mode. You can swap your balance to another token", "insufficient-balance": "Insufficient {{symbol}} Balance", "insufficient-collateral": "Insufficient Collateral", "max-slippage": "Max Slippage", "maximum-cost": "Maximum Cost", "minimum-received": "Minimum Received", "no-history": "No swap history", + "output-reduce-only-warning": "{{symbol}} is in reduce only mode. You can swap to close borrows only", "paid": "Paid", "pay": "You Pay", "preset": "Preset", diff --git a/public/locales/zh_tw/swap.json b/public/locales/zh_tw/swap.json index a918f3e2..4cf427ff 100644 --- a/public/locales/zh_tw/swap.json +++ b/public/locales/zh_tw/swap.json @@ -5,12 +5,14 @@ "fees-paid-to": "Fees Paid to {{route}}", "health-impact": "Health Impact", "hide-fees": "Hide Fees", + "input-reduce-only-warning": "{{symbol}} is in reduce only mode. You can swap your balance to another token", "insufficient-balance": "Insufficient {{symbol}} Balance", "insufficient-collateral": "Insufficient Collateral", "max-slippage": "Max Slippage", "maximum-cost": "Maximum Cost", "minimum-received": "Minimum Received", "no-history": "No swap history", + "output-reduce-only-warning": "{{symbol}} is in reduce only mode. You can swap to close borrows only", "paid": "Paid", "pay": "You Pay", "preset": "Preset", From bb35da14342f5a88cf9cbf23747fdc1c8811dc51 Mon Sep 17 00:00:00 2001 From: saml33 Date: Thu, 9 Feb 2023 12:23:21 +1100 Subject: [PATCH 09/27] fix perp positions count --- hooks/useOpenPerpPositions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hooks/useOpenPerpPositions.ts b/hooks/useOpenPerpPositions.ts index 84d0f54b..ce7485c9 100644 --- a/hooks/useOpenPerpPositions.ts +++ b/hooks/useOpenPerpPositions.ts @@ -11,7 +11,7 @@ const useOpenPerpPositions = () => { return Object.values(perpPositions).filter((p) => p.basePositionLots.toNumber() ) - }, [mangoAccountAddress]) + }, [mangoAccountAddress, perpPositions]) return openPositions } From d838d39a570d94ad64080eeefb319fd2cab96ea2 Mon Sep 17 00:00:00 2001 From: saml33 Date: Thu, 9 Feb 2023 12:37:30 +1100 Subject: [PATCH 10/27] fix perp entry price rounding --- components/trade/PerpPositions.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/trade/PerpPositions.tsx b/components/trade/PerpPositions.tsx index 75ad8b12..167065be 100644 --- a/components/trade/PerpPositions.tsx +++ b/components/trade/PerpPositions.tsx @@ -141,14 +141,14 @@ const PerpPositions = () => { From 1ed50b9616328f459a6d543e63c5a959a0df209a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Brzezi=C5=84ski?= Date: Thu, 9 Feb 2023 02:40:34 +0100 Subject: [PATCH 11/27] Feature/kline websockets (#84) * web sockets * websockets kline chart * disconnect socket kline * fix * fixes --- apis/birdeye/datafeed.ts | 1 + apis/birdeye/helpers.ts | 2 + apis/birdeye/streaming.ts | 13 +- components/trade/TradingViewChartKline.tsx | 559 ++++++++++++--------- 4 files changed, 320 insertions(+), 255 deletions(-) diff --git a/apis/birdeye/datafeed.ts b/apis/birdeye/datafeed.ts index 7d7ae735..3aca0628 100644 --- a/apis/birdeye/datafeed.ts +++ b/apis/birdeye/datafeed.ts @@ -86,6 +86,7 @@ export const queryBars = async ( if (!data.success || data.data.items.length === 0) { return [] } + let bars: Bar[] = [] for (const bar of data.data.items) { if (bar.unixTime >= from && bar.unixTime < to) { diff --git a/apis/birdeye/helpers.ts b/apis/birdeye/helpers.ts index 24c2ff5c..0c08ace6 100644 --- a/apis/birdeye/helpers.ts +++ b/apis/birdeye/helpers.ts @@ -4,6 +4,8 @@ export const NEXT_PUBLIC_BIRDEYE_API_KEY = export const API_URL = 'https://public-api.birdeye.so/' +export const socketUrl = `wss://public-api.birdeye.so/socket?x-api-key=${NEXT_PUBLIC_BIRDEYE_API_KEY}` + // Make requests to CryptoCompare API export async function makeApiRequest(path: string) { try { diff --git a/apis/birdeye/streaming.ts b/apis/birdeye/streaming.ts index 7a786397..5b0366aa 100644 --- a/apis/birdeye/streaming.ts +++ b/apis/birdeye/streaming.ts @@ -1,16 +1,9 @@ -import { - parseResolution, - getNextBarTime, - NEXT_PUBLIC_BIRDEYE_API_KEY, -} from './helpers' +import { parseResolution, getNextBarTime, socketUrl } from './helpers' let subscriptionItem: any = {} // Create WebSocket connection. -const socket = new WebSocket( - `wss://public-api.birdeye.so/socket?x-api-key=${NEXT_PUBLIC_BIRDEYE_API_KEY}`, - 'echo-protocol' -) +const socket = new WebSocket(socketUrl, 'echo-protocol') // Connection opened socket.addEventListener('open', (_event) => { @@ -20,7 +13,6 @@ socket.addEventListener('open', (_event) => { // Listen for messages socket.addEventListener('message', (msg) => { const data = JSON.parse(msg.data) - if (data.type !== 'PRICE_DATA') return console.warn(data) const currTime = data.data.unixTime * 1000 @@ -75,7 +67,6 @@ export function subscribeOnStream( currency: symbolInfo.type || 'usd', }, } - socket.send(JSON.stringify(msg)) } diff --git a/components/trade/TradingViewChartKline.tsx b/components/trade/TradingViewChartKline.tsx index 22045b0d..66c301a8 100644 --- a/components/trade/TradingViewChartKline.tsx +++ b/components/trade/TradingViewChartKline.tsx @@ -1,6 +1,6 @@ -import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react' +import { Dispatch, SetStateAction, useEffect, useState } from 'react' import mangoStore from '@store/mangoStore' -import klinecharts, { init, dispose } from 'klinecharts' +import klinecharts, { init, dispose, KLineData } from 'klinecharts' import { useViewport } from 'hooks/useViewport' import usePrevious from '@components/shared/usePrevious' import Modal from '@components/shared/Modal' @@ -23,8 +23,11 @@ import { COLORS } from 'styles/colors' import { IconButton } from '@components/shared/Button' import { ArrowsPointingOutIcon, XMarkIcon } from '@heroicons/react/20/solid' import { queryBars } from 'apis/birdeye/datafeed' - -const UPDATE_INTERVAL = 10000 +import { + getNextBarTime, + parseResolution, + socketUrl, +} from 'apis/birdeye/helpers' type Props = { setIsFullView?: Dispatch> @@ -32,10 +35,234 @@ type Props = { } const TradingViewChartKline = ({ setIsFullView, isFullView }: Props) => { - const { width } = useViewport() const { theme } = useTheme() + const styles = { + grid: { + show: false, + }, + candle: { + bar: { + upColor: COLORS.UP[theme], + downColor: COLORS.DOWN[theme], + }, + tooltip: { + labels: ['', 'O:', 'C:', 'H:', 'L:', 'V:'], + text: { + size: 12, + family: 'TT Mono', + weight: 'normal', + color: COLORS.FGD4[theme], + marginLeft: 8, + marginTop: 6, + marginRight: 8, + marginBottom: 0, + }, + }, + priceMark: { + show: true, + high: { + show: true, + color: COLORS.FGD4[theme], + textMargin: 5, + textSize: 10, + textFamily: 'TT Mono', + textWeight: 'normal', + }, + low: { + show: true, + color: COLORS.FGD4[theme], + textMargin: 5, + textSize: 10, + textFamily: 'TT Mono', + textWeight: 'normal', + }, + last: { + show: true, + upColor: COLORS.BKG4[theme], + downColor: COLORS.BKG4[theme], + noChangeColor: COLORS.BKG4[theme], + line: { + show: true, + // 'solid'|'dash' + style: 'dash', + dashValue: [4, 4], + size: 1, + }, + text: { + show: true, + size: 10, + paddingLeft: 2, + paddingTop: 2, + paddingRight: 2, + paddingBottom: 2, + color: '#FFFFFF', + family: 'TT Mono', + weight: 'normal', + borderRadius: 2, + }, + }, + }, + }, + xAxis: { + axisLine: { + show: true, + color: COLORS.BKG4[theme], + size: 1, + }, + tickLine: { + show: true, + size: 1, + length: 3, + color: COLORS.BKG4[theme], + }, + tickText: { + show: true, + color: COLORS.FGD4[theme], + family: 'TT Mono', + weight: 'normal', + size: 10, + }, + }, + yAxis: { + axisLine: { + show: true, + color: COLORS.BKG4[theme], + size: 1, + }, + tickLine: { + show: true, + size: 1, + length: 3, + color: COLORS.BKG4[theme], + }, + tickText: { + show: true, + color: COLORS.FGD4[theme], + family: 'TT Mono', + weight: 'normal', + size: 10, + }, + }, + crosshair: { + show: true, + horizontal: { + show: true, + line: { + show: true, + style: 'dash', + dashValue: [4, 2], + size: 1, + color: COLORS.FGD4[theme], + }, + text: { + show: true, + color: '#FFFFFF', + size: 10, + family: 'TT Mono', + weight: 'normal', + paddingLeft: 2, + paddingRight: 2, + paddingTop: 2, + paddingBottom: 2, + borderSize: 1, + borderColor: COLORS.FGD4[theme], + borderRadius: 2, + backgroundColor: COLORS.FGD4[theme], + }, + }, + vertical: { + show: true, + line: { + show: true, + style: 'dash', + dashValue: [4, 2], + size: 1, + color: COLORS.FGD4[theme], + }, + text: { + show: true, + color: '#FFFFFF', + size: 10, + family: 'TT Mono', + weight: 'normal', + paddingLeft: 2, + paddingRight: 2, + paddingTop: 2, + paddingBottom: 2, + borderSize: 1, + borderColor: COLORS.FGD4[theme], + borderRadius: 2, + backgroundColor: COLORS.FGD4[theme], + }, + }, + }, + technicalIndicator: { + margin: { + top: 0.2, + bottom: 0.1, + }, + bar: { + upColor: COLORS.UP[theme], + downColor: COLORS.DOWN[theme], + noChangeColor: '#888888', + }, + line: { + size: 1, + colors: ['#FF9600', '#9D65C9', '#2196F3', '#E11D74', '#01C5C4'], + }, + circle: { + upColor: '#26A69A', + downColor: '#EF5350', + noChangeColor: '#888888', + }, + lastValueMark: { + show: false, + text: { + show: false, + color: '#ffffff', + size: 12, + family: 'Helvetica Neue', + weight: 'normal', + paddingLeft: 3, + paddingTop: 2, + paddingRight: 3, + paddingBottom: 2, + borderRadius: 2, + }, + }, + tooltip: { + // 'always' | 'follow_cross' | 'none' + showRule: 'always', + // 'standard' | 'rect' + showType: 'standard', + showName: true, + showParams: true, + defaultValue: 'n/a', + text: { + size: 12, + family: 'TT Mono', + weight: 'normal', + color: COLORS.FGD4[theme], + marginTop: 6, + marginRight: 8, + marginBottom: 0, + marginLeft: 8, + }, + }, + }, + separator: { + size: 2, + color: COLORS.BKG4[theme], + }, + } + const socket = new WebSocket(socketUrl, 'echo-protocol') + const unsub_msg = { + type: 'UNSUBSCRIBE_PRICE', + } + const { width } = useViewport() const prevWidth = usePrevious(width) const selectedMarket = mangoStore((s) => s.selectedMarket.current) + const [socketConnected, setSocketConnected] = useState(false) const selectedMarketName = selectedMarket?.name const [isTechnicalModalOpen, setIsTechnicalModalOpen] = useState(false) const [mainTechnicalIndicators, setMainTechnicalIndicators] = useState< @@ -50,13 +277,17 @@ const TradingViewChartKline = ({ setIsFullView, isFullView }: Props) => { const [chart, setChart] = useState(null) const previousChart = usePrevious(chart) const [baseChartQuery, setQuery] = useState(null) - const clearTimerRef = useRef(null) - const fetchData = async (baseQuery: BASE_CHART_QUERY, from: number) => { + const fetchData = async ( + baseQuery: BASE_CHART_QUERY, + from: number, + to?: number + ) => { try { setIsLoading(true) const query: CHART_QUERY = { ...baseQuery, time_from: from, + time_to: to ? to : baseQuery.time_to, } const response = await queryBars(query.address, query.type, { firstDataRequest: false, @@ -82,24 +313,65 @@ const TradingViewChartKline = ({ setIsFullView, isFullView }: Props) => { } //update data every 10 secs - function updateData( + function setupSocket( kLineChart: klinecharts.Chart, baseQuery: BASE_CHART_QUERY ) { - if (clearTimerRef.current) { - clearInterval(clearTimerRef.current) - } - clearTimerRef.current = setTimeout(async () => { - if (kLineChart) { - const from = baseQuery.time_to - resolution.seconds - const newData = (await fetchData(baseQuery!, from))[0] - if (newData) { - newData.timestamp += UPDATE_INTERVAL - kLineChart.updateData(newData) - updateData(kLineChart, baseQuery) + // Connection opened + socket.addEventListener('open', (_event) => { + console.log('[socket] Kline Connected') + }) + socket.addEventListener('message', (msg) => { + const data = JSON.parse(msg.data) + if (data.type === 'WELLCOME') { + setSocketConnected(true) + socket.send(JSON.stringify(unsub_msg)) + const msg = { + type: 'SUBSCRIBE_PRICE', + data: { + chartType: parseResolution(baseQuery.type), + address: baseQuery.address, + currency: 'pair', + }, } + socket.send(JSON.stringify(msg)) } - }, UPDATE_INTERVAL) + if (data.type === 'PRICE_DATA') { + const dataList = kLineChart.getDataList() + const lastItem = dataList[dataList.length - 1] + const currTime = data.data.unixTime * 1000 + if (!dataList.length) { + return + } + const lastBar: KLineData & { time: number } = { + ...lastItem, + time: lastItem.timestamp, + } + const resolution = parseResolution(baseQuery.type) + const nextBarTime = getNextBarTime(lastBar, resolution) + let bar: KLineData + + if (currTime >= nextBarTime) { + bar = { + timestamp: nextBarTime, + open: data.data.o, + high: data.data.h, + low: data.data.l, + close: data.data.c, + volume: data.data.v, + } + } else { + bar = { + ...lastBar, + high: Math.max(lastBar.high, data.data.h), + low: Math.min(lastBar.low, data.data.l), + close: data.data.c, + volume: data.data.v, + } + } + kLineChart.updateData(bar) + } + }) } const fetchFreshData = async (daysToSubtractFromToday: number) => { const from = @@ -108,7 +380,7 @@ const TradingViewChartKline = ({ setIsFullView, isFullView }: Props) => { if (chart) { chart.applyNewData(data) //after we fetch fresh data start to update data every x seconds - updateData(chart, baseChartQuery!) + setupSocket(chart, baseChartQuery!) } } @@ -126,15 +398,26 @@ const TradingViewChartKline = ({ setIsFullView, isFullView }: Props) => { //when base query change we refetch with fresh data useEffect(() => { if (chart && baseChartQuery) { - fetchFreshData(14) + //becuase bird eye send onlu 1k records at one time + //we query for lower amounts of days at the start + const halfDayThreshold = ['1', '3'] + const twoDaysThreshold = ['5', '15', '30'] + const daysToSub = halfDayThreshold.includes(baseChartQuery.type) + ? 0.5 + : twoDaysThreshold.includes(baseChartQuery.type) + ? 2 + : 5 + fetchFreshData(daysToSub) //add callback to fetch more data when zoom out - chart.loadMore(() => { + chart.loadMore(async (timestamp: number) => { try { - fetchFreshData(365) + const unixTime = timestamp / 1000 + const from = unixTime - ONE_DAY_SECONDS * daysToSub + const data = await fetchData(baseChartQuery!, from, unixTime) + chart.applyMoreData(data) } catch (e) { console.error('Error fetching new data') } - chart.loadMore(() => null) }) } }, [baseChartQuery]) @@ -175,230 +458,18 @@ const TradingViewChartKline = ({ setIsFullView, isFullView }: Props) => { useEffect(() => { const initKline = async () => { const kLineChart = init('update-k-line') - kLineChart.setStyleOptions({ - grid: { - show: false, - }, - candle: { - bar: { - upColor: COLORS.UP[theme], - downColor: COLORS.DOWN[theme], - }, - tooltip: { - labels: ['', 'O:', 'C:', 'H:', 'L:', 'V:'], - text: { - size: 12, - family: 'TT Mono', - weight: 'normal', - color: COLORS.FGD4[theme], - marginLeft: 8, - marginTop: 6, - marginRight: 8, - marginBottom: 0, - }, - }, - priceMark: { - show: true, - high: { - show: true, - color: COLORS.FGD4[theme], - textMargin: 5, - textSize: 10, - textFamily: 'TT Mono', - textWeight: 'normal', - }, - low: { - show: true, - color: COLORS.FGD4[theme], - textMargin: 5, - textSize: 10, - textFamily: 'TT Mono', - textWeight: 'normal', - }, - last: { - show: true, - upColor: COLORS.BKG4[theme], - downColor: COLORS.BKG4[theme], - noChangeColor: COLORS.BKG4[theme], - line: { - show: true, - // 'solid'|'dash' - style: 'dash', - dashValue: [4, 4], - size: 1, - }, - text: { - show: true, - size: 10, - paddingLeft: 2, - paddingTop: 2, - paddingRight: 2, - paddingBottom: 2, - color: '#FFFFFF', - family: 'TT Mono', - weight: 'normal', - borderRadius: 2, - }, - }, - }, - }, - xAxis: { - axisLine: { - show: true, - color: COLORS.BKG4[theme], - size: 1, - }, - tickLine: { - show: true, - size: 1, - length: 3, - color: COLORS.BKG4[theme], - }, - tickText: { - show: true, - color: COLORS.FGD4[theme], - family: 'TT Mono', - weight: 'normal', - size: 10, - }, - }, - yAxis: { - axisLine: { - show: true, - color: COLORS.BKG4[theme], - size: 1, - }, - tickLine: { - show: true, - size: 1, - length: 3, - color: COLORS.BKG4[theme], - }, - tickText: { - show: true, - color: COLORS.FGD4[theme], - family: 'TT Mono', - weight: 'normal', - size: 10, - }, - }, - crosshair: { - show: true, - horizontal: { - show: true, - line: { - show: true, - style: 'dash', - dashValue: [4, 2], - size: 1, - color: COLORS.FGD4[theme], - }, - text: { - show: true, - color: '#FFFFFF', - size: 10, - family: 'TT Mono', - weight: 'normal', - paddingLeft: 2, - paddingRight: 2, - paddingTop: 2, - paddingBottom: 2, - borderSize: 1, - borderColor: COLORS.FGD4[theme], - borderRadius: 2, - backgroundColor: COLORS.FGD4[theme], - }, - }, - vertical: { - show: true, - line: { - show: true, - style: 'dash', - dashValue: [4, 2], - size: 1, - color: COLORS.FGD4[theme], - }, - text: { - show: true, - color: '#FFFFFF', - size: 10, - family: 'TT Mono', - weight: 'normal', - paddingLeft: 2, - paddingRight: 2, - paddingTop: 2, - paddingBottom: 2, - borderSize: 1, - borderColor: COLORS.FGD4[theme], - borderRadius: 2, - backgroundColor: COLORS.FGD4[theme], - }, - }, - }, - technicalIndicator: { - margin: { - top: 0.2, - bottom: 0.1, - }, - bar: { - upColor: COLORS.UP[theme], - downColor: COLORS.DOWN[theme], - noChangeColor: '#888888', - }, - line: { - size: 1, - colors: ['#FF9600', '#9D65C9', '#2196F3', '#E11D74', '#01C5C4'], - }, - circle: { - upColor: '#26A69A', - downColor: '#EF5350', - noChangeColor: '#888888', - }, - lastValueMark: { - show: false, - text: { - show: false, - color: '#ffffff', - size: 12, - family: 'Helvetica Neue', - weight: 'normal', - paddingLeft: 3, - paddingTop: 2, - paddingRight: 3, - paddingBottom: 2, - borderRadius: 2, - }, - }, - tooltip: { - // 'always' | 'follow_cross' | 'none' - showRule: 'always', - // 'standard' | 'rect' - showType: 'standard', - showName: true, - showParams: true, - defaultValue: 'n/a', - text: { - size: 12, - family: 'TT Mono', - weight: 'normal', - color: COLORS.FGD4[theme], - marginTop: 6, - marginRight: 8, - marginBottom: 0, - marginLeft: 8, - }, - }, - }, - separator: { - size: 2, - color: COLORS.BKG4[theme], - }, - }) + kLineChart.setStyleOptions({ ...styles }) setChart(kLineChart) } initKline() + return () => { dispose('update-k-line') + if (socketConnected) { + console.log('[socket] kline disconnected') + socket.send(JSON.stringify(unsub_msg)) + socket.close() + } } }, []) From 8fa244a09b9078de54800f203c93de650b405056 Mon Sep 17 00:00:00 2001 From: saml33 Date: Thu, 9 Feb 2023 13:01:31 +1100 Subject: [PATCH 12/27] allow positions and trade history tables to scroll horizontally --- components/trade/PerpPositions.tsx | 202 ++++++++++++++--------------- components/trade/TradeHistory.tsx | 164 +++++++++++------------ 2 files changed, 184 insertions(+), 182 deletions(-) diff --git a/components/trade/PerpPositions.tsx b/components/trade/PerpPositions.tsx index 167065be..da01d0c0 100644 --- a/components/trade/PerpPositions.tsx +++ b/components/trade/PerpPositions.tsx @@ -72,114 +72,114 @@ const PerpPositions = () => { ) return mangoAccountAddress && openPerpPositions.length ? ( -
- - - - - - - - - - - - - {openPerpPositions.map((position) => { - const market = group.getPerpMarketByMarketIndex( - position.marketIndex - ) - const basePosition = position.getBasePositionUi(market) - const floorBasePosition = floorToDecimal( - basePosition, - getDecimalCount(market.minOrderSize) - ).toNumber() - const isSelectedMarket = - selectedMarket instanceof PerpMarket && - selectedMarket.perpMarketIndex === position.marketIndex + <> +
+
{t('market')}{t('trade:side')}{t('trade:size')}{t('trade:notional')}{t('trade:entry-price')}{`${t('trade:unsettled')} ${t( - 'pnl' - )}`}{t('pnl')} - -
+ + + + + + + + + + + + {openPerpPositions.map((position) => { + const market = group.getPerpMarketByMarketIndex( + position.marketIndex + ) + const basePosition = position.getBasePositionUi(market) + const floorBasePosition = floorToDecimal( + basePosition, + getDecimalCount(market.minOrderSize) + ).toNumber() + const isSelectedMarket = + selectedMarket instanceof PerpMarket && + selectedMarket.perpMarketIndex === position.marketIndex - if (!basePosition) return null + if (!basePosition) return null - const unsettledPnl = position.getUnsettledPnlUi(group, market) - const cummulativePnl = position.cumulativePnlOverPositionLifetimeUi( - group, - market - ) + const unsettledPnl = position.getUnsettledPnlUi(group, market) + const cummulativePnl = + position.cumulativePnlOverPositionLifetimeUi(group, market) - return ( - - - - + + - - - - - + + + + - - ) - })} - -
{t('market')}{t('trade:side')}{t('trade:size')}{t('trade:notional')}{t('trade:entry-price')}{`${t('trade:unsettled')} ${t( + 'pnl' + )}`}{t('pnl')} + +
- - - - -

- {isSelectedMarket ? ( - - handlePositionClick(floorBasePosition, market) - } - > + return ( + +

+ + + + +

+ {isSelectedMarket ? ( + + handlePositionClick(floorBasePosition, market) + } + > + + + ) : ( - - ) : ( - - )} -

-
- - - - - - 0 ? 'text-th-up' : 'text-th-down' - }`} - > - - - + + + + + + 0 ? 'text-th-up' : 'text-th-down' + }`} > - Close - -
+ + + + + + + ) + })} + + +
{showMarketCloseModal && positionToClose ? ( { position={positionToClose} /> ) : null} -
+ ) : mangoAccountAddress || connected ? (
diff --git a/components/trade/TradeHistory.tsx b/components/trade/TradeHistory.tsx index d27dce09..31c94fe3 100644 --- a/components/trade/TradeHistory.tsx +++ b/components/trade/TradeHistory.tsx @@ -240,88 +240,90 @@ const TradeHistory = () => { (combinedTradeHistory.length || loadingTradeHistory) ? ( <> {showTableView ? ( - - - - - - - - - - - - - {combinedTradeHistory.map((trade: any, index: number) => { - return ( - - - - - - - - + + + + + ) + })} + +
{t('market')}{t('trade:side')}{t('trade:size')}{t('price')}{t('value')}{t('fee')}{t('date')} - -
- - - - {trade.size} - - - - - - - -

- {trade.liquidity} -

-
- {trade.block_datetime ? ( - + + + + + + + + + + + + + {combinedTradeHistory.map((trade: any, index: number) => { + return ( + + + + + + - - - ) - })} - -
{t('market')}{t('trade:side')}{t('trade:size')}{t('price')}{t('value')}{t('fee')}{t('date')} + +
+ + + + {trade.size} + + + - ) : ( - 'Recent' - )} - - {trade.market.name.includes('PERP') ? ( -
- - - - - - - -
- ) : null} -
+
+ + + +

+ {trade.liquidity} +

+
+ {trade.block_datetime ? ( + + ) : ( + 'Recent' + )} + + {trade.market.name.includes('PERP') ? ( +
+ + + + + + + +
+ ) : null} +
+
) : (
{combinedTradeHistory.map((trade: any, index: number) => { From ed79cf745267b87ab080f12d2a44f7dbd3b045fb Mon Sep 17 00:00:00 2001 From: saml33 Date: Thu, 9 Feb 2023 13:08:17 +1100 Subject: [PATCH 13/27] round spread to ticksize --- components/trade/Orderbook.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/components/trade/Orderbook.tsx b/components/trade/Orderbook.tsx index 6af66e5f..e4134cd7 100644 --- a/components/trade/Orderbook.tsx +++ b/components/trade/Orderbook.tsx @@ -7,7 +7,11 @@ import useInterval from '@components/shared/useInterval' import isEqual from 'lodash/isEqual' import usePrevious from '@components/shared/usePrevious' import useLocalStorageState from 'hooks/useLocalStorageState' -import { floorToDecimal, getDecimalCount } from 'utils/numbers' +import { + floorToDecimal, + formatNumericValue, + getDecimalCount, +} from 'utils/numbers' import { ANIMATION_SETTINGS_KEY } from 'utils/constants' import { useTranslation } from 'next-i18next' import Decimal from 'decimal.js' @@ -543,7 +547,10 @@ const Orderbook = () => {
- {orderbookData?.spread.toFixed(2)} + {formatNumericValue( + orderbookData?.spread, + market ? getDecimalCount(market.tickSize) : undefined + )}
) : null} From 6277984325d948df8276bb2b75b56efc4f24e347 Mon Sep 17 00:00:00 2001 From: tjs Date: Thu, 9 Feb 2023 16:22:54 -0500 Subject: [PATCH 14/27] use latest client --- components/MangoProvider.tsx | 4 ++-- components/account/CreateAccountForm.tsx | 4 ++-- components/trade/UnsettledTrades.tsx | 9 ++++----- hooks/useUnsettledPerpPositions.ts | 2 +- pages/dashboard/index.tsx | 24 ------------------------ pages/dashboard/mangoaccount.tsx | 2 +- store/mangoStore.ts | 4 ++-- yarn.lock | 22 +++++++++++----------- 8 files changed, 23 insertions(+), 48 deletions(-) diff --git a/components/MangoProvider.tsx b/components/MangoProvider.tsx index a00941ed..59a19fcb 100644 --- a/components/MangoProvider.tsx +++ b/components/MangoProvider.tsx @@ -82,7 +82,7 @@ const HydrateStore = () => { mangoAccount.publicKey, decodedMangoAccount ) - await newMangoAccount.reloadAccountData(client) + await newMangoAccount.reloadSerum3OpenOrders(client) actions.fetchOpenOrders() // newMangoAccount.spotOpenOrdersAccounts = // mangoAccount.spotOpenOrdersAccounts @@ -120,7 +120,7 @@ const ReadOnlyMangoAccount = () => { const client = mangoStore.getState().client const pk = new PublicKey(ma) const readOnlyMangoAccount = await client.getMangoAccount(pk) - await readOnlyMangoAccount.reloadAccountData(client) + await readOnlyMangoAccount.reloadSerum3OpenOrders(client) await actions.fetchOpenOrders(readOnlyMangoAccount) set((state) => { state.mangoAccount.current = readOnlyMangoAccount diff --git a/components/account/CreateAccountForm.tsx b/components/account/CreateAccountForm.tsx index 8c745aa0..f0885b11 100644 --- a/components/account/CreateAccountForm.tsx +++ b/components/account/CreateAccountForm.tsx @@ -62,13 +62,13 @@ const CreateAccountForm = ({ const pk = wallet.adapter.publicKey const mangoAccounts = await client.getMangoAccountsForOwner(group, pk!) const reloadedMangoAccounts = await Promise.all( - mangoAccounts.map((ma) => ma.reloadAccountData(client)) + mangoAccounts.map((ma) => ma.reloadSerum3OpenOrders(client)) ) const newAccount = mangoAccounts.find( (acc) => acc.accountNum === newAccountNum ) if (newAccount) { - await newAccount.reloadAccountData(client) + await newAccount.reloadSerum3OpenOrders(client) set((s) => { s.mangoAccount.current = newAccount s.mangoAccounts = reloadedMangoAccounts diff --git a/components/trade/UnsettledTrades.tsx b/components/trade/UnsettledTrades.tsx index 6b8d518d..2fa5bad5 100644 --- a/components/trade/UnsettledTrades.tsx +++ b/components/trade/UnsettledTrades.tsx @@ -79,7 +79,7 @@ const UnsettledTrades = ({ try { const mangoAccounts = await client.getAllMangoAccounts(group) const perpPosition = mangoAccount.getPerpPosition(market.perpMarketIndex) - const mangoAccountPnl = perpPosition?.getEquityUi(group, market) + const mangoAccountPnl = perpPosition?.getEquityUi(market) if (mangoAccountPnl === undefined) throw new Error('Unable to get account P&L') @@ -89,9 +89,8 @@ const UnsettledTrades = ({ .map((m) => ({ mangoAccount: m, pnl: - m - ?.getPerpPosition(market.perpMarketIndex) - ?.getEquityUi(group, market) || 0, + m?.getPerpPosition(market.perpMarketIndex)?.getEquityUi(market) || + 0, })) .sort((a, b) => sign * (a.pnl - b.pnl)) @@ -199,7 +198,7 @@ const UnsettledTrades = ({ {' '} USDC diff --git a/hooks/useUnsettledPerpPositions.ts b/hooks/useUnsettledPerpPositions.ts index b826280c..e0da07f9 100644 --- a/hooks/useUnsettledPerpPositions.ts +++ b/hooks/useUnsettledPerpPositions.ts @@ -13,7 +13,7 @@ const useUnsettledPerpPositions = () => { return perpPositions.filter((p) => { const market = group?.getPerpMarketByMarketIndex(p.marketIndex) if (!market || !group) return false - return p.getUnsettledPnlUi(group, market) !== 0 + return p.getUnsettledPnlUi(market) !== 0 }) }, [mangoAccountAddress]) diff --git a/pages/dashboard/index.tsx b/pages/dashboard/index.tsx index 06284943..0c0dfe17 100644 --- a/pages/dashboard/index.tsx +++ b/pages/dashboard/index.tsx @@ -193,12 +193,6 @@ const Dashboard: NextPage = () => { label="Collected fees native" value={bank.collectedFeesNative.toNumber()} /> - { )}/ ${perpMarket.initBaseLiabWeight.toFixed(4)}`} /> - - - { /> ()( } if (newSelectedMangoAccount) { - await newSelectedMangoAccount.reloadAccountData(client) + await newSelectedMangoAccount.reloadSerum3OpenOrders(client) set((state) => { state.mangoAccount.current = newSelectedMangoAccount state.mangoAccount.initialLoad = false @@ -735,7 +735,7 @@ const mangoStore = create()( } await Promise.all( - mangoAccounts.map((ma) => ma.reloadAccountData(client)) + mangoAccounts.map((ma) => ma.reloadSerum3OpenOrders(client)) ) set((state) => { diff --git a/yarn.lock b/yarn.lock index ff3f0d68..86d7abf6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23,8 +23,8 @@ regenerator-runtime "^0.13.11" "@blockworks-foundation/mango-v4@https://github.com/blockworks-foundation/mango-v4.git#ts-client": - version "0.0.1-beta.6" - resolved "https://github.com/blockworks-foundation/mango-v4.git#2f754115d06745282b863e7a905bdb25bf85d309" + version "0.4.3" + resolved "https://github.com/blockworks-foundation/mango-v4.git#35763da947e3b15175dcee5c81633e409803b2f7" dependencies: "@project-serum/anchor" "^0.25.0" "@project-serum/serum" "^0.13.65" @@ -369,9 +369,9 @@ sha.js "^2.4.11" "@noble/ed25519@^1.7.0": - version "1.7.1" - resolved "https://registry.yarnpkg.com/@noble/ed25519/-/ed25519-1.7.1.tgz#6899660f6fbb97798a6fbd227227c4589a454724" - integrity sha512-Rk4SkJFaXZiznFyC/t77Q0NKS4FL7TLJJsVG2V2oiEq3kJVeTdxysEe/yRWSpnWMe808XRDJ+VFh5pt/FN5plw== + version "1.7.3" + resolved "https://registry.yarnpkg.com/@noble/ed25519/-/ed25519-1.7.3.tgz#57e1677bf6885354b466c38e2b620c62f45a7123" + integrity sha512-iR8GBkDt0Q3GyaVcIu7mSsVIqnFbkbRzGLWlvhwunacoLwt4J3swfKhfaM6rN6WY+TBGoYT1GtT1mIh2/jGbRQ== "@noble/hashes@^1.1.2": version "1.2.0" @@ -1509,9 +1509,9 @@ integrity sha512-evMDG1bC4rgQg4ku9tKpuMh5iBNEwNa3tf9zRHdP1qlv+1WUg44xat4IxCE14gIpZRGUUWAx2VhItCZc25NfMA== "@types/node@*": - version "18.11.18" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.18.tgz#8dfb97f0da23c2293e554c5a50d61ef134d7697f" - integrity sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA== + version "18.13.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.13.0.tgz#0400d1e6ce87e9d3032c19eb6c58205b0d3f7850" + integrity sha512-gC3TazRzGoOnoKAhUx+Q0t8S9Tzs74z7m0ipwGpSqQrleP14hKxP4/JUeEQcD3W1/aIpnWl8pHowI7WokuZpXg== "@types/node@17.0.23": version "17.0.23" @@ -2313,9 +2313,9 @@ bin-links@4.0.1: write-file-atomic "^5.0.0" binance-api-node@^0.12.0: - version "0.12.2" - resolved "https://registry.yarnpkg.com/binance-api-node/-/binance-api-node-0.12.2.tgz#a7f9b8d94c2d75f64cb709d7b041b80da1e0e79d" - integrity sha512-X9zKjYhcp+smUMxmZvJdcqd22wQnD8gyjRKCmf1dno9Ft/mr9ZavtzHzjJaoXGbHbcGI2gSSg6fa8ozfT6B6Yg== + version "0.12.3" + resolved "https://registry.yarnpkg.com/binance-api-node/-/binance-api-node-0.12.3.tgz#1703282ce7ef1b52a893d7de046fd305806808f7" + integrity sha512-JMBOmcva/nlM9k0SDG3nBm2i/kSNva74jDU55j/mpoXMbb4AYP9luG1JuI5dgPvmkaKiR2A05MPI5aQiLhWTDw== dependencies: https-proxy-agent "^5.0.0" isomorphic-fetch "^3.0.0" From efdd8a893433f8a7125f22f89a70316b5e3f975c Mon Sep 17 00:00:00 2001 From: tjs Date: Thu, 9 Feb 2023 17:04:57 -0500 Subject: [PATCH 15/27] fix typecheck errors --- components/trade/PerpPositions.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/trade/PerpPositions.tsx b/components/trade/PerpPositions.tsx index da01d0c0..81bd27db 100644 --- a/components/trade/PerpPositions.tsx +++ b/components/trade/PerpPositions.tsx @@ -105,9 +105,9 @@ const PerpPositions = () => { if (!basePosition) return null - const unsettledPnl = position.getUnsettledPnlUi(group, market) + const unsettledPnl = position.getUnsettledPnlUi(market) const cummulativePnl = - position.cumulativePnlOverPositionLifetimeUi(group, market) + position.cumulativePnlOverPositionLifetimeUi(market) return ( From 73162e554439d2834c5b80c49dbe60f7047b95cf Mon Sep 17 00:00:00 2001 From: saml33 Date: Fri, 10 Feb 2023 10:26:28 +1100 Subject: [PATCH 16/27] fix negative swap max --- components/swap/useTokenMax.tsx | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/components/swap/useTokenMax.tsx b/components/swap/useTokenMax.tsx index 5b676433..a721b557 100644 --- a/components/swap/useTokenMax.tsx +++ b/components/swap/useTokenMax.tsx @@ -53,15 +53,17 @@ export const getTokenInMax = ( ? inputTokenBalance : new Decimal(0) - const maxUiAmountWithBorrow = floorToDecimal( - mangoAccount.getMaxSourceUiForTokenSwap( - group, - inputBank.mint, - outputBank.mint - ), - inputBank.mintDecimals + const rawMaxUiAmountWithBorrow = mangoAccount.getMaxSourceUiForTokenSwap( + group, + inputBank.mint, + outputBank.mint ) + const maxUiAmountWithBorrow = + rawMaxUiAmountWithBorrow > 0 + ? floorToDecimal(rawMaxUiAmountWithBorrow, inputBank.mintDecimals) + : new Decimal(0) + const inputBankVaultBalance = floorToDecimal( group .getTokenVaultBalanceByMintUi(inputBank.mint) From 97e93faf78b3a531e4d759ed1e8f3bac2ba43ac0 Mon Sep 17 00:00:00 2001 From: saml33 Date: Fri, 10 Feb 2023 10:35:39 +1100 Subject: [PATCH 17/27] pulse health bar and heart when health < 10 --- components/SideNav.tsx | 2 +- components/account/HealthBar.tsx | 4 +++- components/account/HealthHeart.tsx | 9 ++------- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/components/SideNav.tsx b/components/SideNav.tsx index c57c20da..74d3b55f 100644 --- a/components/SideNav.tsx +++ b/components/SideNav.tsx @@ -167,7 +167,7 @@ const SideNav = ({ collapsed }: { collapsed: boolean }) => { health={ group && mangoAccount ? mangoAccount.getHealthRatioUi(group, HealthType.maint) - : undefined + : 0 } size={32} /> diff --git a/components/account/HealthBar.tsx b/components/account/HealthBar.tsx index ab71a825..12640e21 100644 --- a/components/account/HealthBar.tsx +++ b/components/account/HealthBar.tsx @@ -32,7 +32,9 @@ const HealthBar = ({ health }: { health: number }) => { ...sharedStyles, width: `${barWidths[0]}%`, }} - className={`flex rounded-full`} + className={`flex rounded-full ${ + health && health < 10 ? 'animate-pulse' : '' + }`} />
diff --git a/components/account/HealthHeart.tsx b/components/account/HealthHeart.tsx index 7bf275ce..bb16500b 100644 --- a/components/account/HealthHeart.tsx +++ b/components/account/HealthHeart.tsx @@ -1,12 +1,6 @@ import { useMemo } from 'react' -const HealthHeart = ({ - health, - size, -}: { - health: number | undefined - size: number -}) => { +const HealthHeart = ({ health, size }: { health: number; size: number }) => { const fillColor = useMemo(() => { if (!health) return 'var(--fgd-4)' if (health <= 25) { @@ -30,6 +24,7 @@ const HealthHeart = ({ return ( Date: Fri, 10 Feb 2023 11:38:20 +1100 Subject: [PATCH 18/27] add mango perp stats charts --- components/stats/MangoPerpStatsCharts.tsx | 141 ++++++++++++++++++ components/stats/MangoStats.tsx | 104 +------------ components/stats/TotalDepositBorrowCharts.tsx | 12 +- 3 files changed, 148 insertions(+), 109 deletions(-) create mode 100644 components/stats/MangoPerpStatsCharts.tsx diff --git a/components/stats/MangoPerpStatsCharts.tsx b/components/stats/MangoPerpStatsCharts.tsx new file mode 100644 index 00000000..d5f3fb88 --- /dev/null +++ b/components/stats/MangoPerpStatsCharts.tsx @@ -0,0 +1,141 @@ +import { useTranslation } from 'next-i18next' +import { useMemo, useState } from 'react' +import dynamic from 'next/dynamic' +import mangoStore from '@store/mangoStore' +const DetailedAreaChart = dynamic( + () => import('@components/shared/DetailedAreaChart'), + { ssr: false } +) + +interface OiValueItem { + date: string + openInterest: number +} + +interface FeeValueItem { + date: string + feeValue: number +} + +const MangoPerpStatsCharts = () => { + const { t } = useTranslation(['common', 'token', 'trade']) + const loadingPerpStats = mangoStore((s) => s.perpStats.loading) + const perpStats = mangoStore((s) => s.perpStats.data) + const [oiDaysToShow, setOiDaysToShow] = useState('30') + const [feesDaysToShow, setFeesDaysToShow] = useState('30') + // const perpMarkets = mangoStore((s) => s.perpMarkets) + + // const currentTotalOpenInterestValue = useMemo(() => { + // if (!perpMarkets.length) return 0 + // return perpMarkets.reduce((a: number, c: PerpMarket) => { + // const value = a + c.openInterest.toNumber() * c.uiPrice + // return value + // }, 0) + // }, [perpMarkets]) + + const totalFeeValues = useMemo(() => { + if (!perpStats.length) return [] + const values = perpStats.reduce((a, c) => { + const hasDate = a.find((d: any) => d.date === c.date_hour) + if (!hasDate) { + a.push({ + date: c.date_hour, + feeValue: c.fees_accrued, + }) + } else { + hasDate.feeValue = hasDate.feeValue + c.fees_accrued + } + return a + }, []) + return values.reverse() + }, [perpStats]) + + const totalOpenInterestValues = useMemo(() => { + if (!perpStats) return [] + const values = perpStats.reduce((a, c) => { + const hasDate = a.find((d: any) => d.date === c.date_hour) + if (!hasDate) { + a.push({ + date: c.date_hour, + openInterest: Math.floor(c.open_interest * c.price), + }) + } else { + hasDate.openInterest = + hasDate.openInterest + Math.floor(c.open_interest * c.price) + } + return a + }, []) + return values.reverse() + }, [perpStats]) + + const filteredOiValues = useMemo(() => { + if (!totalOpenInterestValues.length) return [] + if (oiDaysToShow !== '30') { + const seconds = Number(oiDaysToShow) * 86400 + const data = totalOpenInterestValues.filter((d: OiValueItem) => { + const dataTime = new Date(d.date).getTime() / 1000 + const now = new Date().getTime() / 1000 + const limit = now - seconds + return dataTime >= limit + }) + return data + } + return totalOpenInterestValues + }, [totalOpenInterestValues, oiDaysToShow]) + + const filteredFeesValues = useMemo(() => { + if (!totalFeeValues.length) return [] + if (feesDaysToShow !== '30') { + const seconds = Number(feesDaysToShow) * 86400 + const data = totalFeeValues.filter((d: FeeValueItem) => { + const dataTime = new Date(d.date).getTime() / 1000 + const now = new Date().getTime() / 1000 + const limit = now - seconds + return dataTime >= limit + }) + return data + } + return totalFeeValues + }, [totalFeeValues, feesDaysToShow]) + + return ( + <> + {totalFeeValues.length ? ( +
+ `$${Math.floor(x)}`} + title={t('trade:open-interest')} + xKey="date" + yKey={'openInterest'} + /> +
+ ) : null} + {totalOpenInterestValues.length ? ( +
+ `$${x.toFixed(2)}`} + title="Perp Fees" + xKey="date" + yKey={'feeValue'} + /> +
+ ) : null} + + ) +} + +export default MangoPerpStatsCharts diff --git a/components/stats/MangoStats.tsx b/components/stats/MangoStats.tsx index 1586ddec..2bf62bc4 100644 --- a/components/stats/MangoStats.tsx +++ b/components/stats/MangoStats.tsx @@ -1,109 +1,11 @@ -import mangoStore from '@store/mangoStore' +import MangoPerpStatsCharts from './MangoPerpStatsCharts' import TotalDepositBorrowCharts from './TotalDepositBorrowCharts' -// import { useTranslation } from 'next-i18next' -// import { PerpMarket } from '@blockworks-foundation/mango-v4' const MangoStats = () => { - // const { t } = useTranslation(['common', 'token', 'trade']) - const tokenStats = mangoStore((s) => s.tokenStats.data) - const loadingStats = mangoStore((s) => s.tokenStats.loading) - // const perpStats = mangoStore((s) => s.perpStats.data) - // const loadingPerpStats = mangoStore((s) => s.perpStats.loading) - // const perpMarkets = mangoStore((s) => s.perpMarkets) - - // const totalFeeValues = useMemo(() => { - // if (!perpStats.length) return [] - // const values = perpStats.reduce((a, c) => { - // const hasDate = a.find((d: any) => d.date === c.date_hour) - // if (!hasDate) { - // a.push({ - // date: c.date_hour, - // feeValue: Math.floor(c.fees_accrued), - // }) - // } else { - // hasDate.feeValue = hasDate.feeValue + Math.floor(c.fees_accrued) - // } - // return a - // }, []) - // return values.reverse() - // }, [perpStats]) - - // const totalOpenInterestValues = useMemo(() => { - // if (!perpStats) return [] - // const values = perpStats.reduce((a, c) => { - // const hasDate = a.find((d: any) => d.date === c.date_hour) - // if (!hasDate) { - // a.push({ - // date: c.date_hour, - // openInterest: Math.floor(c.open_interest * c.price), - // }) - // } else { - // hasDate.openInterest = - // hasDate.openInterest + Math.floor(c.open_interest * c.price) - // } - // return a - // }, []) - // return values.reverse() - // }, [perpStats]) - - // i think c.openInterest below needs some sort of conversion to give the correct number. then this can be added as the current value of the chart - - // const currentTotalOpenInterestValue = useMemo(() => { - // if (!perpMarkets.length) return 0 - // return perpMarkets.reduce((a: number, c: PerpMarket) => { - // const value = a + c.openInterest.toNumber() * c.uiPrice - // return value - // }, 0) - // }, [perpMarkets]) - return (
- - {/* uncomment below when perps launch */} - - {/* {loadingPerpStats ? ( -
- -
- -
- ) : totalFeeValues.length ? ( -
- `$${Math.floor(x)}`} - title={t('trade:open-interest')} - xKey="date" - yKey={'openInterest'} - /> -
- ) : null} - {loadingPerpStats ? ( -
- -
- -
- ) : totalOpenInterestValues.length ? ( -
- `$${x.toFixed(2)}`} - title="Perp Fees" - xKey="date" - yKey={'feeValue'} - /> -
- ) : null} */} + +
) } diff --git a/components/stats/TotalDepositBorrowCharts.tsx b/components/stats/TotalDepositBorrowCharts.tsx index a839eabd..af9a9c56 100644 --- a/components/stats/TotalDepositBorrowCharts.tsx +++ b/components/stats/TotalDepositBorrowCharts.tsx @@ -1,4 +1,4 @@ -import { TokenStatsItem } from '@store/mangoStore' +import mangoStore, { TokenStatsItem } from '@store/mangoStore' import { useTranslation } from 'next-i18next' import dynamic from 'next/dynamic' import { useMemo, useState } from 'react' @@ -16,14 +16,10 @@ interface TotalValueItem { depositValue: number } -const TotalDepositBorrowCharts = ({ - tokenStats, - loadingStats, -}: { - tokenStats: TokenStatsItem[] | null - loadingStats: boolean -}) => { +const TotalDepositBorrowCharts = () => { const { t } = useTranslation(['common', 'token', 'trade']) + const tokenStats = mangoStore((s) => s.tokenStats.data) + const loadingStats = mangoStore((s) => s.tokenStats.loading) const [borrowDaysToShow, setBorrowDaysToShow] = useState('30') const [depositDaysToShow, setDepositDaysToShow] = useState('30') const banks = useBanksWithBalances() From 49f203f7e7def74fe76ccc488bb871ca340ecfbe Mon Sep 17 00:00:00 2001 From: saml33 Date: Fri, 10 Feb 2023 14:20:46 +1100 Subject: [PATCH 19/27] fix market change values --- components/stats/MangoPerpStatsCharts.tsx | 14 ++--- components/stats/PerpMarketsTable.tsx | 73 +++++++++++++---------- components/stats/SpotMarketsTable.tsx | 12 ++-- components/trade/AdvancedMarketHeader.tsx | 43 +++++++++---- store/mangoStore.ts | 17 +++++- 5 files changed, 99 insertions(+), 60 deletions(-) diff --git a/components/stats/MangoPerpStatsCharts.tsx b/components/stats/MangoPerpStatsCharts.tsx index d5f3fb88..b12f0b1a 100644 --- a/components/stats/MangoPerpStatsCharts.tsx +++ b/components/stats/MangoPerpStatsCharts.tsx @@ -1,7 +1,7 @@ import { useTranslation } from 'next-i18next' import { useMemo, useState } from 'react' import dynamic from 'next/dynamic' -import mangoStore from '@store/mangoStore' +import mangoStore, { PerpStatsItem } from '@store/mangoStore' const DetailedAreaChart = dynamic( () => import('@components/shared/DetailedAreaChart'), { ssr: false } @@ -34,9 +34,9 @@ const MangoPerpStatsCharts = () => { // }, [perpMarkets]) const totalFeeValues = useMemo(() => { - if (!perpStats.length) return [] - const values = perpStats.reduce((a, c) => { - const hasDate = a.find((d: any) => d.date === c.date_hour) + if (!perpStats || !perpStats.length) return [] + const values = perpStats.reduce((a: FeeValueItem[], c: PerpStatsItem) => { + const hasDate = a.find((d: FeeValueItem) => d.date === c.date_hour) if (!hasDate) { a.push({ date: c.date_hour, @@ -51,9 +51,9 @@ const MangoPerpStatsCharts = () => { }, [perpStats]) const totalOpenInterestValues = useMemo(() => { - if (!perpStats) return [] - const values = perpStats.reduce((a, c) => { - const hasDate = a.find((d: any) => d.date === c.date_hour) + if (!perpStats || !perpStats.length) return [] + const values = perpStats.reduce((a: OiValueItem[], c: PerpStatsItem) => { + const hasDate = a.find((d: OiValueItem) => d.date === c.date_hour) if (!hasDate) { a.push({ date: c.date_hour, diff --git a/components/stats/PerpMarketsTable.tsx b/components/stats/PerpMarketsTable.tsx index 9ec2c303..2a91c928 100644 --- a/components/stats/PerpMarketsTable.tsx +++ b/components/stats/PerpMarketsTable.tsx @@ -2,14 +2,13 @@ import { PerpMarket } from '@blockworks-foundation/mango-v4' import { useTranslation } from 'next-i18next' import { useTheme } from 'next-themes' import { useViewport } from '../../hooks/useViewport' -import mangoStore from '@store/mangoStore' +import mangoStore, { PerpStatsItem } from '@store/mangoStore' import { COLORS } from '../../styles/colors' import { breakpoints } from '../../utils/theme' import ContentBox from '../shared/ContentBox' import Change from '../shared/Change' import MarketLogos from '@components/trade/MarketLogos' import dynamic from 'next/dynamic' -import { useCoingecko } from 'hooks/useCoingecko' import { Table, Td, Th, TrBody, TrHead } from '@components/shared/TableElements' import { usePerpFundingRate } from '@components/trade/PerpFundingRate' import { IconButton } from '@components/shared/Button' @@ -21,14 +20,33 @@ const SimpleAreaChart = dynamic( { ssr: false } ) +export const getOneDayPerpStats = ( + stats: PerpStatsItem[] | null, + marketName: string +) => { + return stats + ? stats + .filter((s) => s.perp_market === marketName) + .filter((f) => { + const seconds = 86400 + const dataTime = new Date(f.date_hour).getTime() / 1000 + const now = new Date().getTime() / 1000 + const limit = now - seconds + return dataTime >= limit + }) + .reverse() + : [] +} + const PerpMarketsTable = ({ setShowPerpDetails, }: { setShowPerpDetails: (x: string) => void }) => { const { t } = useTranslation(['common', 'trade']) - const { isLoading: loadingPrices, data: coingeckoPrices } = useCoingecko() const perpMarkets = mangoStore((s) => s.perpMarkets) + const loadingPerpStats = mangoStore((s) => s.perpStats.loading) + const perpStats = mangoStore((s) => s.perpStats.data) const { theme } = useTheme() const { width } = useViewport() const showTableView = width ? width > breakpoints.md : false @@ -46,25 +64,20 @@ const PerpMarketsTable = ({ {t('trade:funding-rate')} {t('trade:open-interest')} {t('rolling-change')} + {perpMarkets.map((market) => { const symbol = market.name.split('-')[0] + const marketStats = getOneDayPerpStats(perpStats, market.name) - const coingeckoData = coingeckoPrices.find( - (asset) => asset.symbol.toUpperCase() === symbol.toUpperCase() - ) - - const change = coingeckoData - ? ((coingeckoData.prices[coingeckoData.prices.length - 1][1] - - coingeckoData.prices[0][1]) / - coingeckoData.prices[0][1]) * + const change = marketStats.length + ? ((market.uiPrice - marketStats[0].price) / + marketStats[0].price) * 100 : 0 - const chartData = coingeckoData ? coingeckoData.prices : undefined - let fundingRate if (rate.isSuccess && market instanceof PerpMarket) { const marketRate = rate?.data?.find( @@ -95,8 +108,8 @@ const PerpMarketsTable = ({
- {!loadingPrices ? ( - chartData !== undefined ? ( + {!loadingPerpStats ? ( + marketStats.length ? (
) : symbol === 'USDC' || symbol === 'USDT' ? null : ( @@ -178,23 +191,19 @@ export default PerpMarketsTable const MobilePerpMarketItem = ({ market }: { market: PerpMarket }) => { const { t } = useTranslation('common') - const { isLoading: loadingPrices, data: coingeckoPrices } = useCoingecko() + const loadingPerpStats = mangoStore((s) => s.perpStats.loading) + const perpStats = mangoStore((s) => s.perpStats.data) const { theme } = useTheme() // const rate = usePerpFundingRate() const symbol = market.name.split('-')[0] - const coingeckoData = coingeckoPrices.find((asset) => asset.symbol === symbol) + const marketStats = getOneDayPerpStats(perpStats, market.name) - const change = coingeckoData - ? ((coingeckoData.prices[coingeckoData.prices.length - 1][1] - - coingeckoData.prices[0][1]) / - coingeckoData.prices[0][1]) * - 100 + const change = marketStats.length + ? ((market.uiPrice - marketStats[0].price) / marketStats[0].price) * 100 : 0 - const chartData = coingeckoData ? coingeckoData.prices : undefined - // let fundingRate // if ( // rate.isSuccess @@ -224,15 +233,15 @@ const MobilePerpMarketItem = ({ market }: { market: PerpMarket }) => {
- {!loadingPrices ? ( - chartData !== undefined ? ( + {!loadingPerpStats ? ( + marketStats.length ? (
= 0 ? COLORS.UP[theme] : COLORS.DOWN[theme]} - data={chartData} + data={marketStats} name={market.name} - xKey="0" - yKey="1" + xKey="date_hour" + yKey="price" />
) : symbol === 'USDC' || symbol === 'USDT' ? null : ( diff --git a/components/stats/SpotMarketsTable.tsx b/components/stats/SpotMarketsTable.tsx index b9841246..aad218a9 100644 --- a/components/stats/SpotMarketsTable.tsx +++ b/components/stats/SpotMarketsTable.tsx @@ -52,12 +52,12 @@ const SpotMarketsTable = () => { asset.symbol.toUpperCase() === bank?.name.toUpperCase() ) - const change = coingeckoData - ? ((coingeckoData.prices[coingeckoData.prices.length - 1][1] - - coingeckoData.prices[0][1]) / - coingeckoData.prices[0][1]) * - 100 - : 0 + const change = + coingeckoData && oraclePrice + ? ((oraclePrice - coingeckoData.prices[0][1]) / + coingeckoData.prices[0][1]) * + 100 + : 0 const chartData = coingeckoData ? coingeckoData.prices : undefined diff --git a/components/trade/AdvancedMarketHeader.tsx b/components/trade/AdvancedMarketHeader.tsx index a8e3ddf8..3ee77c25 100644 --- a/components/trade/AdvancedMarketHeader.tsx +++ b/components/trade/AdvancedMarketHeader.tsx @@ -1,11 +1,13 @@ import { PerpMarket } from '@blockworks-foundation/mango-v4' import { IconButton } from '@components/shared/Button' import Change from '@components/shared/Change' +import { getOneDayPerpStats } from '@components/stats/PerpMarketsTable' import { ChartBarIcon } from '@heroicons/react/20/solid' +import mangoStore from '@store/mangoStore' import { useCoingecko } from 'hooks/useCoingecko' import useSelectedMarket from 'hooks/useSelectedMarket' import { useTranslation } from 'next-i18next' -import { useMemo } from 'react' +import { useEffect, useMemo } from 'react' import { getDecimalCount } from 'utils/numbers' import MarketSelectDropdown from './MarketSelectDropdown' import PerpFundingRate from './PerpFundingRate' @@ -18,23 +20,38 @@ const AdvancedMarketHeader = ({ setShowChart?: (x: boolean) => void }) => { const { t } = useTranslation(['common', 'trade']) + const perpStats = mangoStore((s) => s.perpStats.data) const { serumOrPerpMarket, baseSymbol, price } = useSelectedMarket() + const selectedMarketName = mangoStore((s) => s.selectedMarket.name) const { data: tokenPrices } = useCoingecko() - const coingeckoData = useMemo(() => { - return tokenPrices.find( - (asset) => asset.symbol.toUpperCase() === baseSymbol?.toUpperCase() - ) - }, [baseSymbol, tokenPrices]) + useEffect(() => { + if (serumOrPerpMarket instanceof PerpMarket) { + const actions = mangoStore.getState().actions + actions.fetchPerpStats() + } + }, [serumOrPerpMarket]) + + const changeData = useMemo(() => { + if (serumOrPerpMarket instanceof PerpMarket) { + return getOneDayPerpStats(perpStats, selectedMarketName) + } else { + return tokenPrices.find( + (asset) => asset.symbol.toUpperCase() === baseSymbol?.toUpperCase() + ) + } + }, [baseSymbol, perpStats, serumOrPerpMarket, tokenPrices]) const change = useMemo(() => { - return coingeckoData - ? ((coingeckoData.prices[coingeckoData.prices.length - 1][1] - - coingeckoData.prices[0][1]) / - coingeckoData.prices[0][1]) * - 100 - : 0 - }, [coingeckoData]) + if (!changeData || !price || !serumOrPerpMarket) return 0 + if (serumOrPerpMarket instanceof PerpMarket) { + return changeData.length + ? ((price - changeData[0].price) / changeData[0].price) * 100 + : 0 + } else { + return ((price - changeData.prices[0][1]) / changeData.prices[0][1]) * 100 + } + }, [changeData, price, serumOrPerpMarket]) return (
diff --git a/store/mangoStore.ts b/store/mangoStore.ts index 42136c9f..e3946ad8 100644 --- a/store/mangoStore.ts +++ b/store/mangoStore.ts @@ -167,6 +167,19 @@ interface NFT { image: string } +export interface PerpStatsItem { + date_hour: string + fees_accrued: number + funding_rate_hourly: number + instantaneous_funding_rate: number + mango_group: string + market_index: number + open_interest: number + perp_market: string + price: number + stable_price: number +} + interface ProfileDetails { profile_image_url?: string profile_name: string @@ -273,7 +286,7 @@ export type MangoStore = { perpMarkets: PerpMarket[] perpStats: { loading: boolean - data: any[] + data: PerpStatsItem[] | null } profile: { details: ProfileDetails | null @@ -830,7 +843,7 @@ const mangoStore = create()( const set = get().set const group = get().group const stats = get().perpStats.data - if (stats.length || !group) return + if ((stats && stats.length) || !group) return set((state) => { state.perpStats.loading = true }) From 00e08c3b31a994608f7d82349a7ab571ec703358 Mon Sep 17 00:00:00 2001 From: saml33 Date: Fri, 10 Feb 2023 16:05:07 +1100 Subject: [PATCH 20/27] add stable price to perp stats table --- components/stats/PerpMarketsTable.tsx | 24 ++++++++++++++++++++++-- public/locales/en/trade.json | 2 ++ public/locales/es/trade.json | 2 ++ public/locales/ru/trade.json | 2 ++ public/locales/zh/trade.json | 2 ++ public/locales/zh_tw/trade.json | 2 ++ 6 files changed, 32 insertions(+), 2 deletions(-) diff --git a/components/stats/PerpMarketsTable.tsx b/components/stats/PerpMarketsTable.tsx index 2a91c928..f5692c8c 100644 --- a/components/stats/PerpMarketsTable.tsx +++ b/components/stats/PerpMarketsTable.tsx @@ -15,6 +15,7 @@ import { IconButton } from '@components/shared/Button' import { ChevronRightIcon } from '@heroicons/react/20/solid' import FormatNumericValue from '@components/shared/FormatNumericValue' import { getDecimalCount } from 'utils/numbers' +import Tooltip from '@components/shared/Tooltip' const SimpleAreaChart = dynamic( () => import('@components/shared/SimpleAreaChart'), { ssr: false } @@ -60,7 +61,14 @@ const PerpMarketsTable = ({ {t('market')} {t('price')} - + + + + + {t('trade:stable-price')} + + + {t('trade:funding-rate')} {t('trade:open-interest')} {t('rolling-change')} @@ -97,7 +105,9 @@ const PerpMarketsTable = ({
-

{market.name}

+

+ {market.name} +

@@ -130,6 +140,16 @@ const PerpMarketsTable = ({
)} + +
+

+ +

+
+

{fundingRate}

diff --git a/public/locales/en/trade.json b/public/locales/en/trade.json index 6a1e53a6..03f41b9c 100644 --- a/public/locales/en/trade.json +++ b/public/locales/en/trade.json @@ -46,10 +46,12 @@ "side": "Side", "size": "Size", "spread": "Spread", + "stable-price": "Stable Price", "tooltip-enable-margin": "Enable spot margin for this trade", "tooltip-ioc": "Immediate-Or-Cancel (IOC) orders are guaranteed to be the taker and must be executed immediately. Any portion of the order that can't be filled immediately will be cancelled", "tooltip-post": "Post orders are guaranteed to be the maker or they will be canceled", "tooltip-slippage": "An estimate of the difference between the current price and the price your trade will be executed at", + "tooltip-stable-price": "Stable price is used in a safety mechanism that limits a user's ability to enter risky positions when the oracle price is changing rapidly", "trade-sounds-tooltip": "Play a sound alert for every new trade", "trades": "Trades", "unsettled": "Unsettled" diff --git a/public/locales/es/trade.json b/public/locales/es/trade.json index 6a1e53a6..03f41b9c 100644 --- a/public/locales/es/trade.json +++ b/public/locales/es/trade.json @@ -46,10 +46,12 @@ "side": "Side", "size": "Size", "spread": "Spread", + "stable-price": "Stable Price", "tooltip-enable-margin": "Enable spot margin for this trade", "tooltip-ioc": "Immediate-Or-Cancel (IOC) orders are guaranteed to be the taker and must be executed immediately. Any portion of the order that can't be filled immediately will be cancelled", "tooltip-post": "Post orders are guaranteed to be the maker or they will be canceled", "tooltip-slippage": "An estimate of the difference between the current price and the price your trade will be executed at", + "tooltip-stable-price": "Stable price is used in a safety mechanism that limits a user's ability to enter risky positions when the oracle price is changing rapidly", "trade-sounds-tooltip": "Play a sound alert for every new trade", "trades": "Trades", "unsettled": "Unsettled" diff --git a/public/locales/ru/trade.json b/public/locales/ru/trade.json index 6a1e53a6..03f41b9c 100644 --- a/public/locales/ru/trade.json +++ b/public/locales/ru/trade.json @@ -46,10 +46,12 @@ "side": "Side", "size": "Size", "spread": "Spread", + "stable-price": "Stable Price", "tooltip-enable-margin": "Enable spot margin for this trade", "tooltip-ioc": "Immediate-Or-Cancel (IOC) orders are guaranteed to be the taker and must be executed immediately. Any portion of the order that can't be filled immediately will be cancelled", "tooltip-post": "Post orders are guaranteed to be the maker or they will be canceled", "tooltip-slippage": "An estimate of the difference between the current price and the price your trade will be executed at", + "tooltip-stable-price": "Stable price is used in a safety mechanism that limits a user's ability to enter risky positions when the oracle price is changing rapidly", "trade-sounds-tooltip": "Play a sound alert for every new trade", "trades": "Trades", "unsettled": "Unsettled" diff --git a/public/locales/zh/trade.json b/public/locales/zh/trade.json index 6a1e53a6..03f41b9c 100644 --- a/public/locales/zh/trade.json +++ b/public/locales/zh/trade.json @@ -46,10 +46,12 @@ "side": "Side", "size": "Size", "spread": "Spread", + "stable-price": "Stable Price", "tooltip-enable-margin": "Enable spot margin for this trade", "tooltip-ioc": "Immediate-Or-Cancel (IOC) orders are guaranteed to be the taker and must be executed immediately. Any portion of the order that can't be filled immediately will be cancelled", "tooltip-post": "Post orders are guaranteed to be the maker or they will be canceled", "tooltip-slippage": "An estimate of the difference between the current price and the price your trade will be executed at", + "tooltip-stable-price": "Stable price is used in a safety mechanism that limits a user's ability to enter risky positions when the oracle price is changing rapidly", "trade-sounds-tooltip": "Play a sound alert for every new trade", "trades": "Trades", "unsettled": "Unsettled" diff --git a/public/locales/zh_tw/trade.json b/public/locales/zh_tw/trade.json index 6a1e53a6..03f41b9c 100644 --- a/public/locales/zh_tw/trade.json +++ b/public/locales/zh_tw/trade.json @@ -46,10 +46,12 @@ "side": "Side", "size": "Size", "spread": "Spread", + "stable-price": "Stable Price", "tooltip-enable-margin": "Enable spot margin for this trade", "tooltip-ioc": "Immediate-Or-Cancel (IOC) orders are guaranteed to be the taker and must be executed immediately. Any portion of the order that can't be filled immediately will be cancelled", "tooltip-post": "Post orders are guaranteed to be the maker or they will be canceled", "tooltip-slippage": "An estimate of the difference between the current price and the price your trade will be executed at", + "tooltip-stable-price": "Stable price is used in a safety mechanism that limits a user's ability to enter risky positions when the oracle price is changing rapidly", "trade-sounds-tooltip": "Play a sound alert for every new trade", "trades": "Trades", "unsettled": "Unsettled" From c79045aaa00e40666483410374750f303603509e Mon Sep 17 00:00:00 2001 From: saml33 Date: Fri, 10 Feb 2023 20:09:42 +1100 Subject: [PATCH 21/27] align trade page tab order --- components/trade/TradeInfoTabs.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/trade/TradeInfoTabs.tsx b/components/trade/TradeInfoTabs.tsx index a52436c6..f634c625 100644 --- a/components/trade/TradeInfoTabs.tsx +++ b/components/trade/TradeInfoTabs.tsx @@ -27,9 +27,9 @@ const TradeInfoTabs = () => { unsettledPerpPositions?.length return [ ['balances', 0], + ['trade:positions', openPerpPositions.length], ['trade:orders', Object.values(openOrders).flat().length], ['trade:unsettled', unsettledTradeCount], - ['trade:positions', openPerpPositions.length], ['trade-history', 0], ] }, [ From 4f26846caf2c822b03dfffe891b01a684f5d77fb Mon Sep 17 00:00:00 2001 From: saml33 Date: Fri, 10 Feb 2023 20:41:06 +1100 Subject: [PATCH 22/27] fix account action modals max bug --- components/BorrowForm.tsx | 4 ++-- components/DepositForm.tsx | 4 ++-- components/RepayForm.tsx | 4 ++-- components/WithdrawForm.tsx | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/components/BorrowForm.tsx b/components/BorrowForm.tsx index a79c8fc3..70ed6c04 100644 --- a/components/BorrowForm.tsx +++ b/components/BorrowForm.tsx @@ -103,7 +103,7 @@ function BorrowForm({ onSuccess, token }: BorrowFormProps) { new Decimal(percentage).div(100).mul(tokenMax), bank.mintDecimals ) - setInputAmount(amount.toString()) + setInputAmount(amount.toFixed()) }, [tokenMax, bank] ) @@ -111,7 +111,7 @@ function BorrowForm({ onSuccess, token }: BorrowFormProps) { const setMax = useCallback(() => { if (!bank) return const max = floorToDecimal(tokenMax, bank.mintDecimals) - setInputAmount(max.toString()) + setInputAmount(max.toFixed()) handleSizePercentage('100') }, [bank, tokenMax, handleSizePercentage]) diff --git a/components/DepositForm.tsx b/components/DepositForm.tsx index 6879b5b1..71505d18 100644 --- a/components/DepositForm.tsx +++ b/components/DepositForm.tsx @@ -126,7 +126,7 @@ function DepositForm({ onSuccess, token }: DepositFormProps) { const setMax = useCallback(() => { const max = floorToDecimal(tokenMax.maxAmount, tokenMax.maxDecimals) - setInputAmount(max.toString()) + setInputAmount(max.toFixed()) setSizePercentage('100') }, [tokenMax]) @@ -137,7 +137,7 @@ function DepositForm({ onSuccess, token }: DepositFormProps) { new Decimal(tokenMax.maxAmount).mul(percentage).div(100), tokenMax.maxDecimals ) - setInputAmount(amount.toString()) + setInputAmount(amount.toFixed()) }, [tokenMax] ) diff --git a/components/RepayForm.tsx b/components/RepayForm.tsx index 05a3e6e9..19e8afae 100644 --- a/components/RepayForm.tsx +++ b/components/RepayForm.tsx @@ -92,7 +92,7 @@ function RepayForm({ onSuccess, token }: RepayFormProps) { bank.mintDecimals, Decimal.ROUND_UP ) - setInputAmount(amount.toString()) + setInputAmount(amount.toFixed()) setSizePercentage('100') }, [bank, borrowAmount]) @@ -105,7 +105,7 @@ function RepayForm({ onSuccess, token }: RepayFormProps) { .div(100) .toDecimalPlaces(bank.mintDecimals, Decimal.ROUND_UP) - setInputAmount(amount.toString()) + setInputAmount(amount.toFixed()) }, [bank, borrowAmount] ) diff --git a/components/WithdrawForm.tsx b/components/WithdrawForm.tsx index d580be91..f117fba3 100644 --- a/components/WithdrawForm.tsx +++ b/components/WithdrawForm.tsx @@ -90,7 +90,7 @@ function WithdrawForm({ onSuccess, token }: WithdrawFormProps) { new Decimal(tokenMax).mul(percentage).div(100), bank.mintDecimals ) - setInputAmount(amount.toString()) + setInputAmount(amount.toFixed()) }, [bank, tokenMax] ) @@ -98,7 +98,7 @@ function WithdrawForm({ onSuccess, token }: WithdrawFormProps) { const setMax = useCallback(() => { if (!bank) return const max = floorToDecimal(tokenMax, bank.mintDecimals) - setInputAmount(max.toString()) + setInputAmount(max.toFixed()) setSizePercentage('100') }, [bank, tokenMax]) From 7bbff02367f3245e22bbda34d5f101f375aa906c Mon Sep 17 00:00:00 2001 From: saml33 Date: Fri, 10 Feb 2023 21:21:26 +1100 Subject: [PATCH 23/27] add deposit info if repayment > borrow --- components/RepayForm.tsx | 29 ++++++++++++++++++++--------- public/locales/en/common.json | 1 + public/locales/es/common.json | 1 + public/locales/ru/common.json | 1 + public/locales/zh/common.json | 1 + public/locales/zh_tw/common.json | 1 + 6 files changed, 25 insertions(+), 9 deletions(-) diff --git a/components/RepayForm.tsx b/components/RepayForm.tsx index 19e8afae..51051654 100644 --- a/components/RepayForm.tsx +++ b/components/RepayForm.tsx @@ -127,12 +127,12 @@ function RepayForm({ onSuccess, token }: RepayFormProps) { if (!mangoAccount || !group || !bank || !publicKey) return - //we don't want to left negative dust in account if someone wants to repay full amount + // we don't want to leave negative dust in the account if someone wants to repay the full amount const actualAmount = sizePercentage === '100' - ? mangoAccount.getTokenBorrowsUi(bank) < parseFloat(amount) - ? parseFloat(amount) - : mangoAccount.getTokenBorrowsUi(bank) + ? borrowAmount.toNumber() > parseFloat(amount) + ? borrowAmount.toNumber() + : parseFloat(amount) : parseFloat(amount) setSubmitting(true) @@ -178,6 +178,9 @@ function RepayForm({ onSuccess, token }: RepayFormProps) { const showInsufficientBalance = walletBalance.maxAmount < Number(inputAmount) + const outstandingAmount = borrowAmount.toNumber() - parseFloat(inputAmount) + const isDeposit = parseFloat(inputAmount) > borrowAmount.toNumber() + return banks.length ? ( <> {t('repayment-amount')}

+ {isDeposit ? ( +
+

{t('deposit-amount')}

+ +
+ ) : null}

{t('outstanding-balance')}

- {formatNumericValue( - Number(borrowAmount) - Number(inputAmount), - bank.mintDecimals - )}{' '} + {outstandingAmount > 0 + ? formatNumericValue(outstandingAmount, bank.mintDecimals) + : 0}{' '} {selectedToken} @@ -326,7 +337,7 @@ function RepayForm({ onSuccess, token }: RepayFormProps) { ) : (

- {t('repay')} + {isDeposit ? t('repay-deposit') : t('repay')}
)} diff --git a/public/locales/en/common.json b/public/locales/en/common.json index e927842a..43511d36 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -101,6 +101,7 @@ "remove": "Remove", "remove-delegate": "Remove Delegate", "repay": "Repay", + "repay-deposit": "Repay & Deposit", "repay-borrow": "Repay Borrow", "repayment-amount": "Repayment Amount", "rolling-change": "24h Change", diff --git a/public/locales/es/common.json b/public/locales/es/common.json index 3e528707..1da50ac2 100644 --- a/public/locales/es/common.json +++ b/public/locales/es/common.json @@ -101,6 +101,7 @@ "remove": "Remove", "remove-delegate": "Remove Delegate", "repay": "Repay", + "repay-deposit": "Repay & Deposit", "repay-borrow": "Repay Borrow", "repayment-amount": "Repayment Amount", "rolling-change": "24h Change", diff --git a/public/locales/ru/common.json b/public/locales/ru/common.json index 3e528707..1da50ac2 100644 --- a/public/locales/ru/common.json +++ b/public/locales/ru/common.json @@ -101,6 +101,7 @@ "remove": "Remove", "remove-delegate": "Remove Delegate", "repay": "Repay", + "repay-deposit": "Repay & Deposit", "repay-borrow": "Repay Borrow", "repayment-amount": "Repayment Amount", "rolling-change": "24h Change", diff --git a/public/locales/zh/common.json b/public/locales/zh/common.json index 3e528707..1da50ac2 100644 --- a/public/locales/zh/common.json +++ b/public/locales/zh/common.json @@ -101,6 +101,7 @@ "remove": "Remove", "remove-delegate": "Remove Delegate", "repay": "Repay", + "repay-deposit": "Repay & Deposit", "repay-borrow": "Repay Borrow", "repayment-amount": "Repayment Amount", "rolling-change": "24h Change", diff --git a/public/locales/zh_tw/common.json b/public/locales/zh_tw/common.json index 3e528707..1da50ac2 100644 --- a/public/locales/zh_tw/common.json +++ b/public/locales/zh_tw/common.json @@ -101,6 +101,7 @@ "remove": "Remove", "remove-delegate": "Remove Delegate", "repay": "Repay", + "repay-deposit": "Repay & Deposit", "repay-borrow": "Repay Borrow", "repayment-amount": "Repayment Amount", "rolling-change": "24h Change", From 180d7bf79ce2ff1bf51fce8b076bc87596793c0c Mon Sep 17 00:00:00 2001 From: saml33 Date: Fri, 10 Feb 2023 23:55:06 +1100 Subject: [PATCH 24/27] improve detailed charts --- components/shared/Change.tsx | 4 +- components/shared/DetailedAreaChart.tsx | 75 ++++++++--- components/stats/MangoPerpStatsCharts.tsx | 38 +----- components/stats/PerpMarketDetails.tsx | 116 ++++++++++++------ components/stats/PerpMarketsTable.tsx | 51 +++++--- components/stats/PerpStats.tsx | 2 +- components/stats/TotalDepositBorrowCharts.tsx | 34 +---- components/token/ChartTabs.tsx | 36 +++--- 8 files changed, 191 insertions(+), 165 deletions(-) diff --git a/components/shared/Change.tsx b/components/shared/Change.tsx index e877a9bb..3bb5d95f 100644 --- a/components/shared/Change.tsx +++ b/components/shared/Change.tsx @@ -4,11 +4,13 @@ import FormatNumericValue from './FormatNumericValue' const Change = ({ change, + decimals, prefix, size, suffix, }: { change: number | typeof NaN + decimals?: number prefix?: string size?: 'small' suffix?: string @@ -44,7 +46,7 @@ const Change = ({ {prefix ? prefix : ''} {suffix ? suffix : ''}

diff --git a/components/shared/DetailedAreaChart.tsx b/components/shared/DetailedAreaChart.tsx index d38aae06..ed0277dd 100644 --- a/components/shared/DetailedAreaChart.tsx +++ b/components/shared/DetailedAreaChart.tsx @@ -45,6 +45,7 @@ interface DetailedAreaChartProps { tickFormat?: (x: number) => string title?: string xKey: string + yDecimals?: number yKey: string } @@ -72,6 +73,7 @@ const DetailedAreaChart: FunctionComponent = ({ tickFormat, title, xKey, + yDecimals, yKey, }) => { const { t } = useTranslation('common') @@ -92,22 +94,43 @@ const DetailedAreaChart: FunctionComponent = ({ setMouseData(null) } - const calculateChartChange = () => { - if (data.length) { - if (mouseData) { - const index = data.findIndex((d: any) => d[xKey] === mouseData[xKey]) - const change = index >= 0 ? data[index][yKey] - data[0][yKey] : 0 - return isNaN(change) ? 0 : change - } else return data[data.length - 1][yKey] - data[0][yKey] - } - return 0 - } - const flipGradientCoords = useMemo(() => { if (!data.length) return return data[0][yKey] <= 0 && data[data.length - 1][yKey] <= 0 }, [data]) + const filteredData = useMemo(() => { + if (!data.length) return [] + if (daysToShow !== '30') { + const seconds = Number(daysToShow) * 86400 + const filtered = data.filter((d: any) => { + const dataTime = new Date(d[xKey]).getTime() / 1000 + const now = new Date().getTime() / 1000 + const limit = now - seconds + return dataTime >= limit + }) + return filtered + } + return data + }, [data, daysToShow]) + + const calculateChartChange = () => { + if (filteredData.length) { + if (mouseData) { + const index = filteredData.findIndex( + (d: any) => d[xKey] === mouseData[xKey] + ) + const change = + index >= 0 ? filteredData[index][yKey] - filteredData[0][yKey] : 0 + return isNaN(change) ? 0 : change + } else + return ( + filteredData[filteredData.length - 1][yKey] - filteredData[0][yKey] + ) + } + return 0 + } + return ( @@ -119,7 +142,7 @@ const DetailedAreaChart: FunctionComponent = ({ } w-full rounded-lg bg-th-bkg-2`} /> - ) : data.length ? ( + ) : filteredData.length ? (
@@ -157,7 +180,8 @@ const DetailedAreaChart: FunctionComponent = ({ numbers={`${ mouseData[yKey] < 0 ? '-' : '' }${prefix}${formatNumericValue( - Math.abs(mouseData[yKey]) + Math.abs(mouseData[yKey]), + yDecimals )}${suffix}`} /> ) : ( @@ -166,6 +190,7 @@ const DetailedAreaChart: FunctionComponent = ({ {prefix} {suffix} @@ -174,6 +199,7 @@ const DetailedAreaChart: FunctionComponent = ({ @@ -203,17 +229,25 @@ const DetailedAreaChart: FunctionComponent = ({ width={small ? 17 : 30} play numbers={`${ - data[data.length - 1][yKey] < 0 ? '-' : '' + filteredData[filteredData.length - 1][yKey] < 0 + ? '-' + : '' }${prefix}${formatNumericValue( - Math.abs(data[data.length - 1][yKey]) + Math.abs( + filteredData[filteredData.length - 1][yKey] + ), + yDecimals )}${suffix}`} /> ) : ( - {data[data.length - 1][yKey] < 0 ? '-' : ''} + {filteredData[filteredData.length - 1][yKey] < 0 + ? '-' + : ''} {prefix} {suffix} @@ -222,6 +256,7 @@ const DetailedAreaChart: FunctionComponent = ({ @@ -233,9 +268,9 @@ const DetailedAreaChart: FunctionComponent = ({ small ? 'text-xs' : 'text-sm' } text-th-fgd-4`} > - {dayjs(data[data.length - 1][xKey]).format( - 'DD MMM YY, h:mma' - )} + {dayjs( + filteredData[filteredData.length - 1][xKey] + ).format('DD MMM YY, h:mma')}

)} @@ -258,7 +293,7 @@ const DetailedAreaChart: FunctionComponent = ({
diff --git a/components/stats/MangoPerpStatsCharts.tsx b/components/stats/MangoPerpStatsCharts.tsx index b12f0b1a..779b73e8 100644 --- a/components/stats/MangoPerpStatsCharts.tsx +++ b/components/stats/MangoPerpStatsCharts.tsx @@ -68,42 +68,12 @@ const MangoPerpStatsCharts = () => { return values.reverse() }, [perpStats]) - const filteredOiValues = useMemo(() => { - if (!totalOpenInterestValues.length) return [] - if (oiDaysToShow !== '30') { - const seconds = Number(oiDaysToShow) * 86400 - const data = totalOpenInterestValues.filter((d: OiValueItem) => { - const dataTime = new Date(d.date).getTime() / 1000 - const now = new Date().getTime() / 1000 - const limit = now - seconds - return dataTime >= limit - }) - return data - } - return totalOpenInterestValues - }, [totalOpenInterestValues, oiDaysToShow]) - - const filteredFeesValues = useMemo(() => { - if (!totalFeeValues.length) return [] - if (feesDaysToShow !== '30') { - const seconds = Number(feesDaysToShow) * 86400 - const data = totalFeeValues.filter((d: FeeValueItem) => { - const dataTime = new Date(d.date).getTime() / 1000 - const now = new Date().getTime() / 1000 - const limit = now - seconds - return dataTime >= limit - }) - return data - } - return totalFeeValues - }, [totalFeeValues, feesDaysToShow]) - return ( <> - {totalFeeValues.length ? ( + {totalOpenInterestValues.length ? (
{ />
) : null} - {totalOpenInterestValues.length ? ( + {totalFeeValues.length ? (
import('@components/shared/DetailedAreaChart'), { ssr: false } ) const PerpMarketDetails = ({ - perpMarket, + perpMarketName, setShowPerpDetails, }: { - perpMarket: string + perpMarketName: string setShowPerpDetails: (x: string) => void }) => { const { t } = useTranslation(['common', 'trade']) + const perpMarkets = mangoStore((s) => s.perpMarkets) const perpStats = mangoStore((s) => s.perpStats.data) const loadingPerpStats = mangoStore((s) => s.perpStats.loading) + const [priceDaysToShow, setPriceDaysToShow] = useState('30') + const [oiDaysToShow, setOiDaysToShow] = useState('30') + const [hourlyFundingeDaysToShow, setHourlyFundingDaysToShow] = useState('30') + const [instantFundingDaysToShow, setInstantFundingDaysToShow] = useState('30') + const rate = usePerpFundingRate() - const marketStats = useMemo(() => { - if (!perpStats) return [] - return perpStats.filter((stat) => stat.perp_market === perpMarket).reverse() + const perpMarket = useMemo(() => { + return perpMarkets.find((m) => (m.name = perpMarketName)) + }, [perpMarkets, perpMarketName]) + + const [marketStats, lastStat] = useMemo(() => { + if (!perpStats) return [[], undefined] + const stats = perpStats + .filter((stat) => stat.perp_market === perpMarketName) + .reverse() + return [stats, stats[stats.length - 1]] }, [perpStats]) + const fundingRate = useMemo(() => { + if (!lastStat) return 0 + if (rate?.isSuccess) { + const marketRate = rate?.data?.find( + (r) => r.market_index === perpMarket?.perpMarketIndex + ) + return marketRate?.funding_rate_hourly + } + return lastStat.instantaneous_funding_rate + }, [rate, lastStat]) + return (
@@ -38,38 +63,24 @@ const PerpMarketDetails = ({ > -

{`${perpMarket} ${t('stats')}`}

+

{`${perpMarketName} ${t('stats')}`}

- {loadingPerpStats ? ( - <> -
- -
- -
-
- -
- -
-
- -
- -
-
- -
- -
- - ) : marketStats.length ? ( + {marketStats.length && lastStat ? ( <>
formatYAxis(x)} title={t('price')} @@ -79,10 +90,21 @@ const PerpMarketDetails = ({
Math.floor(x).toString()} + loading={loadingPerpStats} + loaderHeightClass="h-[350px]" + tickFormat={(x) => formatYAxis(x)} title={t('trade:open-interest')} xKey="date_hour" yKey={'open_interest'} @@ -91,25 +113,39 @@ const PerpMarketDetails = ({
formatNumericValue(x, 4)} title={t('trade:hourly-funding')} xKey="date_hour" yKey={'funding_rate_hourly'} + yDecimals={5} />
formatNumericValue(x, 4)} title={t('trade:instantaneous-funding')} xKey="date_hour" yKey={'instantaneous_funding_rate'} + yDecimals={5} />
diff --git a/components/stats/PerpMarketsTable.tsx b/components/stats/PerpMarketsTable.tsx index f5692c8c..f67565b6 100644 --- a/components/stats/PerpMarketsTable.tsx +++ b/components/stats/PerpMarketsTable.tsx @@ -87,7 +87,7 @@ const PerpMarketsTable = ({ : 0 let fundingRate - if (rate.isSuccess && market instanceof PerpMarket) { + if (rate.isSuccess) { const marketRate = rate?.data?.find( (r) => r.market_index === market.perpMarketIndex ) @@ -198,6 +198,7 @@ const PerpMarketsTable = ({ ) })} @@ -209,7 +210,13 @@ const PerpMarketsTable = ({ export default PerpMarketsTable -const MobilePerpMarketItem = ({ market }: { market: PerpMarket }) => { +const MobilePerpMarketItem = ({ + market, + setShowPerpDetails, +}: { + market: PerpMarket + setShowPerpDetails: (x: string) => void +}) => { const { t } = useTranslation('common') const loadingPerpStats = mangoStore((s) => s.perpStats.loading) const perpStats = mangoStore((s) => s.perpStats.data) @@ -252,24 +259,30 @@ const MobilePerpMarketItem = ({ market }: { market: PerpMarket }) => {
+ {!loadingPerpStats ? ( + marketStats.length ? ( +
+ = 0 ? COLORS.UP[theme] : COLORS.DOWN[theme]} + data={marketStats} + name={market.name} + xKey="date_hour" + yKey="price" + /> +
+ ) : symbol === 'USDC' || symbol === 'USDT' ? null : ( +

{t('unavailable')}

+ ) + ) : ( +
+ )}
- {!loadingPerpStats ? ( - marketStats.length ? ( -
- = 0 ? COLORS.UP[theme] : COLORS.DOWN[theme]} - data={marketStats} - name={market.name} - xKey="date_hour" - yKey="price" - /> -
- ) : symbol === 'USDC' || symbol === 'USDT' ? null : ( -

{t('unavailable')}

- ) - ) : ( -
- )} + setShowPerpDetails(market.name)} + size="medium" + > + +
) diff --git a/components/stats/PerpStats.tsx b/components/stats/PerpStats.tsx index f09eb8ab..74e29597 100644 --- a/components/stats/PerpStats.tsx +++ b/components/stats/PerpStats.tsx @@ -8,7 +8,7 @@ const PerpStats = () => { ) : ( ) diff --git a/components/stats/TotalDepositBorrowCharts.tsx b/components/stats/TotalDepositBorrowCharts.tsx index af9a9c56..cd392c5a 100644 --- a/components/stats/TotalDepositBorrowCharts.tsx +++ b/components/stats/TotalDepositBorrowCharts.tsx @@ -48,36 +48,6 @@ const TotalDepositBorrowCharts = () => { return values.reverse() }, [tokenStats]) - const filteredBorrowValues = useMemo(() => { - if (!totalDepositBorrowValues) return [] - if (borrowDaysToShow !== '30') { - const seconds = Number(borrowDaysToShow) * 86400 - const data = totalDepositBorrowValues.filter((d) => { - const dataTime = new Date(d.date).getTime() / 1000 - const now = new Date().getTime() / 1000 - const limit = now - seconds - return dataTime >= limit - }) - return data - } - return totalDepositBorrowValues - }, [totalDepositBorrowValues, borrowDaysToShow]) - - const filteredDepositValues = useMemo(() => { - if (!totalDepositBorrowValues) return [] - if (depositDaysToShow !== '30') { - const seconds = Number(depositDaysToShow) * 86400 - const data = totalDepositBorrowValues.filter((d) => { - const dataTime = new Date(d.date).getTime() / 1000 - const now = new Date().getTime() / 1000 - const limit = now - seconds - return dataTime >= limit - }) - return data - } - return totalDepositBorrowValues - }, [totalDepositBorrowValues, depositDaysToShow]) - const [currentTotalDepositValue, currentTotalBorrowValue] = useMemo(() => { if (banks.length) { return [ @@ -92,7 +62,7 @@ const TotalDepositBorrowCharts = () => { <>
{
{ }, []) }, [tokenStats]) - const filterStats = (daysToShow: string) => { - if (!statsHistory.length) return [] - if (daysToShow !== '30') { - const seconds = Number(daysToShow) * 86400 - const data = statsHistory.filter((d) => { - const dataTime = new Date(d.date_hour).getTime() / 1000 - const now = new Date().getTime() / 1000 - const limit = now - seconds - return dataTime >= limit - }) - return data - } - return statsHistory - } + // const filterStats = (daysToShow: string) => { + // if (!statsHistory.length) return [] + // if (daysToShow !== '30') { + // const seconds = Number(daysToShow) * 86400 + // const data = statsHistory.filter((d) => { + // const dataTime = new Date(d.date_hour).getTime() / 1000 + // const now = new Date().getTime() / 1000 + // const limit = now - seconds + // return dataTime >= limit + // }) + // return data + // } + // return statsHistory + // } return (
@@ -77,7 +77,7 @@ const ChartTabs = ({ token }: { token: string }) => {
{activeDepositsTab === 'token:deposits' ? ( { /> ) : ( {
{activeBorrowsTab === 'token:borrows' ? ( { /> ) : ( Date: Fri, 10 Feb 2023 13:44:49 -0500 Subject: [PATCH 25/27] pnl and account value change improvements. UX was too confusing --- components/account/AccountPage.tsx | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/components/account/AccountPage.tsx b/components/account/AccountPage.tsx index 9c4f57ba..b68b2f64 100644 --- a/components/account/AccountPage.tsx +++ b/components/account/AccountPage.tsx @@ -16,11 +16,7 @@ const SimpleAreaChart = dynamic( import { COLORS } from '../../styles/colors' import { useTheme } from 'next-themes' import { IconButton } from '../shared/Button' -import { - ArrowsPointingOutIcon, - ChartBarIcon, - ClockIcon, -} from '@heroicons/react/20/solid' +import { ArrowsPointingOutIcon, ChartBarIcon } from '@heroicons/react/20/solid' import { Transition } from '@headlessui/react' import AccountTabs from './AccountTabs' import SheenLoader from '../shared/SheenLoader' @@ -271,7 +267,7 @@ const AccountPage = () => {
-

{t('today')}

+

24h Change

{performanceInitialLoad ? ( @@ -467,7 +463,7 @@ const AccountPage = () => { ) : null} - + {/* { > - + */}
) : null}
@@ -486,10 +482,10 @@ const AccountPage = () => { isUsd={true} />

-
+ {/*

{t('today')}

-
+
*/}
From 0974463f00c04d360a27fbbab47e3c88c980e812 Mon Sep 17 00:00:00 2001 From: tjs Date: Fri, 10 Feb 2023 13:50:32 -0500 Subject: [PATCH 26/27] tweak yaxis for chart data --- components/shared/DetailedAreaChart.tsx | 4 ++-- components/shared/SimpleAreaChart.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/components/shared/DetailedAreaChart.tsx b/components/shared/DetailedAreaChart.tsx index ed0277dd..d34255de 100644 --- a/components/shared/DetailedAreaChart.tsx +++ b/components/shared/DetailedAreaChart.tsx @@ -371,9 +371,9 @@ const DetailedAreaChart: FunctionComponent = ({ if (difference < 0.1) { return [dataMin - 0.01, dataMax + 0.01] } else if (difference < 1) { - return [dataMin - 1, dataMax + 1] + return [dataMin - 0.1, dataMax + 0.1] } else if (difference < 10) { - return [dataMin - 10, dataMax + 10] + return [dataMin - 1, dataMax + 1] } else { return [dataMin, dataMax] } diff --git a/components/shared/SimpleAreaChart.tsx b/components/shared/SimpleAreaChart.tsx index 2c2ca4c1..206c58bb 100644 --- a/components/shared/SimpleAreaChart.tsx +++ b/components/shared/SimpleAreaChart.tsx @@ -47,9 +47,9 @@ const SimpleAreaChart = ({ if (difference < 0.1) { return [dataMin - 0.01, dataMax + 0.01] } else if (difference < 1) { - return [dataMin - 1, dataMax + 1] + return [dataMin - 0.1, dataMax + 0.11] } else if (difference < 10) { - return [dataMin - 10, dataMax + 10] + return [dataMin - 1, dataMax + 1] } else { return [dataMin, dataMax] } From 508c4213a1f12b80f1c3dcdd998faec722cefe30 Mon Sep 17 00:00:00 2001 From: saml33 Date: Sat, 11 Feb 2023 23:40:23 +1100 Subject: [PATCH 27/27] fix account performance data --- components/account/AccountChart.tsx | 57 ++++------------ components/account/AccountPage.tsx | 87 +++++++++++-------------- components/borrow/BorrowPage.tsx | 12 ---- components/shared/ChartRangeButtons.tsx | 50 +++++++------- components/shared/DetailedAreaChart.tsx | 19 +++--- components/wallet/ConnectedMenu.tsx | 3 +- store/mangoStore.ts | 11 +--- 7 files changed, 83 insertions(+), 156 deletions(-) diff --git a/components/account/AccountChart.tsx b/components/account/AccountChart.tsx index c67c8fed..6c20bb7f 100644 --- a/components/account/AccountChart.tsx +++ b/components/account/AccountChart.tsx @@ -1,7 +1,6 @@ -import { toUiDecimalsForQuote } from '@blockworks-foundation/mango-v4' import { useTranslation } from 'next-i18next' -import { useEffect, useMemo, useState } from 'react' -import mangoStore from '@store/mangoStore' +import { useMemo, useState } from 'react' +import { PerformanceDataItem } from '@store/mangoStore' import dynamic from 'next/dynamic' import { formatYAxis } from 'utils/formatting' const DetailedAreaChart = dynamic( @@ -11,73 +10,39 @@ const DetailedAreaChart = dynamic( const AccountChart = ({ chartToShow, + data, hideChart, - mangoAccountAddress, yKey, }: { chartToShow: string + data: PerformanceDataItem[] hideChart: () => void - mangoAccountAddress: string yKey: string }) => { const { t } = useTranslation('common') - const actions = mangoStore.getState().actions const [daysToShow, setDaysToShow] = useState('1') - const loading = mangoStore((s) => s.mangoAccount.performance.loading) - const performanceData = mangoStore((s) => s.mangoAccount.performance.data) - useEffect(() => { - if (mangoAccountAddress) { - actions.fetchAccountPerformance(mangoAccountAddress, 1) - } - }, [actions, mangoAccountAddress]) - - const data: any = useMemo(() => { - if (!performanceData.length) return [] + const chartData: any = useMemo(() => { + if (!data.length) return [] if (chartToShow === 'cumulative-interest-value') { - performanceData.map((d) => ({ + data.map((d) => ({ interest_value: d.borrow_interest_cumulative_usd + d.deposit_interest_cumulative_usd, time: d.time, })) } - return performanceData - }, [performanceData]) - - const handleDaysToShow = async (days: string) => { - const mangoAccount = mangoStore.getState().mangoAccount.current - if (mangoAccount) { - await actions.fetchAccountPerformance( - mangoAccount.publicKey.toString(), - parseInt(days) - ) - setDaysToShow(days) - } - } - - const currentValue = useMemo(() => { - const mangoAccount = mangoStore.getState().mangoAccount.current - const group = mangoStore.getState().group - if (group && mangoAccount && chartToShow === 'account-value') { - const currentAccountValue = toUiDecimalsForQuote( - mangoAccount.getEquity(group).toNumber() - ) - const time = Date.now() - return [{ account_equity: currentAccountValue, time: time }] - } - return [] - }, [chartToShow]) + return data + }, [data]) return ( `$${formatYAxis(x)}`} title={t(chartToShow)} xKey="time" diff --git a/components/account/AccountPage.tsx b/components/account/AccountPage.tsx index b68b2f64..954f5a71 100644 --- a/components/account/AccountPage.tsx +++ b/components/account/AccountPage.tsx @@ -44,10 +44,10 @@ const AccountPage = () => { const { t } = useTranslation(['common', 'account']) // const { connected } = useWallet() const { group } = useMangoGroup() - const { mangoAccount, mangoAccountAddress, initialLoad } = useMangoAccount() + const { mangoAccount, mangoAccountAddress } = useMangoAccount() const actions = mangoStore.getState().actions - const performanceInitialLoad = mangoStore( - (s) => s.mangoAccount.performance.initialLoad + const performanceLoading = mangoStore( + (s) => s.mangoAccount.performance.loading ) const performanceData = mangoStore((s) => s.mangoAccount.performance.data) const totalInterestData = mangoStore( @@ -56,9 +56,6 @@ const AccountPage = () => { const [chartToShow, setChartToShow] = useState< 'account-value' | 'cumulative-interest-value' | 'pnl' | '' >('') - const [oneDayPerformanceData, setOneDayPerformanceData] = useState< - PerformanceDataItem[] - >([]) const [showExpandChart, setShowExpandChart] = useState(false) const [showPnlHistory, setShowPnlHistory] = useState(false) const { theme } = useTheme() @@ -72,26 +69,21 @@ const AccountPage = () => { ) useEffect(() => { - if (mangoAccountAddress || (!initialLoad && !mangoAccountAddress)) { - const set = mangoStore.getState().set - set((s) => { - s.mangoAccount.performance.initialLoad = false - }) - setOneDayPerformanceData([]) - actions.fetchAccountPerformance(mangoAccountAddress, 1) + if (mangoAccountAddress) { + console.log('fired') + actions.fetchAccountPerformance(mangoAccountAddress, 31) actions.fetchAccountInterestTotals(mangoAccountAddress) } - }, [actions, initialLoad, mangoAccountAddress]) + }, [actions, mangoAccountAddress]) - useEffect(() => { - if ( - performanceData.length && - performanceInitialLoad && - !oneDayPerformanceData.length - ) { - setOneDayPerformanceData(performanceData) - } - }, [performanceInitialLoad, oneDayPerformanceData, performanceData]) + const oneDayPerformanceData: PerformanceDataItem[] | [] = useMemo(() => { + if (!performanceData || !performanceData.length) return [] + const nowDate = new Date() + return performanceData.filter((d) => { + const dataTime = new Date(d.time).getTime() + return dataTime >= nowDate.getTime() - 86400000 + }) + }, [performanceData]) const onHoverMenu = (open: boolean, action: string) => { if ( @@ -108,10 +100,6 @@ const AccountPage = () => { } const handleHideChart = () => { - const set = mangoStore.getState().set - set((s) => { - s.mangoAccount.performance.data = oneDayPerformanceData - }) setChartToShow('') } @@ -172,15 +160,16 @@ const AccountPage = () => { const oneDayInterestChange = useMemo(() => { if (oneDayPerformanceData.length) { - const startDayInterest = - oneDayPerformanceData[0].borrow_interest_cumulative_usd + - oneDayPerformanceData[0].deposit_interest_cumulative_usd + const first = oneDayPerformanceData[0] + const latest = oneDayPerformanceData[oneDayPerformanceData.length - 1] - const latest = oneDayPerformanceData.length - 1 + const startDayInterest = + first.borrow_interest_cumulative_usd + + first.deposit_interest_cumulative_usd const endDayInterest = - oneDayPerformanceData[latest].borrow_interest_cumulative_usd + - oneDayPerformanceData[latest].deposit_interest_cumulative_usd + latest.borrow_interest_cumulative_usd + + latest.deposit_interest_cumulative_usd return endDayInterest - startDayInterest } @@ -209,18 +198,18 @@ const AccountPage = () => { const latestAccountData = useMemo(() => { if (!accountValue || !performanceData.length) return [] - const latestIndex = performanceData.length - 1 + const latestDataItem = performanceData[performanceData.length - 1] return [ { account_equity: accountValue, time: dayjs(Date.now()).toISOString(), borrow_interest_cumulative_usd: - performanceData[latestIndex].borrow_interest_cumulative_usd, + latestDataItem.borrow_interest_cumulative_usd, deposit_interest_cumulative_usd: - performanceData[latestIndex].deposit_interest_cumulative_usd, - pnl: performanceData[latestIndex].pnl, - spot_value: performanceData[latestIndex].spot_value, - transfer_balance: performanceData[latestIndex].transfer_balance, + latestDataItem.deposit_interest_cumulative_usd, + pnl: latestDataItem.pnl, + spot_value: latestDataItem.spot_value, + transfer_balance: latestDataItem.transfer_balance, }, ] }, [accountValue, performanceData]) @@ -267,10 +256,10 @@ const AccountPage = () => {
-

24h Change

+

{t('rolling-change')}

- {performanceInitialLoad ? ( + {!performanceLoading ? ( oneDayPerformanceData.length ? (
{ isUsd={true} />

- {/*
+
-

{t('today')}

-
*/} +

{t('rolling-change')}

+
@@ -522,9 +511,9 @@ const AccountPage = () => { isUsd={true} />

-
+
-

{t('today')}

+

{t('rolling-change')}

@@ -546,22 +535,22 @@ const AccountPage = () => { {chartToShow === 'account-value' ? ( ) : chartToShow === 'pnl' ? ( ) : ( )} diff --git a/components/borrow/BorrowPage.tsx b/components/borrow/BorrowPage.tsx index 475a0b46..0a7bb18f 100644 --- a/components/borrow/BorrowPage.tsx +++ b/components/borrow/BorrowPage.tsx @@ -7,7 +7,6 @@ import { useTranslation } from 'next-i18next' import { ANIMATION_SETTINGS_KEY } from 'utils/constants' import FlipNumbers from 'react-flip-numbers' import Button from '@components/shared/Button' -import mangoStore from '@store/mangoStore' import { formatCurrencyValue } from 'utils/numbers' import { useEffect, useMemo, useState } from 'react' import YourBorrowsTable from './YourBorrowsTable' @@ -47,17 +46,6 @@ const BorrowPage = () => { ANIMATION_SETTINGS_KEY, INITIAL_ANIMATION_SETTINGS ) - const actions = mangoStore((s) => s.actions) - - useEffect(() => { - if (mangoAccountAddress) { - const set = mangoStore.getState().set - set((s) => { - s.mangoAccount.performance.initialLoad = false - }) - actions.fetchAccountPerformance(mangoAccountAddress, 1) - } - }, [actions, mangoAccountAddress]) const filteredBanks = useMemo(() => { if (banks.length) { diff --git a/components/shared/ChartRangeButtons.tsx b/components/shared/ChartRangeButtons.tsx index f05aa399..9dc4d784 100644 --- a/components/shared/ChartRangeButtons.tsx +++ b/components/shared/ChartRangeButtons.tsx @@ -16,38 +16,36 @@ const ChartRangeButtons: FunctionComponent = ({ names, }) => { return ( -
-
- {activeValue && values.includes(activeValue) ? ( -
v === activeValue) * 100 - }%)`, - width: `${100 / values.length}%`, - }} - /> - ) : null} - {values.map((v, i) => ( - - ))} -
+ key={`${v}${i}`} + onClick={() => onChange(v)} + style={{ + width: `${100 / values.length}%`, + }} + > + {names ? names[i] : v} + + ))}
) } diff --git a/components/shared/DetailedAreaChart.tsx b/components/shared/DetailedAreaChart.tsx index d34255de..cdcb0359 100644 --- a/components/shared/DetailedAreaChart.tsx +++ b/components/shared/DetailedAreaChart.tsx @@ -101,17 +101,14 @@ const DetailedAreaChart: FunctionComponent = ({ const filteredData = useMemo(() => { if (!data.length) return [] - if (daysToShow !== '30') { - const seconds = Number(daysToShow) * 86400 - const filtered = data.filter((d: any) => { - const dataTime = new Date(d[xKey]).getTime() / 1000 - const now = new Date().getTime() / 1000 - const limit = now - seconds - return dataTime >= limit - }) - return filtered - } - return data + const start = Number(daysToShow) * 86400000 + const filtered = data.filter((d: any) => { + const dataTime = new Date(d[xKey]).getTime() + const now = new Date().getTime() + const limit = now - start + return dataTime >= limit + }) + return filtered }, [data, daysToShow]) const calculateChartChange = () => { diff --git a/components/wallet/ConnectedMenu.tsx b/components/wallet/ConnectedMenu.tsx index d63ca8fd..74393296 100644 --- a/components/wallet/ConnectedMenu.tsx +++ b/components/wallet/ConnectedMenu.tsx @@ -49,8 +49,7 @@ const ConnectedMenu = () => { state.mangoAccount.interestTotals = { data: [], loading: false } state.mangoAccount.performance = { data: [], - loading: false, - initialLoad: false, + loading: true, } }) disconnect() diff --git a/store/mangoStore.ts b/store/mangoStore.ts index e3946ad8..f981faba 100644 --- a/store/mangoStore.ts +++ b/store/mangoStore.ts @@ -268,7 +268,6 @@ export type MangoStore = { performance: { data: PerformanceDataItem[] loading: boolean - initialLoad: boolean } swapHistory: { data: SwapHistoryItem[] @@ -411,7 +410,7 @@ const mangoStore = create()( perpPositions: [], spotBalances: {}, interestTotals: { data: [], loading: false }, - performance: { data: [], loading: false, initialLoad: false }, + performance: { data: [], loading: true }, swapHistory: { data: [], loading: true }, tradeHistory: { data: [], loading: true }, }, @@ -521,9 +520,6 @@ const mangoStore = create()( range: number ) => { const set = get().set - set((state) => { - state.mangoAccount.performance.loading = true - }) try { const response = await fetch( `${MANGO_DATA_API_URL}/stats/performance_account?mango-account=${mangoAccountPk}&start-date=${dayjs() @@ -547,13 +543,8 @@ const mangoStore = create()( } catch (e) { console.error('Failed to load account performance data', e) } finally { - const hasLoaded = - mangoStore.getState().mangoAccount.performance.initialLoad set((state) => { state.mangoAccount.performance.loading = false - if (!hasLoaded) { - state.mangoAccount.performance.initialLoad = true - } }) } },