perp funding rate
This commit is contained in:
parent
02495833e5
commit
0757af2213
|
@ -1,159 +1,13 @@
|
|||
import { PerpMarket } from '@blockworks-foundation/mango-v4'
|
||||
import Change from '@components/shared/Change'
|
||||
import ChartRangeButtons from '@components/shared/ChartRangeButtons'
|
||||
import FavoriteMarketButton from '@components/shared/FavoriteMarketButton'
|
||||
import TabUnderline from '@components/shared/TabUnderline'
|
||||
import { Popover } from '@headlessui/react'
|
||||
import { ChevronDownIcon } from '@heroicons/react/20/solid'
|
||||
import mangoStore from '@store/mangoStore'
|
||||
import { useCoingecko } from 'hooks/useCoingecko'
|
||||
import useOraclePrice from 'hooks/useOraclePrice'
|
||||
import useSelectedMarket from 'hooks/useSelectedMarket'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import Link from 'next/link'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { DEFAULT_MARKET_NAME } from 'utils/constants'
|
||||
import { useMemo } from 'react'
|
||||
import { formatFixedDecimals } from 'utils/numbers'
|
||||
import MarketLogos from './MarketLogos'
|
||||
|
||||
const MarketSelectDropdown = () => {
|
||||
const { selectedMarket } = useSelectedMarket()
|
||||
const serumMarkets = mangoStore((s) => s.serumMarkets)
|
||||
const perpMarkets = mangoStore((s) => s.perpMarkets)
|
||||
const [activeTab, setActiveTab] = useState('perp')
|
||||
const [spotBaseFilter, setSpotBaseFilter] = useState('All')
|
||||
|
||||
const spotBaseTokens: string[] = useMemo(() => {
|
||||
if (serumMarkets.length) {
|
||||
const baseTokens: string[] = []
|
||||
serumMarkets.map((m) => {
|
||||
const base = m.name.split('/')[1]
|
||||
if (!baseTokens.includes(base)) {
|
||||
baseTokens.push(base)
|
||||
}
|
||||
})
|
||||
return baseTokens
|
||||
}
|
||||
return []
|
||||
}, [serumMarkets])
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
{({ open }) => (
|
||||
<div
|
||||
className="relative flex flex-col overflow-visible"
|
||||
id="trade-step-one"
|
||||
>
|
||||
<Popover.Button className="default-transition flex w-full items-center justify-between hover:text-th-primary">
|
||||
<>
|
||||
{selectedMarket ? <MarketLogos market={selectedMarket} /> : null}
|
||||
</>
|
||||
<div className="text-xl font-bold text-th-fgd-1 md:text-base">
|
||||
{selectedMarket?.name || DEFAULT_MARKET_NAME}
|
||||
</div>
|
||||
<ChevronDownIcon
|
||||
className={`${
|
||||
open ? 'rotate-180' : 'rotate-360'
|
||||
} mt-0.5 ml-2 h-6 w-6 flex-shrink-0 text-th-fgd-3`}
|
||||
/>
|
||||
</Popover.Button>
|
||||
<Popover.Panel className="absolute -left-5 top-[46px] z-50 mr-4 w-screen bg-th-bkg-2 pb-2 pt-4 sm:w-72 md:top-[37px]">
|
||||
<TabUnderline
|
||||
activeValue={activeTab}
|
||||
onChange={(v) => setActiveTab(v)}
|
||||
small
|
||||
values={['perp', 'spot']}
|
||||
/>
|
||||
{activeTab === 'spot' ? (
|
||||
serumMarkets?.length ? (
|
||||
<>
|
||||
<div className="mb-2 w-56 px-4">
|
||||
<ChartRangeButtons
|
||||
activeValue={spotBaseFilter}
|
||||
values={['All', ...spotBaseTokens]}
|
||||
onChange={(v) => setSpotBaseFilter(v)}
|
||||
/>
|
||||
</div>
|
||||
{serumMarkets
|
||||
.filter((mkt) => {
|
||||
if (spotBaseFilter === 'All') {
|
||||
return mkt
|
||||
} else {
|
||||
return mkt.name.split('/')[1] === spotBaseFilter
|
||||
}
|
||||
})
|
||||
.map((m) => {
|
||||
return (
|
||||
<div
|
||||
className="flex items-center justify-between py-2 px-4"
|
||||
key={m.publicKey.toString()}
|
||||
>
|
||||
<Link
|
||||
href={{
|
||||
pathname: '/trade',
|
||||
query: { name: m.name },
|
||||
}}
|
||||
shallow={true}
|
||||
>
|
||||
<div className="default-transition flex items-center hover:cursor-pointer hover:text-th-primary">
|
||||
<MarketLogos market={m} />
|
||||
<span
|
||||
className={
|
||||
m.name === selectedMarket?.name
|
||||
? 'text-th-primary'
|
||||
: ''
|
||||
}
|
||||
>
|
||||
{m.name}
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
<FavoriteMarketButton market={m} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
) : null
|
||||
) : null}
|
||||
{activeTab === 'perp'
|
||||
? perpMarkets?.length
|
||||
? perpMarkets.map((m) => {
|
||||
return (
|
||||
<div
|
||||
className="flex items-center justify-between py-2 px-4"
|
||||
key={m.publicKey.toString()}
|
||||
>
|
||||
<Link
|
||||
href={{
|
||||
pathname: '/trade',
|
||||
query: { name: m.name },
|
||||
}}
|
||||
shallow={true}
|
||||
>
|
||||
<div className="default-transition flex items-center hover:cursor-pointer hover:bg-th-bkg-2">
|
||||
<MarketLogos market={m} />
|
||||
<span
|
||||
className={
|
||||
m.name === selectedMarket?.name
|
||||
? 'text-th-primary'
|
||||
: ''
|
||||
}
|
||||
>
|
||||
{m.name}
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
<FavoriteMarketButton market={m} />
|
||||
</div>
|
||||
)
|
||||
})
|
||||
: null
|
||||
: null}
|
||||
</Popover.Panel>
|
||||
</div>
|
||||
)}
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
import MarketSelectDropdown from './MarketSelectDropdown'
|
||||
import PerpFundingRate from './PerpFundingRate'
|
||||
|
||||
const AdvancedMarketHeader = () => {
|
||||
const { t } = useTranslation(['common', 'trade'])
|
||||
|
@ -198,14 +52,13 @@ const AdvancedMarketHeader = () => {
|
|||
<div className="ml-6 flex-col">
|
||||
<div className="text-xs text-th-fgd-4">{t('rolling-change')}</div>
|
||||
<Change change={change} size="small" />
|
||||
{/* <div
|
||||
className={`font-mono text-xs ${
|
||||
change < 0 ? 'text-th-red' : 'text-th-gree'
|
||||
}`}
|
||||
>
|
||||
{isNaN(change) ? '0.00' : change.toFixed(2)}%
|
||||
</div> */}
|
||||
</div>
|
||||
{selectedMarket instanceof PerpMarket ? (
|
||||
<div className="ml-6 flex-col">
|
||||
<div className="text-xs text-th-fgd-4">Funding Rate</div>
|
||||
<PerpFundingRate />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,153 @@
|
|||
import ChartRangeButtons from '@components/shared/ChartRangeButtons'
|
||||
import FavoriteMarketButton from '@components/shared/FavoriteMarketButton'
|
||||
import TabUnderline from '@components/shared/TabUnderline'
|
||||
import { Popover } from '@headlessui/react'
|
||||
import { ChevronDownIcon } from '@heroicons/react/20/solid'
|
||||
import mangoStore from '@store/mangoStore'
|
||||
import useSelectedMarket from 'hooks/useSelectedMarket'
|
||||
import Link from 'next/link'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { DEFAULT_MARKET_NAME } from 'utils/constants'
|
||||
import MarketLogos from './MarketLogos'
|
||||
|
||||
const MarketSelectDropdown = () => {
|
||||
const { selectedMarket } = useSelectedMarket()
|
||||
const serumMarkets = mangoStore((s) => s.serumMarkets)
|
||||
const perpMarkets = mangoStore((s) => s.perpMarkets)
|
||||
const [activeTab, setActiveTab] = useState('perp')
|
||||
const [spotBaseFilter, setSpotBaseFilter] = useState('All')
|
||||
|
||||
const spotBaseTokens: string[] = useMemo(() => {
|
||||
if (serumMarkets.length) {
|
||||
const baseTokens: string[] = []
|
||||
serumMarkets.map((m) => {
|
||||
const base = m.name.split('/')[1]
|
||||
if (!baseTokens.includes(base)) {
|
||||
baseTokens.push(base)
|
||||
}
|
||||
})
|
||||
return baseTokens
|
||||
}
|
||||
return []
|
||||
}, [serumMarkets])
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
{({ open }) => (
|
||||
<div
|
||||
className="relative flex flex-col overflow-visible"
|
||||
id="trade-step-one"
|
||||
>
|
||||
<Popover.Button className="default-transition flex w-full items-center justify-between hover:text-th-primary">
|
||||
<>
|
||||
{selectedMarket ? <MarketLogos market={selectedMarket} /> : null}
|
||||
</>
|
||||
<div className="text-xl font-bold text-th-fgd-1 md:text-base">
|
||||
{selectedMarket?.name || DEFAULT_MARKET_NAME}
|
||||
</div>
|
||||
<ChevronDownIcon
|
||||
className={`${
|
||||
open ? 'rotate-180' : 'rotate-360'
|
||||
} mt-0.5 ml-2 h-6 w-6 flex-shrink-0 text-th-fgd-3`}
|
||||
/>
|
||||
</Popover.Button>
|
||||
<Popover.Panel className="absolute -left-5 top-[46px] z-50 mr-4 w-screen bg-th-bkg-2 pb-2 pt-4 sm:w-72 md:top-[37px]">
|
||||
<TabUnderline
|
||||
activeValue={activeTab}
|
||||
onChange={(v) => setActiveTab(v)}
|
||||
small
|
||||
values={['perp', 'spot']}
|
||||
/>
|
||||
{activeTab === 'spot' ? (
|
||||
serumMarkets?.length ? (
|
||||
<>
|
||||
<div className="mb-2 w-56 px-4">
|
||||
<ChartRangeButtons
|
||||
activeValue={spotBaseFilter}
|
||||
values={['All', ...spotBaseTokens]}
|
||||
onChange={(v) => setSpotBaseFilter(v)}
|
||||
/>
|
||||
</div>
|
||||
{serumMarkets
|
||||
.filter((mkt) => {
|
||||
if (spotBaseFilter === 'All') {
|
||||
return mkt
|
||||
} else {
|
||||
return mkt.name.split('/')[1] === spotBaseFilter
|
||||
}
|
||||
})
|
||||
.map((m) => {
|
||||
return (
|
||||
<div
|
||||
className="flex items-center justify-between py-2 px-4"
|
||||
key={m.publicKey.toString()}
|
||||
>
|
||||
<Link
|
||||
href={{
|
||||
pathname: '/trade',
|
||||
query: { name: m.name },
|
||||
}}
|
||||
shallow={true}
|
||||
>
|
||||
<div className="default-transition flex items-center hover:cursor-pointer hover:text-th-primary">
|
||||
<MarketLogos market={m} />
|
||||
<span
|
||||
className={
|
||||
m.name === selectedMarket?.name
|
||||
? 'text-th-primary'
|
||||
: ''
|
||||
}
|
||||
>
|
||||
{m.name}
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
<FavoriteMarketButton market={m} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
) : null
|
||||
) : null}
|
||||
{activeTab === 'perp'
|
||||
? perpMarkets?.length
|
||||
? perpMarkets.map((m) => {
|
||||
return (
|
||||
<div
|
||||
className="flex items-center justify-between py-2 px-4"
|
||||
key={m.publicKey.toString()}
|
||||
>
|
||||
<Link
|
||||
href={{
|
||||
pathname: '/trade',
|
||||
query: { name: m.name },
|
||||
}}
|
||||
shallow={true}
|
||||
>
|
||||
<div className="default-transition flex items-center hover:cursor-pointer hover:bg-th-bkg-2">
|
||||
<MarketLogos market={m} />
|
||||
<span
|
||||
className={
|
||||
m.name === selectedMarket?.name
|
||||
? 'text-th-primary'
|
||||
: ''
|
||||
}
|
||||
>
|
||||
{m.name}
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
<FavoriteMarketButton market={m} />
|
||||
</div>
|
||||
)
|
||||
})
|
||||
: null
|
||||
: null}
|
||||
</Popover.Panel>
|
||||
</div>
|
||||
)}
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
export default MarketSelectDropdown
|
|
@ -25,33 +25,38 @@ import {
|
|||
import useSelectedMarket from 'hooks/useSelectedMarket'
|
||||
import { INITIAL_ANIMATION_SETTINGS } from '@components/settings/AnimationSettings'
|
||||
|
||||
function decodeBookL2(
|
||||
export const decodeBookL2 = (book: SpotOrderBook | BookSide): number[][] => {
|
||||
const depth = 40
|
||||
if (book instanceof SpotOrderBook) {
|
||||
book.getL2(depth).map(([price, size]) => [price, size])
|
||||
} else if (book instanceof BookSide) {
|
||||
return book.getL2Ui(depth)
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
function decodeBook(
|
||||
client: MangoClient,
|
||||
market: Market | PerpMarket,
|
||||
accInfo: AccountInfo<Buffer>,
|
||||
side: 'bids' | 'asks'
|
||||
): number[][] {
|
||||
if (market && accInfo?.data) {
|
||||
const depth = 40
|
||||
if (market instanceof Market) {
|
||||
const book = SpotOrderBook.decode(market, accInfo.data)
|
||||
return book.getL2(depth).map(([price, size]) => [price, size])
|
||||
} else if (market instanceof PerpMarket) {
|
||||
// FIXME: Review the null being passed here
|
||||
const decodedAcc = client.program.coder.accounts.decode(
|
||||
'bookSide',
|
||||
accInfo.data
|
||||
)
|
||||
const book = BookSide.from(
|
||||
client,
|
||||
market,
|
||||
side === 'bids' ? BookSideType.bids : BookSideType.asks,
|
||||
decodedAcc
|
||||
)
|
||||
return book.getL2Ui(depth)
|
||||
}
|
||||
): SpotOrderBook | BookSide {
|
||||
if (market instanceof Market) {
|
||||
const book = SpotOrderBook.decode(market, accInfo.data)
|
||||
return book
|
||||
} else {
|
||||
const decodedAcc = client.program.coder.accounts.decode(
|
||||
'bookSide',
|
||||
accInfo.data
|
||||
)
|
||||
const book = BookSide.from(
|
||||
client,
|
||||
market,
|
||||
side === 'bids' ? BookSideType.bids : BookSideType.asks,
|
||||
decodedAcc
|
||||
)
|
||||
return book
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
// export function decodeBook(
|
||||
|
@ -327,14 +332,10 @@ const Orderbook = () => {
|
|||
if (bidsPk) {
|
||||
connection.getAccountInfo(bidsPk).then((info) => {
|
||||
if (!info) return
|
||||
const decodedBook = decodeBook(client, market, info, 'bids')
|
||||
set((state) => {
|
||||
// state.accountInfos[bidsPk.toString()] = info
|
||||
state.selectedMarket.orderbook.bids = decodeBookL2(
|
||||
client,
|
||||
market,
|
||||
info,
|
||||
'bids'
|
||||
)
|
||||
state.selectedMarket.bidsAccount = decodedBook
|
||||
state.selectedMarket.orderbook.bids = decodeBookL2(decodedBook)
|
||||
})
|
||||
})
|
||||
console.log('bidsPk', bidsPk)
|
||||
|
@ -347,15 +348,10 @@ const Orderbook = () => {
|
|||
previousBidInfo.lamports !== info.lamports
|
||||
) {
|
||||
previousBidInfo = info
|
||||
// info['parsed'] = decodeBook(serum3MarketExternal, info)
|
||||
const decodedBook = decodeBook(client, market, info, 'bids')
|
||||
set((state) => {
|
||||
// state.accountInfos[bidsPk.toString()] = info
|
||||
state.selectedMarket.orderbook.bids = decodeBookL2(
|
||||
client,
|
||||
market,
|
||||
info,
|
||||
'bids'
|
||||
)
|
||||
state.selectedMarket.bidsAccount = decodedBook
|
||||
state.selectedMarket.orderbook.bids = decodeBookL2(decodedBook)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -367,14 +363,10 @@ const Orderbook = () => {
|
|||
if (asksPk) {
|
||||
connection.getAccountInfo(asksPk).then((info) => {
|
||||
if (!info) return
|
||||
const decodedBook = decodeBook(client, market, info, 'asks')
|
||||
set((state) => {
|
||||
// state.accountInfos[bidsPk.toString()] = info
|
||||
state.selectedMarket.orderbook.asks = decodeBookL2(
|
||||
client,
|
||||
market,
|
||||
info,
|
||||
'bids'
|
||||
)
|
||||
state.selectedMarket.asksAccount = decodedBook
|
||||
state.selectedMarket.orderbook.asks = decodeBookL2(decodedBook)
|
||||
})
|
||||
})
|
||||
askSubscriptionId = connection.onAccountChange(
|
||||
|
@ -386,15 +378,10 @@ const Orderbook = () => {
|
|||
previousAskInfo.lamports !== info.lamports
|
||||
) {
|
||||
previousAskInfo = info
|
||||
// info['parsed'] = decodeBook(serum3MarketExternal, info)
|
||||
const decodedBook = decodeBook(client, market, info, 'asks')
|
||||
set((state) => {
|
||||
// state.accountInfos[asksPk.toString()] = info
|
||||
state.selectedMarket.orderbook.asks = decodeBookL2(
|
||||
client,
|
||||
market,
|
||||
info,
|
||||
'asks'
|
||||
)
|
||||
state.selectedMarket.asksAccount = decodedBook
|
||||
state.selectedMarket.orderbook.asks = decodeBookL2(decodedBook)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
import { BookSide, PerpMarket } from '@blockworks-foundation/mango-v4'
|
||||
import mangoStore from '@store/mangoStore'
|
||||
import useSelectedMarket from 'hooks/useSelectedMarket'
|
||||
|
||||
const PerpFundingRate = () => {
|
||||
const { selectedMarket } = useSelectedMarket()
|
||||
const bids = mangoStore((s) => s.selectedMarket.bidsAccount)
|
||||
const asks = mangoStore((s) => s.selectedMarket.asksAccount)
|
||||
|
||||
return (
|
||||
<div className="font-mono text-xs text-th-fgd-2">
|
||||
{selectedMarket instanceof PerpMarket &&
|
||||
bids instanceof BookSide &&
|
||||
asks instanceof BookSide
|
||||
? selectedMarket.getCurrentFundingRate(bids, asks)
|
||||
: '-'}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PerpFundingRate
|
|
@ -5,6 +5,7 @@ import { subscribeWithSelector } from 'zustand/middleware'
|
|||
import { AnchorProvider, Wallet, web3 } from '@project-serum/anchor'
|
||||
import { Connection, Keypair, PublicKey } from '@solana/web3.js'
|
||||
import { OpenOrders, Order } from '@project-serum/serum/lib/market'
|
||||
import { Orderbook as SpotOrderBook } from '@project-serum/serum'
|
||||
import { Wallet as WalletAdapter } from '@solana/wallet-adapter-react'
|
||||
import {
|
||||
MangoClient,
|
||||
|
@ -15,6 +16,7 @@ import {
|
|||
Bank,
|
||||
PerpOrder,
|
||||
PerpPosition,
|
||||
BookSide,
|
||||
} from '@blockworks-foundation/mango-v4'
|
||||
|
||||
import EmptyWallet from '../utils/wallet'
|
||||
|
@ -194,6 +196,8 @@ export type MangoStore = {
|
|||
name: string
|
||||
current: Serum3Market | PerpMarket | undefined
|
||||
fills: any
|
||||
bidsAccount: BookSide | SpotOrderBook | undefined
|
||||
asksAccount: BookSide | SpotOrderBook | undefined
|
||||
orderbook: Orderbook
|
||||
}
|
||||
serumMarkets: Serum3Market[]
|
||||
|
@ -295,6 +299,8 @@ const mangoStore = create<MangoStore>()(
|
|||
name: DEFAULT_MARKET_NAME,
|
||||
current: undefined,
|
||||
fills: [],
|
||||
bidsAccount: undefined,
|
||||
asksAccount: undefined,
|
||||
orderbook: {
|
||||
bids: [],
|
||||
asks: [],
|
||||
|
|
Loading…
Reference in New Issue