Files
auto-trade/features/trade/components/orderbook/OrderBook.tsx

243 lines
8.9 KiB
TypeScript
Raw Normal View History

2026-02-10 11:16:39 +09:00
import { useMemo } from "react";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { ScrollArea } from "@/components/ui/scroll-area";
import type {
DashboardRealtimeTradeTick,
DashboardStockOrderBookResponse,
2026-02-11 16:31:28 +09:00
} from "@/features/trade/types/trade.types";
2026-02-26 09:05:17 +09:00
import type { BookRow } from "./orderbook-utils";
import {
buildBookRows,
buildFallbackLevelsFromTick,
hasOrderBookLevelData,
resolveReferencePrice,
} from "./orderbook-utils";
import {
BookHeader,
BookSideRows,
CumulativeRows,
CurrentPriceBar,
OrderBookSkeleton,
SummaryPanel,
TradeTape,
} from "./orderbook-sections";
2026-02-10 11:16:39 +09:00
interface OrderBookProps {
symbol?: string;
referencePrice?: number;
latestTick: DashboardRealtimeTradeTick | null;
recentTicks: DashboardRealtimeTradeTick[];
orderBook: DashboardStockOrderBookResponse | null;
isLoading?: boolean;
}
/**
2026-02-26 09:05:17 +09:00
* @description · 10 .
* @see features/trade/components/orderbook/orderbook-utils.ts /
* @see features/trade/components/orderbook/orderbook-sections.tsx // UI
2026-02-10 11:16:39 +09:00
*/
export function OrderBook({
symbol,
referencePrice,
latestTick,
recentTicks,
orderBook,
isLoading,
}: OrderBookProps) {
2026-02-13 16:41:10 +09:00
const realtimeLevels = useMemo(() => orderBook?.levels ?? [], [orderBook]);
const fallbackLevelsFromTick = useMemo(
() => buildFallbackLevelsFromTick(latestTick),
[latestTick],
);
2026-02-26 09:05:17 +09:00
const hasRealtimeLevelData = useMemo(
() => hasOrderBookLevelData(realtimeLevels),
[realtimeLevels],
);
2026-02-13 16:41:10 +09:00
const levels = useMemo(() => {
2026-02-26 09:05:17 +09:00
if (hasRealtimeLevelData) return realtimeLevels;
2026-02-13 16:41:10 +09:00
return fallbackLevelsFromTick;
2026-02-26 09:05:17 +09:00
}, [fallbackLevelsFromTick, hasRealtimeLevelData, realtimeLevels]);
2026-02-13 16:41:10 +09:00
const isTickFallbackActive =
2026-02-26 09:05:17 +09:00
!hasRealtimeLevelData && fallbackLevelsFromTick.length > 0;
2026-02-10 11:16:39 +09:00
const latestPrice =
latestTick?.price && latestTick.price > 0 ? latestTick.price : 0;
2026-02-23 15:37:22 +09:00
const basePrice = resolveReferencePrice({ referencePrice, latestTick });
2026-02-10 11:16:39 +09:00
const askRows: BookRow[] = useMemo(
() =>
2026-02-23 15:37:22 +09:00
buildBookRows({
levels,
side: "ask",
basePrice,
latestPrice,
}),
2026-02-10 11:16:39 +09:00
[levels, basePrice, latestPrice],
);
const bidRows: BookRow[] = useMemo(
() =>
2026-02-23 15:37:22 +09:00
buildBookRows({
levels,
side: "bid",
basePrice,
latestPrice,
}),
2026-02-10 11:16:39 +09:00
[levels, basePrice, latestPrice],
);
2026-02-26 09:05:17 +09:00
const askMax = useMemo(() => Math.max(1, ...askRows.map((r) => r.size)), [askRows]);
const bidMax = useMemo(() => Math.max(1, ...bidRows.map((r) => r.size)), [bidRows]);
const mobileAskRows = useMemo(() => askRows.slice(0, 6), [askRows]);
const mobileBidRows = useMemo(() => bidRows.slice(0, 6), [bidRows]);
2026-02-10 11:16:39 +09:00
2026-02-26 09:05:17 +09:00
const { bestAsk, spread, totalAsk, totalBid, imbalance } = useMemo(() => {
const resolvedBestAsk = levels.find((level) => level.askPrice > 0)?.askPrice ?? 0;
const resolvedBestBid = levels.find((level) => level.bidPrice > 0)?.bidPrice ?? 0;
const resolvedSpread =
resolvedBestAsk > 0 && resolvedBestBid > 0
? resolvedBestAsk - resolvedBestBid
: 0;
const resolvedTotalAsk =
orderBook?.totalAskSize && orderBook.totalAskSize > 0
? orderBook.totalAskSize
: (latestTick?.totalAskSize ?? 0);
const resolvedTotalBid =
orderBook?.totalBidSize && orderBook.totalBidSize > 0
? orderBook.totalBidSize
: (latestTick?.totalBidSize ?? 0);
const resolvedImbalance =
resolvedTotalAsk + resolvedTotalBid > 0
? ((resolvedTotalBid - resolvedTotalAsk) /
(resolvedTotalAsk + resolvedTotalBid)) *
100
: 0;
2026-02-10 11:16:39 +09:00
2026-02-26 09:05:17 +09:00
return {
bestAsk: resolvedBestAsk,
spread: resolvedSpread,
totalAsk: resolvedTotalAsk,
totalBid: resolvedTotalBid,
imbalance: resolvedImbalance,
};
}, [latestTick?.totalAskSize, latestTick?.totalBidSize, levels, orderBook]);
2026-02-10 11:16:39 +09:00
if (!symbol) {
return (
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">
.
</div>
);
}
2026-02-13 16:41:10 +09:00
if (isLoading && !orderBook && fallbackLevelsFromTick.length === 0) {
return <OrderBookSkeleton />;
}
if (!orderBook && fallbackLevelsFromTick.length === 0) {
2026-02-10 11:16:39 +09:00
return (
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">
.
</div>
);
}
return (
<div className="flex h-full min-h-0 flex-col bg-white dark:bg-[linear-gradient(180deg,rgba(13,13,24,0.95),rgba(8,8,18,0.98))]">
2026-02-10 11:16:39 +09:00
<Tabs defaultValue="normal" className="h-full min-h-0">
2026-02-26 09:05:17 +09:00
{/* ========== ORDERBOOK TAB HEADER ========== */}
<div className="border-b border-border/60 bg-muted/15 px-2 pt-2 dark:border-brand-800/50 dark:bg-brand-950/60">
2026-02-10 11:16:39 +09:00
<TabsList variant="line" className="w-full justify-start">
<TabsTrigger value="normal" className="px-3">
</TabsTrigger>
<TabsTrigger value="cumulative" className="px-3">
</TabsTrigger>
<TabsTrigger value="order" className="px-3">
</TabsTrigger>
</TabsList>
</div>
2026-02-26 09:05:17 +09:00
{/* ========== ORDERBOOK NORMAL TAB ========== */}
2026-02-10 11:16:39 +09:00
<TabsContent value="normal" className="min-h-0 flex-1">
<div className="flex h-full min-h-0 flex-col border-t dark:border-brand-800/45 xl:grid xl:grid-cols-[minmax(0,1fr)_220px_220px] 2xl:grid-cols-[minmax(0,1fr)_250px_240px] xl:overflow-hidden">
2026-02-13 16:41:10 +09:00
<div className="flex min-h-0 flex-col xl:border-r dark:border-brand-800/45">
{isTickFallbackActive && (
<div className="border-b border-amber-200 bg-amber-50 px-2 py-1 text-[11px] text-amber-700 dark:border-amber-700/40 dark:bg-amber-900/20 dark:text-amber-200">
(`H0STOAA0`) .
(`H0UNCNT0`) 1 .
2026-02-13 16:41:10 +09:00
</div>
)}
<BookHeader />
<div className="xl:hidden">
<BookSideRows rows={mobileAskRows} side="ask" maxSize={askMax} />
<CurrentPriceBar
latestPrice={latestPrice}
basePrice={basePrice}
bestAsk={bestAsk}
totalAsk={totalAsk}
totalBid={totalBid}
/>
<BookSideRows rows={mobileBidRows} side="bid" maxSize={bidMax} />
</div>
<ScrollArea className="hidden min-h-0 flex-1 xl:block">
2026-02-13 16:41:10 +09:00
<BookSideRows rows={askRows} side="ask" maxSize={askMax} />
<CurrentPriceBar
latestPrice={latestPrice}
basePrice={basePrice}
bestAsk={bestAsk}
totalAsk={totalAsk}
totalBid={totalBid}
/>
2026-02-13 16:41:10 +09:00
<BookSideRows rows={bidRows} side="bid" maxSize={bidMax} />
</ScrollArea>
2026-02-10 11:16:39 +09:00
</div>
<div className="min-h-[180px] border-t border-border/60 dark:border-brand-800/45 xl:min-h-0 xl:border-t-0 xl:border-r">
<div className="h-full min-h-0">
<TradeTape ticks={recentTicks} maxRows={10} />
</div>
2026-02-10 11:16:39 +09:00
</div>
2026-02-13 16:41:10 +09:00
<div className="min-h-[180px] border-t border-border/60 dark:border-brand-800/45 xl:min-h-0 xl:border-t-0">
<div className="h-full min-h-0">
<SummaryPanel
orderBook={orderBook}
latestTick={latestTick}
spread={spread}
imbalance={imbalance}
totalAsk={totalAsk}
totalBid={totalBid}
/>
</div>
2026-02-13 16:41:10 +09:00
</div>
2026-02-10 11:16:39 +09:00
</div>
</TabsContent>
2026-02-26 09:05:17 +09:00
{/* ========== ORDERBOOK CUMULATIVE TAB ========== */}
2026-02-10 11:16:39 +09:00
<TabsContent value="cumulative" className="min-h-0 flex-1">
2026-02-11 14:06:06 +09:00
<ScrollArea className="h-full border-t dark:border-brand-800/45">
2026-02-10 11:16:39 +09:00
<div className="p-3">
<div className="mb-2 grid grid-cols-3 text-[11px] font-medium text-muted-foreground">
<span></span>
<span className="text-center"></span>
<span className="text-right"></span>
</div>
<CumulativeRows asks={askRows} bids={bidRows} />
</div>
</ScrollArea>
</TabsContent>
2026-02-26 09:05:17 +09:00
{/* ========== ORDERBOOK ORDER TAB ========== */}
2026-02-10 11:16:39 +09:00
<TabsContent value="order" className="min-h-0 flex-1">
2026-02-11 14:06:06 +09:00
<div className="flex h-full items-center justify-center border-t px-4 text-sm text-muted-foreground dark:border-brand-800/45 dark:text-brand-100/75">
2026-02-10 11:16:39 +09:00
.
</div>
</TabsContent>
</Tabs>
</div>
);
}