perp funding rate

This commit is contained in:
tjs 2022-11-30 10:46:20 -05:00
parent 02495833e5
commit 0757af2213
5 changed files with 229 additions and 209 deletions

View File

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

View File

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

View File

@ -25,19 +25,26 @@ 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
): SpotOrderBook | BookSide {
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
return book
} else {
const decodedAcc = client.program.coder.accounts.decode(
'bookSide',
accInfo.data
@ -48,11 +55,9 @@ function decodeBookL2(
side === 'bids' ? BookSideType.bids : BookSideType.asks,
decodedAcc
)
return book.getL2Ui(depth)
return book
}
}
return []
}
// export function decodeBook(
// market: Market,
@ -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)
})
}
}

View File

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

View File

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