merge main

This commit is contained in:
saml33 2023-02-10 21:38:05 +11:00
commit f4209c3f88
41 changed files with 859 additions and 688 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) {

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

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

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

@ -0,0 +1,141 @@
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])
const filteredOiValues = useMemo(() => {
if (!totalOpenInterestValues.length) return []
if (oiDaysToShow !== '30') {
const seconds = Number(oiDaysToShow) * 86400
const data = totalOpenInterestValues.filter((d: OiValueItem) => {
const dataTime = new Date(d.date).getTime() / 1000
const now = new Date().getTime() / 1000
const limit = now - seconds
return dataTime >= limit
})
return data
}
return totalOpenInterestValues
}, [totalOpenInterestValues, oiDaysToShow])
const filteredFeesValues = useMemo(() => {
if (!totalFeeValues.length) return []
if (feesDaysToShow !== '30') {
const seconds = Number(feesDaysToShow) * 86400
const data = totalFeeValues.filter((d: FeeValueItem) => {
const dataTime = new Date(d.date).getTime() / 1000
const now = new Date().getTime() / 1000
const limit = now - seconds
return dataTime >= limit
})
return data
}
return totalFeeValues
}, [totalFeeValues, feesDaysToShow])
return (
<>
{totalFeeValues.length ? (
<div className="col-span-2 border-b border-th-bkg-3 py-4 px-6 md:col-span-1">
<DetailedAreaChart
data={filteredOiValues}
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}
{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={filteredFeesValues}
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

@ -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,29 +61,31 @@ 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) {
const marketRate = rate?.data?.find(
@ -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>
@ -178,23 +211,19 @@ export default PerpMarketsTable
const MobilePerpMarketItem = ({ market }: { market: PerpMarket }) => {
const { t } = useTranslation('common')
const { isLoading: loadingPrices, data: coingeckoPrices } = useCoingecko()
const loadingPerpStats = mangoStore((s) => s.perpStats.loading)
const perpStats = mangoStore((s) => s.perpStats.data)
const { theme } = useTheme()
// const rate = usePerpFundingRate()
const symbol = market.name.split('-')[0]
const coingeckoData = coingeckoPrices.find((asset) => asset.symbol === symbol)
const marketStats = getOneDayPerpStats(perpStats, market.name)
const change = coingeckoData
? ((coingeckoData.prices[coingeckoData.prices.length - 1][1] -
coingeckoData.prices[0][1]) /
coingeckoData.prices[0][1]) *
100
const change = marketStats.length
? ((market.uiPrice - marketStats[0].price) / marketStats[0].price) * 100
: 0
const chartData = coingeckoData ? coingeckoData.prices : undefined
// let fundingRate
// if (
// rate.isSuccess
@ -224,15 +253,15 @@ const MobilePerpMarketItem = ({ market }: { market: PerpMarket }) => {
</div>
</div>
</div>
{!loadingPrices ? (
chartData !== undefined ? (
{!loadingPerpStats ? (
marketStats.length ? (
<div className="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 : (

View File

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

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()

View File

@ -59,18 +59,19 @@ export const getTokenInMax = (
? inputTokenBalance
: new Decimal(0)
const rawMaxUiAmountWithBorrow = mangoAccount.getMaxSourceUiForTokenSwap(
group,
inputBank.mint,
outputBank.mint
)
const maxUiAmountWithBorrow =
outputBank.reduceOnly &&
(outputTokenBalance.gt(0) || outputTokenBalance.eq(0))
? new Decimal(0)
: floorToDecimal(
mangoAccount.getMaxSourceUiForTokenSwap(
group,
inputBank.mint,
outputBank.mint
),
inputBank.mintDecimals
)
: rawMaxUiAmountWithBorrow > 0
? floorToDecimal(rawMaxUiAmountWithBorrow, inputBank.mintDecimals)
: new Decimal(0)
const inputBankVaultBalance = floorToDecimal(
group

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(() => {
return tokenPrices.find(
(asset) => asset.symbol.toUpperCase() === baseSymbol?.toUpperCase()
)
}, [baseSymbol, tokenPrices])
useEffect(() => {
if (serumOrPerpMarket instanceof PerpMarket) {
const actions = mangoStore.getState().actions
actions.fetchPerpStats()
}
}, [serumOrPerpMarket])
const changeData = useMemo(() => {
if (serumOrPerpMarket instanceof PerpMarket) {
return getOneDayPerpStats(perpStats, selectedMarketName)
} else {
return tokenPrices.find(
(asset) => asset.symbol.toUpperCase() === baseSymbol?.toUpperCase()
)
}
}, [baseSymbol, perpStats, serumOrPerpMarket, tokenPrices])
const change = useMemo(() => {
return coingeckoData
? ((coingeckoData.prices[coingeckoData.prices.length - 1][1] -
coingeckoData.prices[0][1]) /
coingeckoData.prices[0][1]) *
100
: 0
}, [coingeckoData])
if (!changeData || !price || !serumOrPerpMarket) return 0
if (serumOrPerpMarket instanceof PerpMarket) {
return changeData.length
? ((price - changeData[0].price) / changeData[0].price) * 100
: 0
} else {
return ((price - changeData.prices[0][1]) / changeData.prices[0][1]) * 100
}
}, [changeData, price, serumOrPerpMarket])
return (
<div className="flex flex-col bg-th-bkg-1 md:h-12 md:flex-row md:items-center">

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

@ -72,114 +72,114 @@ const PerpPositions = () => {
)
return mangoAccountAddress && openPerpPositions.length ? (
<div>
<Table>
<thead>
<TrHead>
<Th className="text-left">{t('market')}</Th>
<Th className="text-right">{t('trade:side')}</Th>
<Th className="text-right">{t('trade:size')}</Th>
<Th className="text-right">{t('trade:notional')}</Th>
<Th className="text-right">{t('trade:entry-price')}</Th>
<Th className="text-right">{`${t('trade:unsettled')} ${t(
'pnl'
)}`}</Th>
<Th className="text-right">{t('pnl')}</Th>
<Th />
</TrHead>
</thead>
<tbody>
{openPerpPositions.map((position) => {
const market = group.getPerpMarketByMarketIndex(
position.marketIndex
)
const basePosition = position.getBasePositionUi(market)
const floorBasePosition = floorToDecimal(
basePosition,
getDecimalCount(market.minOrderSize)
).toNumber()
const isSelectedMarket =
selectedMarket instanceof PerpMarket &&
selectedMarket.perpMarketIndex === position.marketIndex
<>
<div className="thin-scroll overflow-x-auto">
<Table>
<thead>
<TrHead>
<Th className="text-left">{t('market')}</Th>
<Th className="text-right">{t('trade:side')}</Th>
<Th className="text-right">{t('trade:size')}</Th>
<Th className="text-right">{t('trade:notional')}</Th>
<Th className="text-right">{t('trade:entry-price')}</Th>
<Th className="text-right">{`${t('trade:unsettled')} ${t(
'pnl'
)}`}</Th>
<Th className="text-right">{t('pnl')}</Th>
<Th />
</TrHead>
</thead>
<tbody>
{openPerpPositions.map((position) => {
const market = group.getPerpMarketByMarketIndex(
position.marketIndex
)
const basePosition = position.getBasePositionUi(market)
const floorBasePosition = floorToDecimal(
basePosition,
getDecimalCount(market.minOrderSize)
).toNumber()
const isSelectedMarket =
selectedMarket instanceof PerpMarket &&
selectedMarket.perpMarketIndex === position.marketIndex
if (!basePosition) return null
if (!basePosition) return null
const unsettledPnl = position.getUnsettledPnlUi(group, market)
const cummulativePnl = position.cumulativePnlOverPositionLifetimeUi(
group,
market
)
const unsettledPnl = position.getUnsettledPnlUi(market)
const cummulativePnl =
position.cumulativePnlOverPositionLifetimeUi(market)
return (
<TrBody key={`${position.marketIndex}`} className="my-1 p-2">
<Td>
<TableMarketName market={market} />
</Td>
<Td className="text-right">
<PerpSideBadge basePosition={basePosition} />
</Td>
<Td className="text-right font-mono">
<p className="flex justify-end">
{isSelectedMarket ? (
<LinkButton
onClick={() =>
handlePositionClick(floorBasePosition, market)
}
>
return (
<TrBody key={`${position.marketIndex}`} className="my-1 p-2">
<Td>
<TableMarketName market={market} />
</Td>
<Td className="text-right">
<PerpSideBadge basePosition={basePosition} />
</Td>
<Td className="text-right font-mono">
<p className="flex justify-end">
{isSelectedMarket ? (
<LinkButton
onClick={() =>
handlePositionClick(floorBasePosition, market)
}
>
<FormatNumericValue
value={Math.abs(basePosition)}
decimals={getDecimalCount(market.minOrderSize)}
/>
</LinkButton>
) : (
<FormatNumericValue
value={Math.abs(basePosition)}
decimals={getDecimalCount(market.minOrderSize)}
/>
</LinkButton>
) : (
<FormatNumericValue
value={Math.abs(basePosition)}
decimals={getDecimalCount(market.minOrderSize)}
/>
)}
</p>
</Td>
<Td className="text-right font-mono">
<FormatNumericValue
value={floorBasePosition * market._uiPrice}
decimals={2}
isUsd
/>
</Td>
<Td className="text-right font-mono">
<FormatNumericValue
value={position.getAverageEntryPriceUi(market)}
isUsd
/>
</Td>
<Td className={`text-right font-mono`}>
<FormatNumericValue
value={unsettledPnl}
decimals={market.baseDecimals}
/>
</Td>
<Td
className={`text-right font-mono ${
cummulativePnl > 0 ? 'text-th-up' : 'text-th-down'
}`}
>
<FormatNumericValue value={cummulativePnl} isUsd />
</Td>
<Td className={`text-right`}>
<Button
className="text-xs"
secondary
size="small"
onClick={() => showClosePositionModal(position)}
)}
</p>
</Td>
<Td className="text-right font-mono">
<FormatNumericValue
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>
<Td className={`text-right font-mono`}>
<FormatNumericValue
value={unsettledPnl}
decimals={market.baseDecimals}
/>
</Td>
<Td
className={`text-right font-mono ${
cummulativePnl > 0 ? 'text-th-up' : 'text-th-down'
}`}
>
Close
</Button>
</Td>
</TrBody>
)
})}
</tbody>
</Table>
<FormatNumericValue value={cummulativePnl} isUsd />
</Td>
<Td className={`text-right`}>
<Button
className="text-xs"
secondary
size="small"
onClick={() => showClosePositionModal(position)}
>
Close
</Button>
</Td>
</TrBody>
)
})}
</tbody>
</Table>
</div>
{showMarketCloseModal && positionToClose ? (
<MarketCloseModal
isOpen={showMarketCloseModal}
@ -187,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

@ -240,88 +240,90 @@ const TradeHistory = () => {
(combinedTradeHistory.length || loadingTradeHistory) ? (
<>
{showTableView ? (
<Table>
<thead>
<TrHead>
<Th className="text-left">{t('market')}</Th>
<Th className="text-right">{t('trade:side')}</Th>
<Th className="text-right">{t('trade:size')}</Th>
<Th className="text-right">{t('price')}</Th>
<Th className="text-right">{t('value')}</Th>
<Th className="text-right">{t('fee')}</Th>
<Th className="text-right">{t('date')}</Th>
<Th />
</TrHead>
</thead>
<tbody>
{combinedTradeHistory.map((trade: any, index: number) => {
return (
<TrBody
key={`${trade.signature || trade.marketIndex}${index}`}
className="my-1 p-2"
>
<Td className="">
<TableMarketName market={trade.market} />
</Td>
<Td className="text-right">
<SideBadge side={trade.side} />
</Td>
<Td className="text-right font-mono">{trade.size}</Td>
<Td className="text-right font-mono">
<FormatNumericValue value={trade.price} />
</Td>
<Td className="text-right font-mono">
<FormatNumericValue
value={trade.price * trade.size}
decimals={2}
isUsd
/>
</Td>
<Td className="text-right">
<span className="font-mono">
<FormatNumericValue value={trade.feeCost} />
</span>
<p className="font-body text-xs text-th-fgd-4">
{trade.liquidity}
</p>
</Td>
<Td className="whitespace-nowrap text-right">
{trade.block_datetime ? (
<TableDateDisplay
date={trade.block_datetime}
showSeconds
<div className="thin-scroll overflow-x-auto">
<Table>
<thead>
<TrHead>
<Th className="text-left">{t('market')}</Th>
<Th className="text-right">{t('trade:side')}</Th>
<Th className="text-right">{t('trade:size')}</Th>
<Th className="text-right">{t('price')}</Th>
<Th className="text-right">{t('value')}</Th>
<Th className="text-right">{t('fee')}</Th>
<Th className="text-right">{t('date')}</Th>
<Th />
</TrHead>
</thead>
<tbody>
{combinedTradeHistory.map((trade: any, index: number) => {
return (
<TrBody
key={`${trade.signature || trade.marketIndex}${index}`}
className="my-1 p-2"
>
<Td className="">
<TableMarketName market={trade.market} />
</Td>
<Td className="text-right">
<SideBadge side={trade.side} />
</Td>
<Td className="text-right font-mono">{trade.size}</Td>
<Td className="text-right font-mono">
<FormatNumericValue value={trade.price} />
</Td>
<Td className="text-right font-mono">
<FormatNumericValue
value={trade.price * trade.size}
decimals={2}
isUsd
/>
) : (
'Recent'
)}
</Td>
<Td className="xl:!pl-0">
{trade.market.name.includes('PERP') ? (
<div className="flex justify-end">
<Tooltip content="View Counterparty" delay={250}>
<a
className=""
target="_blank"
rel="noopener noreferrer"
href={`/?address=${
trade.liquidity === 'Taker'
? trade.maker
: trade.taker
}`}
>
<IconButton size="small">
<UsersIcon className="h-4 w-4" />
</IconButton>
</a>
</Tooltip>
</div>
) : null}
</Td>
</TrBody>
)
})}
</tbody>
</Table>
</Td>
<Td className="text-right">
<span className="font-mono">
<FormatNumericValue value={trade.feeCost} />
</span>
<p className="font-body text-xs text-th-fgd-4">
{trade.liquidity}
</p>
</Td>
<Td className="whitespace-nowrap text-right">
{trade.block_datetime ? (
<TableDateDisplay
date={trade.block_datetime}
showSeconds
/>
) : (
'Recent'
)}
</Td>
<Td className="xl:!pl-0">
{trade.market.name.includes('PERP') ? (
<div className="flex justify-end">
<Tooltip content="View Counterparty" delay={250}>
<a
className=""
target="_blank"
rel="noopener noreferrer"
href={`/?address=${
trade.liquidity === 'Taker'
? trade.maker
: trade.taker
}`}
>
<IconButton size="small">
<UsersIcon className="h-4 w-4" />
</IconButton>
</a>
</Tooltip>
</div>
) : null}
</Td>
</TrBody>
)
})}
</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

@ -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,10 +35,234 @@ type Props = {
}
const TradingViewChartKline = ({ setIsFullView, isFullView }: Props) => {
const { width } = useViewport()
const { theme } = useTheme()
const styles = {
grid: {
show: false,
},
candle: {
bar: {
upColor: COLORS.UP[theme],
downColor: COLORS.DOWN[theme],
},
tooltip: {
labels: ['', 'O:', 'C:', 'H:', 'L:', 'V:'],
text: {
size: 12,
family: 'TT Mono',
weight: 'normal',
color: COLORS.FGD4[theme],
marginLeft: 8,
marginTop: 6,
marginRight: 8,
marginBottom: 0,
},
},
priceMark: {
show: true,
high: {
show: true,
color: COLORS.FGD4[theme],
textMargin: 5,
textSize: 10,
textFamily: 'TT Mono',
textWeight: 'normal',
},
low: {
show: true,
color: COLORS.FGD4[theme],
textMargin: 5,
textSize: 10,
textFamily: 'TT Mono',
textWeight: 'normal',
},
last: {
show: true,
upColor: COLORS.BKG4[theme],
downColor: COLORS.BKG4[theme],
noChangeColor: COLORS.BKG4[theme],
line: {
show: true,
// 'solid'|'dash'
style: 'dash',
dashValue: [4, 4],
size: 1,
},
text: {
show: true,
size: 10,
paddingLeft: 2,
paddingTop: 2,
paddingRight: 2,
paddingBottom: 2,
color: '#FFFFFF',
family: 'TT Mono',
weight: 'normal',
borderRadius: 2,
},
},
},
},
xAxis: {
axisLine: {
show: true,
color: COLORS.BKG4[theme],
size: 1,
},
tickLine: {
show: true,
size: 1,
length: 3,
color: COLORS.BKG4[theme],
},
tickText: {
show: true,
color: COLORS.FGD4[theme],
family: 'TT Mono',
weight: 'normal',
size: 10,
},
},
yAxis: {
axisLine: {
show: true,
color: COLORS.BKG4[theme],
size: 1,
},
tickLine: {
show: true,
size: 1,
length: 3,
color: COLORS.BKG4[theme],
},
tickText: {
show: true,
color: COLORS.FGD4[theme],
family: 'TT Mono',
weight: 'normal',
size: 10,
},
},
crosshair: {
show: true,
horizontal: {
show: true,
line: {
show: true,
style: 'dash',
dashValue: [4, 2],
size: 1,
color: COLORS.FGD4[theme],
},
text: {
show: true,
color: '#FFFFFF',
size: 10,
family: 'TT Mono',
weight: 'normal',
paddingLeft: 2,
paddingRight: 2,
paddingTop: 2,
paddingBottom: 2,
borderSize: 1,
borderColor: COLORS.FGD4[theme],
borderRadius: 2,
backgroundColor: COLORS.FGD4[theme],
},
},
vertical: {
show: true,
line: {
show: true,
style: 'dash',
dashValue: [4, 2],
size: 1,
color: COLORS.FGD4[theme],
},
text: {
show: true,
color: '#FFFFFF',
size: 10,
family: 'TT Mono',
weight: 'normal',
paddingLeft: 2,
paddingRight: 2,
paddingTop: 2,
paddingBottom: 2,
borderSize: 1,
borderColor: COLORS.FGD4[theme],
borderRadius: 2,
backgroundColor: COLORS.FGD4[theme],
},
},
},
technicalIndicator: {
margin: {
top: 0.2,
bottom: 0.1,
},
bar: {
upColor: COLORS.UP[theme],
downColor: COLORS.DOWN[theme],
noChangeColor: '#888888',
},
line: {
size: 1,
colors: ['#FF9600', '#9D65C9', '#2196F3', '#E11D74', '#01C5C4'],
},
circle: {
upColor: '#26A69A',
downColor: '#EF5350',
noChangeColor: '#888888',
},
lastValueMark: {
show: false,
text: {
show: false,
color: '#ffffff',
size: 12,
family: 'Helvetica Neue',
weight: 'normal',
paddingLeft: 3,
paddingTop: 2,
paddingRight: 3,
paddingBottom: 2,
borderRadius: 2,
},
},
tooltip: {
// 'always' | 'follow_cross' | 'none'
showRule: 'always',
// 'standard' | 'rect'
showType: 'standard',
showName: true,
showParams: true,
defaultValue: 'n/a',
text: {
size: 12,
family: 'TT Mono',
weight: 'normal',
color: COLORS.FGD4[theme],
marginTop: 6,
marginRight: 8,
marginBottom: 0,
marginLeft: 8,
},
},
},
separator: {
size: 2,
color: COLORS.BKG4[theme],
},
}
const socket = new WebSocket(socketUrl, 'echo-protocol')
const unsub_msg = {
type: 'UNSUBSCRIBE_PRICE',
}
const { width } = useViewport()
const prevWidth = usePrevious(width)
const selectedMarket = mangoStore((s) => s.selectedMarket.current)
const [socketConnected, setSocketConnected] = useState(false)
const selectedMarketName = selectedMarket?.name
const [isTechnicalModalOpen, setIsTechnicalModalOpen] = useState(false)
const [mainTechnicalIndicators, setMainTechnicalIndicators] = useState<
@ -50,13 +277,17 @@ const TradingViewChartKline = ({ setIsFullView, isFullView }: Props) => {
const [chart, setChart] = useState<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) => {
const fetchData = async (
baseQuery: BASE_CHART_QUERY,
from: number,
to?: number
) => {
try {
setIsLoading(true)
const query: CHART_QUERY = {
...baseQuery,
time_from: from,
time_to: to ? to : baseQuery.time_to,
}
const response = await queryBars(query.address, query.type, {
firstDataRequest: false,
@ -82,24 +313,65 @@ const TradingViewChartKline = ({ setIsFullView, isFullView }: Props) => {
}
//update data every 10 secs
function updateData(
function setupSocket(
kLineChart: klinecharts.Chart,
baseQuery: BASE_CHART_QUERY
) {
if (clearTimerRef.current) {
clearInterval(clearTimerRef.current)
}
clearTimerRef.current = setTimeout(async () => {
if (kLineChart) {
const from = baseQuery.time_to - resolution.seconds
const newData = (await fetchData(baseQuery!, from))[0]
if (newData) {
newData.timestamp += UPDATE_INTERVAL
kLineChart.updateData(newData)
updateData(kLineChart, baseQuery)
// Connection opened
socket.addEventListener('open', (_event) => {
console.log('[socket] Kline Connected')
})
socket.addEventListener('message', (msg) => {
const data = JSON.parse(msg.data)
if (data.type === 'WELLCOME') {
setSocketConnected(true)
socket.send(JSON.stringify(unsub_msg))
const msg = {
type: 'SUBSCRIBE_PRICE',
data: {
chartType: parseResolution(baseQuery.type),
address: baseQuery.address,
currency: 'pair',
},
}
socket.send(JSON.stringify(msg))
}
}, UPDATE_INTERVAL)
if (data.type === 'PRICE_DATA') {
const dataList = kLineChart.getDataList()
const lastItem = dataList[dataList.length - 1]
const currTime = data.data.unixTime * 1000
if (!dataList.length) {
return
}
const lastBar: KLineData & { time: number } = {
...lastItem,
time: lastItem.timestamp,
}
const resolution = parseResolution(baseQuery.type)
const nextBarTime = getNextBarTime(lastBar, resolution)
let bar: KLineData
if (currTime >= nextBarTime) {
bar = {
timestamp: nextBarTime,
open: data.data.o,
high: data.data.h,
low: data.data.l,
close: data.data.c,
volume: data.data.v,
}
} else {
bar = {
...lastBar,
high: Math.max(lastBar.high, data.data.h),
low: Math.min(lastBar.low, data.data.l),
close: data.data.c,
volume: data.data.v,
}
}
kLineChart.updateData(bar)
}
})
}
const fetchFreshData = async (daysToSubtractFromToday: number) => {
const from =
@ -108,7 +380,7 @@ const TradingViewChartKline = ({ setIsFullView, isFullView }: Props) => {
if (chart) {
chart.applyNewData(data)
//after we fetch fresh data start to update data every x seconds
updateData(chart, baseChartQuery!)
setupSocket(chart, baseChartQuery!)
}
}
@ -126,15 +398,26 @@ const TradingViewChartKline = ({ setIsFullView, isFullView }: Props) => {
//when base query change we refetch with fresh data
useEffect(() => {
if (chart && baseChartQuery) {
fetchFreshData(14)
//becuase bird eye send onlu 1k records at one time
//we query for lower amounts of days at the start
const halfDayThreshold = ['1', '3']
const twoDaysThreshold = ['5', '15', '30']
const daysToSub = halfDayThreshold.includes(baseChartQuery.type)
? 0.5
: twoDaysThreshold.includes(baseChartQuery.type)
? 2
: 5
fetchFreshData(daysToSub)
//add callback to fetch more data when zoom out
chart.loadMore(() => {
chart.loadMore(async (timestamp: number) => {
try {
fetchFreshData(365)
const unixTime = timestamp / 1000
const from = unixTime - ONE_DAY_SECONDS * daysToSub
const data = await fetchData(baseChartQuery!, from, unixTime)
chart.applyMoreData(data)
} catch (e) {
console.error('Error fetching new data')
}
chart.loadMore(() => null)
})
}
}, [baseChartQuery])
@ -175,230 +458,18 @@ const TradingViewChartKline = ({ setIsFullView, isFullView }: Props) => {
useEffect(() => {
const initKline = async () => {
const kLineChart = init('update-k-line')
kLineChart.setStyleOptions({
grid: {
show: false,
},
candle: {
bar: {
upColor: COLORS.UP[theme],
downColor: COLORS.DOWN[theme],
},
tooltip: {
labels: ['', 'O:', 'C:', 'H:', 'L:', 'V:'],
text: {
size: 12,
family: 'TT Mono',
weight: 'normal',
color: COLORS.FGD4[theme],
marginLeft: 8,
marginTop: 6,
marginRight: 8,
marginBottom: 0,
},
},
priceMark: {
show: true,
high: {
show: true,
color: COLORS.FGD4[theme],
textMargin: 5,
textSize: 10,
textFamily: 'TT Mono',
textWeight: 'normal',
},
low: {
show: true,
color: COLORS.FGD4[theme],
textMargin: 5,
textSize: 10,
textFamily: 'TT Mono',
textWeight: 'normal',
},
last: {
show: true,
upColor: COLORS.BKG4[theme],
downColor: COLORS.BKG4[theme],
noChangeColor: COLORS.BKG4[theme],
line: {
show: true,
// 'solid'|'dash'
style: 'dash',
dashValue: [4, 4],
size: 1,
},
text: {
show: true,
size: 10,
paddingLeft: 2,
paddingTop: 2,
paddingRight: 2,
paddingBottom: 2,
color: '#FFFFFF',
family: 'TT Mono',
weight: 'normal',
borderRadius: 2,
},
},
},
},
xAxis: {
axisLine: {
show: true,
color: COLORS.BKG4[theme],
size: 1,
},
tickLine: {
show: true,
size: 1,
length: 3,
color: COLORS.BKG4[theme],
},
tickText: {
show: true,
color: COLORS.FGD4[theme],
family: 'TT Mono',
weight: 'normal',
size: 10,
},
},
yAxis: {
axisLine: {
show: true,
color: COLORS.BKG4[theme],
size: 1,
},
tickLine: {
show: true,
size: 1,
length: 3,
color: COLORS.BKG4[theme],
},
tickText: {
show: true,
color: COLORS.FGD4[theme],
family: 'TT Mono',
weight: 'normal',
size: 10,
},
},
crosshair: {
show: true,
horizontal: {
show: true,
line: {
show: true,
style: 'dash',
dashValue: [4, 2],
size: 1,
color: COLORS.FGD4[theme],
},
text: {
show: true,
color: '#FFFFFF',
size: 10,
family: 'TT Mono',
weight: 'normal',
paddingLeft: 2,
paddingRight: 2,
paddingTop: 2,
paddingBottom: 2,
borderSize: 1,
borderColor: COLORS.FGD4[theme],
borderRadius: 2,
backgroundColor: COLORS.FGD4[theme],
},
},
vertical: {
show: true,
line: {
show: true,
style: 'dash',
dashValue: [4, 2],
size: 1,
color: COLORS.FGD4[theme],
},
text: {
show: true,
color: '#FFFFFF',
size: 10,
family: 'TT Mono',
weight: 'normal',
paddingLeft: 2,
paddingRight: 2,
paddingTop: 2,
paddingBottom: 2,
borderSize: 1,
borderColor: COLORS.FGD4[theme],
borderRadius: 2,
backgroundColor: COLORS.FGD4[theme],
},
},
},
technicalIndicator: {
margin: {
top: 0.2,
bottom: 0.1,
},
bar: {
upColor: COLORS.UP[theme],
downColor: COLORS.DOWN[theme],
noChangeColor: '#888888',
},
line: {
size: 1,
colors: ['#FF9600', '#9D65C9', '#2196F3', '#E11D74', '#01C5C4'],
},
circle: {
upColor: '#26A69A',
downColor: '#EF5350',
noChangeColor: '#888888',
},
lastValueMark: {
show: false,
text: {
show: false,
color: '#ffffff',
size: 12,
family: 'Helvetica Neue',
weight: 'normal',
paddingLeft: 3,
paddingTop: 2,
paddingRight: 3,
paddingBottom: 2,
borderRadius: 2,
},
},
tooltip: {
// 'always' | 'follow_cross' | 'none'
showRule: 'always',
// 'standard' | 'rect'
showType: 'standard',
showName: true,
showParams: true,
defaultValue: 'n/a',
text: {
size: 12,
family: 'TT Mono',
weight: 'normal',
color: COLORS.FGD4[theme],
marginTop: 6,
marginRight: 8,
marginBottom: 0,
marginLeft: 8,
},
},
},
separator: {
size: 2,
color: COLORS.BKG4[theme],
},
})
kLineChart.setStyleOptions({ ...styles })
setChart(kLineChart)
}
initKline()
return () => {
dispose('update-k-line')
if (socketConnected) {
console.log('[socket] kline disconnected')
socket.send(JSON.stringify(unsub_msg))
socket.close()
}
}
}, [])

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

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

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

@ -47,10 +47,12 @@
"side": "Side",
"size": "Size",
"spread": "Spread",
"stable-price": "Stable Price",
"tooltip-enable-margin": "Enable spot margin for this trade",
"tooltip-ioc": "Immediate-Or-Cancel (IOC) orders are guaranteed to be the taker and must be executed immediately. Any portion of the order that can't be filled immediately will be cancelled",
"tooltip-post": "Post orders are guaranteed to be the maker or they will be canceled",
"tooltip-slippage": "An estimate of the difference between the current price and the price your trade will be executed at",
"tooltip-stable-price": "Stable price is used in a safety mechanism that limits a user's ability to enter risky positions when the oracle price is changing rapidly",
"trade-sounds-tooltip": "Play a sound alert for every new trade",
"trades": "Trades",
"unsettled": "Unsettled"

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

@ -47,10 +47,12 @@
"side": "Side",
"size": "Size",
"spread": "Spread",
"stable-price": "Stable Price",
"tooltip-enable-margin": "Enable spot margin for this trade",
"tooltip-ioc": "Immediate-Or-Cancel (IOC) orders are guaranteed to be the taker and must be executed immediately. Any portion of the order that can't be filled immediately will be cancelled",
"tooltip-post": "Post orders are guaranteed to be the maker or they will be canceled",
"tooltip-slippage": "An estimate of the difference between the current price and the price your trade will be executed at",
"tooltip-stable-price": "Stable price is used in a safety mechanism that limits a user's ability to enter risky positions when the oracle price is changing rapidly",
"trade-sounds-tooltip": "Play a sound alert for every new trade",
"trades": "Trades",
"unsettled": "Unsettled"

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

@ -47,10 +47,12 @@
"side": "Side",
"size": "Size",
"spread": "Spread",
"stable-price": "Stable Price",
"tooltip-enable-margin": "Enable spot margin for this trade",
"tooltip-ioc": "Immediate-Or-Cancel (IOC) orders are guaranteed to be the taker and must be executed immediately. Any portion of the order that can't be filled immediately will be cancelled",
"tooltip-post": "Post orders are guaranteed to be the maker or they will be canceled",
"tooltip-slippage": "An estimate of the difference between the current price and the price your trade will be executed at",
"tooltip-stable-price": "Stable price is used in a safety mechanism that limits a user's ability to enter risky positions when the oracle price is changing rapidly",
"trade-sounds-tooltip": "Play a sound alert for every new trade",
"trades": "Trades",
"unsettled": "Unsettled"

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

@ -46,10 +46,12 @@
"side": "Side",
"size": "Size",
"spread": "Spread",
"stable-price": "Stable Price",
"tooltip-enable-margin": "Enable spot margin for this trade",
"tooltip-ioc": "Immediate-Or-Cancel (IOC) orders are guaranteed to be the taker and must be executed immediately. Any portion of the order that can't be filled immediately will be cancelled",
"tooltip-post": "Post orders are guaranteed to be the maker or they will be canceled",
"tooltip-slippage": "An estimate of the difference between the current price and the price your trade will be executed at",
"tooltip-stable-price": "Stable price is used in a safety mechanism that limits a user's ability to enter risky positions when the oracle price is changing rapidly",
"trade-sounds-tooltip": "Play a sound alert for every new trade",
"trades": "Trades",
"unsettled": "Unsettled"

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

@ -46,10 +46,12 @@
"side": "Side",
"size": "Size",
"spread": "Spread",
"stable-price": "Stable Price",
"tooltip-enable-margin": "Enable spot margin for this trade",
"tooltip-ioc": "Immediate-Or-Cancel (IOC) orders are guaranteed to be the taker and must be executed immediately. Any portion of the order that can't be filled immediately will be cancelled",
"tooltip-post": "Post orders are guaranteed to be the maker or they will be canceled",
"tooltip-slippage": "An estimate of the difference between the current price and the price your trade will be executed at",
"tooltip-stable-price": "Stable price is used in a safety mechanism that limits a user's ability to enter risky positions when the oracle price is changing rapidly",
"trade-sounds-tooltip": "Play a sound alert for every new trade",
"trades": "Trades",
"unsettled": "Unsettled"

View File

@ -167,6 +167,19 @@ interface NFT {
image: string
}
export interface PerpStatsItem {
date_hour: string
fees_accrued: number
funding_rate_hourly: number
instantaneous_funding_rate: number
mango_group: string
market_index: number
open_interest: number
perp_market: string
price: number
stable_price: number
}
interface ProfileDetails {
profile_image_url?: string
profile_name: string
@ -273,7 +286,7 @@ export type MangoStore = {
perpMarkets: PerpMarket[]
perpStats: {
loading: boolean
data: any[]
data: PerpStatsItem[] | null
}
profile: {
details: ProfileDetails | null
@ -726,7 +739,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
@ -735,7 +748,7 @@ const mangoStore = create<MangoStore>()(
}
await Promise.all(
mangoAccounts.map((ma) => ma.reloadAccountData(client))
mangoAccounts.map((ma) => ma.reloadSerum3OpenOrders(client))
)
set((state) => {
@ -830,7 +843,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

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