优化:订单簿信息的完善

This commit is contained in:
ivan 2026-02-26 19:50:44 +08:00
parent 6b12938c89
commit 8c455ba00a
3 changed files with 140 additions and 46 deletions

View File

@ -249,11 +249,11 @@ export class ClobSdk {
// 2. 基于 'e' 字段的事件 // 2. 基于 'e' 字段的事件
switch (msg.e) { switch (msg.e) {
case 'price_size_all': case 'price_size_all':
console.log('[ClobSdk] 回调: price_size_all', { m: msg.m, t: msg.t, bids: msg.b, asks: msg.s }); console.log('[ClobSdk] 回调: price_size_all', { i: msg.i, m: msg.m, t: msg.t, bids: msg.b, asks: msg.s });
this.listeners.priceSizeAll.forEach(cb => cb(msg as PriceSizePolyMsg)); this.listeners.priceSizeAll.forEach(cb => cb(msg as PriceSizePolyMsg));
break; break;
case 'price_size_delta': case 'price_size_delta':
console.log('[ClobSdk] 回调: price_size_delta', { m: msg.m, t: msg.t, bids: msg.b, asks: msg.s }); console.log('[ClobSdk] 回调: price_size_delta', { i: msg.i, m: msg.m, t: msg.t, bids: msg.b, asks: msg.s });
this.listeners.priceSizeDelta.forEach(cb => cb(msg as PriceSizePolyMsg)); this.listeners.priceSizeDelta.forEach(cb => cb(msg as PriceSizePolyMsg));
break; break;
case 'trade': case 'trade':

View File

@ -99,18 +99,35 @@ export interface OrderBookRow {
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
/** Yes token 订单簿index 0 */
asksYes?: OrderBookRow[]
bidsYes?: OrderBookRow[]
/** No token 订单簿index 1 */
asksNo?: OrderBookRow[]
bidsNo?: OrderBookRow[]
lastPriceYes?: number
lastPriceNo?: number
spreadYes?: number
spreadNo?: number
/** @deprecated 兼容旧用法,优先使用 asksYes/asksNo */
asks?: OrderBookRow[] asks?: OrderBookRow[]
bids?: OrderBookRow[] bids?: OrderBookRow[]
lastPrice?: number lastPrice?: number
spread?: number spread?: number
loading?: boolean loading?: boolean
connected?: boolean connected?: boolean
/** 市场 Yes 选项文案,来自 market.outcomes[0] */
yesLabel?: string yesLabel?: string
/** 市场 No 选项文案,来自 market.outcomes[1] */
noLabel?: string noLabel?: string
}>(), }>(),
{ {
asksYes: () => [],
bidsYes: () => [],
asksNo: () => [],
bidsNo: () => [],
lastPriceYes: undefined,
lastPriceNo: undefined,
spreadYes: undefined,
spreadNo: undefined,
asks: () => [], asks: () => [],
bids: () => [], bids: () => [],
lastPrice: undefined, lastPrice: undefined,
@ -122,33 +139,60 @@ const props = withDefaults(
}, },
) )
// State // Stateup = Yes down = No
const activeTrade = ref('up') const activeTrade = ref('up')
// 使 props 退 mock mockData
const internalAsks = ref<OrderBookRow[]>([...MOCK_ORDER_BOOK_ASKS]) const internalAsks = ref<OrderBookRow[]>([...MOCK_ORDER_BOOK_ASKS])
const internalBids = ref<OrderBookRow[]>([...MOCK_ORDER_BOOK_BIDS]) const internalBids = ref<OrderBookRow[]>([...MOCK_ORDER_BOOK_BIDS])
const internalLastPrice = ref(MOCK_ORDER_BOOK_LAST_PRICE) const internalLastPrice = ref(MOCK_ORDER_BOOK_LAST_PRICE)
const internalSpread = ref(MOCK_ORDER_BOOK_SPREAD) const internalSpread = ref(MOCK_ORDER_BOOK_SPREAD)
// 使 props USE_MOCK_ORDER_BOOK mock // activeTrade tab
const asks = computed(() => const asks = computed(() => {
props.asks?.length ? props.asks : USE_MOCK_ORDER_BOOK ? internalAsks.value : [], const isYes = activeTrade.value === 'up'
) const fromYes = isYes ? props.asksYes : props.asksNo
const bids = computed(() => const fromLegacy = props.asks
props.bids?.length ? props.bids : USE_MOCK_ORDER_BOOK ? internalBids.value : [], const hasYesNo = (fromYes?.length ?? 0) > 0
) const hasLegacy = (fromLegacy?.length ?? 0) > 0
const displayLastPrice = computed(() => if (hasYesNo) return fromYes ?? []
props.lastPrice ?? (USE_MOCK_ORDER_BOOK ? internalLastPrice.value : 0), if (hasLegacy) return fromLegacy ?? []
) return USE_MOCK_ORDER_BOOK ? internalAsks.value : []
const displaySpread = computed(() => })
props.spread ?? (USE_MOCK_ORDER_BOOK ? internalSpread.value : 0), const bids = computed(() => {
) const isYes = activeTrade.value === 'up'
const fromYes = isYes ? props.bidsYes : props.bidsNo
const fromLegacy = props.bids
const hasYesNo = (fromYes?.length ?? 0) > 0
const hasLegacy = (fromLegacy?.length ?? 0) > 0
if (hasYesNo) return fromYes ?? []
if (hasLegacy) return fromLegacy ?? []
return USE_MOCK_ORDER_BOOK ? internalBids.value : []
})
const displayLastPrice = computed(() => {
const isYes = activeTrade.value === 'up'
const fromYesNo = isYes ? props.lastPriceYes : props.lastPriceNo
if (fromYesNo != null) return fromYesNo
if (props.lastPrice != null) return props.lastPrice
return USE_MOCK_ORDER_BOOK ? internalLastPrice.value : 0
})
const displaySpread = computed(() => {
const isYes = activeTrade.value === 'up'
const fromYesNo = isYes ? props.spreadYes : props.spreadNo
if (fromYesNo != null) return fromYesNo
if (props.spread != null) return props.spread
return USE_MOCK_ORDER_BOOK ? internalSpread.value : 0
})
// mock mock // mock mock
let mockInterval: ReturnType<typeof setInterval> | undefined let mockInterval: ReturnType<typeof setInterval> | undefined
watch( watch(
() => props.connected || (props.asks?.length ?? 0) > 0, () =>
props.connected ||
(props.asksYes?.length ?? 0) > 0 ||
(props.bidsYes?.length ?? 0) > 0 ||
(props.asksNo?.length ?? 0) > 0 ||
(props.bidsNo?.length ?? 0) > 0 ||
(props.asks?.length ?? 0) > 0,
(hasRealData) => { (hasRealData) => {
if (hasRealData && mockInterval) { if (hasRealData && mockInterval) {
clearInterval(mockInterval) clearInterval(mockInterval)

View File

@ -47,10 +47,14 @@
<!-- Order Book Section --> <!-- Order Book Section -->
<v-card class="order-book-card" elevation="0" rounded="lg"> <v-card class="order-book-card" elevation="0" rounded="lg">
<OrderBook <OrderBook
:asks="orderBookAsks" :asks-yes="orderBookAsksYes"
:bids="orderBookBids" :bids-yes="orderBookBidsYes"
:last-price="clobLastPrice" :asks-no="orderBookAsksNo"
:spread="clobSpread" :bids-no="orderBookBidsNo"
:last-price-yes="clobLastPriceYes"
:last-price-no="clobLastPriceNo"
:spread-yes="clobSpreadYes"
:spread-no="clobSpreadNo"
:loading="clobLoading" :loading="clobLoading"
:connected="clobConnected" :connected="clobConnected"
:yes-label="yesLabel" :yes-label="yesLabel"
@ -357,15 +361,30 @@ const currentMarket = computed(() => {
}) })
// --- CLOB WebSocket 簿 --- // --- CLOB WebSocket 簿 ---
// token 0 = Yes1 = No
type OrderBookRows = { price: number; shares: number }[]
const clobSdkRef = ref<ClobSdk | null>(null) const clobSdkRef = ref<ClobSdk | null>(null)
const orderBookAsks = ref<{ price: number; shares: number }[]>([]) const orderBookByToken = ref<Record<number, { asks: OrderBookRows; bids: OrderBookRows }>>({
const orderBookBids = ref<{ price: number; shares: number }[]>([]) 0: { asks: [], bids: [] },
const clobLastPrice = ref<number | undefined>(undefined) 1: { asks: [], bids: [] },
const clobSpread = ref<number | undefined>(undefined) })
const clobLastPriceByToken = ref<Record<number, number | undefined>>({ 0: undefined, 1: undefined })
const clobSpreadByToken = ref<Record<number, number | undefined>>({ 0: undefined, 1: undefined })
const clobConnected = ref(false) const clobConnected = ref(false)
const clobLoading = ref(false) const clobLoading = ref(false)
/** 当前订阅的 tokenIds用于根据 msg.m 匹配 Yes(0)/No(1) */
const clobTokenIdsRef = ref<string[]>([])
function priceSizeToRows(record: Record<string, number> | undefined): { price: number; shares: number }[] { const orderBookAsksYes = computed(() => orderBookByToken.value[0]?.asks ?? [])
const orderBookBidsYes = computed(() => orderBookByToken.value[0]?.bids ?? [])
const orderBookAsksNo = computed(() => orderBookByToken.value[1]?.asks ?? [])
const orderBookBidsNo = computed(() => orderBookByToken.value[1]?.bids ?? [])
const clobLastPriceYes = computed(() => clobLastPriceByToken.value[0])
const clobLastPriceNo = computed(() => clobLastPriceByToken.value[1])
const clobSpreadYes = computed(() => clobSpreadByToken.value[0])
const clobSpreadNo = computed(() => clobSpreadByToken.value[1])
function priceSizeToRows(record: Record<string, number> | undefined): OrderBookRows {
if (!record) return [] if (!record) return []
return Object.entries(record) return Object.entries(record)
.filter(([, shares]) => shares > 0) .filter(([, shares]) => shares > 0)
@ -375,15 +394,32 @@ function priceSizeToRows(record: Record<string, number> | undefined): { price: n
})) }))
} }
function getTokenIndex(msg: PriceSizePolyMsg): number {
const ids = clobTokenIdsRef.value
const m = msg.m
if (m != null && ids.length >= 2) {
const idx = ids.findIndex((id) => String(id) === String(m))
if (idx === 0 || idx === 1) return idx
}
if (typeof msg.i === 'number' && (msg.i === 0 || msg.i === 1)) return msg.i
return 0
}
function applyPriceSizeAll(msg: PriceSizePolyMsg) { function applyPriceSizeAll(msg: PriceSizePolyMsg) {
orderBookAsks.value = priceSizeToRows(msg.s).sort((a, b) => a.price - b.price) const idx = getTokenIndex(msg)
orderBookBids.value = priceSizeToRows(msg.b).sort((a, b) => b.price - a.price) const asks = priceSizeToRows(msg.s).sort((a, b) => a.price - b.price)
updateSpreadFromBook() const bids = priceSizeToRows(msg.b).sort((a, b) => b.price - a.price)
orderBookByToken.value = {
...orderBookByToken.value,
[idx]: { asks, bids },
}
updateSpreadForToken(idx)
} }
function applyPriceSizeDelta(msg: PriceSizePolyMsg) { function applyPriceSizeDelta(msg: PriceSizePolyMsg) {
const idx = getTokenIndex(msg)
const mergeDelta = ( const mergeDelta = (
current: { price: number; shares: number }[], current: OrderBookRows,
delta: Record<string, number> | undefined, delta: Record<string, number> | undefined,
asc: boolean, asc: boolean,
) => { ) => {
@ -399,16 +435,23 @@ function applyPriceSizeDelta(msg: PriceSizePolyMsg) {
.map(([price, shares]) => ({ price, shares })) .map(([price, shares]) => ({ price, shares }))
.sort((a, b) => (asc ? a.price - b.price : b.price - a.price)) .sort((a, b) => (asc ? a.price - b.price : b.price - a.price))
} }
orderBookAsks.value = mergeDelta(orderBookAsks.value, msg.s, true) const prev = orderBookByToken.value[idx] ?? { asks: [], bids: [] }
orderBookBids.value = mergeDelta(orderBookBids.value, msg.b, false) const asks = mergeDelta(prev.asks, msg.s, true)
updateSpreadFromBook() const bids = mergeDelta(prev.bids, msg.b, false)
orderBookByToken.value = {
...orderBookByToken.value,
[idx]: { asks, bids },
}
updateSpreadForToken(idx)
} }
function updateSpreadFromBook() { function updateSpreadForToken(idx: number) {
const bestAsk = orderBookAsks.value[0]?.price const book = orderBookByToken.value[idx]
const bestBid = orderBookBids.value[0]?.price if (!book) return
const bestAsk = book.asks[0]?.price
const bestBid = book.bids[0]?.price
if (bestAsk != null && bestBid != null) { if (bestAsk != null && bestBid != null) {
clobSpread.value = bestAsk - bestBid clobSpreadByToken.value = { ...clobSpreadByToken.value, [idx]: bestAsk - bestBid }
} }
} }
@ -417,8 +460,10 @@ function connectClob(tokenIds: string[]) {
clobSdkRef.value = null clobSdkRef.value = null
clobLoading.value = true clobLoading.value = true
clobConnected.value = false clobConnected.value = false
orderBookAsks.value = [] clobTokenIdsRef.value = tokenIds
orderBookBids.value = [] orderBookByToken.value = { 0: { asks: [], bids: [] }, 1: { asks: [], bids: [] } }
clobLastPriceByToken.value = { 0: undefined, 1: undefined }
clobSpreadByToken.value = { 0: undefined, 1: undefined }
const options = { const options = {
url: getClobWsUrl(), url: getClobWsUrl(),
@ -442,7 +487,10 @@ function connectClob(tokenIds: string[]) {
sdk.onTrade((msg: TradePolyMsg) => { sdk.onTrade((msg: TradePolyMsg) => {
const priceNum = parseFloat(msg.p) const priceNum = parseFloat(msg.p)
if (Number.isFinite(priceNum)) { if (Number.isFinite(priceNum)) {
clobLastPrice.value = Math.round(priceNum * 100) const priceCents = Math.round(priceNum * 100)
const side = msg.side?.toLowerCase()
const tokenIdx = side === 'buy' ? 0 : 1
clobLastPriceByToken.value = { ...clobLastPriceByToken.value, [tokenIdx]: priceCents }
} }
// Activity // Activity
const side = msg.side?.toLowerCase() === 'buy' ? 'Yes' : 'No' const side = msg.side?.toLowerCase() === 'buy' ? 'Yes' : 'No'
@ -919,10 +967,12 @@ watch(
clobTokenIds, clobTokenIds,
(tokenIds) => { (tokenIds) => {
if (tokenIds.length > 0) { if (tokenIds.length > 0) {
// Yes lastPrice
const payload = tradeMarketPayload.value const payload = tradeMarketPayload.value
if (payload?.yesPrice != null) { if (payload?.yesPrice != null) {
clobLastPrice.value = Math.round(payload.yesPrice * 100) clobLastPriceByToken.value = {
...clobLastPriceByToken.value,
0: Math.round(payload.yesPrice * 100),
}
} }
connectClob(tokenIds) connectClob(tokenIds)
} else { } else {