Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
type: added
area: trade
pr: 0
breaking: false
---

Enhanced order book UI with spread reference price display, cumulative depth inspection tooltips, account-aware order price lines on charts, and improved confirmation dialog with staleness detection and network clarity.
24 changes: 21 additions & 3 deletions apps/web/src/features/trade/components/chart/TVChartContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@ import {
TableHeader,
TableRow,
} from "@workspace/ui/components/table"
import { cn } from "@workspace/ui/lib/utils"
import { useOracleCandles } from "../../hooks/useOracleCandles"
import { useLiveBar } from "../../hooks/useLiveBar"
import { canIncrementallyApply, mergeBars } from "../../lib/bar-reconciliation"
import type { LiveBarUpdate } from "../../lib/bar-reconciliation"
import { usePositions } from "../../hooks/usePositions"
import { useOrders } from "../../hooks/useOrders"
import { useWalletStore } from "@/features/wallet/store/wallet-store"
import {
buildCandleOptions,
buildChartOptions,
Expand Down Expand Up @@ -144,6 +145,8 @@ export function TVChartContainer({ symbol, period }: Props) {

const liveBar = useLiveBar(symbol, period)
const { data: positions = [] } = usePositions()
const { data: orders = [] } = useOrders()
const account = useWalletStore((state) => state.address)

const [showTable, setShowTable] = useState(false)
const lastAnnounceRef = useRef(0)
Expand Down Expand Up @@ -329,7 +332,7 @@ export function TVChartContainer({ symbol, period }: Props) {
}
}, [liveUpdate, period])

// ── Draw position entry + liquidation price lines ─────────────────────────
// ── Draw position entry + liquidation price lines + order lines (OB-066) ───
useEffect(() => {
if (!seriesRef.current) return

Expand Down Expand Up @@ -360,6 +363,21 @@ export function TVChartContainer({ symbol, period }: Props) {
return lines
})

// OB-066: Add resting order lines (account-aware)
if (account) {
orders
.filter((order) => order.account === account && order.marketName === symbol && order.status === "active")
.forEach((order) => {
desiredLines.push({
id: `${order.key}-order`,
title: `${order.isLong ? "Long" : "Short"} Order`,
price: order.triggerPrice,
color: order.isLong ? palette.long : palette.down,
lineStyle: LineStyle.Dotted,
})
})
}

const desiredIds = new Set(desiredLines.map((l) => l.id))

// Remove stale lines
Expand Down Expand Up @@ -388,7 +406,7 @@ export function TVChartContainer({ symbol, period }: Props) {
})
priceLineRefs.current.set(line.id, priceLine)
})
}, [positions, symbol, themeVersion])
}, [positions, orders, symbol, account, themeVersion])

// ── Throttled live-price announcements ────────────────────────────────────
const THROTTLE_MS = 5000
Expand Down
146 changes: 122 additions & 24 deletions apps/web/src/features/trade/components/orderbook/DepthLadder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { cn } from "@workspace/ui/lib/utils"
import { useOrderBook } from "../../hooks/useOrderBook"
import type {OrderBookLevel} from "../../hooks/useOrderBook";
import { formatUsd } from "@/shared/lib/format"
import { useRecentTrades } from "../../hooks/useRecentTrades"
import { useState } from "react"

type Props = {
symbol: string | undefined
Expand All @@ -14,19 +16,27 @@ type RowProps = {
level: OrderBookLevel
side: "bid" | "ask"
compact: boolean
onHover?: (level: OrderBookLevel | null) => void
}

function DepthRow({ level, side, compact }: RowProps) {
function DepthRow({ level, side, compact, onHover }: RowProps) {
const isBid = side === "bid"
const depthPct = `${(level.depth * 100).toFixed(1)}%`

return (
<div
role="row"
className={cn(
"relative flex items-center font-mono text-[11px] select-none", // ds-allow: dense orderbook row font size
"relative flex items-center font-mono text-[11px] select-none cursor-pointer transition-colors", // ds-allow: dense orderbook row font size
compact ? "h-[18px]" : "h-[22px]",
"hover:bg-muted/30 focus-within:bg-muted/30",
)}
onMouseEnter={() => onHover?.(level)}
onMouseLeave={() => onHover?.(null)}
onFocus={() => onHover?.(level)}
onBlur={() => onHover?.(null)}
tabIndex={0}
aria-label={`${isBid ? "Bid" : "Ask"} at ${formatUsd(level.price, { decimals: 2 })}, size ${level.size.toFixed(4)}, cumulative ${level.total.toFixed(4)}`}
>
{/* depth fill — absolutely positioned behind text */}
<div
Expand Down Expand Up @@ -76,6 +86,59 @@ function DepthRow({ level, side, compact }: RowProps) {
)
}

// ── cumulative depth inspection tooltip ───────────────────────────────────────

function DepthInspection({
level,
side,
}: {
level: OrderBookLevel
side: "bid" | "ask"
}) {
const avgPrice = level.total > 0 ? (level.price * level.size + (level.total - level.size) * level.price) / level.total : level.price
const notional = level.total * avgPrice

return (
<div
role="tooltip"
className="pointer-events-none absolute z-50 rounded-md border border-border bg-popover px-3 py-2 shadow-lg"
style={{
[side === "bid" ? "left" : "right"]: "100%",
top: "0",
marginLeft: side === "bid" ? "0.5rem" : undefined,
marginRight: side === "ask" ? "0.5rem" : undefined,
}}
>
<div className="space-y-0.5 text-[11px] font-mono whitespace-nowrap"> {/* ds-allow: tooltip detail font size */}
<div className="flex justify-between gap-4">
<span className="text-muted-foreground">Cumulative Qty:</span>
<span className="text-foreground font-medium">
{level.total.toLocaleString(undefined, {
minimumFractionDigits: 4,
maximumFractionDigits: 6,
})}
</span>
</div>
<div className="flex justify-between gap-4">
<span className="text-muted-foreground">Avg Price:</span>
<span className="text-foreground font-medium">
{formatUsd(avgPrice, { decimals: 4 })}
</span>
</div>
<div className="flex justify-between gap-4">
<span className="text-muted-foreground">Notional:</span>
<span className="text-foreground font-medium">
{formatUsd(notional, { decimals: 2 })}
</span>
</div>
<div className="mt-1 pt-1 border-t border-border text-[10px] text-muted-foreground italic"> {/* ds-allow: tooltip disclaimer font size */}
Snapshot estimate
</div>
</div>
</div>
)
}

// ── column headers ────────────────────────────────────────────────────────────

function ColumnHeaders({ compact }: { compact: boolean }) {
Expand All @@ -100,10 +163,14 @@ function SpreadRow({
spread,
spreadPct,
midPrice,
referencePrice,
referencePriceLabel,
}: {
spread: number | null
spreadPct: number | null
midPrice: number | null
referencePrice: number | null
referencePriceLabel: string
}) {
return (
<div
Expand All @@ -120,18 +187,24 @@ function SpreadRow({
) : (
"—"
)}
{spreadPct !== null && <span className="ml-1">({spreadPct.toFixed(3)}%)</span>}
</span>
{midPrice !== null && (
<span>
Mid{" "}
{referencePrice !== null && (
<span title={referencePriceLabel}>
{referencePriceLabel === "Last" && (
<span className="mr-0.5 opacity-60">Last</span>
)}
{referencePriceLabel === "Mid" && (
<span className="mr-0.5 opacity-60">Mid</span>
)}
{referencePriceLabel === "Mark" && (
<span className="mr-0.5 opacity-60">Mark</span>
)}
<span className="text-foreground/70">
{formatUsd(midPrice, { decimals: 2 })}
{formatUsd(referencePrice, { decimals: 2 })}
</span>
</span>
)}
{spreadPct !== null && (
<span>{spreadPct.toFixed(3)}%</span>
)}
</div>
)
}
Expand All @@ -141,6 +214,15 @@ function SpreadRow({
export function DepthLadder({ symbol, compact = false }: Props) {
const { bids, asks, spread, spreadPct, midPrice, status, isLoading } =
useOrderBook(symbol)
const { trades } = useRecentTrades(symbol)

// OB-054: Determine reference price — prefer last trade, fall back to mid price
const lastTradePrice = trades[0]?.price ?? null
const referencePrice = lastTradePrice ?? midPrice
const referencePriceLabel = lastTradePrice !== null ? "Last" : midPrice !== null ? "Mid" : "Mark"

// OB-056: Cumulative depth inspection state
const [hoveredLevel, setHoveredLevel] = useState<{ level: OrderBookLevel; side: "bid" | "ask" } | null>(null)

const isEmpty = bids.length === 0 && asks.length === 0

Expand Down Expand Up @@ -216,37 +298,53 @@ export function DepthLadder({ symbol, compact = false }: Props) {
<div
role="rowgroup"
aria-label="Ask orders"
className="flex min-h-0 flex-1 flex-col overflow-hidden"
className="flex min-h-0 flex-1 flex-col overflow-hidden relative"
>
<ColumnHeaders compact={compact} />
<div className="flex min-h-0 flex-1 flex-col-reverse overflow-y-auto">
{asks.map((level) => (
<DepthRow
key={level.price}
level={level}
side="ask"
compact={compact}
/>
<div key={level.price} className="relative">
<DepthRow
level={level}
side="ask"
compact={compact}
onHover={(lvl) => setHoveredLevel(lvl ? { level: lvl, side: "ask" } : null)}
/>
{hoveredLevel?.level === level && hoveredLevel.side === "ask" && (
<DepthInspection level={level} side="ask" />
)}
</div>
))}
</div>
</div>

{/* Spread */}
<SpreadRow spread={spread} spreadPct={spreadPct} midPrice={midPrice} />
<SpreadRow
spread={spread}
spreadPct={spreadPct}
midPrice={midPrice}
referencePrice={referencePrice}
referencePriceLabel={referencePriceLabel}
/>

{/* Bids (buys) — descending price */}
<div
role="rowgroup"
aria-label="Bid orders"
className="flex min-h-0 flex-1 flex-col overflow-y-auto"
className="flex min-h-0 flex-1 flex-col overflow-y-auto relative"
>
{bids.map((level) => (
<DepthRow
key={level.price}
level={level}
side="bid"
compact={compact}
/>
<div key={level.price} className="relative">
<DepthRow
level={level}
side="bid"
compact={compact}
onHover={(lvl) => setHoveredLevel(lvl ? { level: lvl, side: "bid" } : null)}
/>
{hoveredLevel?.level === level && hoveredLevel.side === "bid" && (
<DepthInspection level={level} side="bid" />
)}
</div>
))}
</div>
</div>
Expand Down
Loading
Loading