Merge pull request #137 from blockworks-foundation/pan/orderbook-feed-v2

Use websocket feed for Orderbook
This commit is contained in:
tlrsssss 2023-04-27 13:55:30 -04:00 committed by GitHub
commit 8fb5c17d92
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 263 additions and 80 deletions

View File

@ -29,6 +29,7 @@ import useSelectedMarket from 'hooks/useSelectedMarket'
import { INITIAL_ANIMATION_SETTINGS } from '@components/settings/AnimationSettings'
import { ArrowPathIcon } from '@heroicons/react/20/solid'
import { sleep } from 'utils'
import { OrderbookFeed } from '@blockworks-foundation/mango-feeds'
const sizeCompacter = Intl.NumberFormat('en', {
maximumFractionDigits: 6,
@ -218,6 +219,7 @@ const Orderbook = () => {
const [tickSize, setTickSize] = useState(0)
const [showBids, setShowBids] = useState(true)
const [showAsks, setShowAsks] = useState(true)
const [useOrderbookFeed, setUseOrderbookFeed] = useState(true)
const currentOrderbookData = useRef<OrderbookL2>()
const orderbookElRef = useRef<HTMLDivElement>(null)
@ -382,6 +384,117 @@ const Orderbook = () => {
const market = getMarket()
if (!group || !market) return
if (useOrderbookFeed) {
let hasConnected = false
const orderbookFeed = new OrderbookFeed(
`wss://api.mngo.cloud/orderbook/v1/`,
{
reconnectionIntervalMs: 5_000,
reconnectionMaxAttempts: 6,
}
)
orderbookFeed.onConnect(() => {
console.log('[OrderbookFeed] connected')
hasConnected = true
orderbookFeed.subscribe({
marketId: market.publicKey.toBase58(),
})
})
orderbookFeed.onDisconnect((reconnectionAttemptsExhausted) => {
// fallback to rpc if we couldn't reconnect or if we never connected
if (reconnectionAttemptsExhausted || !hasConnected) {
console.warn('[OrderbookFeed] disconnected')
setUseOrderbookFeed(false)
} else {
console.log('[OrderbookFeed] reconnecting...')
}
})
let lastWriteVersion = 0
orderbookFeed.onL2Update((update) => {
const selectedMarket = mangoStore.getState().selectedMarket
if (!selectedMarket) return
// ensure updates are applied in the correct order by checking slot and writeVersion
const lastSeenSlot =
update.side == 'bid'
? mangoStore.getState().selectedMarket.lastSeenSlot.bids
: mangoStore.getState().selectedMarket.lastSeenSlot.asks
if (update.slot < lastSeenSlot) return
if (
update.slot == lastSeenSlot &&
update.writeVersion < lastWriteVersion
)
return
lastWriteVersion = update.writeVersion
const bookside =
update.side == 'bid'
? selectedMarket.orderbook.bids
: selectedMarket.orderbook.asks
const new_bookside = Array.from(bookside)
for (const diff of update.update) {
// find existing level for each update
const levelIndex = new_bookside.findIndex(
(level) => level && level.length && level[0] === diff[0]
)
if (diff[1] > 0) {
// level being added or updated
if (levelIndex !== -1) {
new_bookside[levelIndex] = diff
} else {
// add new level and resort
new_bookside.push(diff)
new_bookside.sort((a, b) => {
return update.side == 'bid' ? b[0] - a[0] : a[0] - b[0]
})
}
} else {
// level being removed if zero size
if (levelIndex !== -1) {
new_bookside.splice(levelIndex, 1)
} else {
console.warn('tried to remove missing level')
}
}
}
set((state) => {
if (update.side == 'bid') {
state.selectedMarket.bidsAccount = undefined
state.selectedMarket.orderbook.bids = new_bookside
state.selectedMarket.lastSeenSlot.bids = update.slot
} else {
state.selectedMarket.asksAccount = undefined
state.selectedMarket.orderbook.asks = new_bookside
state.selectedMarket.lastSeenSlot.asks = update.slot
}
})
})
orderbookFeed.onL2Checkpoint((checkpoint) => {
if (checkpoint.market !== market.publicKey.toBase58()) return
set((state) => {
state.selectedMarket.lastSeenSlot.bids = checkpoint.slot
state.selectedMarket.lastSeenSlot.asks = checkpoint.slot
state.selectedMarket.bidsAccount = undefined
state.selectedMarket.asksAccount = undefined
state.selectedMarket.orderbook.bids = checkpoint.bids
state.selectedMarket.orderbook.asks = checkpoint.asks
})
})
orderbookFeed.onStatus((update) => {
console.log('[OrderbookFeed] status', update)
})
return () => {
console.log('[OrderbookFeed] unsubscribe')
orderbookFeed.unsubscribe(market.publicKey.toBase58())
}
} else {
console.log('using rpc orderbook feed')
let bidSubscriptionId: number | undefined = undefined
let askSubscriptionId: number | undefined = undefined
const bidsPk = new PublicKey(bidAccountAddress)
@ -456,6 +569,7 @@ const Orderbook = () => {
)
}
return () => {
console.log('rpc orderbook unsubscribe')
if (typeof bidSubscriptionId !== 'undefined') {
connection.removeAccountChangeListener(bidSubscriptionId)
}
@ -463,7 +577,8 @@ const Orderbook = () => {
connection.removeAccountChangeListener(askSubscriptionId)
}
}
}, [bidAccountAddress, askAccountAddress, connection])
}
}, [bidAccountAddress, askAccountAddress, connection, useOrderbookFeed])
useEffect(() => {
window.addEventListener('resize', verticallyCenterOrderbook)
@ -698,7 +813,7 @@ const OrderbookRow = ({
const formattedSize = useMemo(() => {
return minOrderSize && !isNaN(size)
? floorToDecimal(size, getDecimalCount(minOrderSize))
: new Decimal(size)
: new Decimal(size ?? -1)
}, [size, minOrderSize])
const formattedPrice = useMemo(() => {

View File

@ -8,6 +8,7 @@ import { MANGO_DATA_API_URL } from 'utils/constants'
import Tooltip from '@components/shared/Tooltip'
import { useTranslation } from 'next-i18next'
import mangoStore from '@store/mangoStore'
import { OrderbookL2 } from 'types'
const fetchFundingRate = async (groupPk: string | undefined) => {
const res = await fetch(
@ -38,6 +39,57 @@ export const formatFunding = Intl.NumberFormat('en', {
style: 'percent',
})
function getImpactPriceL2(
bookside: number[][],
baseDepth: number
): number | undefined {
let total = 0
for (const level of bookside) {
total += level[1]
if (total >= baseDepth) {
return level[0]
}
}
return undefined
}
function getInstantaneousFundingRateL2(
market: PerpMarket,
orderbook: OrderbookL2
) {
const MIN_FUNDING = market.minFunding.toNumber()
const MAX_FUNDING = market.maxFunding.toNumber()
const bid = getImpactPriceL2(
orderbook.bids,
market.baseLotsToUi(market.impactQuantity)
)
console.log(bid)
const ask = getImpactPriceL2(
orderbook.asks,
market.baseLotsToUi(market.impactQuantity)
)
console.log(ask)
const indexPrice = market._uiPrice
let funding
if (bid !== undefined && ask !== undefined) {
const bookPrice = (bid + ask) / 2
funding = Math.min(
Math.max(bookPrice / indexPrice - 1, MIN_FUNDING),
MAX_FUNDING
)
} else if (bid !== undefined) {
funding = MAX_FUNDING
} else if (ask !== undefined) {
funding = MIN_FUNDING
} else {
funding = 0
}
return funding
}
const PerpFundingRate = () => {
const { selectedMarket } = useSelectedMarket()
const rate = usePerpFundingRate()
@ -45,6 +97,7 @@ const PerpFundingRate = () => {
const bids = mangoStore((s) => s.selectedMarket.bidsAccount)
const asks = mangoStore((s) => s.selectedMarket.asksAccount)
const orderbook = mangoStore((s) => s.selectedMarket.orderbook)
const fundingRate = useMemo(() => {
if (rate.isSuccess && selectedMarket instanceof PerpMarket) {
@ -56,6 +109,21 @@ const PerpFundingRate = () => {
}
}, [rate, selectedMarket])
const instantaneousRate = useMemo(() => {
if (!(selectedMarket instanceof PerpMarket)) return undefined
if (bids instanceof BookSide && asks instanceof BookSide) {
return selectedMarket.getInstantaneousFundingRateUi(bids, asks).toFixed(4)
}
if (orderbook.asks.length && orderbook.bids.length) {
return (
getInstantaneousFundingRateL2(selectedMarket, orderbook) * 100
).toFixed(4)
}
return undefined
}, [orderbook, bids, asks, selectedMarket])
return (
<>
<div className="ml-6 flex-col whitespace-nowrap">
@ -76,15 +144,9 @@ const PerpFundingRate = () => {
{formatFunding.format(fundingRate * 8760)}.
</div>
) : null}
{selectedMarket instanceof PerpMarket &&
bids instanceof BookSide &&
asks instanceof BookSide ? (
{instantaneousRate ? (
<div className="mt-1">
The latest instantaneous rate is{' '}
{selectedMarket
.getInstantaneousFundingRateUi(bids, asks)
.toFixed(4)}
%
The latest instantaneous rate is {instantaneousRate}%
</div>
) : null}
</>

View File

@ -18,7 +18,7 @@
"postinstall": "tar -xzC public -f vendor/charting_library.tgz;tar -xzC public -f vendor/datafeeds.tgz"
},
"dependencies": {
"@blockworks-foundation/mango-feeds": "0.1.5",
"@blockworks-foundation/mango-feeds": "0.1.6",
"@blockworks-foundation/mango-v4": "^0.13.4",
"@headlessui/react": "1.6.6",
"@heroicons/react": "2.0.10",
@ -66,6 +66,7 @@
"recharts": "2.1.14",
"tsparticles": "2.2.4",
"walktour": "5.1.1",
"webpack-node-externals": "3.0.0",
"zustand": "4.1.3"
},
"peerDependencies": {

View File

@ -14,10 +14,10 @@
dependencies:
regenerator-runtime "^0.13.11"
"@blockworks-foundation/mango-feeds@0.1.5":
version "0.1.5"
resolved "https://registry.yarnpkg.com/@blockworks-foundation/mango-feeds/-/mango-feeds-0.1.5.tgz#c31132e4d71265e0cee4bd06acebd891001a0d5e"
integrity sha512-ZAiKoXk4ULy6/GBB8AIoI6sIAtgdqLNo1JzsweSaiYfhOFOMs4/BG0WGAk+hoRYjpzJMikzqR3Jw4Wn+stmVyw==
"@blockworks-foundation/mango-feeds@0.1.6":
version "0.1.6"
resolved "https://registry.yarnpkg.com/@blockworks-foundation/mango-feeds/-/mango-feeds-0.1.6.tgz#ae82a0f1cb131633b7edd76e2397cfd486cd21c0"
integrity sha512-vsulzORXp8Qyd74ogm809UxxiiL2Gr9UmcxHMeYpd8tohVHLcFF6+SGKmX/9ImSElBrw+zEKIzY5O50w/QM7mQ==
dependencies:
ws "^8.13.0"
@ -8566,6 +8566,11 @@ webidl-conversions@^3.0.0:
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==
webpack-node-externals@3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/webpack-node-externals/-/webpack-node-externals-3.0.0.tgz#1a3407c158d547a9feb4229a9e3385b7b60c9917"
integrity sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==
webrtc-adapter@^7.2.1:
version "7.7.1"
resolved "https://registry.yarnpkg.com/webrtc-adapter/-/webrtc-adapter-7.7.1.tgz#b2c227a6144983b35057df67bd984a7d4bfd17f1"