show trade executions on tv charts
This commit is contained in:
parent
7f88073f03
commit
e141233d79
|
@ -19,11 +19,10 @@ import TradeVolumeAlertModal, {
|
|||
DEFAULT_VOLUME_ALERT_SETTINGS,
|
||||
} from '@components/modals/TradeVolumeAlertModal'
|
||||
import dayjs from 'dayjs'
|
||||
import { isPerpFillEvent } from './TradeHistory'
|
||||
import ErrorBoundary from '@components/ErrorBoundary'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { PerpMarket } from '@blockworks-foundation/mango-v4'
|
||||
import { EmptyObject, PerpTradeHistory } from 'types'
|
||||
import { EmptyObject, isPerpFillEvent, PerpTradeHistory } from 'types'
|
||||
import { Market } from '@project-serum/serum'
|
||||
|
||||
const volumeAlertSound = new Howl({
|
||||
|
|
|
@ -1,9 +1,4 @@
|
|||
import {
|
||||
Group,
|
||||
ParsedFillEvent,
|
||||
PerpMarket,
|
||||
Serum3Market,
|
||||
} from '@blockworks-foundation/mango-v4'
|
||||
import { PerpMarket } from '@blockworks-foundation/mango-v4'
|
||||
import { IconButton, LinkButton } from '@components/shared/Button'
|
||||
import ConnectEmptyState from '@components/shared/ConnectEmptyState'
|
||||
import FormatNumericValue from '@components/shared/FormatNumericValue'
|
||||
|
@ -28,8 +23,6 @@ import useSelectedMarket from 'hooks/useSelectedMarket'
|
|||
import useTradeHistory from 'hooks/useTradeHistory'
|
||||
import { useViewport } from 'hooks/useViewport'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { useMemo } from 'react'
|
||||
import { SerumEvent, PerpTradeHistory, SpotTradeHistory } from 'types'
|
||||
import { PAGINATION_PAGE_LENGTH } from 'utils/constants'
|
||||
import { abbreviateAddress } from 'utils/formatting'
|
||||
import { breakpoints } from 'utils/theme'
|
||||
|
@ -37,151 +30,13 @@ import MarketLogos from './MarketLogos'
|
|||
import PerpSideBadge from './PerpSideBadge'
|
||||
import TableMarketName from './TableMarketName'
|
||||
|
||||
type PerpFillEvent = ParsedFillEvent
|
||||
|
||||
const parsePerpEvent = (mangoAccountAddress: string, event: PerpFillEvent) => {
|
||||
const maker = event.maker.toString() === mangoAccountAddress
|
||||
const orderId = maker ? event.makerOrderId : event.takerOrderId
|
||||
const value = event.quantity * event.price
|
||||
const feeRate = maker ? event.makerFee : event.takerFee
|
||||
const takerSide = event.takerSide === 0 ? 'buy' : 'sell'
|
||||
const side = maker ? (takerSide === 'buy' ? 'sell' : 'buy') : takerSide
|
||||
|
||||
return {
|
||||
...event,
|
||||
key: orderId?.toString(),
|
||||
liquidity: maker ? 'Maker' : 'Taker',
|
||||
size: event.quantity,
|
||||
price: event.price,
|
||||
value,
|
||||
feeCost: feeRate * value,
|
||||
side,
|
||||
}
|
||||
}
|
||||
|
||||
const parseSerumEvent = (event: SerumEvent) => {
|
||||
let liquidity
|
||||
if (event.eventFlags) {
|
||||
liquidity = event?.eventFlags?.maker ? 'Maker' : 'Taker'
|
||||
}
|
||||
|
||||
return {
|
||||
...event,
|
||||
liquidity,
|
||||
key: `${liquidity}-${event.price}`,
|
||||
value: event.price * event.size,
|
||||
side: event.side,
|
||||
}
|
||||
}
|
||||
|
||||
const isApiSpotTradeHistory = (
|
||||
t: SpotTradeHistory | PerpTradeHistory
|
||||
): t is SpotTradeHistory => {
|
||||
if ('open_orders' in t) return true
|
||||
else return false
|
||||
}
|
||||
|
||||
type CombinedTradeHistoryTypes =
|
||||
| SpotTradeHistory
|
||||
| PerpTradeHistory
|
||||
| PerpFillEvent
|
||||
| SerumEvent
|
||||
|
||||
export const isSerumFillEvent = (
|
||||
t: CombinedTradeHistoryTypes
|
||||
): t is SerumEvent => {
|
||||
if ('eventFlags' in t) return true
|
||||
else return false
|
||||
}
|
||||
|
||||
export const isPerpFillEvent = (
|
||||
t: CombinedTradeHistoryTypes
|
||||
): t is PerpFillEvent => {
|
||||
if ('takerSide' in t) return true
|
||||
else return false
|
||||
}
|
||||
|
||||
const parseApiTradeHistory = (
|
||||
mangoAccountAddress: string,
|
||||
trade: SpotTradeHistory | PerpTradeHistory
|
||||
) => {
|
||||
let side: 'buy' | 'sell'
|
||||
let size
|
||||
let feeCost
|
||||
let liquidity
|
||||
if (isApiSpotTradeHistory(trade)) {
|
||||
side = trade.side
|
||||
size = trade.size
|
||||
feeCost = trade.fee_cost
|
||||
liquidity = trade.maker ? 'Maker' : 'Taker'
|
||||
} else {
|
||||
liquidity =
|
||||
trade.taker && trade.taker === mangoAccountAddress ? 'Taker' : 'Maker'
|
||||
if (liquidity == 'Taker') {
|
||||
side = trade.taker_side == 'bid' ? 'buy' : 'sell'
|
||||
} else {
|
||||
side = trade.taker_side == 'bid' ? 'sell' : 'buy'
|
||||
}
|
||||
size = trade.quantity
|
||||
const feeRate =
|
||||
trade.maker === mangoAccountAddress ? trade.maker_fee : trade.taker_fee
|
||||
feeCost = (trade.price * trade.quantity * feeRate).toFixed(5)
|
||||
}
|
||||
|
||||
return {
|
||||
...trade,
|
||||
liquidity,
|
||||
side,
|
||||
size,
|
||||
feeCost,
|
||||
}
|
||||
}
|
||||
|
||||
const formatTradeHistory = (
|
||||
group: Group,
|
||||
selectedMarket: Serum3Market | PerpMarket,
|
||||
mangoAccountAddress: string,
|
||||
tradeHistory: Array<CombinedTradeHistoryTypes>
|
||||
) => {
|
||||
return tradeHistory.flat().map((event) => {
|
||||
let trade
|
||||
let market = selectedMarket
|
||||
let time: string | number = ''
|
||||
if (isSerumFillEvent(event)) {
|
||||
trade = parseSerumEvent(event)
|
||||
} else if (isPerpFillEvent(event)) {
|
||||
trade = parsePerpEvent(mangoAccountAddress, event)
|
||||
market = selectedMarket
|
||||
|
||||
time = trade.timestamp.toNumber() * 1000
|
||||
} else {
|
||||
trade = parseApiTradeHistory(mangoAccountAddress, event)
|
||||
time = trade.block_datetime
|
||||
if ('market' in trade) {
|
||||
market = group.getSerum3MarketByExternalMarket(
|
||||
new PublicKey(trade.market)
|
||||
)
|
||||
} else if ('perp_market' in trade) {
|
||||
market = group.getPerpMarketByMarketIndex(trade.market_index)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...trade,
|
||||
market,
|
||||
time,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const TradeHistory = () => {
|
||||
const { t } = useTranslation(['common', 'trade'])
|
||||
const group = mangoStore.getState().group
|
||||
const { selectedMarket } = useSelectedMarket()
|
||||
const { mangoAccount, mangoAccountAddress } = useMangoAccount()
|
||||
const fills = mangoStore((s) => s.selectedMarket.fills)
|
||||
const { mangoAccountAddress } = useMangoAccount()
|
||||
const {
|
||||
data: tradeHistoryFromApi,
|
||||
data: combinedTradeHistory,
|
||||
isLoading: loadingTradeHistory,
|
||||
fetchNextPage,
|
||||
} = useTradeHistory()
|
||||
|
@ -189,66 +44,6 @@ const TradeHistory = () => {
|
|||
const { connected } = useWallet()
|
||||
const showTableView = width ? width > breakpoints.md : false
|
||||
|
||||
const openOrderOwner = useMemo(() => {
|
||||
if (!mangoAccount || !selectedMarket) return
|
||||
if (selectedMarket instanceof PerpMarket) {
|
||||
return mangoAccount.publicKey
|
||||
} else {
|
||||
try {
|
||||
return mangoAccount.getSerum3OoAccount(selectedMarket.marketIndex)
|
||||
.address
|
||||
} catch {
|
||||
console.warn(
|
||||
'Unable to find OO account for mkt index',
|
||||
selectedMarket.marketIndex
|
||||
)
|
||||
}
|
||||
}
|
||||
}, [mangoAccount, selectedMarket])
|
||||
|
||||
const eventQueueFillsForOwner = useMemo(() => {
|
||||
if (!selectedMarket || !openOrderOwner) return []
|
||||
|
||||
return fills.filter((fill) => {
|
||||
if (isSerumFillEvent(fill)) {
|
||||
// handles serum event queue for spot trades
|
||||
return openOrderOwner ? fill.openOrders.equals(openOrderOwner) : false
|
||||
} else if (isPerpFillEvent(fill)) {
|
||||
// handles mango event queue for perp trades
|
||||
return (
|
||||
fill.taker.equals(openOrderOwner) || fill.maker.equals(openOrderOwner)
|
||||
)
|
||||
}
|
||||
})
|
||||
}, [selectedMarket, openOrderOwner, fills])
|
||||
|
||||
const combinedTradeHistory = useMemo(() => {
|
||||
const group = mangoStore.getState().group
|
||||
if (!group || !selectedMarket) return []
|
||||
let newFills: (SerumEvent | PerpFillEvent)[] = []
|
||||
const combinedTradeHistoryPages = tradeHistoryFromApi?.pages.flat() ?? []
|
||||
if (eventQueueFillsForOwner?.length) {
|
||||
newFills = eventQueueFillsForOwner.filter((fill) => {
|
||||
return !combinedTradeHistoryPages.find((t) => {
|
||||
if ('order_id' in t && isSerumFillEvent(fill)) {
|
||||
return t.order_id === fill.orderId.toString()
|
||||
} else if ('seq_num' in t && isPerpFillEvent(fill)) {
|
||||
return t.seq_num === fill.seqNum.toNumber()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
return formatTradeHistory(group, selectedMarket, mangoAccountAddress, [
|
||||
...newFills,
|
||||
...combinedTradeHistoryPages,
|
||||
])
|
||||
}, [
|
||||
eventQueueFillsForOwner,
|
||||
mangoAccountAddress,
|
||||
tradeHistoryFromApi,
|
||||
selectedMarket,
|
||||
])
|
||||
|
||||
if (!selectedMarket || !group) return null
|
||||
|
||||
return mangoAccountAddress &&
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { useEffect, useRef, useMemo, useState, useCallback } from 'react'
|
||||
import { useTheme } from 'next-themes'
|
||||
import {
|
||||
|
@ -7,6 +8,7 @@ import {
|
|||
ResolutionString,
|
||||
IOrderLineAdapter,
|
||||
EntityId,
|
||||
IExecutionLineAdapter,
|
||||
} from '@public/charting_library'
|
||||
import mangoStore from '@store/mangoStore'
|
||||
import { useViewport } from 'hooks/useViewport'
|
||||
|
@ -38,6 +40,8 @@ import Datafeed from 'apis/datafeed'
|
|||
import useStablePrice from 'hooks/useStablePrice'
|
||||
import { isMangoError } from 'types'
|
||||
import { formatPrice } from 'apis/birdeye/helpers'
|
||||
import useTradeHistory from 'hooks/useTradeHistory'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
export interface ChartContainerProps {
|
||||
container: ChartingLibraryWidgetOptions['container']
|
||||
|
@ -65,6 +69,8 @@ function hexToRgb(hex: string) {
|
|||
: null
|
||||
}
|
||||
|
||||
const TRADE_EXECUTION_LIMIT = 100
|
||||
|
||||
const TradingViewChart = () => {
|
||||
const { t } = useTranslation(['tv-chart', 'trade'])
|
||||
const { theme } = useTheme()
|
||||
|
@ -77,6 +83,12 @@ const TradingViewChart = () => {
|
|||
const [showOrderLines, toggleShowOrderLines] = useState(
|
||||
showOrderLinesLocalStorage
|
||||
)
|
||||
const tradeExecutions = mangoStore((s) => s.tradingView.tradeExecutions)
|
||||
const { data: combinedTradeHistory, isLoading: loadingTradeHistory } =
|
||||
useTradeHistory()
|
||||
const [showTradeExecutions, toggleShowTradeExecutions] = useState(false)
|
||||
const [cachedTradeHistory, setCachedTradeHistory] =
|
||||
useState(combinedTradeHistory)
|
||||
|
||||
const [showStablePriceLocalStorage, toggleShowStablePriceLocalStorage] =
|
||||
useLocalStorageState(SHOW_STABLE_PRICE_KEY, false)
|
||||
|
@ -615,6 +627,18 @@ const TradingViewChart = () => {
|
|||
[drawLinesForMarket, deleteLines, theme]
|
||||
)
|
||||
|
||||
const toggleTradeExecutions = useCallback(
|
||||
(el: HTMLElement) => {
|
||||
toggleShowTradeExecutions((prevState) => !prevState)
|
||||
if (el.style.color === hexToRgb(COLORS.ACTIVE[theme])) {
|
||||
el.style.color = COLORS.FGD4[theme]
|
||||
} else {
|
||||
el.style.color = COLORS.ACTIVE[theme]
|
||||
}
|
||||
},
|
||||
[theme]
|
||||
)
|
||||
|
||||
const createStablePriceButton = useCallback(() => {
|
||||
const toggleStablePrice = (button: HTMLElement) => {
|
||||
toggleShowStablePrice((prevState: boolean) => !prevState)
|
||||
|
@ -656,6 +680,21 @@ const TradingViewChart = () => {
|
|||
orderLinesButtonRef.current = button
|
||||
}, [t, toggleOrderLines, showOrderLinesLocalStorage, theme])
|
||||
|
||||
const createTEButton = useCallback(() => {
|
||||
const button = tvWidgetRef?.current?.createButton()
|
||||
if (!button) {
|
||||
return
|
||||
}
|
||||
button.textContent = 'TE'
|
||||
button.setAttribute('title', t('tv-chart:toggle-trade-executions'))
|
||||
button.addEventListener('click', () => toggleTradeExecutions(button))
|
||||
if (showTradeExecutions) {
|
||||
button.style.color = COLORS.ACTIVE[theme]
|
||||
} else {
|
||||
button.style.color = COLORS.FGD4[theme]
|
||||
}
|
||||
}, [t, toggleTradeExecutions, showTradeExecutions, theme])
|
||||
|
||||
useEffect(() => {
|
||||
if (window) {
|
||||
let chartStyleOverrides = {
|
||||
|
@ -775,9 +814,16 @@ const TradingViewChart = () => {
|
|||
!stablePriceButtonRef.current
|
||||
) {
|
||||
createOLButton()
|
||||
createTEButton()
|
||||
createStablePriceButton()
|
||||
}
|
||||
}, [createOLButton, chartReady, createStablePriceButton, headerReady])
|
||||
}, [
|
||||
createOLButton,
|
||||
createTEButton,
|
||||
chartReady,
|
||||
createStablePriceButton,
|
||||
headerReady,
|
||||
])
|
||||
|
||||
// update order lines if a user's open orders change
|
||||
useEffect(() => {
|
||||
|
@ -839,6 +885,94 @@ const TradingViewChart = () => {
|
|||
return subscription
|
||||
}, [chartReady, showOrderLines, deleteLines, drawLinesForMarket])
|
||||
|
||||
// Todo: fix types
|
||||
const drawTradeExecutions = useCallback(
|
||||
(trades: any[]) => {
|
||||
const newTradeExecutions = new Map()
|
||||
const filteredTrades = trades
|
||||
.filter((trade) => {
|
||||
return trade.market.name === selectedMarketName
|
||||
})
|
||||
.slice(0, TRADE_EXECUTION_LIMIT)
|
||||
for (let i = 0; i < filteredTrades.length; i++) {
|
||||
const trade = filteredTrades[i]
|
||||
try {
|
||||
const arrowID = tvWidgetRef
|
||||
.current!.chart()
|
||||
.createExecutionShape()
|
||||
.setTime(dayjs(trade.time).unix())
|
||||
.setDirection(trade.side)
|
||||
.setArrowHeight(6)
|
||||
.setArrowColor(
|
||||
trade.side === 'buy' ? COLORS.UP[theme] : COLORS.DOWN[theme]
|
||||
)
|
||||
.setTooltip(`${trade.size} at ${trade.price}`)
|
||||
if (arrowID) {
|
||||
try {
|
||||
newTradeExecutions.set(`${trade.time}${i}`, arrowID)
|
||||
} catch (error) {
|
||||
console.log('could not set newTradeExecution')
|
||||
}
|
||||
} else {
|
||||
console.log(
|
||||
`Could not create execution shape for trade ${trade.time}${i}`
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`could not draw arrow: ${error}`)
|
||||
}
|
||||
}
|
||||
return newTradeExecutions
|
||||
},
|
||||
[selectedMarketName, theme]
|
||||
)
|
||||
|
||||
const removeTradeExecutions = useCallback(
|
||||
(tradeExecutions: Map<string, IExecutionLineAdapter>) => {
|
||||
const set = mangoStore.getState().set
|
||||
if (chartReady && tvWidgetRef?.current) {
|
||||
for (const val of tradeExecutions.values()) {
|
||||
try {
|
||||
val.remove()
|
||||
} catch (error) {
|
||||
console.log(`arrow ${val} could not be removed`)
|
||||
}
|
||||
}
|
||||
}
|
||||
set((s) => {
|
||||
s.tradingView.tradeExecutions = new Map()
|
||||
})
|
||||
},
|
||||
[chartReady, tvWidgetRef?.current]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!loadingTradeHistory && showTradeExecutions) {
|
||||
setCachedTradeHistory(combinedTradeHistory)
|
||||
}
|
||||
}, [loadingTradeHistory, showTradeExecutions])
|
||||
|
||||
useEffect(() => {
|
||||
if (cachedTradeHistory.length !== combinedTradeHistory.length) {
|
||||
setCachedTradeHistory(combinedTradeHistory)
|
||||
}
|
||||
}, [combinedTradeHistory])
|
||||
|
||||
useEffect(() => {
|
||||
removeTradeExecutions(tradeExecutions)
|
||||
if (
|
||||
showTradeExecutions &&
|
||||
tvWidgetRef &&
|
||||
tvWidgetRef.current &&
|
||||
chartReady
|
||||
) {
|
||||
const set = mangoStore.getState().set
|
||||
set((s) => {
|
||||
s.tradingView.tradeExecutions = drawTradeExecutions(cachedTradeHistory)
|
||||
})
|
||||
}
|
||||
}, [cachedTradeHistory, selectedMarketName, showTradeExecutions])
|
||||
|
||||
return (
|
||||
<div id={defaultProps.container as string} className="tradingview-chart" />
|
||||
)
|
||||
|
|
|
@ -1,12 +1,135 @@
|
|||
import { useInfiniteQuery } from '@tanstack/react-query'
|
||||
import {
|
||||
Group,
|
||||
PerpMarket,
|
||||
Serum3Market,
|
||||
} from '@blockworks-foundation/mango-v4'
|
||||
import { PublicKey } from '@solana/web3.js'
|
||||
import mangoStore from '@store/mangoStore'
|
||||
import { useInfiniteQuery } from '@tanstack/react-query'
|
||||
import { useMemo } from 'react'
|
||||
import {
|
||||
CombinedTradeHistoryTypes,
|
||||
EmptyObject,
|
||||
isApiSpotTradeHistory,
|
||||
isPerpFillEvent,
|
||||
isSerumFillEvent,
|
||||
PerpFillEvent,
|
||||
PerpTradeHistory,
|
||||
SerumEvent,
|
||||
SpotTradeHistory,
|
||||
TradeHistoryApiResponseType,
|
||||
} from 'types'
|
||||
import { MANGO_DATA_API_URL, PAGINATION_PAGE_LENGTH } from 'utils/constants'
|
||||
import useMangoAccount from './useMangoAccount'
|
||||
import useSelectedMarket from './useSelectedMarket'
|
||||
|
||||
const parsePerpEvent = (mangoAccountAddress: string, event: PerpFillEvent) => {
|
||||
const maker = event.maker.toString() === mangoAccountAddress
|
||||
const orderId = maker ? event.makerOrderId : event.takerOrderId
|
||||
const value = event.quantity * event.price
|
||||
const feeRate = maker ? event.makerFee : event.takerFee
|
||||
const takerSide = event.takerSide === 0 ? 'buy' : 'sell'
|
||||
const side = maker ? (takerSide === 'buy' ? 'sell' : 'buy') : takerSide
|
||||
|
||||
return {
|
||||
...event,
|
||||
key: orderId?.toString(),
|
||||
liquidity: maker ? 'Maker' : 'Taker',
|
||||
size: event.quantity,
|
||||
price: event.price,
|
||||
value,
|
||||
feeCost: feeRate * value,
|
||||
side,
|
||||
}
|
||||
}
|
||||
|
||||
const parseSerumEvent = (event: SerumEvent) => {
|
||||
let liquidity
|
||||
if (event.eventFlags) {
|
||||
liquidity = event?.eventFlags?.maker ? 'Maker' : 'Taker'
|
||||
}
|
||||
|
||||
return {
|
||||
...event,
|
||||
liquidity,
|
||||
key: `${liquidity}-${event.price}`,
|
||||
value: event.price * event.size,
|
||||
side: event.side,
|
||||
}
|
||||
}
|
||||
|
||||
const parseApiTradeHistory = (
|
||||
mangoAccountAddress: string,
|
||||
trade: SpotTradeHistory | PerpTradeHistory
|
||||
) => {
|
||||
let side: 'buy' | 'sell'
|
||||
let size
|
||||
let feeCost
|
||||
let liquidity
|
||||
if (isApiSpotTradeHistory(trade)) {
|
||||
side = trade.side
|
||||
size = trade.size
|
||||
feeCost = trade.fee_cost
|
||||
liquidity = trade.maker ? 'Maker' : 'Taker'
|
||||
} else {
|
||||
liquidity =
|
||||
trade.taker && trade.taker === mangoAccountAddress ? 'Taker' : 'Maker'
|
||||
if (liquidity == 'Taker') {
|
||||
side = trade.taker_side == 'bid' ? 'buy' : 'sell'
|
||||
} else {
|
||||
side = trade.taker_side == 'bid' ? 'sell' : 'buy'
|
||||
}
|
||||
size = trade.quantity
|
||||
const feeRate =
|
||||
trade.maker === mangoAccountAddress ? trade.maker_fee : trade.taker_fee
|
||||
feeCost = (trade.price * trade.quantity * feeRate).toFixed(5)
|
||||
}
|
||||
|
||||
return {
|
||||
...trade,
|
||||
liquidity,
|
||||
side,
|
||||
size,
|
||||
feeCost,
|
||||
}
|
||||
}
|
||||
|
||||
const formatTradeHistory = (
|
||||
group: Group,
|
||||
selectedMarket: Serum3Market | PerpMarket,
|
||||
mangoAccountAddress: string,
|
||||
tradeHistory: Array<CombinedTradeHistoryTypes>
|
||||
) => {
|
||||
return tradeHistory.flat().map((event) => {
|
||||
let trade
|
||||
let market = selectedMarket
|
||||
let time: string | number = ''
|
||||
if (isSerumFillEvent(event)) {
|
||||
trade = parseSerumEvent(event)
|
||||
} else if (isPerpFillEvent(event)) {
|
||||
trade = parsePerpEvent(mangoAccountAddress, event)
|
||||
market = selectedMarket
|
||||
|
||||
time = trade.timestamp.toNumber() * 1000
|
||||
} else {
|
||||
trade = parseApiTradeHistory(mangoAccountAddress, event)
|
||||
time = trade.block_datetime
|
||||
if ('market' in trade) {
|
||||
market = group.getSerum3MarketByExternalMarket(
|
||||
new PublicKey(trade.market)
|
||||
)
|
||||
} else if ('perp_market' in trade) {
|
||||
market = group.getPerpMarketByMarketIndex(trade.market_index)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...trade,
|
||||
market,
|
||||
time,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const isTradeHistory = (
|
||||
response: null | EmptyObject | TradeHistoryApiResponseType[]
|
||||
|
@ -40,7 +163,43 @@ const fetchTradeHistory = async (
|
|||
}
|
||||
|
||||
export default function useTradeHistory() {
|
||||
const { mangoAccountAddress } = useMangoAccount()
|
||||
const group = mangoStore.getState().group
|
||||
const { selectedMarket } = useSelectedMarket()
|
||||
const { mangoAccount, mangoAccountAddress } = useMangoAccount()
|
||||
const fills = mangoStore((s) => s.selectedMarket.fills)
|
||||
|
||||
const openOrderOwner = useMemo(() => {
|
||||
if (!mangoAccount || !selectedMarket) return
|
||||
if (selectedMarket instanceof PerpMarket) {
|
||||
return mangoAccount.publicKey
|
||||
} else {
|
||||
try {
|
||||
return mangoAccount.getSerum3OoAccount(selectedMarket.marketIndex)
|
||||
.address
|
||||
} catch {
|
||||
console.warn(
|
||||
'Unable to find OO account for mkt index',
|
||||
selectedMarket.marketIndex
|
||||
)
|
||||
}
|
||||
}
|
||||
}, [mangoAccount, selectedMarket])
|
||||
|
||||
const eventQueueFillsForOwner = useMemo(() => {
|
||||
if (!selectedMarket || !openOrderOwner) return []
|
||||
|
||||
return fills.filter((fill) => {
|
||||
if (isSerumFillEvent(fill)) {
|
||||
// handles serum event queue for spot trades
|
||||
return openOrderOwner ? fill.openOrders.equals(openOrderOwner) : false
|
||||
} else if (isPerpFillEvent(fill)) {
|
||||
// handles mango event queue for perp trades
|
||||
return (
|
||||
fill.taker.equals(openOrderOwner) || fill.maker.equals(openOrderOwner)
|
||||
)
|
||||
}
|
||||
})
|
||||
}, [selectedMarket, openOrderOwner, fills])
|
||||
|
||||
const response = useInfiniteQuery(
|
||||
['trade-history', mangoAccountAddress],
|
||||
|
@ -57,5 +216,27 @@ export default function useTradeHistory() {
|
|||
}
|
||||
)
|
||||
|
||||
return { ...response, data: response.data }
|
||||
const combinedTradeHistory = useMemo(() => {
|
||||
const group = mangoStore.getState().group
|
||||
if (!group || !selectedMarket || response.isLoading) return []
|
||||
let newFills: (SerumEvent | PerpFillEvent)[] = []
|
||||
const combinedTradeHistoryPages = response.data?.pages.flat() ?? []
|
||||
if (eventQueueFillsForOwner?.length) {
|
||||
newFills = eventQueueFillsForOwner.filter((fill) => {
|
||||
return !combinedTradeHistoryPages.find((t) => {
|
||||
if ('order_id' in t && isSerumFillEvent(fill)) {
|
||||
return t.order_id === fill.orderId.toString()
|
||||
} else if ('seq_num' in t && isPerpFillEvent(fill)) {
|
||||
return t.seq_num === fill.seqNum.toNumber()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
return formatTradeHistory(group, selectedMarket, mangoAccountAddress, [
|
||||
...newFills,
|
||||
...combinedTradeHistoryPages,
|
||||
])
|
||||
}, [eventQueueFillsForOwner, mangoAccountAddress, response, selectedMarket])
|
||||
|
||||
return { ...response, data: combinedTradeHistory }
|
||||
}
|
||||
|
|
|
@ -64,6 +64,7 @@ import perpPositionsUpdater from './perpPositionsUpdater'
|
|||
import { DEFAULT_PRIORITY_FEE } from '@components/settings/RpcSettings'
|
||||
import {
|
||||
EntityId,
|
||||
IExecutionLineAdapter,
|
||||
IOrderLineAdapter,
|
||||
} from '@public/charting_library/charting_library'
|
||||
|
||||
|
@ -214,6 +215,7 @@ export type MangoStore = {
|
|||
tradingView: {
|
||||
stablePriceLine: EntityId | undefined
|
||||
orderLines: Map<string | BN, IOrderLineAdapter>
|
||||
tradeExecutions: Map<string, IExecutionLineAdapter>
|
||||
}
|
||||
wallet: {
|
||||
tokens: TokenAccount[]
|
||||
|
@ -365,6 +367,7 @@ const mangoStore = create<MangoStore>()(
|
|||
tradingView: {
|
||||
stablePriceLine: undefined,
|
||||
orderLines: new Map(),
|
||||
tradeExecutions: new Map(),
|
||||
},
|
||||
wallet: {
|
||||
tokens: [],
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { PerpMarket, Serum3Market } from '@blockworks-foundation/mango-v4'
|
||||
import {
|
||||
ParsedFillEvent,
|
||||
PerpMarket,
|
||||
Serum3Market,
|
||||
} from '@blockworks-foundation/mango-v4'
|
||||
import { Modify } from '@blockworks-foundation/mango-v4'
|
||||
import { Event } from '@project-serum/serum/lib/queue'
|
||||
|
||||
|
@ -55,6 +59,35 @@ export interface PerpTradeHistory {
|
|||
seq_num: number
|
||||
}
|
||||
|
||||
export const isApiSpotTradeHistory = (
|
||||
t: SpotTradeHistory | PerpTradeHistory
|
||||
): t is SpotTradeHistory => {
|
||||
if ('open_orders' in t) return true
|
||||
else return false
|
||||
}
|
||||
|
||||
export type PerpFillEvent = ParsedFillEvent
|
||||
|
||||
export type CombinedTradeHistoryTypes =
|
||||
| SpotTradeHistory
|
||||
| PerpTradeHistory
|
||||
| PerpFillEvent
|
||||
| SerumEvent
|
||||
|
||||
export const isSerumFillEvent = (
|
||||
t: CombinedTradeHistoryTypes
|
||||
): t is SerumEvent => {
|
||||
if ('eventFlags' in t) return true
|
||||
else return false
|
||||
}
|
||||
|
||||
export const isPerpFillEvent = (
|
||||
t: CombinedTradeHistoryTypes
|
||||
): t is PerpFillEvent => {
|
||||
if ('takerSide' in t) return true
|
||||
else return false
|
||||
}
|
||||
|
||||
export type SerumEvent = Modify<
|
||||
Event,
|
||||
{
|
||||
|
|
Loading…
Reference in New Issue