From ba4e2b73547303134482e9436862cf713e2efc14 Mon Sep 17 00:00:00 2001 From: crthpl Date: Fri, 26 Dec 2025 17:16:22 -0800 Subject: [PATCH 1/5] add account and round (group) pnl --- frontend/src/lib/portfolioMetrics.ts | 71 +++++++++++++++++++++++++ frontend/src/routes/+layout.svelte | 24 ++++++--- frontend/src/routes/home/+page.svelte | 24 ++++++++- frontend/src/routes/market/+page.svelte | 9 +++- 4 files changed, 120 insertions(+), 8 deletions(-) diff --git a/frontend/src/lib/portfolioMetrics.ts b/frontend/src/lib/portfolioMetrics.ts index c0f8aafc..d6ce36f4 100644 --- a/frontend/src/lib/portfolioMetrics.ts +++ b/frontend/src/lib/portfolioMetrics.ts @@ -143,3 +143,74 @@ export const computePortfolioMetrics = ( } }; }; + +/** + * Calculate subaccount PnL: Current MTM - Net Transfers In + */ +export const calculateSubaccountPnL = ( + accountId: number, + transfers: websocket_api.ITransfer[], + markToMarket: number +): number => { + let netTransfersIn = 0; + for (const transfer of transfers) { + if (transfer.toAccountId === accountId) { + netTransfersIn += transfer.amount ?? 0; + } + if (transfer.fromAccountId === accountId) { + netTransfersIn -= transfer.amount ?? 0; + } + } + return markToMarket - netTransfersIn; +}; + +/** + * Calculate group PnL from trade history for all markets in the group + */ +export const calculateGroupPnL = ( + groupId: number, + markets: Map, + actingAs: number | undefined +): number => { + if (actingAs === undefined) return 0; + + let totalPnL = 0; + + for (const [, marketData] of markets) { + if (marketData.definition.groupId !== groupId) continue; + + // Calculate cost basis and position from trades + let costBasis = 0; // positive = net money spent + let position = 0; + + for (const trade of marketData.trades) { + const size = trade.size ?? 0; + const price = trade.price ?? 0; + + if (trade.buyerId === actingAs) { + costBasis += price * size; + position += size; + } + if (trade.sellerId === actingAs) { + costBasis -= price * size; + position -= size; + } + } + + // Get current price: settlePrice if closed, lastPrice if open + let currentPrice: number | undefined; + if (marketData.definition.closed) { + currentPrice = marketData.definition.closed.settlePrice ?? undefined; + } else { + currentPrice = lastTradePrice(marketData); + } + + // Calculate PnL: current value - cost basis + const currentValue = currentPrice !== undefined ? position * currentPrice : 0; + const marketPnL = currentValue - costBasis; + + totalPnL += marketPnL; + } + + return totalPnL; +}; diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index 96f5f09d..1d059abb 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -7,7 +7,7 @@ import { Button } from '$lib/components/ui/button'; import * as Sidebar from '$lib/components/ui/sidebar/index.js'; import { Toaster } from '$lib/components/ui/sonner'; - import { computePortfolioMetrics } from '$lib/portfolioMetrics'; + import { calculateGroupPnL, computePortfolioMetrics } from '$lib/portfolioMetrics'; import { cn, formatMarketName } from '$lib/utils'; import { ModeWatcher } from 'mode-watcher'; import { onMount } from 'svelte'; @@ -21,6 +21,11 @@ ? serverState.markets.get(marketId)?.definition?.name : undefined ); + let currentGroupId = $derived( + marketId !== undefined && !Number.isNaN(marketId) + ? serverState.markets.get(marketId)?.definition?.groupId + : undefined + ); let { children } = $props(); let scrolled = $state(false); @@ -188,14 +193,21 @@ {#if serverState.portfolio} {@const availableBalance = formatBalance(serverState.portfolio.availableBalance)} - {@const mtmValue = new Intl.NumberFormat(undefined, { maximumFractionDigits: 0 }).format(portfolioMetrics.totals.markToMarket)} + {@const isGroupView = currentGroupId !== undefined && currentGroupId !== 0} + {@const displayValue = isGroupView + ? calculateGroupPnL(currentGroupId, serverState.markets, serverState.actingAs) + : portfolioMetrics.totals.markToMarket} + {@const formattedValue = new Intl.NumberFormat(undefined, { maximumFractionDigits: 0 }).format(displayValue)} + {@const fullLabel = isGroupView ? 'Round PnL' : 'Mark to Market'} + {@const shortLabel = isGroupView ? 'Round' : 'MtM'} + {@const valueColor = isGroupView ? (displayValue >= 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400') : ''}