From 8c455ba00a10369d8f84275b2a404325ad70bbed Mon Sep 17 00:00:00 2001 From: ivan Date: Thu, 26 Feb 2026 19:50:44 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=EF=BC=9A=E8=AE=A2=E5=8D=95?= =?UTF-8?q?=E7=B0=BF=E4=BF=A1=E6=81=AF=E7=9A=84=E5=AE=8C=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sdk/clobSocket.ts | 4 +- src/components/OrderBook.vue | 80 ++++++++++++++++++++------- src/views/TradeDetail.vue | 102 ++++++++++++++++++++++++++--------- 3 files changed, 140 insertions(+), 46 deletions(-) diff --git a/sdk/clobSocket.ts b/sdk/clobSocket.ts index bba438a..fcdfbc0 100644 --- a/sdk/clobSocket.ts +++ b/sdk/clobSocket.ts @@ -249,11 +249,11 @@ export class ClobSdk { // 2. 基于 'e' 字段的事件 switch (msg.e) { 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)); break; 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)); break; case 'trade': diff --git a/src/components/OrderBook.vue b/src/components/OrderBook.vue index 7e33ad3..ff7680b 100644 --- a/src/components/OrderBook.vue +++ b/src/components/OrderBook.vue @@ -99,18 +99,35 @@ export interface OrderBookRow { const props = withDefaults( 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[] bids?: OrderBookRow[] lastPrice?: number spread?: number loading?: boolean connected?: boolean - /** 市场 Yes 选项文案,来自 market.outcomes[0] */ yesLabel?: string - /** 市场 No 选项文案,来自 market.outcomes[1] */ noLabel?: string }>(), { + asksYes: () => [], + bidsYes: () => [], + asksNo: () => [], + bidsNo: () => [], + lastPriceYes: undefined, + lastPriceNo: undefined, + spreadYes: undefined, + spreadNo: undefined, asks: () => [], bids: () => [], lastPrice: undefined, @@ -122,33 +139,60 @@ const props = withDefaults( }, ) -// State +// State:up = Yes 交易,down = No 交易 const activeTrade = ref('up') -// 使用 props 或回退到 mock 数据(来自 mockData 统一封装) const internalAsks = ref([...MOCK_ORDER_BOOK_ASKS]) const internalBids = ref([...MOCK_ORDER_BOOK_BIDS]) const internalLastPrice = ref(MOCK_ORDER_BOOK_LAST_PRICE) const internalSpread = ref(MOCK_ORDER_BOOK_SPREAD) -// 当有外部数据时使用 props,否则在 USE_MOCK_ORDER_BOOK 时用 mock -const asks = computed(() => - props.asks?.length ? props.asks : USE_MOCK_ORDER_BOOK ? internalAsks.value : [], -) -const bids = computed(() => - props.bids?.length ? props.bids : USE_MOCK_ORDER_BOOK ? internalBids.value : [], -) -const displayLastPrice = computed(() => - props.lastPrice ?? (USE_MOCK_ORDER_BOOK ? internalLastPrice.value : 0), -) -const displaySpread = computed(() => - props.spread ?? (USE_MOCK_ORDER_BOOK ? internalSpread.value : 0), -) +// 根据 activeTrade 选择当前 tab 对应的数据 +const asks = computed(() => { + const isYes = activeTrade.value === 'up' + const fromYes = isYes ? props.asksYes : props.asksNo + const fromLegacy = props.asks + 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 ? internalAsks.value : [] +}) +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 更新 let mockInterval: ReturnType | undefined 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) => { if (hasRealData && mockInterval) { clearInterval(mockInterval) diff --git a/src/views/TradeDetail.vue b/src/views/TradeDetail.vue index c4b4db8..1cae831 100644 --- a/src/views/TradeDetail.vue +++ b/src/views/TradeDetail.vue @@ -47,10 +47,14 @@ { }) // --- CLOB WebSocket 订单簿与成交 --- +// 按 token 索引区分:0 = Yes,1 = No +type OrderBookRows = { price: number; shares: number }[] const clobSdkRef = ref(null) -const orderBookAsks = ref<{ price: number; shares: number }[]>([]) -const orderBookBids = ref<{ price: number; shares: number }[]>([]) -const clobLastPrice = ref(undefined) -const clobSpread = ref(undefined) +const orderBookByToken = ref>({ + 0: { asks: [], bids: [] }, + 1: { asks: [], bids: [] }, +}) +const clobLastPriceByToken = ref>({ 0: undefined, 1: undefined }) +const clobSpreadByToken = ref>({ 0: undefined, 1: undefined }) const clobConnected = ref(false) const clobLoading = ref(false) +/** 当前订阅的 tokenIds,用于根据 msg.m 匹配 Yes(0)/No(1) */ +const clobTokenIdsRef = ref([]) -function priceSizeToRows(record: Record | 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 | undefined): OrderBookRows { if (!record) return [] return Object.entries(record) .filter(([, shares]) => shares > 0) .map(([p, shares]) => ({ - price: Math.round(parseFloat(p) /100), + price: Math.round(parseFloat(p) / 100), shares, })) } +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) { - orderBookAsks.value = priceSizeToRows(msg.s).sort((a, b) => a.price - b.price) - orderBookBids.value = priceSizeToRows(msg.b).sort((a, b) => b.price - a.price) - updateSpreadFromBook() + const idx = getTokenIndex(msg) + const asks = priceSizeToRows(msg.s).sort((a, b) => a.price - b.price) + 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) { + const idx = getTokenIndex(msg) const mergeDelta = ( - current: { price: number; shares: number }[], + current: OrderBookRows, delta: Record | undefined, asc: boolean, ) => { @@ -399,16 +435,23 @@ function applyPriceSizeDelta(msg: PriceSizePolyMsg) { .map(([price, shares]) => ({ price, shares })) .sort((a, b) => (asc ? a.price - b.price : b.price - a.price)) } - orderBookAsks.value = mergeDelta(orderBookAsks.value, msg.s, true) - orderBookBids.value = mergeDelta(orderBookBids.value, msg.b, false) - updateSpreadFromBook() + const prev = orderBookByToken.value[idx] ?? { asks: [], bids: [] } + const asks = mergeDelta(prev.asks, msg.s, true) + const bids = mergeDelta(prev.bids, msg.b, false) + orderBookByToken.value = { + ...orderBookByToken.value, + [idx]: { asks, bids }, + } + updateSpreadForToken(idx) } -function updateSpreadFromBook() { - const bestAsk = orderBookAsks.value[0]?.price - const bestBid = orderBookBids.value[0]?.price +function updateSpreadForToken(idx: number) { + const book = orderBookByToken.value[idx] + if (!book) return + const bestAsk = book.asks[0]?.price + const bestBid = book.bids[0]?.price 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 clobLoading.value = true clobConnected.value = false - orderBookAsks.value = [] - orderBookBids.value = [] + clobTokenIdsRef.value = tokenIds + orderBookByToken.value = { 0: { asks: [], bids: [] }, 1: { asks: [], bids: [] } } + clobLastPriceByToken.value = { 0: undefined, 1: undefined } + clobSpreadByToken.value = { 0: undefined, 1: undefined } const options = { url: getClobWsUrl(), @@ -442,7 +487,10 @@ function connectClob(tokenIds: string[]) { sdk.onTrade((msg: TradePolyMsg) => { const priceNum = parseFloat(msg.p) 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 列表 const side = msg.side?.toLowerCase() === 'buy' ? 'Yes' : 'No' @@ -919,10 +967,12 @@ watch( clobTokenIds, (tokenIds) => { if (tokenIds.length > 0) { - // 用接口返回的 Yes 价格作为初始 lastPrice const payload = tradeMarketPayload.value if (payload?.yesPrice != null) { - clobLastPrice.value = Math.round(payload.yesPrice * 100) + clobLastPriceByToken.value = { + ...clobLastPriceByToken.value, + 0: Math.round(payload.yesPrice * 100), + } } connectClob(tokenIds) } else {