diff --git a/.changelog/unreleased/706-708-718-730-orderbook-enhancements.md b/.changelog/unreleased/706-708-718-730-orderbook-enhancements.md
new file mode 100644
index 0000000..5751456
--- /dev/null
+++ b/.changelog/unreleased/706-708-718-730-orderbook-enhancements.md
@@ -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.
diff --git a/apps/web/src/features/trade/components/chart/TVChartContainer.tsx b/apps/web/src/features/trade/components/chart/TVChartContainer.tsx
index 1533fff..d3a067d 100644
--- a/apps/web/src/features/trade/components/chart/TVChartContainer.tsx
+++ b/apps/web/src/features/trade/components/chart/TVChartContainer.tsx
@@ -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,
@@ -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)
@@ -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
@@ -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
@@ -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
diff --git a/apps/web/src/features/trade/components/orderbook/DepthLadder.tsx b/apps/web/src/features/trade/components/orderbook/DepthLadder.tsx
index 6a7e3cd..69805d9 100644
--- a/apps/web/src/features/trade/components/orderbook/DepthLadder.tsx
+++ b/apps/web/src/features/trade/components/orderbook/DepthLadder.tsx
@@ -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
@@ -14,9 +16,10 @@ 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)}%`
@@ -24,9 +27,16 @@ function DepthRow({ level, side, compact }: RowProps) {
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 */}
0 ? (level.price * level.size + (level.total - level.size) * level.price) / level.total : level.price
+ const notional = level.total * avgPrice
+
+ return (
+
+
{/* ds-allow: tooltip detail font size */}
+
+ Cumulative Qty:
+
+ {level.total.toLocaleString(undefined, {
+ minimumFractionDigits: 4,
+ maximumFractionDigits: 6,
+ })}
+
+
+
+ Avg Price:
+
+ {formatUsd(avgPrice, { decimals: 4 })}
+
+
+
+ Notional:
+
+ {formatUsd(notional, { decimals: 2 })}
+
+
+
{/* ds-allow: tooltip disclaimer font size */}
+ Snapshot estimate
+
+
+
+ )
+}
+
// ── column headers ────────────────────────────────────────────────────────────
function ColumnHeaders({ compact }: { compact: boolean }) {
@@ -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 (
({spreadPct.toFixed(3)}%)}
- {midPrice !== null && (
-
- Mid{" "}
+ {referencePrice !== null && (
+
+ {referencePriceLabel === "Last" && (
+ Last
+ )}
+ {referencePriceLabel === "Mid" && (
+ Mid
+ )}
+ {referencePriceLabel === "Mark" && (
+ Mark
+ )}
- {formatUsd(midPrice, { decimals: 2 })}
+ {formatUsd(referencePrice, { decimals: 2 })}
)}
- {spreadPct !== null && (
- {spreadPct.toFixed(3)}%
- )}
)
}
@@ -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
@@ -216,37 +298,53 @@ export function DepthLadder({ symbol, compact = false }: Props) {
{asks.map((level) => (
-
+
+ setHoveredLevel(lvl ? { level: lvl, side: "ask" } : null)}
+ />
+ {hoveredLevel?.level === level && hoveredLevel.side === "ask" && (
+
+ )}
+
))}
{/* Spread */}
-
+
{/* Bids (buys) — descending price */}
{bids.map((level) => (
-
+
+ setHoveredLevel(lvl ? { level: lvl, side: "bid" } : null)}
+ />
+ {hoveredLevel?.level === level && hoveredLevel.side === "bid" && (
+
+ )}
+
))}
diff --git a/apps/web/src/features/trade/components/trade-panel/ConfirmationDialog.tsx b/apps/web/src/features/trade/components/trade-panel/ConfirmationDialog.tsx
index 8e69791..e370889 100644
--- a/apps/web/src/features/trade/components/trade-panel/ConfirmationDialog.tsx
+++ b/apps/web/src/features/trade/components/trade-panel/ConfirmationDialog.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useMemo, useState } from "react"
+import { useEffect, useMemo, useState, useRef } from "react"
import {
Dialog,
DialogContent,
@@ -60,6 +60,16 @@ export function ConfirmationDialog({
const [estimatingFee, setEstimatingFee] = useState(false)
const account = useWalletStore((state: { address: string | null }) => state.address)
+ // OB-077: Track input snapshot to detect material changes requiring review
+ const inputSnapshotRef = useRef<{
+ sizeUsd: number
+ entryPrice: number
+ leverage: number
+ triggerPrice: string
+ collateralAmount: string
+ } | null>(null)
+ const [isStale, setIsStale] = useState(false)
+
const {
tradeFlags,
toTokenAddress,
@@ -110,6 +120,41 @@ export function ConfirmationDialog({
: null
const executionValidation = validateExecutionRequest({ attachedOrders: sidecarOrders })
+ // OB-077: Detect material input changes that invalidate the preview
+ useEffect(() => {
+ if (!open) {
+ inputSnapshotRef.current = null
+ setIsStale(false)
+ return
+ }
+
+ const currentInputs = {
+ sizeUsd,
+ entryPrice,
+ leverage,
+ triggerPrice: triggerPrice ?? "",
+ collateralAmount: fromAmount ?? "",
+ }
+
+ if (!inputSnapshotRef.current) {
+ // First open — capture snapshot
+ inputSnapshotRef.current = currentInputs
+ setIsStale(false)
+ } else {
+ // Check for material changes (>1% for prices/size, exact match for others)
+ const prev = inputSnapshotRef.current
+ const sizeChanged = Math.abs(currentInputs.sizeUsd - prev.sizeUsd) / Math.max(prev.sizeUsd, 1) > 0.01
+ const priceChanged = Math.abs(currentInputs.entryPrice - prev.entryPrice) / Math.max(prev.entryPrice, 1) > 0.01
+ const leverageChanged = currentInputs.leverage !== prev.leverage
+ const triggerChanged = currentInputs.triggerPrice !== prev.triggerPrice
+ const collateralChanged = currentInputs.collateralAmount !== prev.collateralAmount
+
+ if (sizeChanged || priceChanged || leverageChanged || triggerChanged || collateralChanged) {
+ setIsStale(true)
+ }
+ }
+ }, [open, sizeUsd, entryPrice, leverage, triggerPrice, fromAmount])
+
const sidecarCreateOrders = useMemo((): Array
=> {
if (!account || sidecarOrders.length === 0) return []
return sidecarOrders.map((order, index) => ({
@@ -206,6 +251,9 @@ export function ConfirmationDialog({
])
async function handleConfirm() {
+ // OB-077: Prevent rapid double-clicks from creating duplicate orders
+ if (isSubmitting) return
+
setIsSubmitting(true)
try {
if (!executionValidation.valid) {
@@ -280,8 +328,23 @@ export function ConfirmationDialog({
Confirm {typeLabel} {!tradeFlags.isSwap && formatAddress(toTokenAddress)}
+ {/* OB-077: Display network for clarity */}
+
+ Network: {ENV.NETWORK}
+
+ {/* OB-077: Staleness warning */}
+ {isStale && (
+
+
Inputs changed — review required
+
+ Price, size, or execution parameters have changed since this dialog opened.
+ Please review the updated values before confirming.
+
+
+ )}
+
{!tradeFlags.isSwap && (
<>