diff --git a/condor/web/routes/market.py b/condor/web/routes/market.py index b5b92a99..9afc2388 100644 --- a/condor/web/routes/market.py +++ b/condor/web/routes/market.py @@ -153,6 +153,35 @@ async def get_trading_rules( return TradingRulesResponse(connector=connector, rules=rules) +@router.get("/servers/{name}/market/pair-volumes") +async def get_pair_volumes( + name: str, + connector: str = Query(...), + user: WebUser = Depends(get_current_user), +): + """24h quote volume per trading pair, used to rank/curate the pair selector. + + Returns {"connector": str, "volumes": {trading_pair: quote_volume_24h}}. A value of 0.0 + marks a listed-but-untraded market. Only supported on some exchanges (hyperliquid, binance, + okx and their perp variants); returns an empty map for the rest so the caller falls back to + an alphabetical list. + """ + cm = get_config_manager() + if not cm.has_server_access(user.id, name): + raise HTTPException(status_code=403, detail="No access") + + client = await cm.get_client(name) + try: + result = await client.market_data.get_24h_volumes(connector) + except Exception as e: + # Unsupported connector (400) or upstream error — degrade to alphabetical ordering. + logger.info(f"pair-volumes unavailable for {connector}: {e}") + return {"connector": connector, "volumes": {}} + + volumes = result.get("volumes", {}) if isinstance(result, dict) else {} + return {"connector": connector, "volumes": volumes} + + @router.get("/servers/{name}/market/order-book", response_model=OrderBookResponse) async def get_order_book( name: str, diff --git a/frontend/src/components/market/PairSelector.tsx b/frontend/src/components/market/PairSelector.tsx index 3627194c..91fe3791 100644 --- a/frontend/src/components/market/PairSelector.tsx +++ b/frontend/src/components/market/PairSelector.tsx @@ -13,6 +13,25 @@ interface PairSelectorProps { const MAX_VISIBLE = 50; +// Quote-asset display priority. Ranking by raw 24h volume alone mixes currencies (a fiat-quoted +// pair like USDT-IDR has a huge numeric quote volume), so we group by quote first, then sort by +// volume within each group. Single-quote exchanges (e.g. Hyperliquid, all USDC) become pure +// volume ranking. +const QUOTE_PRIORITY = ["USDT", "USDC", "USD", "BTC", "ETH"]; + +function quoteRank(pair: string): number { + const q = pair.split("-").pop() ?? ""; + const i = QUOTE_PRIORITY.indexOf(q); + return i === -1 ? QUOTE_PRIORITY.length : i; +} + +function formatVolume(v: number): string { + if (v >= 1e9) return `${(v / 1e9).toFixed(1)}B`; + if (v >= 1e6) return `${(v / 1e6).toFixed(1)}M`; + if (v >= 1e3) return `${(v / 1e3).toFixed(0)}K`; + return `${Math.round(v)}`; +} + export function PairSelector({ server, connector, @@ -33,10 +52,37 @@ export function PairSelector({ staleTime: 5 * 60 * 1000, }); - const pairs = useMemo( - () => rulesData?.rules?.map((r) => r.trading_pair).sort() ?? [], - [rulesData], - ); + // 24h quote volume per pair, for ranking + hiding untraded markets. Empty/unsupported → falls + // back to an alphabetical list. + const { data: volData } = useQuery({ + queryKey: ["pair-volumes", server, connector], + queryFn: () => api.getPairVolumes(server, connector), + enabled: !!server && !!connector, + staleTime: 60 * 1000, + }); + + const pairs = useMemo(() => { + const all = rulesData?.rules?.map((r) => r.trading_pair) ?? []; + const volumes = volData?.volumes ?? {}; + const hasVolume = Object.keys(volumes).length > 0; + if (!hasVolume) return [...all].sort(); + + return all + // Hide listed-but-untraded markets (24h volume exactly 0, e.g. Hyperliquid's tokenized + // equities like AAPL-USDC). Keep the active pair and any pair with unknown volume. + .filter((p) => volumes[p] !== 0 || p === value) + .sort((a, b) => { + const qr = quoteRank(a) - quoteRank(b); + if (qr !== 0) return qr; + // Unknown volume (not in the map, e.g. HIP-3 perps) sorts after known volume. + const va = volumes[a] ?? -1; + const vb = volumes[b] ?? -1; + if (vb !== va) return vb - va; + return a.localeCompare(b); + }); + }, [rulesData, volData, value]); + + const volumes = volData?.volumes ?? {}; // Group pairs by quote asset const quoteGroups = useMemo(() => { @@ -203,6 +249,11 @@ export function PairSelector({ }`} > {base}-{quote} + {volumes[p] > 0 && ( + + {formatVolume(volumes[p])} + + )} ); }) diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index a3dcaeaa..72ee9033 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -870,6 +870,11 @@ export const api = { `/api/v1/servers/${server}/market/trading-rules?connector=${connector}`, ), + getPairVolumes: (server: string, connector: string) => + apiFetch<{ connector: string; volumes: Record }>( + `/api/v1/servers/${server}/market/pair-volumes?connector=${connector}`, + ), + getOrderBook: ( server: string, connector: string,