优化:订单簿显示优化

This commit is contained in:
ivan 2026-03-02 09:41:11 +08:00
parent 0069ae8357
commit ff7bc3b685
4 changed files with 74 additions and 30 deletions

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/favicon.ico"> <link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title> <title>TestMarket</title>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

View File

@ -43,9 +43,9 @@
:trackStyle="{ backgroundColor: 'transparent' }" :trackStyle="{ backgroundColor: 'transparent' }"
/> />
</div> </div>
<div class="order-price asks-price">{{ ask.price }}¢</div> <div class="order-price asks-price">{{ formatOrderBookPrice(ask) }}¢</div>
<div class="order-shares">{{ ask.shares.toFixed(2) }}</div> <div class="order-shares">{{ trimTrailingZeros(ask.shares.toFixed(2)) }}</div>
<div class="order-total">{{ ask.cumulativeTotal.toFixed(2) }}</div> <div class="order-total">{{ trimTrailingZeros(ask.cumulativeTotal.toFixed(4)) }}</div>
</div> </div>
<!-- Bids Orders --> <!-- Bids Orders -->
<div class="asks-label">{{ t('trade.orderBookAsks') }}</div> <div class="asks-label">{{ t('trade.orderBookAsks') }}</div>
@ -63,9 +63,9 @@
:trackStyle="{ backgroundColor: 'transparent' }" :trackStyle="{ backgroundColor: 'transparent' }"
/> />
</div> </div>
<div class="order-price bids-price">{{ bid.price }}¢</div> <div class="order-price bids-price">{{ formatOrderBookPrice(bid) }}¢</div>
<div class="order-shares">{{ bid.shares.toFixed(2) }}</div> <div class="order-shares">{{ trimTrailingZeros(bid.shares.toFixed(2)) }}</div>
<div class="order-total">{{ bid.cumulativeTotal.toFixed(2) }}</div> <div class="order-total">{{ trimTrailingZeros(bid.cumulativeTotal.toFixed(4)) }}</div>
</div> </div>
</div> </div>
</div> </div>
@ -93,8 +93,12 @@ import { USE_MOCK_ORDER_BOOK } from '../config/mock'
const { t } = useI18n() const { t } = useI18n()
export interface OrderBookRow { export interface OrderBookRow {
/** 价格(美分),用于展示 */
price: number price: number
/** 份额数量 */
shares: number shares: number
/** 原始精度单价(美元/份),用于计算总价,避免舍入误差 */
priceRaw?: number
} }
const props = withDefaults( const props = withDefaults(
@ -223,6 +227,21 @@ watch(
{ immediate: true }, { immediate: true },
) )
/** 去掉小数点后多余的 0如 1.00 → 10.10 → 0.1 */
function trimTrailingZeros(s: string): string {
return s.replace(/\.?0+$/, '')
}
/** 订单簿价格展示:小于 1 美分时最多显示两位小数,使用原始精度 priceRaw小数点后全为 0 则不显示 */
function formatOrderBookPrice(row: OrderBookRow): string {
const cents =
row.priceRaw != null && Number.isFinite(row.priceRaw) ? row.priceRaw * 100 : row.price
if (!Number.isFinite(cents)) return '0'
if (cents < 1) return trimTrailingZeros(cents.toFixed(2))
if (cents < 100) return trimTrailingZeros(cents.toFixed(1))
return String(Math.round(cents))
}
// Calculate cumulative total for asks // Calculate cumulative total for asks
const asksWithCumulativeTotal = computed(() => { const asksWithCumulativeTotal = computed(() => {
let cumulativeTotal = 0 let cumulativeTotal = 0
@ -231,8 +250,10 @@ const asksWithCumulativeTotal = computed(() => {
return sortedAsks return sortedAsks
.map((ask) => { .map((ask) => {
// Calculate current ask's value // 退 price/100
const askValue = (ask.price * ask.shares) / 100 // Convert cents to dollars const priceDollars =
ask.priceRaw != null && Number.isFinite(ask.priceRaw) ? ask.priceRaw : ask.price / 100
const askValue = priceDollars * ask.shares
cumulativeTotal += askValue cumulativeTotal += askValue
return { return {
...ask, ...ask,
@ -249,9 +270,10 @@ const bidsWithCumulativeTotal = computed(() => {
const sortedBids = [...bids.value].sort((a, b) => b.price - a.price) const sortedBids = [...bids.value].sort((a, b) => b.price - a.price)
return sortedBids.map((bid) => { return sortedBids.map((bid) => {
// Calculate current bid's value // 退 price/100
console.log('bid.price', bid.price) const priceDollars =
const bidValue = (bid.price * bid.shares) / 100.0 // Convert cents to dollars bid.priceRaw != null && Number.isFinite(bid.priceRaw) ? bid.priceRaw : bid.price / 100
const bidValue = priceDollars * bid.shares
cumulativeTotal += bidValue cumulativeTotal += bidValue
return { return {
...bid, ...bid,

View File

@ -1680,27 +1680,35 @@ const emit = defineEmits<{
// Computed properties // Computed properties
/** Limit Price 显示值(美分),与 Yes/No 按钮单位一致 */ /** Limit Price 显示值(美分),与 Yes/No 按钮单位一致 */
/** 去掉小数点后多余的 0如 1.00 → 10.10 → 0.1 */
function trimTrailingZeros(s: string): string {
return s.replace(/\.?0+$/, '')
}
const limitPriceCentsDisplay = computed(() => Math.round(limitPrice.value * 10000) / 100) const limitPriceCentsDisplay = computed(() => Math.round(limitPrice.value * 10000) / 100)
const currentPrice = computed(() => { const currentPrice = computed(() => {
return `${(limitPrice.value * 100).toFixed(0)}¢` return `${trimTrailingZeros((limitPrice.value * 100).toFixed(0))}¢`
}) })
const totalPrice = computed(() => { const totalPrice = computed(() => {
let raw: number
if (activeTab.value === 'sell' && limitType.value === 'Market') { if (activeTab.value === 'sell' && limitType.value === 'Market') {
const bestBidCents = const bestBidCents =
selectedOption.value === 'yes' selectedOption.value === 'yes'
? (props.market?.bestBidYesCents ?? 0) ? (props.market?.bestBidYesCents ?? 0)
: (props.market?.bestBidNoCents ?? 0) : (props.market?.bestBidNoCents ?? 0)
const price = bestBidCents / 100 raw = (bestBidCents / 100) * shares.value
return (price * shares.value).toFixed(2) } else {
raw = limitPrice.value * shares.value
} }
return (limitPrice.value * shares.value).toFixed(2) if (!Number.isFinite(raw)) return '0'
return trimTrailingZeros(raw.toFixed(4))
}) })
/** Sell 模式下的平均单价(¢) */ /** Sell 模式下的平均单价(¢) */
const avgPriceCents = computed(() => { const avgPriceCents = computed(() => {
const p = limitPrice.value const p = limitPrice.value
return (p * 100).toFixed(1) return trimTrailingZeros((p * 100).toFixed(1))
}) })
/** To win = 份数 × 1U = shares × 1 USDC */ /** To win = 份数 × 1U = shares × 1 USDC */
@ -1711,9 +1719,9 @@ const toWinValue = computed(() => {
? (props.market?.yesPrice ?? 0.5) ? (props.market?.yesPrice ?? 0.5)
: (props.market?.noPrice ?? 0.5) : (props.market?.noPrice ?? 0.5)
const sharesFromAmount = price > 0 ? amount.value / price : 0 const sharesFromAmount = price > 0 ? amount.value / price : 0
return sharesFromAmount.toFixed(2) return trimTrailingZeros(sharesFromAmount.toFixed(2))
} }
return (shares.value * 1).toFixed(2) return trimTrailingZeros((shares.value * 1).toFixed(2))
}) })
const limitTypeDisplay = computed(() => const limitTypeDisplay = computed(() =>

View File

@ -510,7 +510,7 @@ const currentMarket = computed(() => {
// --- CLOB WebSocket 簿 --- // --- CLOB WebSocket 簿 ---
// token 0 = Yes1 = No // token 0 = Yes1 = No
type OrderBookRows = { price: number; shares: number }[] type OrderBookRows = { price: number; shares: number; priceRaw?: number }[]
const clobSdkRef = ref<ClobSdk | null>(null) const clobSdkRef = ref<ClobSdk | null>(null)
const orderBookByToken = ref<Record<number, { asks: OrderBookRows; bids: OrderBookRows }>>({ const orderBookByToken = ref<Record<number, { asks: OrderBookRows; bids: OrderBookRows }>>({
0: { asks: [], bids: [] }, 0: { asks: [], bids: [] },
@ -560,14 +560,21 @@ const clobSpreadNo = computed(() => clobSpreadByToken.value[1])
/** 订单簿份额接口按 6 位小数传1_000_000 = 1 share需除以该系数转为展示值 */ /** 订单簿份额接口按 6 位小数传1_000_000 = 1 share需除以该系数转为展示值 */
const ORDER_BOOK_SIZE_SCALE = 1_000_000 const ORDER_BOOK_SIZE_SCALE = 1_000_000
/** 接口 key p 为价格(与 price 美分同源,除以 100 为美分priceRaw 为美元单价保留原始精度供订单簿算总价 */
function priceSizeToRows(record: Record<string, number> | undefined): OrderBookRows { function priceSizeToRows(record: Record<string, number> | undefined): OrderBookRows {
if (!record) return [] if (!record) return []
return Object.entries(record) return Object.entries(record)
.filter(([, rawShares]) => rawShares > 0) .filter(([, rawShares]) => rawShares > 0)
.map(([p, rawShares]) => ({ .map(([p, rawShares]) => {
price: Math.round(parseFloat(p) / 100), const numP = parseFloat(p)
shares: rawShares / ORDER_BOOK_SIZE_SCALE, const priceCents = Math.round(numP / 100)
})) const priceRawDollars = Number.isFinite(numP) ? numP / 10000 : priceCents / 100
return {
price: priceCents,
shares: rawShares / ORDER_BOOK_SIZE_SCALE,
priceRaw: priceRawDollars,
}
})
} }
function getTokenIndex(msg: PriceSizePolyMsg): number { function getTokenIndex(msg: PriceSizePolyMsg): number {
@ -599,17 +606,24 @@ function applyPriceSizeDelta(msg: PriceSizePolyMsg) {
delta: Record<string, number> | undefined, delta: Record<string, number> | undefined,
asc: boolean, asc: boolean,
) => { ) => {
const map = new Map(current.map((r) => [r.price, r.shares])) const map = new Map<string, { price: number; shares: number; priceRaw: number }>()
const keyOf = (price: number, priceRaw: number) => priceRaw.toFixed(8)
current.forEach((r) => {
const pr = r.priceRaw ?? r.price / 100
map.set(keyOf(r.price, pr), { price: r.price, shares: r.shares, priceRaw: pr })
})
if (delta) { if (delta) {
Object.entries(delta).forEach(([p, rawShares]) => { Object.entries(delta).forEach(([p, rawShares]) => {
const price = Math.round(parseFloat(p) / 100) const numP = parseFloat(p)
const price = Math.round(numP / 100)
const priceRaw = Number.isFinite(numP) ? numP / 10000 : price / 100
const shares = rawShares / ORDER_BOOK_SIZE_SCALE const shares = rawShares / ORDER_BOOK_SIZE_SCALE
if (shares <= 0) map.delete(price) const key = keyOf(price, priceRaw)
else map.set(price, shares) if (shares <= 0) map.delete(key)
else map.set(key, { price, shares, priceRaw })
}) })
} }
return Array.from(map.entries()) return Array.from(map.values())
.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))
} }
const prev = orderBookByToken.value[idx] ?? { asks: [], bids: [] } const prev = orderBookByToken.value[idx] ?? { asks: [], bids: [] }