Merge pull request #137 from blockworks-foundation/pan/orderbook-feed-v2
Use websocket feed for Orderbook
This commit is contained in:
commit
8fb5c17d92
|
@ -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,88 +384,201 @@ const Orderbook = () => {
|
|||
const market = getMarket()
|
||||
if (!group || !market) return
|
||||
|
||||
let bidSubscriptionId: number | undefined = undefined
|
||||
let askSubscriptionId: number | undefined = undefined
|
||||
const bidsPk = new PublicKey(bidAccountAddress)
|
||||
if (bidsPk) {
|
||||
connection
|
||||
.getAccountInfoAndContext(bidsPk)
|
||||
.then(({ context, value: info }) => {
|
||||
if (!info) return
|
||||
const decodedBook = decodeBook(client, market, info, 'bids')
|
||||
set((state) => {
|
||||
state.selectedMarket.lastSeenSlot.bids = context.slot
|
||||
state.selectedMarket.bidsAccount = decodedBook
|
||||
state.selectedMarket.orderbook.bids = decodeBookL2(decodedBook)
|
||||
})
|
||||
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(),
|
||||
})
|
||||
bidSubscriptionId = connection.onAccountChange(
|
||||
bidsPk,
|
||||
(info, context) => {
|
||||
const lastSeenSlot =
|
||||
mangoStore.getState().selectedMarket.lastSeenSlot.bids
|
||||
if (context.slot > lastSeenSlot) {
|
||||
const market = getMarket()
|
||||
if (!market) return
|
||||
const decodedBook = decodeBook(client, market, info, 'bids')
|
||||
if (decodedBook instanceof BookSide) {
|
||||
updatePerpMarketOnGroup(decodedBook, 'bids')
|
||||
})
|
||||
|
||||
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)
|
||||
if (bidsPk) {
|
||||
connection
|
||||
.getAccountInfoAndContext(bidsPk)
|
||||
.then(({ context, value: info }) => {
|
||||
if (!info) return
|
||||
const decodedBook = decodeBook(client, market, info, 'bids')
|
||||
set((state) => {
|
||||
state.selectedMarket.lastSeenSlot.bids = context.slot
|
||||
state.selectedMarket.bidsAccount = decodedBook
|
||||
state.selectedMarket.orderbook.bids = decodeBookL2(decodedBook)
|
||||
state.selectedMarket.lastSeenSlot.bids = context.slot
|
||||
})
|
||||
}
|
||||
},
|
||||
'processed'
|
||||
)
|
||||
}
|
||||
|
||||
const asksPk = new PublicKey(askAccountAddress)
|
||||
if (asksPk) {
|
||||
connection
|
||||
.getAccountInfoAndContext(asksPk)
|
||||
.then(({ context, value: info }) => {
|
||||
if (!info) return
|
||||
const decodedBook = decodeBook(client, market, info, 'asks')
|
||||
set((state) => {
|
||||
state.selectedMarket.asksAccount = decodedBook
|
||||
state.selectedMarket.orderbook.asks = decodeBookL2(decodedBook)
|
||||
state.selectedMarket.lastSeenSlot.asks = context.slot
|
||||
})
|
||||
})
|
||||
askSubscriptionId = connection.onAccountChange(
|
||||
asksPk,
|
||||
(info, context) => {
|
||||
const lastSeenSlot =
|
||||
mangoStore.getState().selectedMarket.lastSeenSlot.asks
|
||||
if (context.slot > lastSeenSlot) {
|
||||
const market = getMarket()
|
||||
if (!market) return
|
||||
const decodedBook = decodeBook(client, market, info, 'asks')
|
||||
if (decodedBook instanceof BookSide) {
|
||||
updatePerpMarketOnGroup(decodedBook, 'asks')
|
||||
bidSubscriptionId = connection.onAccountChange(
|
||||
bidsPk,
|
||||
(info, context) => {
|
||||
const lastSeenSlot =
|
||||
mangoStore.getState().selectedMarket.lastSeenSlot.bids
|
||||
if (context.slot > lastSeenSlot) {
|
||||
const market = getMarket()
|
||||
if (!market) return
|
||||
const decodedBook = decodeBook(client, market, info, 'bids')
|
||||
if (decodedBook instanceof BookSide) {
|
||||
updatePerpMarketOnGroup(decodedBook, 'bids')
|
||||
}
|
||||
set((state) => {
|
||||
state.selectedMarket.bidsAccount = decodedBook
|
||||
state.selectedMarket.orderbook.bids = decodeBookL2(decodedBook)
|
||||
state.selectedMarket.lastSeenSlot.bids = context.slot
|
||||
})
|
||||
}
|
||||
},
|
||||
'processed'
|
||||
)
|
||||
}
|
||||
|
||||
const asksPk = new PublicKey(askAccountAddress)
|
||||
if (asksPk) {
|
||||
connection
|
||||
.getAccountInfoAndContext(asksPk)
|
||||
.then(({ context, value: info }) => {
|
||||
if (!info) return
|
||||
const decodedBook = decodeBook(client, market, info, 'asks')
|
||||
set((state) => {
|
||||
state.selectedMarket.asksAccount = decodedBook
|
||||
state.selectedMarket.orderbook.asks = decodeBookL2(decodedBook)
|
||||
state.selectedMarket.lastSeenSlot.asks = context.slot
|
||||
})
|
||||
}
|
||||
},
|
||||
'processed'
|
||||
)
|
||||
}
|
||||
return () => {
|
||||
if (typeof bidSubscriptionId !== 'undefined') {
|
||||
connection.removeAccountChangeListener(bidSubscriptionId)
|
||||
})
|
||||
askSubscriptionId = connection.onAccountChange(
|
||||
asksPk,
|
||||
(info, context) => {
|
||||
const lastSeenSlot =
|
||||
mangoStore.getState().selectedMarket.lastSeenSlot.asks
|
||||
if (context.slot > lastSeenSlot) {
|
||||
const market = getMarket()
|
||||
if (!market) return
|
||||
const decodedBook = decodeBook(client, market, info, 'asks')
|
||||
if (decodedBook instanceof BookSide) {
|
||||
updatePerpMarketOnGroup(decodedBook, 'asks')
|
||||
}
|
||||
set((state) => {
|
||||
state.selectedMarket.asksAccount = decodedBook
|
||||
state.selectedMarket.orderbook.asks = decodeBookL2(decodedBook)
|
||||
state.selectedMarket.lastSeenSlot.asks = context.slot
|
||||
})
|
||||
}
|
||||
},
|
||||
'processed'
|
||||
)
|
||||
}
|
||||
if (typeof askSubscriptionId !== 'undefined') {
|
||||
connection.removeAccountChangeListener(askSubscriptionId)
|
||||
return () => {
|
||||
console.log('rpc orderbook unsubscribe')
|
||||
if (typeof bidSubscriptionId !== 'undefined') {
|
||||
connection.removeAccountChangeListener(bidSubscriptionId)
|
||||
}
|
||||
if (typeof askSubscriptionId !== 'undefined') {
|
||||
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(() => {
|
||||
|
|
|
@ -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}
|
||||
</>
|
||||
|
|
|
@ -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": {
|
||||
|
|
13
yarn.lock
13
yarn.lock
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue