merge main

This commit is contained in:
saml33 2023-02-12 20:33:50 +11:00
commit 93956ca44f
76 changed files with 1896 additions and 1162 deletions

View File

@ -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) {
@ -156,6 +157,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,

View File

@ -1,9 +1,11 @@
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/'
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 {

View File

@ -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))
}

246
apis/mngo/datafeed.ts Normal file
View File

@ -0,0 +1,246 @@
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<Bar[]> => {
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,
has_empty_bars: true,
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()
},
}

55
apis/mngo/helpers.ts Normal file
View File

@ -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<string, string> = {
'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
}

80
apis/mngo/streaming.ts Normal file
View File

@ -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))
}

View File

@ -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])

View File

@ -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]
)

View File

@ -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

View File

@ -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]
)
@ -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 ? (
<>
<EnterBottomExitBottom
@ -289,15 +292,23 @@ function RepayForm({ onSuccess, token }: RepayFormProps) {
<p>{t('repayment-amount')}</p>
<BankAmountWithValue amount={inputAmount} bank={bank} />
</div>
{isDeposit ? (
<div className="flex justify-between">
<p>{t('deposit-amount')}</p>
<BankAmountWithValue
amount={parseFloat(inputAmount) - borrowAmount.toNumber()}
bank={bank}
/>
</div>
) : null}
<div className="flex justify-between">
<div className="flex items-center">
<p>{t('outstanding-balance')}</p>
</div>
<p className="font-mono text-th-fgd-2">
{formatNumericValue(
Number(borrowAmount) - Number(inputAmount),
bank.mintDecimals
)}{' '}
{outstandingAmount > 0
? formatNumericValue(outstandingAmount, bank.mintDecimals)
: 0}{' '}
<span className="font-body text-th-fgd-4">
{selectedToken}
</span>
@ -326,7 +337,7 @@ function RepayForm({ onSuccess, token }: RepayFormProps) {
) : (
<div className="flex items-center">
<ArrowDownRightIcon className="mr-2 h-5 w-5" />
{t('repay')}
{isDeposit ? t('repay-deposit') : t('repay')}
</div>
)}
</Button>

View File

@ -167,7 +167,7 @@ const SideNav = ({ collapsed }: { collapsed: boolean }) => {
health={
group && mangoAccount
? mangoAccount.getHealthRatioUi(group, HealthType.maint)
: undefined
: 0
}
size={32}
/>

View File

@ -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])

View File

@ -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<string>('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 (
<DetailedAreaChart
data={data.concat(currentValue)}
data={chartData}
daysToShow={daysToShow}
heightClass="h-[calc(100vh-200px)]"
loaderHeightClass="h-[calc(100vh-116px)]"
hideChart={hideChart}
loading={loading}
prefix="$"
setDaysToShow={handleDaysToShow}
setDaysToShow={setDaysToShow}
tickFormat={(x) => `$${formatYAxis(x)}`}
title={t(chartToShow)}
xKey="time"

View File

@ -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'
@ -48,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(
@ -60,9 +56,6 @@ const AccountPage = () => {
const [chartToShow, setChartToShow] = useState<
'account-value' | 'cumulative-interest-value' | 'pnl' | ''
>('')
const [oneDayPerformanceData, setOneDayPerformanceData] = useState<
PerformanceDataItem[]
>([])
const [showExpandChart, setShowExpandChart] = useState<boolean>(false)
const [showPnlHistory, setShowPnlHistory] = useState<boolean>(false)
const { theme } = useTheme()
@ -76,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 (
@ -112,10 +100,6 @@ const AccountPage = () => {
}
const handleHideChart = () => {
const set = mangoStore.getState().set
set((s) => {
s.mangoAccount.performance.data = oneDayPerformanceData
})
setChartToShow('')
}
@ -176,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
}
@ -213,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])
@ -271,10 +256,10 @@ const AccountPage = () => {
</div>
<div className="flex items-center space-x-1.5">
<Change change={accountValueChange} prefix="$" />
<p className="text-th-fgd-4">{t('today')}</p>
<p className="text-xs text-th-fgd-4">{t('rolling-change')}</p>
</div>
</div>
{performanceInitialLoad ? (
{!performanceLoading ? (
oneDayPerformanceData.length ? (
<div
className="relative mt-4 flex h-40 items-end md:mt-0 md:h-24 md:w-48"
@ -467,7 +452,7 @@ const AccountPage = () => {
</IconButton>
</Tooltip>
) : null}
<Tooltip content={t('account:pnl-history')} delay={250}>
{/* <Tooltip content={t('account:pnl-history')} delay={250}>
<IconButton
className="text-th-fgd-3"
hideBg
@ -475,7 +460,7 @@ const AccountPage = () => {
>
<ClockIcon className="h-5 w-5" />
</IconButton>
</Tooltip>
</Tooltip> */}
</div>
) : null}
</div>
@ -486,9 +471,9 @@ const AccountPage = () => {
isUsd={true}
/>
</p>
<div className="flex space-x-1">
<div className="flex space-x-1.5">
<Change change={oneDayPnlChange} prefix="$" size="small" />
<p className="text-xs text-th-fgd-4">{t('today')}</p>
<p className="text-xs text-th-fgd-4">{t('rolling-change')}</p>
</div>
</div>
</div>
@ -526,9 +511,9 @@ const AccountPage = () => {
isUsd={true}
/>
</p>
<div className="flex space-x-1">
<div className="flex space-x-1.5">
<Change change={oneDayInterestChange} prefix="$" size="small" />
<p className="text-xs text-th-fgd-4">{t('today')}</p>
<p className="text-xs text-th-fgd-4">{t('rolling-change')}</p>
</div>
</div>
</div>
@ -550,22 +535,22 @@ const AccountPage = () => {
{chartToShow === 'account-value' ? (
<AccountChart
chartToShow="account-value"
data={performanceData.concat(latestAccountData)}
hideChart={handleHideChart}
mangoAccountAddress={mangoAccountAddress}
yKey="account_equity"
/>
) : chartToShow === 'pnl' ? (
<AccountChart
chartToShow="pnl"
data={performanceData}
hideChart={handleHideChart}
mangoAccountAddress={mangoAccountAddress}
yKey="pnl"
/>
) : (
<AccountChart
chartToShow="cumulative-interest-value"
data={performanceData}
hideChart={handleHideChart}
mangoAccountAddress={mangoAccountAddress}
yKey="interest_value"
/>
)}

View File

@ -209,9 +209,7 @@ const ActivityFeedTable = ({
<span className="tooltip-underline">{t('fee')}</span>
</Tooltip>
</Th>
<Th className="bg-th-bkg-1 text-right">
{t('activity:activity-value')}
</Th>
<Th className="bg-th-bkg-1 text-right">{t('value')}</Th>
<Th className="bg-th-bkg-1 text-right">{t('explorer')}</Th>
</TrHead>
</thead>

View File

@ -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

View File

@ -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' : ''
}`}
/>
</div>
<div className="col-span-1 flex h-1 rounded-full bg-th-bkg-3">

View File

@ -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 (
<svg
className={health && health < 10 ? 'animate-pulse' : ''}
id="account-step-eleven"
xmlns="http://www.w3.org/2000/svg"
style={styles}

View File

@ -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 = () => {
</button>
))}
</div>
{activeTab === 'activity:activity' ? <ActivityFilters /> : null}
{activeTab === 'activity:activity-feed' ? <ActivityFilters /> : null}
</div>
<TabContent activeTab={activeTab} />
</>
@ -48,7 +48,7 @@ const HistoryTabs = () => {
const TabContent = ({ activeTab }: { activeTab: string }) => {
switch (activeTab) {
case 'activity:activity':
case 'activity:activity-feed':
return <ActivityFeed />
case 'activity:swaps':
return <SwapHistoryTable />

View File

@ -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) {

View File

@ -1,5 +1,13 @@
const Label = ({ text, optional }: { text: string; optional?: boolean }) => (
<p className="mb-2 text-left text-sm text-th-fgd-3">
const Label = ({
text,
optional,
className,
}: {
text: string
optional?: boolean
className?: string
}) => (
<p className={`mb-2 text-left text-sm text-th-fgd-3 ${className}`}>
{text}{' '}
{optional ? (
<span className="ml-1 text-xs text-th-fgd-4">(Optional)</span>

View File

@ -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 (
<Modal isOpen={isOpen} onClose={onClose}>
<h2 className="mb-2">{t('trade:volume-alert')}</h2>
<p className="mb-2">{t('trade:volume-alert-desc')}</p>
<LinkButton
className="mb-4 flex w-full items-center justify-center"
onClick={() => volumeAlertSound.play()}
>
<PlayIcon className="mr-1.5 h-4 w-4" />
<span>{t('trade:preview-sound')}</span>
</LinkButton>
<div className="flex items-center justify-between rounded-md bg-th-bkg-3 p-3">
<p>{t('trade:activate-volume-alert')}</p>
<Switch
className="text-th-fgd-3"
checked={soundSettings['recent-trades']}
onChange={() =>
setSoundSettings({
...soundSettings,
'recent-trades': !soundSettings['recent-trades'],
})
}
/>
</div>
{soundSettings['recent-trades'] ? (
<>
<div className="my-4">
<Label text={t('trade:interval-seconds')} />
<NumberFormat
name="seconds"
id="seconds"
inputMode="numeric"
thousandSeparator=","
allowNegative={false}
decimalScale={0}
isNumericString={true}
className={INPUT_CLASSES}
placeholder="e.g. 30"
value={formValues.seconds}
onValueChange={(e: NumberFormatValues) =>
setFormValues({
...formValues,
seconds: e.value,
})
}
/>
</div>
<div className="mb-6">
<Label text={t('trade:notional-volume')} />
<NumberFormat
name="value"
id="value"
inputMode="numeric"
thousandSeparator=","
allowNegative={false}
isNumericString={true}
className={INPUT_CLASSES}
placeholder="e.g. 10,000"
value={formValues.value}
onValueChange={(e: NumberFormatValues) =>
setFormValues({
...formValues,
value: e.value,
})
}
/>
</div>
<Button
className="w-full"
disabled={!formValues.seconds || !formValues.value}
onClick={handleSave}
size="large"
>
{t('save')}
</Button>
</>
) : null}
</Modal>
)
}
export default TradeVolumeAlertModal

View File

@ -41,13 +41,13 @@ const SoundSettings = () => {
onChange={() => handleToggleSoundSetting('all')}
/>
</div>
<div className="flex items-center justify-between border-t border-th-bkg-3 p-4">
{/* <div className="flex items-center justify-between border-t border-th-bkg-3 p-4">
<p>{t('settings:recent-trades')}</p>
<Switch
checked={soundSettings['recent-trades']}
onChange={() => handleToggleSoundSetting('recent-trades')}
/>
</div>
</div> */}
<div className="flex items-center justify-between border-t border-th-bkg-3 p-4">
<p>{t('settings:swap-success')}</p>
<Switch

View File

@ -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 : ''}
<FormatNumericValue
value={isNaN(change) ? '0.00' : Math.abs(change)}
decimals={2}
decimals={decimals ? decimals : 2}
/>
{suffix ? suffix : ''}
</p>

View File

@ -16,7 +16,6 @@ const ChartRangeButtons: FunctionComponent<ChartRangeButtonsProps> = ({
names,
}) => {
return (
<div>
<div className="relative flex">
{activeValue && values.includes(activeValue) ? (
<div
@ -31,7 +30,7 @@ const ChartRangeButtons: FunctionComponent<ChartRangeButtonsProps> = ({
) : null}
{values.map((v, i) => (
<button
className={`${className} default-transition relative h-6 w-1/2 cursor-pointer rounded-md px-3 text-center text-xs
className={`${className} default-transition relative h-6 cursor-pointer rounded-md px-3 text-center text-xs
${
v === activeValue
? `text-th-active`
@ -48,7 +47,6 @@ const ChartRangeButtons: FunctionComponent<ChartRangeButtonsProps> = ({
</button>
))}
</div>
</div>
)
}

View File

@ -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<DetailedAreaChartProps> = ({
tickFormat,
title,
xKey,
yDecimals,
yKey,
}) => {
const { t } = useTranslation('common')
@ -92,22 +94,40 @@ const DetailedAreaChart: FunctionComponent<DetailedAreaChartProps> = ({
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 []
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 = () => {
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 (
<FadeInFadeOut show={true}>
<ContentBox hideBorder hidePadding>
@ -119,7 +139,7 @@ const DetailedAreaChart: FunctionComponent<DetailedAreaChartProps> = ({
} w-full rounded-lg bg-th-bkg-2`}
/>
</SheenLoader>
) : data.length ? (
) : filteredData.length ? (
<div className="relative">
<div className="flex items-start justify-between">
<div className="flex flex-col md:flex-row md:items-start md:space-x-6">
@ -157,7 +177,8 @@ const DetailedAreaChart: FunctionComponent<DetailedAreaChartProps> = ({
numbers={`${
mouseData[yKey] < 0 ? '-' : ''
}${prefix}${formatNumericValue(
Math.abs(mouseData[yKey])
Math.abs(mouseData[yKey]),
yDecimals
)}${suffix}`}
/>
) : (
@ -166,6 +187,7 @@ const DetailedAreaChart: FunctionComponent<DetailedAreaChartProps> = ({
{prefix}
<FormatNumericValue
value={Math.abs(mouseData[yKey])}
decimals={yDecimals}
/>
{suffix}
</span>
@ -174,6 +196,7 @@ const DetailedAreaChart: FunctionComponent<DetailedAreaChartProps> = ({
<span className="ml-3">
<Change
change={calculateChartChange()}
decimals={yDecimals}
prefix={prefix}
suffix={suffix}
/>
@ -203,17 +226,25 @@ const DetailedAreaChart: FunctionComponent<DetailedAreaChartProps> = ({
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}`}
/>
) : (
<span>
{data[data.length - 1][yKey] < 0 ? '-' : ''}
{filteredData[filteredData.length - 1][yKey] < 0
? '-'
: ''}
{prefix}
<FormatNumericValue
value={Math.abs(data[data.length - 1][yKey])}
decimals={yDecimals}
/>
{suffix}
</span>
@ -222,6 +253,7 @@ const DetailedAreaChart: FunctionComponent<DetailedAreaChartProps> = ({
<span className="ml-3">
<Change
change={calculateChartChange()}
decimals={yDecimals}
prefix={prefix}
suffix={suffix}
/>
@ -233,9 +265,9 @@ const DetailedAreaChart: FunctionComponent<DetailedAreaChartProps> = ({
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')}
</p>
</div>
)}
@ -258,7 +290,7 @@ const DetailedAreaChart: FunctionComponent<DetailedAreaChartProps> = ({
<div className="-mx-6 mt-6 h-full">
<ResponsiveContainer width="100%" height="100%">
<AreaChart
data={data}
data={filteredData}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
>
@ -336,9 +368,9 @@ const DetailedAreaChart: FunctionComponent<DetailedAreaChartProps> = ({
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]
}

View File

@ -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]
}

View File

@ -0,0 +1,111 @@
import { useTranslation } from 'next-i18next'
import { useMemo, useState } from 'react'
import dynamic from 'next/dynamic'
import mangoStore, { PerpStatsItem } 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 || !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,
feeValue: c.fees_accrued,
})
} else {
hasDate.feeValue = hasDate.feeValue + c.fees_accrued
}
return a
}, [])
return values.reverse()
}, [perpStats])
const totalOpenInterestValues = useMemo(() => {
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,
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])
return (
<>
{totalOpenInterestValues.length ? (
<div className="col-span-2 border-b border-th-bkg-3 py-4 px-6 md:col-span-1">
<DetailedAreaChart
data={totalOpenInterestValues}
daysToShow={oiDaysToShow}
setDaysToShow={setOiDaysToShow}
heightClass="h-64"
loading={loadingPerpStats}
loaderHeightClass="h-[350px]"
prefix="$"
tickFormat={(x) => `$${Math.floor(x)}`}
title={t('trade:open-interest')}
xKey="date"
yKey={'openInterest'}
/>
</div>
) : null}
{totalFeeValues.length ? (
<div className="col-span-2 border-b border-th-bkg-3 py-4 px-6 md:col-span-1 md:border-l md:pl-6">
<DetailedAreaChart
data={totalFeeValues}
daysToShow={feesDaysToShow}
setDaysToShow={setFeesDaysToShow}
heightClass="h-64"
loading={loadingPerpStats}
loaderHeightClass="h-[350px]"
prefix="$"
tickFormat={(x) => `$${x.toFixed(2)}`}
title="Perp Fees"
xKey="date"
yKey={'feeValue'}
/>
</div>
) : null}
</>
)
}
export default MangoPerpStatsCharts

View File

@ -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 (
<div className="grid grid-cols-2">
<TotalDepositBorrowCharts
tokenStats={tokenStats}
loadingStats={loadingStats}
/>
{/* uncomment below when perps launch */}
{/* {loadingPerpStats ? (
<div className="col-span-2 border-b border-th-bkg-3 py-4 px-6 md:col-span-1">
<SheenLoader className="flex flex-1">
<div className="h-96 w-full rounded-lg bg-th-bkg-2" />
</SheenLoader>
</div>
) : totalFeeValues.length ? (
<div className="col-span-2 border-b border-th-bkg-3 py-4 px-6 md:col-span-1">
<DetailedAreaChart
data={totalOpenInterestValues}
daysToShow={'999'}
heightClass="h-64"
prefix="$"
tickFormat={(x) => `$${Math.floor(x)}`}
title={t('trade:open-interest')}
xKey="date"
yKey={'openInterest'}
/>
</div>
) : null}
{loadingPerpStats ? (
<div className="col-span-2 border-b border-th-bkg-3 py-4 px-6 md:col-span-1 md:border-l md:pl-6">
<SheenLoader className="flex flex-1">
<div className="h-96 w-full rounded-lg bg-th-bkg-2" />
</SheenLoader>
</div>
) : totalOpenInterestValues.length ? (
<div className="col-span-2 border-b border-th-bkg-3 py-4 px-6 md:col-span-1 md:border-l md:pl-6">
<DetailedAreaChart
data={totalFeeValues}
daysToShow={'999'}
heightClass="h-64"
prefix="$"
tickFormat={(x) => `$${x.toFixed(2)}`}
title="Perp Fees"
xKey="date"
yKey={'feeValue'}
/>
</div>
) : null} */}
<TotalDepositBorrowCharts />
<MangoPerpStatsCharts />
</div>
)
}

View File

@ -1,33 +1,58 @@
import { IconButton } from '@components/shared/Button'
import SheenLoader from '@components/shared/SheenLoader'
import { ChevronLeftIcon } from '@heroicons/react/20/solid'
import mangoStore from '@store/mangoStore'
import dayjs from 'dayjs'
import { useTranslation } from 'next-i18next'
import dynamic from 'next/dynamic'
import { useMemo } from 'react'
import { useMemo, useState } from 'react'
import { formatYAxis } from 'utils/formatting'
import { formatNumericValue } from 'utils/numbers'
import { usePerpFundingRate } from '@components/trade/PerpFundingRate'
const DetailedAreaChart = dynamic(
() => 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 (
<div className="grid grid-cols-2">
<div className="col-span-2 flex items-center border-b border-th-bkg-3 px-6 py-3">
@ -38,38 +63,24 @@ const PerpMarketDetails = ({
>
<ChevronLeftIcon className="h-5 w-5" />
</IconButton>
<h2 className="text-lg">{`${perpMarket} ${t('stats')}`}</h2>
<h2 className="text-lg">{`${perpMarketName} ${t('stats')}`}</h2>
</div>
{loadingPerpStats ? (
<>
<div className="col-span-2 border-b border-th-bkg-3 py-4 px-6 md:col-span-1">
<SheenLoader className="flex flex-1">
<div className="h-96 w-full rounded-lg bg-th-bkg-2" />
</SheenLoader>
</div>
<div className="col-span-2 border-b border-th-bkg-3 py-4 px-6 md:col-span-1 md:border-l md:pl-6">
<SheenLoader className="flex flex-1">
<div className="h-96 w-full rounded-lg bg-th-bkg-2" />
</SheenLoader>
</div>
<div className="col-span-2 border-b border-th-bkg-3 py-4 px-6 md:col-span-1">
<SheenLoader className="flex flex-1">
<div className="h-96 w-full rounded-lg bg-th-bkg-2" />
</SheenLoader>
</div>
<div className="col-span-2 border-b border-th-bkg-3 py-4 px-6 md:col-span-1 md:border-l md:pl-6">
<SheenLoader className="flex flex-1">
<div className="h-96 w-full rounded-lg bg-th-bkg-2" />
</SheenLoader>
</div>
</>
) : marketStats.length ? (
{marketStats.length && lastStat ? (
<>
<div className="col-span-2 border-b border-th-bkg-3 py-4 px-6 md:col-span-1">
<DetailedAreaChart
data={marketStats}
daysToShow={'999'}
data={marketStats.concat([
{
...lastStat,
date_hour: dayjs().toISOString(),
price: perpMarket?._uiPrice || lastStat.price,
},
])}
daysToShow={priceDaysToShow}
setDaysToShow={setPriceDaysToShow}
heightClass="h-64"
loading={loadingPerpStats}
loaderHeightClass="h-[350px]"
prefix="$"
tickFormat={(x) => formatYAxis(x)}
title={t('price')}
@ -79,10 +90,21 @@ const PerpMarketDetails = ({
</div>
<div className="col-span-2 border-b border-th-bkg-3 py-4 px-6 md:col-span-1 md:border-l md:pl-6">
<DetailedAreaChart
data={marketStats}
daysToShow={'999'}
data={marketStats.concat([
{
...lastStat,
date_hour: dayjs().toISOString(),
open_interest:
perpMarket?.baseLotsToUi(perpMarket.openInterest) ||
lastStat.open_interest,
},
])}
daysToShow={oiDaysToShow}
setDaysToShow={setOiDaysToShow}
heightClass="h-64"
tickFormat={(x) => 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 = ({
<div className="col-span-2 border-b border-th-bkg-3 py-4 px-6 md:col-span-1">
<DetailedAreaChart
data={marketStats}
daysToShow={'999'}
daysToShow={hourlyFundingeDaysToShow}
setDaysToShow={setHourlyFundingDaysToShow}
heightClass="h-64"
loading={loadingPerpStats}
loaderHeightClass="h-[350px]"
suffix="%"
tickFormat={(x) => formatNumericValue(x, 4)}
title={t('trade:hourly-funding')}
xKey="date_hour"
yKey={'funding_rate_hourly'}
yDecimals={5}
/>
</div>
<div className="col-span-2 border-b border-th-bkg-3 py-4 px-6 md:col-span-1 md:border-l md:pl-6">
<DetailedAreaChart
data={marketStats}
daysToShow={'999'}
data={marketStats.concat([
{
...lastStat,
date_hour: dayjs().toISOString(),
instantaneous_funding_rate: fundingRate,
},
])}
daysToShow={instantFundingDaysToShow}
setDaysToShow={setInstantFundingDaysToShow}
heightClass="h-64"
loading={loadingPerpStats}
loaderHeightClass="h-[350px]"
suffix="%"
tickFormat={(x) => formatNumericValue(x, 4)}
title={t('trade:instantaneous-funding')}
xKey="date_hour"
yKey={'instantaneous_funding_rate'}
yDecimals={5}
/>
</div>
</>

View File

@ -2,33 +2,52 @@ 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'
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 }
)
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
@ -42,31 +61,33 @@ const PerpMarketsTable = ({
<TrHead>
<Th className="text-left">{t('market')}</Th>
<Th className="text-right">{t('price')}</Th>
<Th className="hidden text-right lg:block"></Th>
<Th className="text-right"></Th>
<Th className="text-right">
<Tooltip content={t('trade:tooltip-stable-price')}>
<span className="tooltip-underline">
{t('trade:stable-price')}
</span>
</Tooltip>
</Th>
<Th className="text-right">{t('trade:funding-rate')}</Th>
<Th className="text-right">{t('trade:open-interest')}</Th>
<Th className="text-right">{t('rolling-change')}</Th>
<Th />
</TrHead>
</thead>
<tbody>
{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) {
if (rate.isSuccess) {
const marketRate = rate?.data?.find(
(r) => r.market_index === market.perpMarketIndex
)
@ -84,7 +105,9 @@ const PerpMarketsTable = ({
<Td>
<div className="flex items-center">
<MarketLogos market={market} />
<p className="font-body">{market.name}</p>
<p className="whitespace-nowrap font-body">
{market.name}
</p>
</div>
</Td>
<Td>
@ -95,8 +118,8 @@ const PerpMarketsTable = ({
</div>
</Td>
<Td>
{!loadingPrices ? (
chartData !== undefined ? (
{!loadingPerpStats ? (
marketStats.length ? (
<div className="h-10 w-24">
<SimpleAreaChart
color={
@ -104,10 +127,10 @@ const PerpMarketsTable = ({
? COLORS.UP[theme]
: COLORS.DOWN[theme]
}
data={chartData}
data={marketStats}
name={symbol}
xKey="0"
yKey="1"
xKey="date_hour"
yKey="price"
/>
</div>
) : symbol === 'USDC' || symbol === 'USDT' ? null : (
@ -117,6 +140,16 @@ const PerpMarketsTable = ({
<div className="h-10 w-[104px] animate-pulse rounded bg-th-bkg-3" />
)}
</Td>
<Td>
<div className="flex flex-col text-right">
<p>
<FormatNumericValue
value={market.stablePriceModel.stablePrice}
isUsd
/>
</p>
</div>
</Td>
<Td>
<div className="flex flex-col text-right">
<p>{fundingRate}</p>
@ -165,6 +198,7 @@ const PerpMarketsTable = ({
<MobilePerpMarketItem
key={market.publicKey.toString()}
market={market}
setShowPerpDetails={setShowPerpDetails}
/>
)
})}
@ -176,25 +210,27 @@ 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 { 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
@ -223,16 +259,15 @@ const MobilePerpMarketItem = ({ market }: { market: PerpMarket }) => {
<Change change={change} suffix="%" />
</div>
</div>
</div>
{!loadingPrices ? (
chartData !== undefined ? (
<div className="h-10 w-24">
{!loadingPerpStats ? (
marketStats.length ? (
<div className="ml-4 h-10 w-24">
<SimpleAreaChart
color={change >= 0 ? COLORS.UP[theme] : COLORS.DOWN[theme]}
data={chartData}
data={marketStats}
name={market.name}
xKey="0"
yKey="1"
xKey="date_hour"
yKey="price"
/>
</div>
) : symbol === 'USDC' || symbol === 'USDT' ? null : (
@ -242,6 +277,13 @@ const MobilePerpMarketItem = ({ market }: { market: PerpMarket }) => {
<div className="h-10 w-[104px] animate-pulse rounded bg-th-bkg-3" />
)}
</div>
<IconButton
onClick={() => setShowPerpDetails(market.name)}
size="medium"
>
<ChevronRightIcon className="h-5 w-5" />
</IconButton>
</div>
</div>
)
}

View File

@ -8,7 +8,7 @@ const PerpStats = () => {
<PerpMarketsTable setShowPerpDetails={setShowPerpDetails} />
) : (
<PerpMarketDetails
perpMarket={showPerpDetails}
perpMarketName={showPerpDetails}
setShowPerpDetails={setShowPerpDetails}
/>
)

View File

@ -52,9 +52,9 @@ const SpotMarketsTable = () => {
asset.symbol.toUpperCase() === bank?.name.toUpperCase()
)
const change = coingeckoData
? ((coingeckoData.prices[coingeckoData.prices.length - 1][1] -
coingeckoData.prices[0][1]) /
const change =
coingeckoData && oraclePrice
? ((oraclePrice - coingeckoData.prices[0][1]) /
coingeckoData.prices[0][1]) *
100
: 0

View File

@ -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()
@ -52,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 [
@ -96,7 +62,7 @@ const TotalDepositBorrowCharts = ({
<>
<div className="col-span-2 h-96 border-b border-th-bkg-3 py-4 px-6 md:col-span-1">
<DetailedAreaChart
data={filteredDepositValues.concat([
data={totalDepositBorrowValues.concat([
{
date: dayjs().toISOString(),
depositValue: Math.floor(currentTotalDepositValue),
@ -117,7 +83,7 @@ const TotalDepositBorrowCharts = ({
</div>
<div className="col-span-2 h-96 border-b border-th-bkg-3 py-4 px-6 md:col-span-1 md:border-l md:pl-6">
<DetailedAreaChart
data={filteredBorrowValues.concat([
data={totalDepositBorrowValues.concat([
{
date: dayjs().toISOString(),
borrowValue: Math.floor(currentTotalBorrowValue),

View File

@ -63,7 +63,7 @@ const SwapForm = () => {
//initial state is undefined null is returned on error
const [selectedRoute, setSelectedRoute] = useState<RouteInfo | null>()
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}
>
<SwapFormTokenList
onClose={() => setShowTokenSelect('')}
onClose={() => setShowTokenSelect(undefined)}
onTokenSelect={
showTokenSelect === 'input'
? handleTokenInSelect
@ -451,6 +451,26 @@ const SwapForm = () => {
{group && inputBank ? (
<TokenVaultWarnings bank={inputBank} type="swap" />
) : null}
{inputBank && inputBank.reduceOnly ? (
<div className="pb-4">
<InlineNotification
type="warning"
desc={t('swap:input-reduce-only-warning', {
symbol: inputBank.name,
})}
/>
</div>
) : null}
{outputBank && outputBank.reduceOnly ? (
<div className="pb-4">
<InlineNotification
type="warning"
desc={t('swap:output-reduce-only-warning', {
symbol: outputBank.name,
})}
/>
</div>
) : null}
<div className="space-y-2">
<div id="swap-step-four">
<HealthImpact maintProjectedHealth={maintProjectedHealth} />

View File

@ -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 (
<div>
<button
@ -64,10 +71,18 @@ const TokenItem = ({
<img src={logoURI} width="24" height="24" alt={symbol} />
</picture>
<div className="ml-2.5">
<div className="text-left text-th-fgd-2">{symbol || 'unknown'}</div>
<div className="text-left text-xs text-th-fgd-4">
<p className="text-left text-th-fgd-2">
{symbol || 'unknown'}
{bank?.reduceOnly ? (
<span className="ml-1.5 text-xxs text-th-warning">
{t('reduce-only')}
</span>
) : null}
</p>
<p className="text-left text-xs text-th-fgd-4">
{name || 'unknown'}
</div>
</p>
</div>
</div>
{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'])

View File

@ -49,19 +49,30 @@ export const getTokenInMax = (
mangoAccount.getTokenBalanceUi(inputBank)
)
const maxAmountWithoutMargin = inputTokenBalance.gt(0)
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 = floorToDecimal(
mangoAccount.getMaxSourceUiForTokenSwap(
const rawMaxUiAmountWithBorrow = mangoAccount.getMaxSourceUiForTokenSwap(
group,
inputBank.mint,
outputBank.mint
),
inputBank.mintDecimals
)
const maxUiAmountWithBorrow =
outputBank.reduceOnly &&
(outputTokenBalance.gt(0) || outputTokenBalance.eq(0))
? new Decimal(0)
: rawMaxUiAmountWithBorrow > 0
? floorToDecimal(rawMaxUiAmountWithBorrow, inputBank.mintDecimals)
: new Decimal(0)
const inputBankVaultBalance = floorToDecimal(
group
.getTokenVaultBalanceByMintUi(inputBank.mint)
@ -81,10 +92,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,

View File

@ -46,20 +46,20 @@ const ChartTabs = ({ token }: { token: string }) => {
}, [])
}, [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 (
<div className="grid grid-cols-1 md:grid-cols-2">
@ -77,7 +77,7 @@ const ChartTabs = ({ token }: { token: string }) => {
<div className="h-96 border-t border-th-bkg-3 px-6 py-6">
{activeDepositsTab === 'token:deposits' ? (
<DetailedAreaChart
data={filterStats(depositDaysToShow)}
data={statsHistory}
daysToShow={depositDaysToShow}
setDaysToShow={setDepositDaysToShow}
heightClass="h-64"
@ -92,7 +92,7 @@ const ChartTabs = ({ token }: { token: string }) => {
/>
) : (
<DetailedAreaChart
data={filterStats(depositRateDaysToShow)}
data={statsHistory}
daysToShow={depositRateDaysToShow}
setDaysToShow={setDepositRateDaysToShow}
heightClass="h-64"
@ -125,7 +125,7 @@ const ChartTabs = ({ token }: { token: string }) => {
<div className="h-96 border-t border-th-bkg-3 px-6 py-6">
{activeBorrowsTab === 'token:borrows' ? (
<DetailedAreaChart
data={filterStats(borrowDaysToShow)}
data={statsHistory}
daysToShow={borrowDaysToShow}
setDaysToShow={setBorrowDaysToShow}
heightClass="h-64"
@ -140,7 +140,7 @@ const ChartTabs = ({ token }: { token: string }) => {
/>
) : (
<DetailedAreaChart
data={filterStats(borrowRateDaysToShow)}
data={statsHistory}
daysToShow={borrowRateDaysToShow}
setDaysToShow={setBorrowRateDaysToShow}
heightClass="h-64"

View File

@ -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(() => {
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, tokenPrices])
}
}, [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
if (!changeData || !price || !serumOrPerpMarket) return 0
if (serumOrPerpMarket instanceof PerpMarket) {
return changeData.length
? ((price - changeData[0].price) / changeData[0].price) * 100
: 0
}, [coingeckoData])
} else {
return ((price - changeData.prices[0][1]) / changeData.prices[0][1]) * 100
}
}, [changeData, price, serumOrPerpMarket])
return (
<div className="flex flex-col bg-th-bkg-1 md:h-12 md:flex-row md:items-center">

View File

@ -561,7 +561,7 @@ const AdvancedTradeForm = () => {
checked={tradeForm.reduceOnly}
onChange={(e) => handleReduceOnlyChange(e.target.checked)}
>
Reduce Only
{t('trade:reduce-only')}
</Checkbox>
</div>
</Tooltip>

View File

@ -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 = () => {
</div>
</div>
<div className="col-span-1 text-right font-mono">
{orderbookData?.spread.toFixed(2)}
{formatNumericValue(
orderbookData?.spread,
market ? getDecimalCount(market.tickSize) : undefined
)}
</div>
</div>
) : null}

View File

@ -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()
})
}
@ -72,7 +72,8 @@ const PerpPositions = () => {
)
return mangoAccountAddress && openPerpPositions.length ? (
<div>
<>
<div className="thin-scroll overflow-x-auto">
<Table>
<thead>
<TrHead>
@ -104,11 +105,9 @@ const PerpPositions = () => {
if (!basePosition) return null
const unsettledPnl = position.getUnsettledPnlUi(group, market)
const cummulativePnl = position.cumulativePnlOverPositionLifetimeUi(
group,
market
)
const unsettledPnl = position.getUnsettledPnlUi(market)
const cummulativePnl =
position.cumulativePnlOverPositionLifetimeUi(market)
return (
<TrBody key={`${position.marketIndex}`} className="my-1 p-2">
@ -122,7 +121,9 @@ const PerpPositions = () => {
<p className="flex justify-end">
{isSelectedMarket ? (
<LinkButton
onClick={() => handlePositionClick(floorBasePosition)}
onClick={() =>
handlePositionClick(floorBasePosition, market)
}
>
<FormatNumericValue
value={Math.abs(basePosition)}
@ -139,14 +140,14 @@ const PerpPositions = () => {
</Td>
<Td className="text-right font-mono">
<FormatNumericValue
value={floorBasePosition * market._uiPrice}
decimals={2}
value={Math.abs(floorBasePosition) * market._uiPrice}
isUsd
/>
</Td>
<Td className="text-right font-mono">
<FormatNumericValue
value={position.getAverageEntryPriceUi(market)}
decimals={getDecimalCount(market.tickSize)}
isUsd
/>
</Td>
@ -178,6 +179,7 @@ const PerpPositions = () => {
})}
</tbody>
</Table>
</div>
{showMarketCloseModal && positionToClose ? (
<MarketCloseModal
isOpen={showMarketCloseModal}
@ -185,7 +187,7 @@ const PerpPositions = () => {
position={positionToClose}
/>
) : null}
</div>
</>
) : mangoAccountAddress || connected ? (
<div className="flex flex-col items-center p-8">
<NoSymbolIcon className="mb-2 h-6 w-6 text-th-fgd-4" />

View File

@ -1,6 +1,6 @@
import useInterval from '@components/shared/useInterval'
import mangoStore from '@store/mangoStore'
import { useEffect, useMemo } from 'react'
import { useEffect, useMemo, useState } from 'react'
import { formatNumericValue, getDecimalCount } from 'utils/numbers'
import { ChartTradeType } from 'types'
import { useTranslation } from 'next-i18next'
@ -8,56 +8,63 @@ import useSelectedMarket from 'hooks/useSelectedMarket'
import { Howl } from 'howler'
import { IconButton } from '@components/shared/Button'
import useLocalStorageState from 'hooks/useLocalStorageState'
import { SOUND_SETTINGS_KEY } from 'utils/constants'
import { SpeakerWaveIcon, SpeakerXMarkIcon } from '@heroicons/react/20/solid'
import { SOUND_SETTINGS_KEY, TRADE_VOLUME_ALERT_KEY } from 'utils/constants'
import { BellAlertIcon, BellSlashIcon } from '@heroicons/react/20/solid'
import Tooltip from '@components/shared/Tooltip'
import { INITIAL_SOUND_SETTINGS } from '@components/settings/SoundSettings'
import usePrevious from '@components/shared/usePrevious'
import TradeVolumeAlertModal, {
DEFAULT_VOLUME_ALERT_SETTINGS,
} from '@components/modals/TradeVolumeAlertModal'
import dayjs from 'dayjs'
const buySound = new Howl({
const volumeAlertSound = new Howl({
src: ['/sounds/trade-buy.mp3'],
volume: 0.5,
})
const sellSound = new Howl({
src: ['/sounds/trade-sell.mp3'],
volume: 0.5,
volume: 0.8,
})
const RecentTrades = () => {
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
@ -114,23 +121,19 @@ const RecentTrades = () => {
}, [fills])
return (
<>
<div className="thin-scroll h-full overflow-y-scroll">
<div className="flex items-center justify-between border-b border-th-bkg-3 p-1 xl:px-2">
<Tooltip content={t('trade:trade-sounds-tooltip')} delay={250}>
<div className="flex items-center justify-between border-b border-th-bkg-3 py-1 px-2">
<Tooltip content={t('trade:tooltip-volume-alert')} delay={250}>
<IconButton
onClick={() =>
setSoundSettings({
...soundSettings,
'recent-trades': !soundSettings['recent-trades'],
})
}
onClick={() => setShowVolumeAlertModal(true)}
size="small"
hideBg
>
{soundSettings['recent-trades'] ? (
<SpeakerWaveIcon className="h-4 w-4 text-th-fgd-3" />
<BellAlertIcon className="h-4 w-4 text-th-fgd-3" />
) : (
<SpeakerXMarkIcon className="h-4 w-4 text-th-fgd-3" />
<BellSlashIcon className="h-4 w-4 text-th-fgd-3" />
)}
</IconButton>
</Tooltip>
@ -139,10 +142,12 @@ const RecentTrades = () => {
<span className="text-th-up">{(buyRatio * 100).toFixed(1)}%</span>
<span className="px-2">|</span>
{t('trade:sells')}:{' '}
<span className="text-th-down">{(sellRatio * 100).toFixed(1)}%</span>
<span className="text-th-down">
{(sellRatio * 100).toFixed(1)}%
</span>
</span>
</div>
<div className="px-1 xl:px-2">
<div className="px-2">
<table className="min-w-full">
<thead>
<tr className="text-right text-xxs text-th-fgd-4">
@ -188,15 +193,13 @@ const RecentTrades = () => {
>
{formattedPrice}
</td>
<td className="pb-1.5 text-right text-th-fgd-3">
{formattedSize}
</td>
<td className="pb-1.5 text-right">{formattedSize}</td>
<td className="pb-1.5 text-right text-th-fgd-4">
{trade.time
? new Date(trade.time).toLocaleTimeString()
: trade.timestamp
? dayjs(trade.timestamp.toNumber() * 1000).format(
'h:mma'
'hh:mma'
)
: '-'}
</td>
@ -207,6 +210,13 @@ const RecentTrades = () => {
</table>
</div>
</div>
{showVolumeAlertModal ? (
<TradeVolumeAlertModal
isOpen={showVolumeAlertModal}
onClose={() => setShowVolumeAlertModal(false)}
/>
) : null}
</>
)
}

View File

@ -240,6 +240,7 @@ const TradeHistory = () => {
(combinedTradeHistory.length || loadingTradeHistory) ? (
<>
{showTableView ? (
<div className="thin-scroll overflow-x-auto">
<Table>
<thead>
<TrHead>
@ -322,6 +323,7 @@ const TradeHistory = () => {
})}
</tbody>
</Table>
</div>
) : (
<div>
{combinedTradeHistory.map((trade: any, index: number) => {

View File

@ -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],
]
}, [

View File

@ -16,7 +16,6 @@ import {
} from 'utils/constants'
import { breakpoints } from 'utils/theme'
import { COLORS } from 'styles/colors'
import Datafeed from 'apis/birdeye/datafeed'
import { useTranslation } from 'next-i18next'
import { notify } from 'utils/notifications'
import {
@ -33,6 +32,8 @@ import { PublicKey } from '@solana/web3.js'
import useLocalStorageState from 'hooks/useLocalStorageState'
import { formatNumericValue, getDecimalCount } from 'utils/numbers'
import { BN } from '@project-serum/anchor'
import SpotDatafeed from 'apis/birdeye/datafeed'
import PerpDatafeed from 'apis/mngo/datafeed'
export interface ChartContainerProps {
container: ChartingLibraryWidgetOptions['container']
@ -129,20 +130,26 @@ const TradingViewChart = () => {
}
})
useEffect(() => {
const selectedMarket = useMemo(() => {
const group = mangoStore.getState().group
if (tvWidgetRef.current && chartReady && selectedMarketName && group) {
try {
let symbolName
if (!selectedMarketName.toLowerCase().includes('PERP')) {
symbolName = group
if (!group || !selectedMarketName)
return '8BnEgHoWFysVcuFFX7QztDmzuH8r5ZFvyP3sYwn1XTh6'
if (!selectedMarketName.toLowerCase().includes('perp')) {
return group
.getSerum3MarketByName(selectedMarketName)
.serumMarketExternal.toString()
} else {
symbolName = selectedMarketName
return group.getPerpMarketByName(selectedMarketName).publicKey.toString()
}
}, [selectedMarketName])
useEffect(() => {
const group = mangoStore.getState().group
if (tvWidgetRef.current && chartReady && selectedMarket && group) {
try {
tvWidgetRef.current.setSymbol(
symbolName,
selectedMarket,
tvWidgetRef.current.activeChart().resolution(),
() => {
return
@ -152,7 +159,7 @@ const TradingViewChart = () => {
console.warn('Trading View change symbol error: ', e)
}
}
}, [selectedMarketName, chartReady, tvWidgetRef])
}, [selectedMarket, chartReady])
useEffect(() => {
if (
@ -170,22 +177,12 @@ 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',
symbol: selectedMarket,
// 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:

View File

@ -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<SetStateAction<boolean>>
@ -32,150 +35,8 @@ type Props = {
}
const TradingViewChartKline = ({ setIsFullView, isFullView }: Props) => {
const { width } = useViewport()
const { theme } = useTheme()
const prevWidth = usePrevious(width)
const selectedMarket = mangoStore((s) => s.selectedMarket.current)
const selectedMarketName = selectedMarket?.name
const [isTechnicalModalOpen, setIsTechnicalModalOpen] = useState(false)
const [mainTechnicalIndicators, setMainTechnicalIndicators] = useState<
string[]
>([])
const [subTechnicalIndicators, setSubTechnicalIndicators] = useState<{
//indicatorName: class
[indicatorName: string]: string
}>({})
const [isLoading, setIsLoading] = useState(false)
const [resolution, setResolution] = useState(RES_NAME_TO_RES_VAL['1H'])
const [chart, setChart] = useState<klinecharts.Chart | null>(null)
const previousChart = usePrevious(chart)
const [baseChartQuery, setQuery] = useState<BASE_CHART_QUERY | null>(null)
const clearTimerRef = useRef<NodeJS.Timeout | null>(null)
const fetchData = async (baseQuery: BASE_CHART_QUERY, from: number) => {
try {
setIsLoading(true)
const query: CHART_QUERY = {
...baseQuery,
time_from: from,
}
const response = await queryBars(query.address, query.type, {
firstDataRequest: false,
from: query.time_from,
to: query.time_to,
})
const dataSize = response.length
const dataList = []
for (let i = 0; i < dataSize; i++) {
const row = response[i]
const kLineModel = {
...row,
}
dataList.push(kLineModel)
}
setIsLoading(false)
return dataList
} catch (e) {
setIsLoading(false)
console.error(e)
return []
}
}
//update data every 10 secs
function updateData(
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)
}
}
}, UPDATE_INTERVAL)
}
const fetchFreshData = async (daysToSubtractFromToday: number) => {
const from =
Math.floor(Date.now() / 1000) - ONE_DAY_SECONDS * daysToSubtractFromToday
const data = await fetchData(baseChartQuery!, from)
if (chart) {
chart.applyNewData(data)
//after we fetch fresh data start to update data every x seconds
updateData(chart, baseChartQuery!)
}
}
//size change
useEffect(() => {
if (width !== prevWidth && chart) {
//wait for event que to be empty
//to have current width
setTimeout(() => {
chart?.resize()
}, 0)
}
}, [width])
//when base query change we refetch with fresh data
useEffect(() => {
if (chart && baseChartQuery) {
fetchFreshData(14)
//add callback to fetch more data when zoom out
chart.loadMore(() => {
try {
fetchFreshData(365)
} catch (e) {
console.error('Error fetching new data')
}
chart.loadMore(() => null)
})
}
}, [baseChartQuery])
//change query based on market and resolution
useEffect(() => {
if (selectedMarketName && resolution) {
setQuery({
type: resolution.val,
address: '8BnEgHoWFysVcuFFX7QztDmzuH8r5ZFvyP3sYwn1XTh6',
time_to: Math.floor(Date.now() / 1000),
})
}
}, [selectedMarketName, resolution])
// init default technical indicators after init of chart
useEffect(() => {
if (chart !== null && previousChart === null) {
if (DEFAULT_SUB_INDICATOR) {
const subId = chart.createTechnicalIndicator(
DEFAULT_SUB_INDICATOR,
true
)
setSubTechnicalIndicators({ [DEFAULT_SUB_INDICATOR]: subId })
}
if (DEFAULT_MAIN_INDICATORS?.length) {
for (const type of DEFAULT_MAIN_INDICATORS) {
chart?.createTechnicalIndicator(type, true, {
id: MAIN_INDICATOR_CLASS,
})
}
setMainTechnicalIndicators(DEFAULT_MAIN_INDICATORS)
}
}
}, [chart !== null])
//init chart without data
useEffect(() => {
const initKline = async () => {
const kLineChart = init('update-k-line')
kLineChart.setStyleOptions({
const styles = {
grid: {
show: false,
},
@ -393,12 +254,222 @@ const TradingViewChartKline = ({ setIsFullView, isFullView }: Props) => {
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<
string[]
>([])
const [subTechnicalIndicators, setSubTechnicalIndicators] = useState<{
//indicatorName: class
[indicatorName: string]: string
}>({})
const [isLoading, setIsLoading] = useState(false)
const [resolution, setResolution] = useState(RES_NAME_TO_RES_VAL['1H'])
const [chart, setChart] = useState<klinecharts.Chart | null>(null)
const previousChart = usePrevious(chart)
const [baseChartQuery, setQuery] = useState<BASE_CHART_QUERY | null>(null)
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,
from: query.time_from,
to: query.time_to,
})
const dataSize = response.length
const dataList = []
for (let i = 0; i < dataSize; i++) {
const row = response[i]
const kLineModel = {
...row,
}
dataList.push(kLineModel)
}
setIsLoading(false)
return dataList
} catch (e) {
setIsLoading(false)
console.error(e)
return []
}
}
//update data every 10 secs
function setupSocket(
kLineChart: klinecharts.Chart,
baseQuery: BASE_CHART_QUERY
) {
// 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))
}
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 =
Math.floor(Date.now() / 1000) - ONE_DAY_SECONDS * daysToSubtractFromToday
const data = await fetchData(baseChartQuery!, from)
if (chart) {
chart.applyNewData(data)
//after we fetch fresh data start to update data every x seconds
setupSocket(chart, baseChartQuery!)
}
}
//size change
useEffect(() => {
if (width !== prevWidth && chart) {
//wait for event que to be empty
//to have current width
setTimeout(() => {
chart?.resize()
}, 0)
}
}, [width])
//when base query change we refetch with fresh data
useEffect(() => {
if (chart && baseChartQuery) {
//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(async (timestamp: number) => {
try {
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')
}
})
}
}, [baseChartQuery])
//change query based on market and resolution
useEffect(() => {
if (selectedMarketName && resolution) {
setQuery({
type: resolution.val,
address: '8BnEgHoWFysVcuFFX7QztDmzuH8r5ZFvyP3sYwn1XTh6',
time_to: Math.floor(Date.now() / 1000),
})
}
}, [selectedMarketName, resolution])
// init default technical indicators after init of chart
useEffect(() => {
if (chart !== null && previousChart === null) {
if (DEFAULT_SUB_INDICATOR) {
const subId = chart.createTechnicalIndicator(
DEFAULT_SUB_INDICATOR,
true
)
setSubTechnicalIndicators({ [DEFAULT_SUB_INDICATOR]: subId })
}
if (DEFAULT_MAIN_INDICATORS?.length) {
for (const type of DEFAULT_MAIN_INDICATORS) {
chart?.createTechnicalIndicator(type, true, {
id: MAIN_INDICATOR_CLASS,
})
}
setMainTechnicalIndicators(DEFAULT_MAIN_INDICATORS)
}
}
}, [chart !== null])
//init chart without data
useEffect(() => {
const initKline = async () => {
const kLineChart = init('update-k-line')
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()
}
}
}, [])

View File

@ -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 = ({
</Td>
<Td className="text-right font-mono">
<FormatNumericValue
value={position.getUnsettledPnlUi(group, market)}
value={position.getUnsettledPnlUi(market)}
decimals={market.baseDecimals}
/>{' '}
<span className="font-body text-th-fgd-4">USDC</span>

View File

@ -49,8 +49,7 @@ const ConnectedMenu = () => {
state.mangoAccount.interestTotals = { data: [], loading: false }
state.mangoAccount.performance = {
data: [],
loading: false,
initialLoad: false,
loading: true,
}
})
disconnect()

View File

@ -11,7 +11,7 @@ const useOpenPerpPositions = () => {
return Object.values(perpPositions).filter((p) =>
p.basePositionLots.toNumber()
)
}, [mangoAccountAddress])
}, [mangoAccountAddress, perpPositions])
return openPositions
}

View File

@ -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])

View File

@ -193,12 +193,6 @@ const Dashboard: NextPage = () => {
label="Collected fees native"
value={bank.collectedFeesNative.toNumber()}
/>
<KeyValuePair
label="Liquidation fee"
value={`${(
10000 * bank.liquidationFee.toNumber()
).toFixed(2)} bps`}
/>
<KeyValuePair
label="Dust"
value={bank.dust.toNumber()}
@ -453,24 +447,6 @@ const Dashboard: NextPage = () => {
)}/
${perpMarket.initBaseLiabWeight.toFixed(4)}`}
/>
<KeyValuePair
label="Maint PNL Asset weight"
value={`${perpMarket.maintPnlAssetWeight.toFixed(
4
)}`}
/>
<KeyValuePair
label="Init PNL Asset weight"
value={`${perpMarket.initPnlAssetWeight.toFixed(
4
)}`}
/>
<KeyValuePair
label="Liquidation Fee"
value={`${(
100 * perpMarket.liquidationFee.toNumber()
).toFixed(4)}%`}
/>
<KeyValuePair
label="Trading Fees"
value={`${(

View File

@ -175,7 +175,7 @@ const Dashboard: NextPage = () => {
/>
<KeyValuePair
label="Equity"
value={`$${perp.getEquityUi(group, market).toFixed(6)}`}
value={`$${perp.getEquityUi(market).toFixed(6)}`}
/>
<KeyValuePair
label="Unsettled Funding"

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -1,4 +1,5 @@
{
"activate-volume-alert": "Activate Volume Alert",
"amount": "Amount",
"base": "Base",
"book": "Book",
@ -19,6 +20,7 @@
"hourly-funding": "Hourly Funding",
"in-orders": "In Orders",
"instantaneous-funding": "Instantaneous Funding",
"interval-seconds": "Interval (seconds)",
"limit-price": "Limit Price",
"long": "Long",
"margin": "Margin",
@ -27,6 +29,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",
@ -37,8 +40,10 @@
"placing-order": "Placing Order",
"positions": "Positions",
"post": "Post",
"preview-sound": "Preview Sound",
"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",
@ -48,11 +53,16 @@
"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-volume-alert": "Volume Alert Settings",
"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"
"unsettled": "Unsettled",
"volume-alert": "Volume Alert",
"volume-alert-desc": "Play a sound whenever volume exceeds your alert threshold"
}

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -1,4 +1,5 @@
{
"activate-volume-alert": "Activate Volume Alert",
"amount": "Amount",
"base": "Base",
"book": "Book",
@ -19,6 +20,7 @@
"hourly-funding": "Hourly Funding",
"in-orders": "In Orders",
"instantaneous-funding": "Instantaneous Funding",
"interval-seconds": "Interval (seconds)",
"limit-price": "Limit Price",
"long": "Long",
"margin": "Margin",
@ -27,6 +29,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",
@ -37,8 +40,10 @@
"placing-order": "Placing Order",
"positions": "Positions",
"post": "Post",
"preview-sound": "Preview Sound",
"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",
@ -48,11 +53,16 @@
"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-volume-alert": "Volume Alert Settings",
"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"
"unsettled": "Unsettled",
"volume-alert": "Volume Alert",
"volume-alert-desc": "Play a sound whenever volume exceeds your alert threshold"
}

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -1,4 +1,5 @@
{
"activate-volume-alert": "Activate Volume Alert",
"amount": "Amount",
"base": "Base",
"book": "Book",
@ -19,6 +20,7 @@
"hourly-funding": "Hourly Funding",
"in-orders": "In Orders",
"instantaneous-funding": "Instantaneous Funding",
"interval-seconds": "Interval (seconds)",
"limit-price": "Limit Price",
"long": "Long",
"margin": "Margin",
@ -27,6 +29,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",
@ -37,8 +40,10 @@
"placing-order": "Placing Order",
"positions": "Positions",
"post": "Post",
"preview-sound": "Preview Sound",
"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",
@ -48,11 +53,16 @@
"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-volume-alert": "Volume Alert Settings",
"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"
"unsettled": "Unsettled",
"volume-alert": "Volume Alert",
"volume-alert-desc": "Play a sound whenever volume exceeds your alert threshold"
}

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -1,4 +1,5 @@
{
"activate-volume-alert": "Activate Volume Alert",
"amount": "Amount",
"base": "Base",
"book": "Book",
@ -19,6 +20,7 @@
"hourly-funding": "Hourly Funding",
"in-orders": "In Orders",
"instantaneous-funding": "Instantaneous Funding",
"interval-seconds": "Interval (seconds)",
"limit-price": "Limit Price",
"long": "Long",
"margin": "Margin",
@ -27,6 +29,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",
@ -37,6 +40,7 @@
"placing-order": "Placing Order",
"positions": "Positions",
"post": "Post",
"preview-sound": "Preview Sound",
"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",
"sells": "Sells",
@ -48,11 +52,16 @@
"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-volume-alert": "Volume Alert Settings",
"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"
"unsettled": "Unsettled",
"volume-alert": "Volume Alert",
"volume-alert-desc": "Play a sound whenever volume exceeds your alert threshold"
}

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -1,4 +1,5 @@
{
"activate-volume-alert": "Activate Volume Alert",
"amount": "Amount",
"base": "Base",
"book": "Book",
@ -19,6 +20,7 @@
"hourly-funding": "Hourly Funding",
"in-orders": "In Orders",
"instantaneous-funding": "Instantaneous Funding",
"interval-seconds": "Interval (seconds)",
"limit-price": "Limit Price",
"long": "Long",
"margin": "Margin",
@ -27,6 +29,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",
@ -37,6 +40,7 @@
"placing-order": "Placing Order",
"positions": "Positions",
"post": "Post",
"preview-sound": "Preview Sound",
"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",
"sells": "Sells",
@ -48,11 +52,16 @@
"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-volume-alert": "Volume Alert Settings",
"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"
"unsettled": "Unsettled",
"volume-alert": "Volume Alert",
"volume-alert-desc": "Play a sound whenever volume exceeds your alert threshold"
}

View File

@ -168,6 +168,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
@ -256,7 +269,6 @@ export type MangoStore = {
performance: {
data: PerformanceDataItem[]
loading: boolean
initialLoad: boolean
}
swapHistory: {
data: SwapHistoryItem[]
@ -274,7 +286,7 @@ export type MangoStore = {
perpMarkets: PerpMarket[]
perpStats: {
loading: boolean
data: any[]
data: PerpStatsItem[] | null
}
profile: {
details: ProfileDetails | null
@ -402,7 +414,7 @@ const mangoStore = create<MangoStore>()(
perpPositions: [],
spotBalances: {},
interestTotals: { data: [], loading: false },
performance: { data: [], loading: false, initialLoad: false },
performance: { data: [], loading: true },
swapHistory: { data: [], loading: true },
tradeHistory: { data: [], loading: true },
},
@ -515,9 +527,6 @@ const mangoStore = create<MangoStore>()(
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()
@ -541,13 +550,8 @@ const mangoStore = create<MangoStore>()(
} 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
}
})
}
},
@ -733,7 +737,7 @@ const mangoStore = create<MangoStore>()(
}
if (newSelectedMangoAccount) {
await newSelectedMangoAccount.reloadAccountData(client)
await newSelectedMangoAccount.reloadSerum3OpenOrders(client)
set((state) => {
state.mangoAccount.current = newSelectedMangoAccount
state.mangoAccount.initialLoad = false
@ -742,7 +746,7 @@ const mangoStore = create<MangoStore>()(
}
await Promise.all(
mangoAccounts.map((ma) => ma.reloadAccountData(client))
mangoAccounts.map((ma) => ma.reloadSerum3OpenOrders(client))
)
set((state) => {
@ -837,7 +841,7 @@ const mangoStore = create<MangoStore>()(
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
})

View File

@ -74,4 +74,6 @@ export const ACCOUNT_ACTION_MODAL_HEIGHT = '506px'
export const ACCOUNT_ACTION_MODAL_INNER_HEIGHT = '444px'
export const TRADE_VOLUME_ALERT_KEY = 'tradeVolumeAlert-0.1'
export const PAGINATION_PAGE_LENGTH = 250

View File

@ -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"