diff --git a/src/components/orderbook/OrderbookContent.tsx b/src/components/orderbook/OrderbookContent.tsx index 53464c2b..a95426c0 100644 --- a/src/components/orderbook/OrderbookContent.tsx +++ b/src/components/orderbook/OrderbookContent.tsx @@ -9,6 +9,7 @@ import { DevBadge } from '@/components/dev/DevBadge' import { Alert, AlertDescription } from '@/components/ui/alert' import { Button } from '@/components/ui/button' import { ButtonGroup } from '@/components/ui/button-group' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { DropdownMenu, DropdownMenuContent, @@ -189,6 +190,43 @@ export const OrderbookContent = ({ enabled, className }: OrderbookContentProps) } }, [tableRowModel]) + const marketSummary = useMemo(() => { + const entries = (tableRowModel?.rows ?? []).map((r) => r.original) + if (entries.length === 0) return null + + const absoluteOffers = entries.filter((offer) => offer.type.isAbsolute) + const relativeOffers = entries.filter((offer) => offer.type.isRelative) + + const counterpartyBonds = new Map() + for (const offer of entries) { + const previous = counterpartyBonds.get(offer.counterparty) ?? 0 + if (offer.bondValue.value > previous) counterpartyBonds.set(offer.counterparty, offer.bondValue.value) + } + const bondedMakers = [...counterpartyBonds.values()].filter((v) => v > 0).length + + const median = (values: number[]) => { + if (values.length === 0) return 0 + // eslint-disable-next-line unicorn/no-array-sort -- toSorted() not supported in target browsers + const sorted = [...values].sort((a, b) => a - b) + const mid = Math.floor(sorted.length / 2) + return sorted.length % 2 !== 0 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2 + } + + const maxSizes = entries.map((offer) => Number(offer.maximumSize)) + const minSizes = entries.map((offer) => Number(offer.minimumSize)) + + return { + medianAbsFee: + absoluteOffers.length > 0 ? Math.round(median(absoluteOffers.map((offer) => offer.fee.value))) : null, + medianRelFee: relativeOffers.length > 0 ? median(relativeOffers.map((offer) => offer.fee.value)) : null, + totalLiquidity: maxSizes.reduce((sum, v) => sum + v, 0), + minOfferSize: Math.min(...minSizes), + maxOfferSize: Math.max(...maxSizes), + bondedMakers, + unbondedMakers: counterpartyBonds.size - bondedMakers, + } + }, [tableRowModel]) + const handleClearAndReload = async () => { await refetchOrderbookRefresh().then(() => refetchOrderbookData()) } @@ -325,6 +363,43 @@ export const OrderbookContent = ({ enabled, className }: OrderbookContentProps) )} + {marketSummary && ( + + + {t('orderbook.market_summary_title')} + + + {marketSummary.medianAbsFee !== null && ( +

{t('orderbook.market_summary_median_abs_fee', { value: marketSummary.medianAbsFee.toLocaleString() })}

+ )} + {marketSummary.medianRelFee !== null && ( +

+ {t('orderbook.market_summary_median_rel_fee', { + value: factorToPercentage(marketSummary.medianRelFee).toFixed(4), + })} +

+ )} +

+ {t('orderbook.market_summary_total_liquidity', { + value: marketSummary.totalLiquidity.toLocaleString(), + })} +

+

+ {t('orderbook.market_summary_offer_size_range', { + min: marketSummary.minOfferSize.toLocaleString(), + max: marketSummary.maxOfferSize.toLocaleString(), + })} +

+

+ {t('orderbook.market_summary_bonded_makers', { + bonded: marketSummary.bondedMakers, + unbonded: marketSummary.unbondedMakers, + })} +

+
+
+ )} + {isLoadingInitially ? (
diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 58daf2e4..f9aea9b2 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -729,6 +729,12 @@ "text_highlight_own_orders_subtitle": "It might take some time for your offer and your bond to appear in your local orderbook.", "label_pin_to_top_own_orders": "Pin my offers", "text_pin_to_top_own_orders_subtitle": "Displays your offers at the top of the table", + "market_summary_title": "Market Summary", + "market_summary_median_abs_fee": "Median absolute fee: {{ value }} sats", + "market_summary_median_rel_fee": "Median relative fee: {{ value }}%", + "market_summary_total_liquidity": "Total liquidity: {{ value }} sats", + "market_summary_offer_size_range": "Offer size range: {{ min }} – {{ max }} sats", + "market_summary_bonded_makers": "{{ bonded }} bonded / {{ unbonded }} unbonded makers", "table": { "heading_type": "Type", "heading_counterparty": "Counterparty",