diff --git a/src/bonsai/calculators/fills.ts b/src/bonsai/calculators/fills.ts index 154cf154f2..fd77fb7c4e 100644 --- a/src/bonsai/calculators/fills.ts +++ b/src/bonsai/calculators/fills.ts @@ -3,11 +3,15 @@ import { weakMapMemoize } from 'reselect'; import { NUM_PARENT_SUBACCOUNTS } from '@/constants/account'; import { EMPTY_ARR } from '@/constants/objects'; -import { IndexerFillType } from '@/types/indexer/indexerApiGen'; +import { + IndexerFillType, + IndexerOrderSide, + IndexerPositionSide, +} from '@/types/indexer/indexerApiGen'; import { IndexerCompositeFillObject } from '@/types/indexer/indexerManual'; import { assertNever } from '@/lib/assertNever'; -import { MustBigNumber } from '@/lib/numbers'; +import { MustBigNumber, MustNumber } from '@/lib/numbers'; import { mergeObjects } from '../lib/mergeObjects'; import { SubaccountFill, SubaccountFillType } from '../types/summaryTypes'; @@ -30,6 +34,7 @@ const calculateFill = weakMapMemoize( ...base, marginMode: (base.subaccountNumber ?? 0) >= NUM_PARENT_SUBACCOUNTS ? 'ISOLATED' : 'CROSS', type: getFillType(base), + closedPnl: calculateClosedPnl(base), }) ); @@ -53,3 +58,45 @@ function getFillType({ assertNever(type); return SubaccountFillType.LIMIT; } + +const calculateClosedPnl = (fill: IndexerCompositeFillObject) => { + const fee = MustNumber(fill.fee ?? '0'); + + // Old fills are not supported so we show -- instead of 0 + if ( + fill.positionSideBefore === undefined || + fill.positionSizeBefore === undefined || + fill.entryPriceBefore === undefined + ) { + return undefined; + } + + // No position before = opening trade, only fees realize + if (fill.positionSizeBefore === 0) { + return -fee; + } + + // Check if position is reducing (opposite side) + const isReducing = + (fill.positionSideBefore === IndexerPositionSide.LONG && fill.side === IndexerOrderSide.SELL) || + (fill.positionSideBefore === IndexerPositionSide.SHORT && fill.side === IndexerOrderSide.BUY); + + if (!isReducing) { + // Position increasing (same side), only fees realize + return -fee; + } + + const size = MustNumber(fill.size ?? '0'); + const price = MustNumber(fill.price ?? '0'); + + // Position reducing - cap closing amount to actual position size + const closingAmount = Math.min(size, fill.positionSizeBefore); + + // Calculate P&L only on the closing portion + const closingPnl = + fill.positionSideBefore === IndexerPositionSide.LONG + ? (price - fill.entryPriceBefore) * closingAmount + : (fill.entryPriceBefore - price) * closingAmount; + + return closingPnl - fee; +}; diff --git a/src/bonsai/types/summaryTypes.ts b/src/bonsai/types/summaryTypes.ts index 7faa7c1881..96c4b0d191 100644 --- a/src/bonsai/types/summaryTypes.ts +++ b/src/bonsai/types/summaryTypes.ts @@ -5,7 +5,6 @@ import { PricesModule, RewardsModule, } from '@dydxprotocol/v4-client-js'; -import { type BigNumber } from 'bignumber.js'; import { IndexerAPITimeInForce, @@ -193,6 +192,7 @@ export enum SubaccountFillType { export type SubaccountFill = Omit & { marginMode: MarginMode; type: SubaccountFillType | undefined; + closedPnl?: number; }; export type LiveTrade = IndexerWsTradeResponseObject; diff --git a/src/pages/portfolio/Overview.tsx b/src/pages/portfolio/Overview.tsx index a1500c1720..055176d57c 100644 --- a/src/pages/portfolio/Overview.tsx +++ b/src/pages/portfolio/Overview.tsx @@ -99,6 +99,7 @@ export const Overview = () => { PositionsTableColumnKey.Size, PositionsTableColumnKey.Value, PositionsTableColumnKey.PnL, + PositionsTableColumnKey.RealizedPnL, PositionsTableColumnKey.Margin, PositionsTableColumnKey.AverageOpen, PositionsTableColumnKey.Oracle, diff --git a/src/pages/portfolio/Portfolio.tsx b/src/pages/portfolio/Portfolio.tsx index b5c1a4c341..ded561a4a6 100644 --- a/src/pages/portfolio/Portfolio.tsx +++ b/src/pages/portfolio/Portfolio.tsx @@ -131,6 +131,7 @@ const PortfolioPage = () => { FillsTableColumnKey.Price, FillsTableColumnKey.Total, FillsTableColumnKey.Fee, + FillsTableColumnKey.ClosedPnl, FillsTableColumnKey.Liquidity, ] } diff --git a/src/pages/portfolio/Positions.tsx b/src/pages/portfolio/Positions.tsx index 7614e8b365..f7925853cd 100644 --- a/src/pages/portfolio/Positions.tsx +++ b/src/pages/portfolio/Positions.tsx @@ -69,6 +69,7 @@ export const Positions = () => { PositionsTableColumnKey.Size, PositionsTableColumnKey.Value, PositionsTableColumnKey.PnL, + PositionsTableColumnKey.RealizedPnL, PositionsTableColumnKey.Margin, PositionsTableColumnKey.AverageOpen, PositionsTableColumnKey.Oracle, diff --git a/src/pages/trade/HorizontalPanel.tsx b/src/pages/trade/HorizontalPanel.tsx index 535bc9a257..889da97b84 100644 --- a/src/pages/trade/HorizontalPanel.tsx +++ b/src/pages/trade/HorizontalPanel.tsx @@ -150,6 +150,7 @@ export const HorizontalPanel = ({ isOpen = true, setIsOpen, handleStartResize }: PositionsTableColumnKey.Size, PositionsTableColumnKey.Value, PositionsTableColumnKey.PnL, + PositionsTableColumnKey.RealizedPnL, PositionsTableColumnKey.Margin, PositionsTableColumnKey.AverageOpen, PositionsTableColumnKey.Oracle, @@ -326,6 +327,7 @@ export const HorizontalPanel = ({ isOpen = true, setIsOpen, handleStartResize }: FillsTableColumnKey.Price, FillsTableColumnKey.Total, FillsTableColumnKey.Fee, + FillsTableColumnKey.ClosedPnl, FillsTableColumnKey.Liquidity, ].filter(isTruthy) } diff --git a/src/types/indexer/indexerManual.ts b/src/types/indexer/indexerManual.ts index 9c0d892abd..1adf1ff376 100644 --- a/src/types/indexer/indexerManual.ts +++ b/src/types/indexer/indexerManual.ts @@ -19,6 +19,7 @@ import { IndexerPerpetualMarketStatus, IndexerPerpetualMarketType, IndexerPerpetualPositionResponseObject, + IndexerPositionSide, IndexerTradeResponseObject, IndexerTransferResponseObject, } from './indexerApiGen'; @@ -156,6 +157,9 @@ export interface IndexerCompositeFillObject { clientMetadata?: string | null; subaccountNumber?: number; market?: string; + positionSideBefore?: IndexerPositionSide; + positionSizeBefore?: number; + entryPriceBefore?: number; } export interface IndexerWsParentSubaccountSubscribedResponse { diff --git a/src/views/tables/FillsTable.tsx b/src/views/tables/FillsTable.tsx index ce65f5eb68..31be463ea6 100644 --- a/src/views/tables/FillsTable.tsx +++ b/src/views/tables/FillsTable.tsx @@ -55,6 +55,7 @@ export enum FillsTableColumnKey { AmountTag = 'Amount-Tag', Total = 'Total', Fee = 'Fee', + ClosedPnl = 'ClosedPnl', // Tablet Only TypeAmount = 'Type-Amount', @@ -215,6 +216,16 @@ const getFillsTableColumnDef = ({ ), }, + [FillsTableColumnKey.ClosedPnl]: { + columnKey: 'closedPnl', + getCellValue: (row) => row.closedPnl, + label: stringGetter({ key: STRING_KEYS.CLOSED_PNL }), + renderCell: ({ closedPnl }) => ( + + + + ), + }, [FillsTableColumnKey.Type]: { columnKey: 'type', getCellValue: (row) => row.type,