Skip to content
Draft
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
29 changes: 29 additions & 0 deletions condor/web/routes/market.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
59 changes: 55 additions & 4 deletions frontend/src/components/market/PairSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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(() => {
Expand Down Expand Up @@ -203,6 +249,11 @@ export function PairSelector({
}`}
>
<span><span className="font-medium">{base}</span><span className="text-[var(--color-text-muted)]">-{quote}</span></span>
{volumes[p] > 0 && (
<span className="ml-auto text-xs tabular-nums text-[var(--color-text-muted)]">
{formatVolume(volumes[p])}
</span>
)}
</button>
);
})
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, number> }>(
`/api/v1/servers/${server}/market/pair-volumes?connector=${connector}`,
),

getOrderBook: (
server: string,
connector: string,
Expand Down