From 8023b9bdd5c9727cc6edc0a7f3d9d1784578ea92 Mon Sep 17 00:00:00 2001 From: dwjanus Date: Thu, 12 Feb 2026 17:14:13 -0500 Subject: [PATCH 1/6] trade history table request and ui scaffolding --- src/bonsai/calculators/trades.ts | 81 +++ src/bonsai/ontology.ts | 11 + src/bonsai/rest/trades.ts | 44 ++ src/bonsai/selectors/account.ts | 16 + src/bonsai/selectors/base.ts | 4 + src/bonsai/storeLifecycles.ts | 2 + src/bonsai/types/rawTypes.ts | 2 + src/bonsai/types/summaryTypes.ts | 36 + src/constants/dialogs.ts | 24 +- src/hooks/useSharePnlImage.ts | 101 +-- src/pages/portfolio/Portfolio.tsx | 34 +- src/state/raw.ts | 10 + src/types/indexer/indexerChecks.ts | 2 + src/types/indexer/indexerManual.ts | 37 + src/views/Lists/Trade/TradeHistoryList.tsx | 180 ++++- src/views/Lists/Trade/TradeHistoryRow.tsx | 139 ++++ src/views/dialogs/SharePNLAnalyticsDialog.tsx | 24 +- src/views/tables/PositionsTable.tsx | 2 + .../PositionsTable/PositionsActionsCell.tsx | 37 +- src/views/tables/TradeHistoryTable.tsx | 655 ++++++++++++++++++ .../TradeHistoryActionsCell.tsx | 102 +++ 21 files changed, 1420 insertions(+), 123 deletions(-) create mode 100644 src/bonsai/calculators/trades.ts create mode 100644 src/bonsai/rest/trades.ts create mode 100644 src/views/Lists/Trade/TradeHistoryRow.tsx create mode 100644 src/views/tables/TradeHistoryTable.tsx create mode 100644 src/views/tables/TradeHistoryTable/TradeHistoryActionsCell.tsx diff --git a/src/bonsai/calculators/trades.ts b/src/bonsai/calculators/trades.ts new file mode 100644 index 0000000000..e773a75178 --- /dev/null +++ b/src/bonsai/calculators/trades.ts @@ -0,0 +1,81 @@ +import { keyBy, maxBy, orderBy } from 'lodash'; +import { weakMapMemoize } from 'reselect'; + +import { EMPTY_ARR } from '@/constants/objects'; +import { IndexerOrderSide, IndexerPositionSide } from '@/types/indexer/indexerApiGen'; +import { IndexerCompositeTradeObject } from '@/types/indexer/indexerManual'; + +import { MustBigNumber, MustNumber } from '@/lib/numbers'; + +import { mergeObjects } from '../lib/mergeObjects'; +import { SubaccountTrade, TradeAction } from '../types/summaryTypes'; + +export function calculateTrades( + restTrades: IndexerCompositeTradeObject[] | undefined, + liveTrades: IndexerCompositeTradeObject[] | undefined +): SubaccountTrade[] { + const getTradesById = (data: IndexerCompositeTradeObject[]) => + keyBy(data, (trade) => trade.id ?? ''); + + const merged = mergeObjects( + getTradesById(restTrades ?? EMPTY_ARR), + getTradesById(liveTrades ?? EMPTY_ARR), + (first, second) => maxBy([first, second], (t) => MustBigNumber(t.time).toNumber())! + ); + + return orderBy(Object.values(merged).map(calculateTrade), [(t) => t.createdAt], ['desc']); +} + +const calculateTrade = weakMapMemoize( + (base: IndexerCompositeTradeObject): SubaccountTrade => ({ + id: base.id ?? '', + marketId: base.marketId ?? '', + positionId: base.positionId ?? '', + orderId: base.orderId ?? '', + side: base.side, + action: deriveTradeAction(base), + price: base.executionPrice, + size: (MustNumber(base.additionalSize ?? 0) + MustNumber(base.prevSize ?? 0)).toString(), + value: + MustNumber(base.executionPrice ?? 0) * MustNumber(base.additionalSize ?? 0) + + MustNumber(base.prevSize ?? '0'), + fee: base.netFee, + closedPnl: base.netRealizedPnl != null ? MustNumber(base.netRealizedPnl) : undefined, + closedPnlPercent: + base.netRealizedPnl != null + ? MustNumber(base.netRealizedPnl) / + (Math.abs(MustNumber(base.additionalSize ?? 0)) * MustNumber(base.executionPrice ?? 0)) + : undefined, + createdAt: base.time, + marginMode: base.marginMode === 'ISOLATED' ? 'ISOLATED' : 'CROSS', + orderType: undefined, // map from base.orderType when API contract is known + }) +); + +function deriveTradeAction(trade: IndexerCompositeTradeObject): TradeAction { + // If the API sends `action` directly, use it: + // if (trade.action) return trade.action as TradeAction; + + // Otherwise derive from position context (same logic you had): + const positionSizeBefore = MustNumber(trade.prevSize ?? '0'); + const tradeSize = MustNumber(trade.additionalSize ?? 0) + MustNumber(trade.prevSize ?? 0); + const isLong = + trade.positionSide === IndexerPositionSide.LONG || + (trade.side === IndexerOrderSide.BUY && trade.action === 'OPEN'); + const isExtend = trade.action === 'EXTEND'; + const isBuy = trade.side === IndexerOrderSide.BUY; + + if (positionSizeBefore === 0) { + return isBuy ? TradeAction.OPEN_LONG : TradeAction.OPEN_SHORT; + } + + if ((isExtend && isBuy) || (isExtend && !isBuy)) { + return isLong ? TradeAction.ADD_TO_LONG : TradeAction.ADD_TO_SHORT; + } + + const isFullClose = tradeSize >= positionSizeBefore; + if (isLong) { + return isFullClose ? TradeAction.CLOSE_LONG : TradeAction.PARTIAL_CLOSE_LONG; + } + return isFullClose ? TradeAction.CLOSE_SHORT : TradeAction.PARTIAL_CLOSE_SHORT; +} diff --git a/src/bonsai/ontology.ts b/src/bonsai/ontology.ts index cb6dc8abe3..30863cd52c 100644 --- a/src/bonsai/ontology.ts +++ b/src/bonsai/ontology.ts @@ -48,6 +48,8 @@ import { selectAccountOrders, selectAccountOrdersLoading, selectAccountStakingTier, + selectAccountTrades, + selectAccountTradesLoading, selectAccountTransfers, selectAccountTransfersLoading, selectChildSubaccountSummaries, @@ -148,6 +150,7 @@ import { SubaccountFill, SubaccountOrder, SubaccountPosition, + SubaccountTrade, SubaccountTransfer, UserStakingTierSummary, UserStats, @@ -188,6 +191,10 @@ interface BonsaiCoreShape { data: BasicSelector; loading: BasicSelector; }; + trades: { + data: BasicSelector; + loading: BasicSelector; + }; transfers: { data: BasicSelector; loading: BasicSelector; @@ -296,6 +303,10 @@ export const BonsaiCore: BonsaiCoreShape = { data: selectAccountFills, loading: selectAccountFillsLoading, }, + trades: { + data: selectAccountTrades, + loading: selectAccountTradesLoading, + }, transfers: { data: selectAccountTransfers, loading: selectAccountTransfersLoading, diff --git a/src/bonsai/rest/trades.ts b/src/bonsai/rest/trades.ts new file mode 100644 index 0000000000..490610c813 --- /dev/null +++ b/src/bonsai/rest/trades.ts @@ -0,0 +1,44 @@ +import { isParentSubaccountTradeResponse } from '@/types/indexer/indexerChecks'; + +import { type RootStore } from '@/state/_store'; +import { setAccountTradesRaw } from '@/state/raw'; + +import { isTruthy } from '@/lib/isTruthy'; + +import { refreshIndexerQueryOnAccountSocketRefresh } from '../accountRefreshSignal'; +import { loadableIdle } from '../lib/loadable'; +import { mapLoadableData } from '../lib/mapLoadable'; +import { selectParentSubaccountInfo } from '../socketSelectors'; +import { createIndexerQueryStoreEffect } from './lib/indexerQueryStoreEffect'; +import { queryResultToLoadable } from './lib/queryResultToLoadable'; + +export function setUpTradesQuery(store: RootStore) { + const cleanupListener = refreshIndexerQueryOnAccountSocketRefresh(['account', 'trades']); + const cleanupEffect = createIndexerQueryStoreEffect(store, { + name: 'trades', + selector: selectParentSubaccountInfo, + getQueryKey: (data) => ['account', 'trades', data.wallet, data.subaccount], + getQueryFn: (indexerClient, data) => { + if (!isTruthy(data.wallet) || data.subaccount == null || data.isPerpsGeoRestricted) { + return null; + } + // TODO: DWJ -- Replace with real trades endpoint when available + // e.g. indexerClient.account.getParentSubaccountNumberTrades(data.wallet!, data.subaccount!) + return () => + indexerClient.account.getParentSubaccountNumberFills(data.wallet!, data.subaccount!); + }, + onResult: (trades) => { + store.dispatch( + setAccountTradesRaw( + mapLoadableData(queryResultToLoadable(trades), isParentSubaccountTradeResponse) + ) + ); + }, + onNoQuery: () => store.dispatch(setAccountTradesRaw(loadableIdle())), + }); + return () => { + cleanupListener(); + cleanupEffect(); + store.dispatch(setAccountTradesRaw(loadableIdle())); + }; +} diff --git a/src/bonsai/selectors/account.ts b/src/bonsai/selectors/account.ts index 68b89d39d9..410b7c38c0 100644 --- a/src/bonsai/selectors/account.ts +++ b/src/bonsai/selectors/account.ts @@ -26,6 +26,7 @@ import { calculateParentSubaccountSummary, calculateUnopenedIsolatedPositions, } from '../calculators/subaccount'; +import { calculateTrades } from '../calculators/trades'; import { calculateTransfers } from '../calculators/transfers'; import { calculateAccountStakingTier } from '../calculators/userStats'; import { mergeLoadableStatus } from '../lib/mapLoadable'; @@ -50,6 +51,9 @@ import { selectRawParentSubaccountData, selectRawSelectedMarketLeverages, selectRawSelectedMarketLeveragesData, + selectRawTradesLiveData, + selectRawTradesRest, + selectRawTradesRestData, selectRawTransfersLiveData, selectRawTransfersRest, selectRawTransfersRestData, @@ -306,3 +310,15 @@ export const selectAccountStakingTier = createAppSelector( [selectRawAccountStakingTierData], (stakingTier) => calculateAccountStakingTier(stakingTier) ); + +export const selectAccountTrades = createAppSelector( + [selectRawTradesRestData, selectRawTradesLiveData], + (rest, live) => { + return calculateTrades(rest?.trades, live); + } +); + +export const selectAccountTradesLoading = createAppSelector( + [selectRawTradesRest, selectRawParentSubaccount], + mergeLoadableStatus +); diff --git a/src/bonsai/selectors/base.ts b/src/bonsai/selectors/base.ts index cce4a2692c..40bb307aef 100644 --- a/src/bonsai/selectors/base.ts +++ b/src/bonsai/selectors/base.ts @@ -20,6 +20,7 @@ export const selectRawParentSubaccountData = (state: RootState) => export const selectRawFillsRestData = (state: RootState) => state.raw.account.fills.data; export const selectRawOrdersRestData = (state: RootState) => state.raw.account.orders.data; +export const selectRawTradesRestData = (state: RootState) => state.raw.account.trades.data; export const selectRawTransfersRestData = (state: RootState) => state.raw.account.transfers.data; export const selectRawBlockTradingRewardsRestData = (state: RootState) => state.raw.account.blockTradingRewards.data; @@ -28,6 +29,8 @@ export const selectRawFillsLiveData = (state: RootState) => state.raw.account.parentSubaccount.data?.live.fills; export const selectRawOrdersLiveData = (state: RootState) => state.raw.account.parentSubaccount.data?.live.orders; +export const selectRawTradesLiveData = (state: RootState) => + state.raw.account.parentSubaccount.data?.live.trades; export const selectRawTransfersLiveData = (state: RootState) => state.raw.account.parentSubaccount.data?.live.transfers; export const selectRawBlockTradingRewardsLiveData = (state: RootState) => @@ -46,6 +49,7 @@ export const selectRawValidatorHeightDataLoadable = (state: RootState) => export const selectRawFillsRest = (state: RootState) => state.raw.account.fills; export const selectRawOrdersRest = (state: RootState) => state.raw.account.orders; +export const selectRawTradesRest = (state: RootState) => state.raw.account.trades; export const selectRawTransfersRest = (state: RootState) => state.raw.account.transfers; export const selectRawBlockTradingRewardsRest = (state: RootState) => state.raw.account.blockTradingRewards; diff --git a/src/bonsai/storeLifecycles.ts b/src/bonsai/storeLifecycles.ts index 311fa76e33..a20a407171 100644 --- a/src/bonsai/storeLifecycles.ts +++ b/src/bonsai/storeLifecycles.ts @@ -26,6 +26,7 @@ import { setUpSpotTokenPriceQuery, setUpTokenMetadataQuery, } from './rest/spot'; +import { setUpTradesQuery } from './rest/trades'; import { setUpTransfersQuery } from './rest/transfers'; import { setUpAccountBalancesQuery, @@ -56,6 +57,7 @@ export const storeLifecycles = [ setUpFillsQuery, setUpUserLeverageParamsQuery, setUpOrdersQuery, + setUpTradesQuery, setUpTransfersQuery, setUpBlockTradingRewardsQuery, setUpOrderbook, diff --git a/src/bonsai/types/rawTypes.ts b/src/bonsai/types/rawTypes.ts index de5b934f63..4a89eb1d8a 100644 --- a/src/bonsai/types/rawTypes.ts +++ b/src/bonsai/types/rawTypes.ts @@ -7,6 +7,7 @@ import { import { IndexerCompositeFillObject, IndexerCompositeOrderObject, + IndexerCompositeTradeObject, IndexerTransferCommonResponseObject, IndexerWsBaseMarketObject, } from '@/types/indexer/indexerManual'; @@ -31,6 +32,7 @@ export interface ParentSubaccountData { tradingRewards?: IndexerHistoricalBlockTradingReward[]; fills?: IndexerCompositeFillObject[]; orders?: OrdersData; + trades?: IndexerCompositeTradeObject[]; transfers?: IndexerTransferCommonResponseObject[]; }; } diff --git a/src/bonsai/types/summaryTypes.ts b/src/bonsai/types/summaryTypes.ts index 1e0340d896..9e4ac8adb3 100644 --- a/src/bonsai/types/summaryTypes.ts +++ b/src/bonsai/types/summaryTypes.ts @@ -11,6 +11,7 @@ import { IndexerOrderSide, IndexerOrderType, IndexerPerpetualPositionResponseObject, + IndexerPositionSide, IndexerTransferResponseObject, } from '@/types/indexer/indexerApiGen'; import { @@ -200,6 +201,41 @@ export type SubaccountFill = Omit & { closedPnl?: number; }; +export enum TradeAction { + OPEN_LONG = 'OPEN_LONG', + OPEN_SHORT = 'OPEN_SHORT', + CLOSE_LONG = 'CLOSE_LONG', + CLOSE_SHORT = 'CLOSE_SHORT', + PARTIAL_CLOSE_LONG = 'PARTIAL_CLOSE_LONG', + PARTIAL_CLOSE_SHORT = 'PARTIAL_CLOSE_SHORT', + ADD_TO_LONG = 'ADD_TO_LONG', + ADD_TO_SHORT = 'ADD_TO_SHORT', +} + +export type SubaccountTrade = { + id: string; + marketId: string; + orderId: string; + positionId: string; + side: IndexerOrderSide | undefined; + positionSide?: IndexerPositionSide | undefined; + action: TradeAction; + price: string | undefined; + size: string | undefined; + prevSize?: string | undefined; + additionalSize?: string | undefined; + value: number | undefined; + fee: string | undefined; + closedPnl: number | undefined; + closedPnlPercent?: number | undefined; + createdAt: string | undefined; + marginMode: MarginMode; + orderType: SubaccountFillType | undefined; + leverage?: number; + entryPrice?: number; + liquidationPrice?: number; +}; + export type LiveTrade = IndexerWsTradeResponseObject; export type PendingIsolatedPosition = { diff --git a/src/constants/dialogs.ts b/src/constants/dialogs.ts index 4da41c2fd0..994f46f992 100644 --- a/src/constants/dialogs.ts +++ b/src/constants/dialogs.ts @@ -4,8 +4,6 @@ import { OrderSide } from '@/bonsai/forms/trade/types'; import { PositionUniqueId, SubaccountPosition } from '@/bonsai/types/summaryTypes'; import { TagsOf, UnionOf, ofType, unionize } from 'unionize'; -import { IndexerPositionSide } from '@/types/indexer/indexerApiGen'; - import { BigNumberish } from '@/lib/numbers'; import { Nullable } from '@/lib/typeUtils'; @@ -67,14 +65,22 @@ export type SetMarketLeverageDialogProps = { marketId: string }; export type SetupPasskeyDialogProps = { onClose: () => void }; export type ShareAffiliateDialogProps = {}; export type SharePNLAnalyticsDialogProps = { - marketId: string; assetId: string; - leverage: Nullable; - oraclePrice: Nullable; - entryPrice: Nullable; - unrealizedPnl: Nullable; - side: Nullable; - sideLabel: Nullable; + marketId?: string; + isLong: boolean; + isCross: boolean; + shareType?: 'open' | 'close' | 'liquidated' | 'partialClose' | undefined; + leverage?: Nullable; + size?: Nullable; + prevSize?: Nullable; + pnl?: Nullable; + unrealizedPnl?: Nullable; + pnlPercentage?: Nullable; + entryPrice?: Nullable; + exitPrice?: Nullable; + liquidationPrice?: Nullable; + oraclePrice?: Nullable; + sideLabel?: Nullable; }; export type SimpleUiTradeDialogProps = | { diff --git a/src/hooks/useSharePnlImage.ts b/src/hooks/useSharePnlImage.ts index e3636fbe74..1606f97ac7 100644 --- a/src/hooks/useSharePnlImage.ts +++ b/src/hooks/useSharePnlImage.ts @@ -1,76 +1,42 @@ import { logBonsaiError } from '@/bonsai/logs'; import { useQuery } from '@tanstack/react-query'; +import { SharePNLAnalyticsDialogProps } from '@/constants/dialogs'; import { timeUnits } from '@/constants/time'; -import { IndexerPerpetualPositionStatus, IndexerPositionSide } from '@/types/indexer/indexerApiGen'; import { useAccounts } from '@/hooks/useAccounts'; +import { useEndpointsConfig } from '@/hooks/useEndpointsConfig'; -import { getOpenPositions } from '@/state/accountSelectors'; -import { useAppSelector } from '@/state/appTypes'; - -import { Nullable } from '@/lib/typeUtils'; import { truncateAddress } from '@/lib/wallet'; -import { useEndpointsConfig } from './useEndpointsConfig'; - -export type SharePnlImageParams = { - assetId: string; - marketId: string; - side: Nullable; - leverage: Nullable; - oraclePrice: Nullable; - entryPrice: Nullable; - unrealizedPnl: Nullable; - type?: 'open' | 'close' | 'liquidated' | undefined; -}; - -export const useSharePnlImage = ({ - assetId, - marketId, - side, - leverage, - oraclePrice, - entryPrice, - unrealizedPnl, - type = 'open', -}: SharePnlImageParams) => { +export const useSharePnlImage = (data: SharePNLAnalyticsDialogProps) => { const { pnlImageApi } = useEndpointsConfig(); const { dydxAddress } = useAccounts(); - const openPositions = useAppSelector(getOpenPositions); - - const position = openPositions?.find((p) => p.market === marketId); - - const positionType = - position?.status === IndexerPerpetualPositionStatus.CLOSED - ? 'close' - : position?.status === IndexerPerpetualPositionStatus.LIQUIDATED - ? 'liquidated' - : 'open'; - - const pnl = (position?.realizedPnl.toNumber() ?? 0) + (unrealizedPnl ?? 0); const queryFn = async (): Promise => { - if (!dydxAddress) { + if (!dydxAddress || !data.marketId) { return undefined; } + const totalPnl = (data.pnl ?? 0) + (data.unrealizedPnl ?? 0); + const requestBody = { - ticker: assetId, - type: positionType, - leverage: leverage ?? 0, + ticker: data.assetId, + type: data.shareType ?? 'open', + leverage: data.leverage ?? 0, username: truncateAddress(dydxAddress), - isLong: side === IndexerPositionSide.LONG, - isCross: position?.marginMode === 'CROSS', - // Optional fields - include if available - size: position?.value.toNumber(), - pnl, - uPnl: unrealizedPnl ?? undefined, - pnlPercentage: position?.updatedUnrealizedPnlPercent?.toNumber(), - entryPx: entryPrice ?? undefined, - exitPx: position?.exitPrice?.toNumber(), - liquidationPx: position?.liquidationPrice?.toNumber(), - markPx: oraclePrice ?? undefined, + isLong: data.isLong, + isCross: data.isCross, + // Optional fields + size: data.size ?? undefined, + prevSize: data.prevSize ?? undefined, + pnl: totalPnl || undefined, + uPnl: data.unrealizedPnl ?? undefined, + pnlPercentage: data.pnlPercentage ?? undefined, + entryPx: data.entryPrice ?? undefined, + exitPx: data.exitPrice ?? undefined, + liquidationPx: data.liquidationPrice ?? undefined, + markPx: data.oraclePrice ?? undefined, }; const response = await fetch(pnlImageApi, { @@ -92,25 +58,26 @@ export const useSharePnlImage = ({ return useQuery({ queryKey: [ 'sharePnlImage', - marketId, + data.marketId, dydxAddress, - side, - leverage, - oraclePrice, - entryPrice, - unrealizedPnl, - type, - position?.marginMode, - position?.unsignedSize.toString(), - position?.liquidationPrice?.toString(), + data.isLong, + data.isCross, + data.shareType, + data.leverage, + data.size, + data.pnl, + data.unrealizedPnl, + data.entryPrice, + data.exitPrice, + data.oraclePrice, ], queryFn, enabled: Boolean(dydxAddress), refetchOnWindowFocus: false, refetchOnReconnect: false, - staleTime: 2 * timeUnits.minute, // 2 minutes + staleTime: 2 * timeUnits.minute, retry: 2, - retryDelay: 1 * timeUnits.second, // 1 second + retryDelay: 1 * timeUnits.second, retryOnMount: true, }); }; diff --git a/src/pages/portfolio/Portfolio.tsx b/src/pages/portfolio/Portfolio.tsx index 71d372fc94..4d03b524db 100644 --- a/src/pages/portfolio/Portfolio.tsx +++ b/src/pages/portfolio/Portfolio.tsx @@ -31,7 +31,7 @@ import { TradeHistoryList } from '@/views/Lists/Trade/TradeHistoryList'; import { AccountHistoryList } from '@/views/Lists/Transfers/AccountHistoryList'; import { FundingHistoryList } from '@/views/Lists/Transfers/FundingHistoryList'; import { VaultTransferList } from '@/views/Lists/Transfers/VaultTransferList'; -import { FillsTable, FillsTableColumnKey } from '@/views/tables/FillsTable'; +import { TradeHistoryTable, TradeHistoryTableColumnKey } from '@/views/tables/TradeHistoryTable'; import { TransferHistoryTable } from '@/views/tables/TransferHistoryTable'; import { getOnboardingState, getSubaccountFreeCollateral } from '@/state/accountSelectors'; @@ -113,26 +113,30 @@ const PortfolioPage = () => { ; fills: Loadable; orders: Loadable; + trades: Loadable; transfers: Loadable; blockTradingRewards: Loadable; }; @@ -148,6 +150,7 @@ const initialState: RawDataState = { stakingTier: loadableIdle(), fills: loadableIdle(), orders: loadableIdle(), + trades: loadableIdle(), transfers: loadableIdle(), blockTradingRewards: loadableIdle(), }, @@ -225,6 +228,12 @@ export const rawSlice = createSlice({ ) => { state.account.fills = action.payload; }, + setAccountTradesRaw: ( + state, + action: PayloadAction> + ) => { + state.account.trades = action.payload; + }, setAccountTransfersRaw: ( state, action: PayloadAction> @@ -418,6 +427,7 @@ export const { setAccountBalancesRaw, setAccountStatsRaw, setAccountFillsRaw, + setAccountTradesRaw, setAccountNobleUsdcBalanceRaw, setAccountOrdersRaw, setAccountTransfersRaw, diff --git a/src/types/indexer/indexerChecks.ts b/src/types/indexer/indexerChecks.ts index c7c5d14907..2a0ef4787a 100644 --- a/src/types/indexer/indexerChecks.ts +++ b/src/types/indexer/indexerChecks.ts @@ -24,6 +24,7 @@ import { IndexerWsParentSubaccountUpdateObject, IndexerWsPerpetualMarketResponse, IndexerWsTradesUpdateObject, + IndexerCompositeTradeResponse, } from './indexerManual'; export const isWsParentSubaccountSubscribed = @@ -57,3 +58,4 @@ export const isPerpetualMarketSparklineResponse = export const isIndexerHistoricalPnlResponse = typia.createAssert(); export const isIndexerHistoricalTradingRewardAggregationResponse = typia.createAssert(); +export const isParentSubaccountTradeResponse = typia.createAssert(); diff --git a/src/types/indexer/indexerManual.ts b/src/types/indexer/indexerManual.ts index ebea568056..6acd1bf01f 100644 --- a/src/types/indexer/indexerManual.ts +++ b/src/types/indexer/indexerManual.ts @@ -162,6 +162,43 @@ export interface IndexerCompositeFillObject { entryPriceBefore?: string | null; } +export enum IndexerTradeAction { + OPEN = 'OPEN', + CLOSE = 'CLOSE', + PARTIAL_CLOSE = 'PARTIAL_CLOSE', + EXTEND = 'EXTEND', +} + +// TODO: DWJ -- review this +export interface IndexerCompositeTradeObject { + id?: string; + marketId?: string; + orderId?: string; + positionId?: string; + side?: IndexerOrderSide; + positionSide?: IndexerPositionSide; + type?: IndexerFillType; // or a new trade-specific type enum + executionPrice?: string; + value?: string; + prevSize?: string; + additionalSize?: string; + netFee?: string; + time?: string; + // Fields the new endpoint will likely add: + action?: IndexerTradeAction; + leverage?: string; + marginMode?: string; // "CROSS" | "ISOLATED" + orderType?: string; + netRealizedPnl?: string; +} + +export interface IndexerCompositeTradeResponse { + pageSize?: number; + totalResults?: number; + offset?: number; + trades?: IndexerCompositeTradeObject[]; +} + export interface IndexerWsParentSubaccountSubscribedResponse { subaccount: IndexerParentSubaccountResponse; blockHeight: string; diff --git a/src/views/Lists/Trade/TradeHistoryList.tsx b/src/views/Lists/Trade/TradeHistoryList.tsx index 92add283d0..dddef1b833 100644 --- a/src/views/Lists/Trade/TradeHistoryList.tsx +++ b/src/views/Lists/Trade/TradeHistoryList.tsx @@ -1,9 +1,11 @@ import { useRef } from 'react'; import { BonsaiCore } from '@/bonsai/ontology'; +import { SubaccountFillType, SubaccountTrade, TradeAction } from '@/bonsai/types/summaryTypes'; import { defaultRangeExtractor, useVirtualizer } from '@tanstack/react-virtual'; import { STRING_KEYS } from '@/constants/localization'; +import { IndexerOrderSide, IndexerPositionSide } from '@/types/indexer/indexerApiGen'; import { useStringGetter } from '@/hooks/useStringGetter'; @@ -11,18 +13,182 @@ import { LoadingSpace } from '@/components/Loading/LoadingSpinner'; import { useAppSelector } from '@/state/appTypes'; -import { TradeRow } from './TradeRow'; +// import { TradeRow } from './TradeRow'; +import { TradeHistoryRow } from './TradeHistoryRow'; const FILL_HEIGHT = 64; +const MOCK_TRADES: SubaccountTrade[] = [ + // --- L → Close scenario --- + { + id: 'trade-001', + market: 'ETH-USD', + side: IndexerOrderSide.SELL, + positionSide: IndexerPositionSide.LONG, + action: TradeAction.CLOSE_LONG, + price: '5000.00', + size: '10.05', + value: 50250, + fee: '0.10', + closedPnl: -20293.09, + closedPnlPercent: -0.0404, + createdAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(), + marginMode: 'CROSS', + orderType: SubaccountFillType.MARKET, + leverage: 10, + }, + { + id: 'trade-002', + market: 'ETH-USD', + side: IndexerOrderSide.BUY, + positionSide: IndexerPositionSide.LONG, + action: TradeAction.OPEN_LONG, + price: '3000.00', + size: '10.05', + value: 30150, + fee: '0.10', + closedPnl: undefined, + closedPnlPercent: undefined, + createdAt: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(), + marginMode: 'CROSS', + orderType: SubaccountFillType.LIMIT, + leverage: 10, + }, + // --- L → S (crossing 0) scenario --- + { + id: 'trade-003', + market: 'ETH-USD', + side: IndexerOrderSide.SELL, + positionSide: IndexerPositionSide.SHORT, + action: TradeAction.OPEN_SHORT, + price: '3000.00', + size: '10.05', + value: 30150, + fee: '0.10', + closedPnl: undefined, + closedPnlPercent: undefined, + createdAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(), + marginMode: 'CROSS', + orderType: SubaccountFillType.MARKET, + leverage: 10, + }, + { + id: 'trade-004', + market: 'ETH-USD', + side: IndexerOrderSide.SELL, + positionSide: IndexerPositionSide.LONG, + action: TradeAction.CLOSE_LONG, + price: '3000.00', + size: '10.05', + value: 30150, + fee: '0.10', + closedPnl: 20293.09, + closedPnlPercent: 0.0404, + createdAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(), + marginMode: 'CROSS', + orderType: SubaccountFillType.MARKET, + leverage: 10, + }, + { + id: 'trade-005', + market: 'ETH-USD', + side: IndexerOrderSide.BUY, + positionSide: IndexerPositionSide.LONG, + action: TradeAction.OPEN_LONG, + price: '3000.00', + size: '10.05', + value: 30150, + fee: '0.10', + closedPnl: undefined, + closedPnlPercent: undefined, + createdAt: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(), + marginMode: 'CROSS', + orderType: SubaccountFillType.LIMIT, + leverage: 10, + }, + // --- L → Partial Close scenario --- + { + id: 'trade-006', + market: 'ETH-USD', + side: IndexerOrderSide.SELL, + positionSide: IndexerPositionSide.LONG, + action: TradeAction.PARTIAL_CLOSE_LONG, + price: '5000.00', + size: '5.00', + value: 25000, + fee: '0.10', + closedPnl: 5293.09, + closedPnlPercent: 0.0212, + createdAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(), + marginMode: 'CROSS', + orderType: SubaccountFillType.MARKET, + leverage: 10, + }, + { + id: 'trade-007', + market: 'ETH-USD', + side: IndexerOrderSide.BUY, + positionSide: IndexerPositionSide.LONG, + action: TradeAction.OPEN_LONG, + price: '3000.00', + size: '10.00', + value: 30000, + fee: '0.10', + closedPnl: undefined, + closedPnlPercent: undefined, + createdAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(), + marginMode: 'CROSS', + orderType: SubaccountFillType.LIMIT, + leverage: 10, + }, + + // --- Add to Long scenario --- + { + id: 'trade-008', + market: 'ETH-USD', + side: IndexerOrderSide.BUY, + positionSide: IndexerPositionSide.LONG, + action: TradeAction.ADD_TO_LONG, + price: '3000.00', + size: '5.00', + value: 15000, + fee: '0.10', + closedPnl: undefined, + closedPnlPercent: undefined, + createdAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(), + marginMode: 'CROSS', + orderType: SubaccountFillType.LIMIT, + leverage: 10, + }, + { + id: 'trade-009', + market: 'ETH-USD', + side: IndexerOrderSide.BUY, + positionSide: IndexerPositionSide.LONG, + action: TradeAction.OPEN_LONG, + price: '3000.00', + size: '10.00', + value: 30000, + fee: '0.10', + closedPnl: undefined, + closedPnlPercent: undefined, + createdAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(), + marginMode: 'CROSS', + orderType: SubaccountFillType.LIMIT, + leverage: 10, + }, +]; + export const TradeHistoryList = () => { const isLoading = useAppSelector(BonsaiCore.account.fills.loading) === 'pending'; - const fills = useAppSelector(BonsaiCore.account.fills.data); + // const fills = useAppSelector(BonsaiCore.account.fills.data); + // const trades = useAppSelector(BonsaiCore.account.trades.data); + const trades = MOCK_TRADES; const parentRef = useRef(null); const stringGetter = useStringGetter(); const rowVirtualizer = useVirtualizer({ - count: fills.length, + count: trades.length, estimateSize: (_index: number) => FILL_HEIGHT, getScrollElement: () => parentRef.current, rangeExtractor: (range) => { @@ -34,9 +200,10 @@ export const TradeHistoryList = () => { return ; } - if (fills.length === 0) { + if (trades.length === 0) { return (
+ {/* TODO: DWJ -- Replace with real trades empty state */}
{stringGetter({ key: STRING_KEYS.FILLS_EMPTY_STATE })}
); @@ -59,7 +226,10 @@ export const TradeHistoryList = () => { transform: `translateY(${virtualRow.start}px)`, }} > - + ))} diff --git a/src/views/Lists/Trade/TradeHistoryRow.tsx b/src/views/Lists/Trade/TradeHistoryRow.tsx new file mode 100644 index 0000000000..f6f43c0099 --- /dev/null +++ b/src/views/Lists/Trade/TradeHistoryRow.tsx @@ -0,0 +1,139 @@ +import { useMemo } from 'react'; + +import { BonsaiHelpers } from '@/bonsai/ontology'; +import { SubaccountTrade, TradeAction } from '@/bonsai/types/summaryTypes'; +import styled from 'styled-components'; +import tw from 'twin.macro'; + +// import { STRING_KEYS } from '@/constants/localization'; +import { IndexerOrderSide } from '@/types/indexer/indexerApiGen'; + +import { useAppSelectorWithArgs } from '@/hooks/useParameterizedSelector'; + +// import { useStringGetter } from '@/hooks/useStringGetter'; +import { AssetIcon } from '@/components/AssetIcon'; +// import { Icon, IconName } from '@/components/Icon'; +import { Output, OutputType, ShowSign } from '@/components/Output'; + +import { getAssetFromMarketId } from '@/lib/assetUtils'; +import { mapIfPresent } from '@/lib/do'; +import { orEmptyObj } from '@/lib/typeUtils'; + +import { DateContent } from '../DateContent'; + +// Maps TradeAction to a display label and color +function getActionDisplayInfo( + action: TradeAction + // stringGetter: ReturnType +) { + switch (action) { + case TradeAction.OPEN_LONG: + return { label: 'Open Long', color: 'var(--color-positive)' }; + case TradeAction.OPEN_SHORT: + return { label: 'Open Short', color: 'var(--color-negative)' }; + case TradeAction.CLOSE_LONG: + return { label: 'Close Long', color: 'var(--color-negative)' }; + case TradeAction.CLOSE_SHORT: + return { label: 'Close Short', color: 'var(--color-positive)' }; + case TradeAction.PARTIAL_CLOSE_LONG: + return { label: 'Partial Close Long', color: 'var(--color-negative)' }; + case TradeAction.PARTIAL_CLOSE_SHORT: + return { label: 'Partial Close Short', color: 'var(--color-positive)' }; + case TradeAction.ADD_TO_LONG: + return { label: 'Add to Long', color: 'var(--color-positive)' }; + case TradeAction.ADD_TO_SHORT: + return { label: 'Add to Short', color: 'var(--color-negative)' }; + default: + return { label: 'Open Long', color: 'var(--color-positive)' }; + } +} + +export const TradeHistoryRow = ({ + className, + trade, +}: { + className?: string; + trade: SubaccountTrade; +}) => { + // const stringGetter = useStringGetter(); + const { market, side, action, price, size, closedPnl, createdAt } = trade; + + const marketData = useAppSelectorWithArgs(BonsaiHelpers.markets.selectMarketSummaryById, market); + const assetInfo = useAppSelectorWithArgs( + BonsaiHelpers.assets.selectAssetInfo, + mapIfPresent(market, getAssetFromMarketId) + ); + + const { logo } = orEmptyObj(assetInfo); + const { displayableAsset, stepSizeDecimals, tickSizeDecimals } = orEmptyObj(marketData); + + const { actionLabel, actionColor, sideColor } = useMemo(() => { + const display = getActionDisplayInfo(action); // , stringGetter); + return { + actionLabel: display.label, + actionColor: display.color, + sideColor: side === IndexerOrderSide.BUY ? 'var(--color-positive)' : 'var(--color-negative)', + }; + }, [action, side]); + + const miniIcon = ( + + ); + + return ( + <$TradeHistoryRow className={className}> +
+
+ + {miniIcon} +
+
+ + {actionLabel}{' '} + {' '} + {displayableAsset} + + +
+
+ +
+
+ {closedPnl != null ? ( + 0 ? ShowSign.None : ShowSign.Negative} + withSignedValueColor + /> + ) : ( + -- + )} + + @ } + /> +
+
+ + ); +}; + +const $TradeHistoryRow = styled.div` + ${tw`row w-full justify-between gap-0.5 px-1.25`} + border-bottom: var(--default-border-width) solid var(--color-layer-3); +`; diff --git a/src/views/dialogs/SharePNLAnalyticsDialog.tsx b/src/views/dialogs/SharePNLAnalyticsDialog.tsx index 10d05f0056..86f4a8254a 100644 --- a/src/views/dialogs/SharePNLAnalyticsDialog.tsx +++ b/src/views/dialogs/SharePNLAnalyticsDialog.tsx @@ -42,31 +42,17 @@ const copyBlobToClipboard = async (blob: Blob | null) => { }; export const SharePNLAnalyticsDialog = ({ - marketId, - assetId, - side, - leverage, - oraclePrice, - entryPrice, - unrealizedPnl, setIsOpen, + ...sharePnlData }: DialogProps) => { const stringGetter = useStringGetter(); const dispatch = useAppDispatch(); - const symbol = getDisplayableAssetFromBaseAsset(assetId); + const symbol = getDisplayableAssetFromBaseAsset(sharePnlData.assetId); const [isCopying, setIsCopying] = useState(false); const [isSharing, setIsSharing] = useState(false); const [isCopied, setIsCopied] = useState(false); - const getPnlImage = useSharePnlImage({ - assetId, - marketId, - side, - leverage, - oraclePrice, - entryPrice, - unrealizedPnl, - }); + const getPnlImage = useSharePnlImage(sharePnlData); const pnlImage = useMemo(() => getPnlImage.data ?? undefined, [getPnlImage.data]); @@ -75,7 +61,7 @@ export const SharePNLAnalyticsDialog = ({ setIsCopying(true); try { await copyBlobToClipboard(pnlImage); - track(AnalyticsEvents.SharePnlCopied({ asset: assetId })); + track(AnalyticsEvents.SharePnlCopied({ asset: sharePnlData.assetId })); setIsCopying(false); setIsCopied(true); setTimeout(() => setIsCopied(false), 2000); @@ -100,7 +86,7 @@ export const SharePNLAnalyticsDialog = ({ })}\n\n#dydx #${symbol}\n[${stringGetter({ key: STRING_KEYS.TWEET_PASTE_IMAGE_AND_DELETE_THIS })}]`, related: 'dYdX', }); - track(AnalyticsEvents.SharePnlShared({ asset: assetId })); + track(AnalyticsEvents.SharePnlShared({ asset: sharePnlData.assetId })); setIsSharing(false); } catch (error) { logBonsaiError('SharePNLAnalyticsDialog/sharePnlImage', 'Failed to share PNL image', { diff --git a/src/views/tables/PositionsTable.tsx b/src/views/tables/PositionsTable.tsx index ed238c73b4..dee01289ff 100644 --- a/src/views/tables/PositionsTable.tsx +++ b/src/views/tables/PositionsTable.tsx @@ -403,6 +403,7 @@ const getPositionsTableColumnDef = ({ allowsSorting: false, hideOnBreakpoint: MediaQueryKeys.isTablet, renderCell: ({ + uniqueId, market, marketSummary, assetId, @@ -412,6 +413,7 @@ const getPositionsTableColumnDef = ({ updatedUnrealizedPnl: unrealizedPnl, }) => ( ; @@ -37,6 +45,7 @@ type ElementProps = { }; export const PositionsActionsCell = ({ + positionId, marketId, assetId, leverage, @@ -55,6 +64,8 @@ export const PositionsActionsCell = ({ const activeTradeBoxDialog = useAppSelector(getActiveTradeBoxDialog); const stringGetter = useStringGetter(); + const position = useAppSelectorWithArgs(getOpenPositionFromId, positionId); + const onCloseButtonToggle = (isPressed: boolean) => { navigate(`${AppRoute.Trade}/${marketId}`); dispatch( @@ -69,16 +80,26 @@ export const PositionsActionsCell = ({ }; const openShareDialog = () => { + const sharePnlData: SharePNLAnalyticsDialogProps = { + assetId, + marketId, + size: position?.value.toNumber() ?? 0, + isLong: side === IndexerPositionSide.LONG, + isCross: position?.marginMode === 'CROSS', + shareType: position?.status === 'OPEN' ? 'open' : 'close', + leverage: leverage?.toNumber(), + oraclePrice: oraclePrice?.toNumber(), + entryPrice: entryPrice?.toNumber(), + unrealizedPnl: unrealizedPnl?.toNumber(), + pnl: position?.realizedPnl.toNumber(), + pnlPercentage: position?.updatedUnrealizedPnlPercent?.toNumber() ?? 0, + liquidationPrice: position?.liquidationPrice?.toNumber(), + }; + dispatch( openDialog( DialogTypes.SharePNLAnalytics({ - marketId, - assetId, - leverage: leverage?.toNumber(), - oraclePrice: oraclePrice?.toNumber(), - entryPrice: entryPrice?.toNumber(), - unrealizedPnl: unrealizedPnl?.toNumber(), - side, + ...sharePnlData, sideLabel, }) ) diff --git a/src/views/tables/TradeHistoryTable.tsx b/src/views/tables/TradeHistoryTable.tsx new file mode 100644 index 0000000000..b0c1ad913a --- /dev/null +++ b/src/views/tables/TradeHistoryTable.tsx @@ -0,0 +1,655 @@ +import { forwardRef, useMemo } from 'react'; + +import { BonsaiCore } from '@/bonsai/ontology'; +import { + PerpetualMarketSummary, + SubaccountFillType, + SubaccountTrade, + TradeAction, +} from '@/bonsai/types/summaryTypes'; +import type { ColumnSize } from '@react-types/table'; +import styled, { css } from 'styled-components'; +import tw from 'twin.macro'; + +import { STRING_KEYS, type StringGetterFunction } from '@/constants/localization'; +import { NumberSign } from '@/constants/numbers'; +import { IndexerOrderSide, IndexerPositionSide } from '@/types/indexer/indexerApiGen'; + +import { useStringGetter } from '@/hooks/useStringGetter'; + +import { defaultTableMixins } from '@/styles/tableMixins'; + +import { AssetIcon } from '@/components/AssetIcon'; +import { Icon, IconName } from '@/components/Icon'; +import { Output, OutputType, ShowSign } from '@/components/Output'; +import { ColumnDef, Table } from '@/components/Table'; +// import { MarketSummaryTableCell } from '@/components/Table/MarketTableCell'; +import { TableCell } from '@/components/Table/TableCell'; +import { TableColumnHeader } from '@/components/Table/TableColumnHeader'; +import { PageSize } from '@/components/Table/TablePaginationRow'; +import { Tag } from '@/components/Tag'; + +import { useAppSelector } from '@/state/appTypes'; + +import { getMarginModeStringKey } from '@/lib/enumToStringKeyHelpers'; +import { getNumberSign, MustBigNumber } from '@/lib/numbers'; +// import { MustBigNumber } from '@/lib/numbers'; +import { Nullable, orEmptyRecord } from '@/lib/typeUtils'; + +import { TradeHistoryActionsCell } from './TradeHistoryTable/TradeHistoryActionsCell'; + +export enum TradeHistoryTableColumnKey { + Time = 'Time', + Market = 'Market', + Leverage = 'Leverage', + Type = 'Type', + Action = 'Action', + Side = 'Side', + Price = 'Price', + Size = 'Size', + Value = 'Value', + Fee = 'Fee', + ClosedPnl = 'ClosedPnl', + Actions = 'Actions', + + // Tablet (stacked) + ActionAmount = 'Action-Amount', + PriceFee = 'Price-Fee', +} + +export type TradeTableRow = { + marketSummary: Nullable; + stepSizeDecimals: number; + tickSizeDecimals: number; +} & SubaccountTrade; + +function getActionDisplayInfo(action: TradeAction) { + switch (action) { + case TradeAction.OPEN_LONG: + return { label: 'Open Long', color: 'var(--color-positive)' }; + case TradeAction.OPEN_SHORT: + return { label: 'Open Short', color: 'var(--color-negative)' }; + case TradeAction.CLOSE_LONG: + return { label: 'Close Long', color: 'var(--color-negative)' }; + case TradeAction.CLOSE_SHORT: + return { label: 'Close Short', color: 'var(--color-positive)' }; + case TradeAction.PARTIAL_CLOSE_LONG: + return { label: 'Partial Close Long', color: 'var(--color-negative)' }; + case TradeAction.PARTIAL_CLOSE_SHORT: + return { label: 'Partial Close Short', color: 'var(--color-positive)' }; + case TradeAction.ADD_TO_LONG: + return { label: 'Add to Long', color: 'var(--color-positive)' }; + case TradeAction.ADD_TO_SHORT: + return { label: 'Add to Short', color: 'var(--color-negative)' }; + default: + return { label: '', color: 'var(--color-text-0)' }; + } +} + +const getTradeHistoryTableColumnDef = ({ + key, + stringGetter, + symbol = '', + width, +}: { + key: TradeHistoryTableColumnKey; + stringGetter: StringGetterFunction; + symbol?: Nullable; + width?: ColumnSize; +}): ColumnDef => ({ + width, + ...( + { + [TradeHistoryTableColumnKey.Time]: { + columnKey: 'time', + getCellValue: (row) => row.createdAt, + label: stringGetter({ key: STRING_KEYS.TIME }), + renderCell: ({ createdAt }) => ( + + ), + }, + + [TradeHistoryTableColumnKey.Market]: { + columnKey: 'market', + getCellValue: (row) => row.marketId, + label: stringGetter({ key: STRING_KEYS.MARKET }), + renderCell: ({ marketSummary }) => ( + + } + > + {marketSummary?.displayableAsset ?? ''} + + ), + }, + + [TradeHistoryTableColumnKey.Leverage]: { + columnKey: 'leverage', + getCellValue: (row) => row.leverage, + label: stringGetter({ key: STRING_KEYS.LEVERAGE }), + renderCell: ({ leverage, positionSide }) => ( + + <$OutputSigned + sign={getNumberSign( + positionSide === IndexerPositionSide.SHORT ? -1 * (leverage ?? 0) : (leverage ?? 0) + )} + type={OutputType.Multiple} + value={leverage} + showSign={ShowSign.None} + /> + + ), + }, + + [TradeHistoryTableColumnKey.Type]: { + columnKey: 'type', + getCellValue: (row) => row.marginMode, + label: stringGetter({ key: STRING_KEYS.TYPE }), + renderCell: ({ marginMode }) => ( + + {stringGetter({ key: getMarginModeStringKey(marginMode) })} + + ), + }, + + [TradeHistoryTableColumnKey.Action]: { + columnKey: 'action', + getCellValue: (row) => row.action, + label: stringGetter({ key: STRING_KEYS.ACTION }), + renderCell: ({ action }) => { + const { label, color } = getActionDisplayInfo(action); + return {label}; + }, + }, + + [TradeHistoryTableColumnKey.Side]: { + columnKey: 'side', + getCellValue: (row) => row.side, + label: stringGetter({ key: STRING_KEYS.SIDE }), + renderCell: ({ side }) => + side != null ? ( + <$Side side={side}>{side === IndexerOrderSide.BUY ? 'Buy' : 'Sell'} + ) : null, + }, + + [TradeHistoryTableColumnKey.Price]: { + columnKey: 'price', + getCellValue: (row) => row.price, + label: stringGetter({ key: STRING_KEYS.PRICE }), + renderCell: ({ price, tickSizeDecimals }) => ( + + ), + }, + + [TradeHistoryTableColumnKey.Size]: { + columnKey: 'size', + getCellValue: (row) => row.size, + label: stringGetter({ key: STRING_KEYS.AMOUNT }), + tag: symbol, + renderCell: ({ size, stepSizeDecimals }) => ( + + ), + }, + + [TradeHistoryTableColumnKey.Value]: { + columnKey: 'total', + getCellValue: (row) => row.value, + label: stringGetter({ key: STRING_KEYS.TOTAL }), + renderCell: ({ value }) => ( + + + + ), + }, + + [TradeHistoryTableColumnKey.Fee]: { + columnKey: 'fee', + getCellValue: (row) => row.fee, + label: stringGetter({ key: STRING_KEYS.FEE }), + renderCell: ({ fee }) => ( + + + + ), + }, + + [TradeHistoryTableColumnKey.ClosedPnl]: { + columnKey: 'closedPnl', + getCellValue: (row) => row.closedPnl, + label: stringGetter({ key: STRING_KEYS.PNL }), + renderCell: ({ closedPnl, closedPnlPercent }) => ( + + {closedPnl != null ? ( + <> + + <$HighlightOutput + isNegative={MustBigNumber(closedPnlPercent).isNegative()} + type={OutputType.Percent} + value={closedPnlPercent} + showSign={ShowSign.Both} + withParentheses + withSignedValueColor + /> + + ) : ( + + )} + + ), + }, + + // --- Tablet stacked columns --- + + [TradeHistoryTableColumnKey.ActionAmount]: { + columnKey: 'actionAmount', + getCellValue: (row) => row.size, + label: ( + + {stringGetter({ key: STRING_KEYS.ACTION })} + {stringGetter({ key: STRING_KEYS.AMOUNT })} + + ), + renderCell: ({ action, size, stepSizeDecimals, marketSummary }) => { + const { label, color } = getActionDisplayInfo(action); + return ( + + } + > + {label} + + + ); + }, + }, + + [TradeHistoryTableColumnKey.PriceFee]: { + columnKey: 'priceFee', + getCellValue: (row) => row.price, + label: ( + + {stringGetter({ key: STRING_KEYS.PRICE })} + {stringGetter({ key: STRING_KEYS.FEE })} + + ), + renderCell: ({ side, price, fee, tickSizeDecimals }) => ( + + <$InlineRow> + <$Side side={side}>{side === IndexerOrderSide.BUY ? 'Buy' : 'Sell'} + @ + + + + + ), + }, + + [TradeHistoryTableColumnKey.Actions]: { + columnKey: 'actions', + isActionable: true, + allowsSorting: false, + label: stringGetter({ key: STRING_KEYS.SHARE }), + renderCell: (row) => , + }, + } satisfies Record> + )[key], +}); + +type ElementProps = { + columnKeys: TradeHistoryTableColumnKey[]; + columnWidths?: Partial>; + initialPageSize?: PageSize; +}; + +type StyleProps = { + withOuterBorder?: boolean; + withInnerBorders?: boolean; +}; + +export const TradeHistoryTable = forwardRef( + ( + { + columnKeys, + columnWidths, + initialPageSize, + withOuterBorder, + withInnerBorders = true, + }: ElementProps & StyleProps, + _ref + ) => { + const stringGetter = useStringGetter(); + + const allTrades = MOCK_TRADES; // useAppSelector(BonsaiCore.account.trades.data); + const marketSummaries = orEmptyRecord(useAppSelector(BonsaiCore.markets.markets.data)); + + const tradesData = useMemo( + () => + allTrades.map( + (trade: SubaccountTrade): TradeTableRow => ({ + ...trade, + marketSummary: marketSummaries[trade.marketId] ?? null, + stepSizeDecimals: marketSummaries[trade.marketId]?.stepSizeDecimals ?? 2, + tickSizeDecimals: marketSummaries[trade.marketId]?.tickSizeDecimals ?? 2, + }) + ), + [allTrades, marketSummaries] + ); + + return ( + <$Table + key="all-trades" + label="Trades" + tableId="trades" + data={tradesData} + getRowKey={(row: TradeTableRow) => row.id} + columns={columnKeys.map((key: TradeHistoryTableColumnKey) => + getTradeHistoryTableColumnDef({ + key, + stringGetter, + width: columnWidths?.[key], + }) + )} + slotEmpty={ + <> + +

{stringGetter({ key: STRING_KEYS.TRADES_EMPTY_STATE })}

+ + } + initialPageSize={initialPageSize} + withOuterBorder={withOuterBorder} + withInnerBorders={withInnerBorders} + withScrollSnapColumns + withScrollSnapRows + withFocusStickyRows + /> + ); + } +); + +const $Table = styled(Table)` + ${defaultTableMixins} +` as typeof Table; +const $InlineRow = tw.div`inlineRow`; +const $Side = styled.span<{ side: Nullable }>` + ${({ side }) => + side && + { + [IndexerOrderSide.BUY]: css` + color: var(--color-positive); + `, + [IndexerOrderSide.SELL]: css` + color: var(--color-negative); + `, + }[side]}; +`; + +const MOCK_TRADES: SubaccountTrade[] = [ + // --- L → Close scenario --- + { + id: 'trade-001', + marketId: 'ETH-USD', + side: IndexerOrderSide.SELL, + positionSide: IndexerPositionSide.LONG, + action: TradeAction.CLOSE_LONG, + price: '5000.00', + size: '10.05', + prevSize: '0.00', + additionalSize: '10.05', + value: 50250, + fee: '0.10', + closedPnl: 20293.09, + closedPnlPercent: 0.404, + createdAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(), + marginMode: 'CROSS', + orderType: SubaccountFillType.MARKET, + leverage: 10, + entryPrice: 3000.01, + liquidationPrice: 2000.01, + orderId: '', + positionId: '', + }, + { + id: 'trade-002', + marketId: 'ETH-USD', + side: IndexerOrderSide.BUY, + positionSide: IndexerPositionSide.LONG, + action: TradeAction.OPEN_LONG, + price: '3000.00', + size: '10.05', + prevSize: '0.00', + additionalSize: '10.05', + value: 30150, + fee: '0.10', + closedPnl: undefined, + closedPnlPercent: undefined, + createdAt: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(), + marginMode: 'CROSS', + orderType: SubaccountFillType.LIMIT, + leverage: 10, + entryPrice: 3000.01, + liquidationPrice: 2000.01, + orderId: '', + positionId: '', + }, + // --- L → S (crossing 0) scenario --- + { + id: 'trade-003', + marketId: 'ETH-USD', + side: IndexerOrderSide.SELL, + positionSide: IndexerPositionSide.SHORT, + action: TradeAction.OPEN_SHORT, + price: '3000.00', + size: '10.05', + prevSize: '0.00', + additionalSize: '10.05', + value: 30150, + fee: '0.10', + closedPnl: undefined, + closedPnlPercent: undefined, + createdAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(), + marginMode: 'CROSS', + orderType: SubaccountFillType.MARKET, + leverage: 10, + entryPrice: 3000.01, + liquidationPrice: 2000.01, + orderId: '', + positionId: '', + }, + { + id: 'trade-004', + marketId: 'ETH-USD', + side: IndexerOrderSide.SELL, + positionSide: IndexerPositionSide.LONG, + action: TradeAction.CLOSE_LONG, + price: '3000.00', + size: '10.05', + prevSize: '0.00', + additionalSize: '10.05', + value: 30150, + fee: '0.10', + closedPnl: -20293.09, + closedPnlPercent: -0.4469, + createdAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(), + marginMode: 'CROSS', + orderType: SubaccountFillType.MARKET, + leverage: 10, + entryPrice: 5000.01, + liquidationPrice: 2000.01, + orderId: '', + positionId: '', + }, + { + id: 'trade-005', + marketId: 'ETH-USD', + side: IndexerOrderSide.BUY, + positionSide: IndexerPositionSide.LONG, + action: TradeAction.OPEN_LONG, + price: '3000.00', + size: '10.05', + prevSize: '0.00', + additionalSize: '10.05', + value: 30150, + fee: '0.10', + closedPnl: undefined, + closedPnlPercent: undefined, + createdAt: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(), + marginMode: 'CROSS', + orderType: SubaccountFillType.LIMIT, + leverage: 10, + entryPrice: 3000.01, + liquidationPrice: 2000.01, + orderId: '', + positionId: '', + }, + // --- L → Partial Close scenario --- + { + id: 'trade-006', + marketId: 'ETH-USD', + side: IndexerOrderSide.SELL, + positionSide: IndexerPositionSide.LONG, + action: TradeAction.PARTIAL_CLOSE_LONG, + price: '5000.00', + size: '5.00', + prevSize: '6.00', + additionalSize: '-5.00', + value: 25000, + fee: '0.10', + closedPnl: 5293.09, + closedPnlPercent: 0.0212, + createdAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(), + marginMode: 'CROSS', + orderType: SubaccountFillType.MARKET, + leverage: 10, + entryPrice: 3000.01, + liquidationPrice: 2000.01, + orderId: '', + positionId: '', + }, + { + id: 'trade-007', + marketId: 'ETH-USD', + side: IndexerOrderSide.BUY, + positionSide: IndexerPositionSide.LONG, + action: TradeAction.OPEN_LONG, + price: '3000.00', + size: '10.00', + prevSize: '0.00', + additionalSize: '10.00', + value: 30000, + fee: '0.10', + closedPnl: undefined, + closedPnlPercent: undefined, + createdAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(), + marginMode: 'CROSS', + orderType: SubaccountFillType.LIMIT, + leverage: 10, + entryPrice: 3000.01, + liquidationPrice: 2000.01, + orderId: '', + positionId: '', + }, + + // --- Add to Long scenario --- + { + id: 'trade-008', + marketId: 'ETH-USD', + side: IndexerOrderSide.BUY, + positionSide: IndexerPositionSide.LONG, + action: TradeAction.ADD_TO_LONG, + price: '3000.00', + size: '5.00', + prevSize: '10.00', + additionalSize: '5.00', + value: 15000, + fee: '0.10', + closedPnl: undefined, + closedPnlPercent: undefined, + createdAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(), + marginMode: 'CROSS', + orderType: SubaccountFillType.LIMIT, + leverage: 10, + entryPrice: 3000.01, + liquidationPrice: 2000.01, + orderId: '', + positionId: '', + }, + { + id: 'trade-009', + marketId: 'ETH-USD', + side: IndexerOrderSide.BUY, + positionSide: IndexerPositionSide.LONG, + action: TradeAction.OPEN_LONG, + price: '3000.00', + size: '10.00', + prevSize: '0.00', + additionalSize: '10.00', + value: 30000, + fee: '0.10', + closedPnl: undefined, + closedPnlPercent: undefined, + createdAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(), + marginMode: 'CROSS', + orderType: SubaccountFillType.LIMIT, + leverage: 10, + entryPrice: 3000.01, + liquidationPrice: 2000.01, + orderId: '', + positionId: '', + }, +]; + +const $OutputSigned = styled(Output)<{ sign: NumberSign }>` + color: ${({ sign }) => + ({ + [NumberSign.Positive]: `var(--color-positive)`, + [NumberSign.Negative]: `var(--color-negative)`, + [NumberSign.Neutral]: `var(--color-text-2)`, + })[sign]}; +`; + +const $HighlightOutput = styled(Output)<{ isNegative?: boolean }>` + color: var(--color-text-1); + --secondary-item-color: currentColor; + --output-sign-color: ${({ isNegative }) => + isNegative !== undefined + ? isNegative + ? `var(--color-negative)` + : `var(--color-positive)` + : `currentColor`}; +`; diff --git a/src/views/tables/TradeHistoryTable/TradeHistoryActionsCell.tsx b/src/views/tables/TradeHistoryTable/TradeHistoryActionsCell.tsx new file mode 100644 index 0000000000..37d7ce9f82 --- /dev/null +++ b/src/views/tables/TradeHistoryTable/TradeHistoryActionsCell.tsx @@ -0,0 +1,102 @@ +import { TradeAction } from '@/bonsai/types/summaryTypes'; +import styled from 'styled-components'; + +import { ButtonShape, ButtonStyle } from '@/constants/buttons'; +import { DialogTypes, SharePNLAnalyticsDialogProps } from '@/constants/dialogs'; +import { STRING_KEYS } from '@/constants/localization'; +import { IndexerPositionSide } from '@/types/indexer/indexerApiGen'; + +import { useStringGetter } from '@/hooks/useStringGetter'; + +import { IconName } from '@/components/Icon'; +import { IconButton } from '@/components/IconButton'; +import { ActionsTableCell } from '@/components/Table/ActionsTableCell'; +import { WithTooltip } from '@/components/WithTooltip'; + +import { useAppDispatch } from '@/state/appTypes'; +import { openDialog } from '@/state/dialogs'; + +import { getIndexerPositionSideStringKey } from '@/lib/enumToStringKeyHelpers'; +import { MaybeBigNumber } from '@/lib/numbers'; + +import { TradeTableRow } from '../TradeHistoryTable'; + +type ElementProps = { + trade: TradeTableRow; + isDisabled?: boolean; +}; + +export const TradeHistoryActionsCell = ({ trade, isDisabled }: ElementProps) => { + const dispatch = useAppDispatch(); + const stringGetter = useStringGetter(); + + const sideLabel = + trade.positionSide && + stringGetter({ + key: getIndexerPositionSideStringKey(trade.positionSide), + }); + + const shareType = + trade.action === TradeAction.OPEN_LONG || trade.action === TradeAction.OPEN_SHORT + ? 'open' + : trade.action === TradeAction.PARTIAL_CLOSE_LONG || + trade.action === TradeAction.PARTIAL_CLOSE_SHORT + ? 'partialClose' + : trade.action === TradeAction.CLOSE_LONG || trade.action === TradeAction.CLOSE_SHORT + ? 'close' + : undefined; + + const sharePnlData: SharePNLAnalyticsDialogProps = { + assetId: trade.marketSummary?.assetId ?? '', + marketId: trade.marketId, + size: trade.value ?? 0, + isLong: trade.positionSide === IndexerPositionSide.LONG, + isCross: trade.marginMode === 'CROSS', + shareType, + leverage: trade.leverage, + oraclePrice: MaybeBigNumber(trade.marketSummary?.oraclePrice)?.toNumber(), + entryPrice: MaybeBigNumber(trade.entryPrice)?.toNumber(), + exitPrice: MaybeBigNumber(trade.price)?.toNumber(), + pnl: trade.closedPnl, + pnlPercentage: trade.closedPnlPercent ?? 0, + liquidationPrice: trade.liquidationPrice, + }; + + const openShareDialog = () => { + dispatch( + openDialog( + DialogTypes.SharePNLAnalytics({ + ...sharePnlData, + sideLabel: sideLabel ?? undefined, + }) + ) + ); + }; + + return ( + <$ActionsTableCell tw="mr-[-0.5rem]"> + + <$TriggersButton + key="share" + onClick={openShareDialog} + iconName={IconName.Share} + shape={ButtonShape.Square} + disabled={isDisabled} + buttonStyle={ButtonStyle.WithoutBackground} + /> + + + ); +}; + +const $ActionsTableCell = styled(ActionsTableCell)` + --toolbar-margin: 0.25rem; +`; + +const $TriggersButton = styled(IconButton)` + --button-icon-size: 1.25em; + --button-textColor: var(--color-text-0); + --button-hover-textColor: var(--color-text-1); + + --button-icon-size: 1em; +`; From efd34a6e0e51d5059232fb7db79a3d52c88071d4 Mon Sep 17 00:00:00 2001 From: dwjanus Date: Mon, 23 Feb 2026 16:26:49 -0500 Subject: [PATCH 2/6] trade history table from api --- pnpm-lock.yaml | 8 +- src/bonsai/calculators/trades.ts | 97 +++-- src/bonsai/ontology.ts | 14 +- .../rest/{trades.ts => tradeHistory.ts} | 28 +- src/bonsai/selectors/account.ts | 46 ++- src/bonsai/selectors/base.ts | 9 +- src/bonsai/storeLifecycles.ts | 4 +- src/bonsai/types/rawTypes.ts | 4 +- src/bonsai/types/summaryTypes.ts | 25 +- src/constants/dialogs.ts | 3 +- src/hooks/useSharePnlImage.ts | 5 +- src/lib/tradeHistoryHelpers.ts | 58 +++ src/pages/portfolio/Portfolio.tsx | 1 - src/state/raw.ts | 10 +- src/types/indexer/indexerChecks.ts | 5 +- src/types/indexer/indexerManual.ts | 28 +- src/views/Lists/Trade/TradeHistoryList.tsx | 190 ++-------- src/views/Lists/Trade/TradeHistoryRow.tsx | 51 +-- src/views/tables/PositionsTable.tsx | 1 - .../PositionsTable/PositionsActionsCell.tsx | 18 +- src/views/tables/TradeHistoryTable.tsx | 337 ++---------------- .../TradeHistoryActionsCell.tsx | 43 +-- 22 files changed, 328 insertions(+), 657 deletions(-) rename src/bonsai/rest/{trades.ts => tradeHistory.ts} (63%) create mode 100644 src/lib/tradeHistoryHelpers.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b91fa6b96c..97c406f13c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -30,8 +30,8 @@ dependencies: specifier: ^5.23.3 version: 5.35.1 '@dydxprotocol/v4-client-js': - specifier: 3.4.0 - version: 3.4.0 + specifier: 3.5.0 + version: 3.5.0 '@dydxprotocol/v4-localization': specifier: 1.1.390 version: 1.1.390 @@ -1671,8 +1671,8 @@ packages: '@datadog/browser-core': 5.35.1 dev: false - /@dydxprotocol/v4-client-js@3.4.0: - resolution: {integrity: sha512-dMEcqfJAreiDQjrgJyUNGqxUH3sh5J5LG5fKItCRIM7MsygAvf5/kpedQByh6nO6ov+tUw4zgT4OFc9vKN+cMQ==} + /@dydxprotocol/v4-client-js@3.5.0: + resolution: {integrity: sha512-s751Se22tax9cdWG7ZPpNgHMgrA9HHR/rZndSm/WlDHoQKc0FVmjWG5Vo1D8+tnPbA5CNyyQ8TLLN6IktmpqIQ==} dependencies: '@cosmjs/amino': 0.32.4 '@cosmjs/encoding': 0.32.4 diff --git a/src/bonsai/calculators/trades.ts b/src/bonsai/calculators/trades.ts index e773a75178..e0cfc554a4 100644 --- a/src/bonsai/calculators/trades.ts +++ b/src/bonsai/calculators/trades.ts @@ -2,8 +2,15 @@ import { keyBy, maxBy, orderBy } from 'lodash'; import { weakMapMemoize } from 'reselect'; import { EMPTY_ARR } from '@/constants/objects'; -import { IndexerOrderSide, IndexerPositionSide } from '@/types/indexer/indexerApiGen'; -import { IndexerCompositeTradeObject } from '@/types/indexer/indexerManual'; +import { + IndexerOrderSide, + IndexerOrderType, + IndexerPositionSide, +} from '@/types/indexer/indexerApiGen'; +import { + IndexerCompositeTradeHistoryObject, + IndexerTradeAction, +} from '@/types/indexer/indexerManual'; import { MustBigNumber, MustNumber } from '@/lib/numbers'; @@ -11,10 +18,10 @@ import { mergeObjects } from '../lib/mergeObjects'; import { SubaccountTrade, TradeAction } from '../types/summaryTypes'; export function calculateTrades( - restTrades: IndexerCompositeTradeObject[] | undefined, - liveTrades: IndexerCompositeTradeObject[] | undefined + restTrades: IndexerCompositeTradeHistoryObject[] | undefined, + liveTrades: IndexerCompositeTradeHistoryObject[] | undefined ): SubaccountTrade[] { - const getTradesById = (data: IndexerCompositeTradeObject[]) => + const getTradesById = (data: IndexerCompositeTradeHistoryObject[]) => keyBy(data, (trade) => trade.id ?? ''); const merged = mergeObjects( @@ -27,55 +34,77 @@ export function calculateTrades( } const calculateTrade = weakMapMemoize( - (base: IndexerCompositeTradeObject): SubaccountTrade => ({ + (base: IndexerCompositeTradeHistoryObject): SubaccountTrade => ({ id: base.id ?? '', marketId: base.marketId ?? '', - positionId: base.positionId ?? '', - orderId: base.orderId ?? '', - side: base.side, + orderId: base.orderId, + positionUniqueId: undefined, + side: base.side as IndexerOrderSide | undefined, + positionSide: base.positionSide as IndexerPositionSide | null | undefined, action: deriveTradeAction(base), - price: base.executionPrice, - size: (MustNumber(base.additionalSize ?? 0) + MustNumber(base.prevSize ?? 0)).toString(), - value: - MustNumber(base.executionPrice ?? 0) * MustNumber(base.additionalSize ?? 0) + - MustNumber(base.prevSize ?? '0'), + price: Number(base.executionPrice), + entryPrice: base.entryPrice ? Number(base.entryPrice) : undefined, + size: + Number(base.executionPrice) * + (MustNumber(base.additionalSize ?? 0) + MustNumber(base.prevSize ?? 0)), + prevSize: base.prevSize ? Number(base.prevSize) : undefined, + additionalSize: base.additionalSize ? Number(base.additionalSize) : undefined, + value: MustNumber(base.value ?? 0), fee: base.netFee, closedPnl: base.netRealizedPnl != null ? MustNumber(base.netRealizedPnl) : undefined, - closedPnlPercent: - base.netRealizedPnl != null - ? MustNumber(base.netRealizedPnl) / - (Math.abs(MustNumber(base.additionalSize ?? 0)) * MustNumber(base.executionPrice ?? 0)) - : undefined, + closedPnlPercent: derivePerTradePnlPercent(base), + netClosedPnlPercent: + base.netRealizedPnlPercent != null ? MustNumber(base.netRealizedPnlPercent) : undefined, createdAt: base.time, marginMode: base.marginMode === 'ISOLATED' ? 'ISOLATED' : 'CROSS', - orderType: undefined, // map from base.orderType when API contract is known + orderType: base.orderType as IndexerOrderType | undefined, + subaccountNumber: base.subaccountNumber, }) ); -function deriveTradeAction(trade: IndexerCompositeTradeObject): TradeAction { - // If the API sends `action` directly, use it: - // if (trade.action) return trade.action as TradeAction; - - // Otherwise derive from position context (same logic you had): - const positionSizeBefore = MustNumber(trade.prevSize ?? '0'); - const tradeSize = MustNumber(trade.additionalSize ?? 0) + MustNumber(trade.prevSize ?? 0); +function deriveTradeAction(trade: IndexerCompositeTradeHistoryObject): TradeAction { const isLong = trade.positionSide === IndexerPositionSide.LONG || (trade.side === IndexerOrderSide.BUY && trade.action === 'OPEN'); const isExtend = trade.action === 'EXTEND'; const isBuy = trade.side === IndexerOrderSide.BUY; + const isLiquidated = + trade.action === 'LIQUIDATION_CLOSE' || trade.action === 'LIQUIDATION_PARTIAL_CLOSE'; + + if (isLiquidated) { + return TradeAction.LIQUIDATION; + } - if (positionSizeBefore === 0) { - return isBuy ? TradeAction.OPEN_LONG : TradeAction.OPEN_SHORT; + if (trade.action === IndexerTradeAction.PARTIAL_CLOSE) { + return isBuy ? TradeAction.PARTIAL_CLOSE_SHORT : TradeAction.PARTIAL_CLOSE_LONG; } - if ((isExtend && isBuy) || (isExtend && !isBuy)) { + if (isExtend) { return isLong ? TradeAction.ADD_TO_LONG : TradeAction.ADD_TO_SHORT; } - const isFullClose = tradeSize >= positionSizeBefore; - if (isLong) { - return isFullClose ? TradeAction.CLOSE_LONG : TradeAction.PARTIAL_CLOSE_LONG; + if (trade.action === IndexerTradeAction.CLOSE) { + return trade.side === IndexerOrderSide.SELL || isLong + ? TradeAction.CLOSE_LONG + : TradeAction.CLOSE_SHORT; + } + + return trade.action === IndexerTradeAction.OPEN && (trade.side === IndexerOrderSide.BUY || isLong) + ? TradeAction.OPEN_LONG + : TradeAction.OPEN_SHORT; +} + +function derivePerTradePnlPercent(trade: IndexerCompositeTradeHistoryObject): number | undefined { + if (!trade.entryPrice || !trade.executionPrice) { + return undefined; } - return isFullClose ? TradeAction.CLOSE_SHORT : TradeAction.PARTIAL_CLOSE_SHORT; + + const entryPrice = MustNumber(trade.entryPrice); + const executionPrice = MustNumber(trade.executionPrice); + const pnl = + trade.positionSide === IndexerPositionSide.LONG + ? executionPrice - entryPrice + : entryPrice - executionPrice; + const pnlPercent = pnl / entryPrice; + return pnlPercent; } diff --git a/src/bonsai/ontology.ts b/src/bonsai/ontology.ts index 30863cd52c..3656d08bd5 100644 --- a/src/bonsai/ontology.ts +++ b/src/bonsai/ontology.ts @@ -48,8 +48,8 @@ import { selectAccountOrders, selectAccountOrdersLoading, selectAccountStakingTier, - selectAccountTrades, - selectAccountTradesLoading, + selectAccountTradesWithLeverage, + selectAccountTradeHistoryLoading, selectAccountTransfers, selectAccountTransfersLoading, selectChildSubaccountSummaries, @@ -191,7 +191,7 @@ interface BonsaiCoreShape { data: BasicSelector; loading: BasicSelector; }; - trades: { + tradeHistory: { data: BasicSelector; loading: BasicSelector; }; @@ -303,9 +303,9 @@ export const BonsaiCore: BonsaiCoreShape = { data: selectAccountFills, loading: selectAccountFillsLoading, }, - trades: { - data: selectAccountTrades, - loading: selectAccountTradesLoading, + tradeHistory: { + data: selectAccountTradesWithLeverage, + loading: selectAccountTradeHistoryLoading, }, transfers: { data: selectAccountTransfers, @@ -421,6 +421,7 @@ interface BonsaiHelpersShape { openOrders: BasicSelector; orderHistory: BasicSelector; fills: BasicSelector; + tradeHistory: BasicSelector; }; orderbook: { selectGroupedData: BasicSelector< @@ -491,6 +492,7 @@ export const BonsaiHelpers: BonsaiHelpersShape = { openOrders: selectCurrentMarketOpenOrders, orderHistory: selectCurrentMarketOrderHistory, fills: getCurrentMarketAccountFills, + tradeHistory: selectAccountTradesWithLeverage, }, }, assets: { diff --git a/src/bonsai/rest/trades.ts b/src/bonsai/rest/tradeHistory.ts similarity index 63% rename from src/bonsai/rest/trades.ts rename to src/bonsai/rest/tradeHistory.ts index 490610c813..971ed1d289 100644 --- a/src/bonsai/rest/trades.ts +++ b/src/bonsai/rest/tradeHistory.ts @@ -1,4 +1,4 @@ -import { isParentSubaccountTradeResponse } from '@/types/indexer/indexerChecks'; +import { isParentSubaccountTradeHistoryResponse } from '@/types/indexer/indexerChecks'; import { type RootStore } from '@/state/_store'; import { setAccountTradesRaw } from '@/state/raw'; @@ -12,25 +12,33 @@ import { selectParentSubaccountInfo } from '../socketSelectors'; import { createIndexerQueryStoreEffect } from './lib/indexerQueryStoreEffect'; import { queryResultToLoadable } from './lib/queryResultToLoadable'; -export function setUpTradesQuery(store: RootStore) { - const cleanupListener = refreshIndexerQueryOnAccountSocketRefresh(['account', 'trades']); +export function setUpTradeHistoryQuery(store: RootStore) { + const cleanupListener = refreshIndexerQueryOnAccountSocketRefresh(['account', 'tradeHistory']); const cleanupEffect = createIndexerQueryStoreEffect(store, { - name: 'trades', + name: 'tradeHistory', selector: selectParentSubaccountInfo, - getQueryKey: (data) => ['account', 'trades', data.wallet, data.subaccount], + getQueryKey: (data) => ['account', 'tradeHistory', data.wallet, data.subaccount], getQueryFn: (indexerClient, data) => { if (!isTruthy(data.wallet) || data.subaccount == null || data.isPerpsGeoRestricted) { return null; } - // TODO: DWJ -- Replace with real trades endpoint when available - // e.g. indexerClient.account.getParentSubaccountNumberTrades(data.wallet!, data.subaccount!) return () => - indexerClient.account.getParentSubaccountNumberFills(data.wallet!, data.subaccount!); + indexerClient.account.getParentSubaccountNumberTradeHistory( + data.wallet!, + data.subaccount!, + undefined, + undefined, + undefined, + undefined + ); }, - onResult: (trades) => { + onResult: (tradeHistory) => { store.dispatch( setAccountTradesRaw( - mapLoadableData(queryResultToLoadable(trades), isParentSubaccountTradeResponse) + mapLoadableData( + queryResultToLoadable(tradeHistory), + isParentSubaccountTradeHistoryResponse + ) ) ); }, diff --git a/src/bonsai/selectors/account.ts b/src/bonsai/selectors/account.ts index 410b7c38c0..693955c877 100644 --- a/src/bonsai/selectors/account.ts +++ b/src/bonsai/selectors/account.ts @@ -1,5 +1,5 @@ import { NOBLE_BECH32_PREFIX } from '@dydxprotocol/v4-client-js'; -import { isEqual, orderBy, pick } from 'lodash'; +import { isEqual, keyBy, orderBy, pick } from 'lodash'; import { shallowEqual } from 'react-redux'; import { EMPTY_ARR } from '@/constants/objects'; @@ -9,7 +9,7 @@ import { createAppSelector } from '@/state/appTypes'; import { getCurrentMarketIdIfTradeable } from '@/state/currentMarketSelectors'; import { convertBech32Address } from '@/lib/addressUtils'; -import { BIG_NUMBERS } from '@/lib/numbers'; +import { BIG_NUMBERS, MaybeBigNumber } from '@/lib/numbers'; import { calculateBlockRewards } from '../calculators/blockRewards'; import { calculateFills } from '../calculators/fills'; @@ -31,7 +31,7 @@ import { calculateTransfers } from '../calculators/transfers'; import { calculateAccountStakingTier } from '../calculators/userStats'; import { mergeLoadableStatus } from '../lib/mapLoadable'; import { selectParentSubaccountInfo } from '../socketSelectors'; -import { SubaccountTransfer } from '../types/summaryTypes'; +import { SubaccountTrade, SubaccountTransfer } from '../types/summaryTypes'; import { selectLatestIndexerHeight, selectLatestValidatorHeight } from './apiStatus'; import { selectRawAccountStakingTierData, @@ -51,9 +51,9 @@ import { selectRawParentSubaccountData, selectRawSelectedMarketLeverages, selectRawSelectedMarketLeveragesData, - selectRawTradesLiveData, - selectRawTradesRest, - selectRawTradesRestData, + selectRawTradeHistoryLiveData, + selectRawTradeHistoryRest, + selectRawTradeHistoryRestData, selectRawTransfersLiveData, selectRawTransfersRest, selectRawTransfersRestData, @@ -311,14 +311,38 @@ export const selectAccountStakingTier = createAppSelector( (stakingTier) => calculateAccountStakingTier(stakingTier) ); -export const selectAccountTrades = createAppSelector( - [selectRawTradesRestData, selectRawTradesLiveData], +export const selectAccountTradeHistory = createAppSelector( + [selectRawTradeHistoryRestData, selectRawTradeHistoryLiveData], (rest, live) => { - return calculateTrades(rest?.trades, live); + return calculateTrades(rest?.tradeHistory, live); } ); -export const selectAccountTradesLoading = createAppSelector( - [selectRawTradesRest, selectRawParentSubaccount], +export const selectAccountTradeHistoryLoading = createAppSelector( + [selectRawTradeHistoryRest, selectRawParentSubaccount], mergeLoadableStatus ); + +export const selectAccountTradesWithLeverage = createAppSelector( + [selectAccountTradeHistory, selectAccountOrders, selectParentSubaccountPositions], + (trades, orders, positions): SubaccountTrade[] => { + const orderById = keyBy(orders, (o) => o.id); + const positionByUniqueId = keyBy(positions ?? [], (p) => p.uniqueId); + + return trades.map((trade) => { + const order = trade.orderId ? orderById[trade.orderId] : undefined; + const position = order ? positionByUniqueId[order.positionUniqueId] : undefined; + + if (!position) { + return trade; + } + + return { + ...trade, + positionUniqueId: position.uniqueId, + leverage: MaybeBigNumber(position.effectiveSelectedLeverage)?.toNumber(), + liquidationPrice: MaybeBigNumber(position.liquidationPrice)?.toNumber(), + }; + }); + } +); diff --git a/src/bonsai/selectors/base.ts b/src/bonsai/selectors/base.ts index 40bb307aef..75f133d0b6 100644 --- a/src/bonsai/selectors/base.ts +++ b/src/bonsai/selectors/base.ts @@ -20,7 +20,8 @@ export const selectRawParentSubaccountData = (state: RootState) => export const selectRawFillsRestData = (state: RootState) => state.raw.account.fills.data; export const selectRawOrdersRestData = (state: RootState) => state.raw.account.orders.data; -export const selectRawTradesRestData = (state: RootState) => state.raw.account.trades.data; +export const selectRawTradeHistoryRestData = (state: RootState) => + state.raw.account.tradeHistory.data; export const selectRawTransfersRestData = (state: RootState) => state.raw.account.transfers.data; export const selectRawBlockTradingRewardsRestData = (state: RootState) => state.raw.account.blockTradingRewards.data; @@ -29,8 +30,8 @@ export const selectRawFillsLiveData = (state: RootState) => state.raw.account.parentSubaccount.data?.live.fills; export const selectRawOrdersLiveData = (state: RootState) => state.raw.account.parentSubaccount.data?.live.orders; -export const selectRawTradesLiveData = (state: RootState) => - state.raw.account.parentSubaccount.data?.live.trades; +export const selectRawTradeHistoryLiveData = (state: RootState) => + state.raw.account.parentSubaccount.data?.live.tradeHistory; export const selectRawTransfersLiveData = (state: RootState) => state.raw.account.parentSubaccount.data?.live.transfers; export const selectRawBlockTradingRewardsLiveData = (state: RootState) => @@ -49,7 +50,7 @@ export const selectRawValidatorHeightDataLoadable = (state: RootState) => export const selectRawFillsRest = (state: RootState) => state.raw.account.fills; export const selectRawOrdersRest = (state: RootState) => state.raw.account.orders; -export const selectRawTradesRest = (state: RootState) => state.raw.account.trades; +export const selectRawTradeHistoryRest = (state: RootState) => state.raw.account.tradeHistory; export const selectRawTransfersRest = (state: RootState) => state.raw.account.transfers; export const selectRawBlockTradingRewardsRest = (state: RootState) => state.raw.account.blockTradingRewards; diff --git a/src/bonsai/storeLifecycles.ts b/src/bonsai/storeLifecycles.ts index a20a407171..d0a9fa7a78 100644 --- a/src/bonsai/storeLifecycles.ts +++ b/src/bonsai/storeLifecycles.ts @@ -26,7 +26,7 @@ import { setUpSpotTokenPriceQuery, setUpTokenMetadataQuery, } from './rest/spot'; -import { setUpTradesQuery } from './rest/trades'; +import { setUpTradeHistoryQuery } from './rest/tradeHistory'; import { setUpTransfersQuery } from './rest/transfers'; import { setUpAccountBalancesQuery, @@ -57,7 +57,7 @@ export const storeLifecycles = [ setUpFillsQuery, setUpUserLeverageParamsQuery, setUpOrdersQuery, - setUpTradesQuery, + setUpTradeHistoryQuery, setUpTransfersQuery, setUpBlockTradingRewardsQuery, setUpOrderbook, diff --git a/src/bonsai/types/rawTypes.ts b/src/bonsai/types/rawTypes.ts index 4a89eb1d8a..be64144519 100644 --- a/src/bonsai/types/rawTypes.ts +++ b/src/bonsai/types/rawTypes.ts @@ -7,7 +7,7 @@ import { import { IndexerCompositeFillObject, IndexerCompositeOrderObject, - IndexerCompositeTradeObject, + IndexerCompositeTradeHistoryObject, IndexerTransferCommonResponseObject, IndexerWsBaseMarketObject, } from '@/types/indexer/indexerManual'; @@ -32,7 +32,7 @@ export interface ParentSubaccountData { tradingRewards?: IndexerHistoricalBlockTradingReward[]; fills?: IndexerCompositeFillObject[]; orders?: OrdersData; - trades?: IndexerCompositeTradeObject[]; + tradeHistory?: IndexerCompositeTradeHistoryObject[]; transfers?: IndexerTransferCommonResponseObject[]; }; } diff --git a/src/bonsai/types/summaryTypes.ts b/src/bonsai/types/summaryTypes.ts index 9e4ac8adb3..ab157fb485 100644 --- a/src/bonsai/types/summaryTypes.ts +++ b/src/bonsai/types/summaryTypes.ts @@ -210,30 +210,33 @@ export enum TradeAction { PARTIAL_CLOSE_SHORT = 'PARTIAL_CLOSE_SHORT', ADD_TO_LONG = 'ADD_TO_LONG', ADD_TO_SHORT = 'ADD_TO_SHORT', + LIQUIDATION = 'LIQUIDATION', } export type SubaccountTrade = { id: string; marketId: string; - orderId: string; - positionId: string; - side: IndexerOrderSide | undefined; - positionSide?: IndexerPositionSide | undefined; + orderId: string | undefined; + positionUniqueId: PositionUniqueId | undefined; + side?: IndexerOrderSide | undefined; + positionSide?: IndexerPositionSide | undefined | null; action: TradeAction; - price: string | undefined; - size: string | undefined; - prevSize?: string | undefined; - additionalSize?: string | undefined; + price: number | undefined; + entryPrice?: number | undefined; + size?: number | undefined; + prevSize?: number | undefined; + additionalSize?: number | undefined; value: number | undefined; fee: string | undefined; - closedPnl: number | undefined; + closedPnl?: number | undefined; closedPnlPercent?: number | undefined; + netClosedPnlPercent?: number | undefined; createdAt: string | undefined; marginMode: MarginMode; - orderType: SubaccountFillType | undefined; + orderType: IndexerOrderType | undefined; leverage?: number; - entryPrice?: number; liquidationPrice?: number; + subaccountNumber?: number; }; export type LiveTrade = IndexerWsTradeResponseObject; diff --git a/src/constants/dialogs.ts b/src/constants/dialogs.ts index 994f46f992..ccaaec6859 100644 --- a/src/constants/dialogs.ts +++ b/src/constants/dialogs.ts @@ -69,7 +69,7 @@ export type SharePNLAnalyticsDialogProps = { marketId?: string; isLong: boolean; isCross: boolean; - shareType?: 'open' | 'close' | 'liquidated' | 'partialClose' | undefined; + shareType?: 'open' | 'close' | 'liquidated' | 'partialClose' | 'extend' | undefined; leverage?: Nullable; size?: Nullable; prevSize?: Nullable; @@ -81,6 +81,7 @@ export type SharePNLAnalyticsDialogProps = { liquidationPrice?: Nullable; oraclePrice?: Nullable; sideLabel?: Nullable; + closeType?: Nullable; }; export type SimpleUiTradeDialogProps = | { diff --git a/src/hooks/useSharePnlImage.ts b/src/hooks/useSharePnlImage.ts index 1606f97ac7..fe21d1508c 100644 --- a/src/hooks/useSharePnlImage.ts +++ b/src/hooks/useSharePnlImage.ts @@ -22,8 +22,8 @@ export const useSharePnlImage = (data: SharePNLAnalyticsDialogProps) => { const requestBody = { ticker: data.assetId, - type: data.shareType ?? 'open', - leverage: data.leverage ?? 0, + type: data.shareType, + leverage: data.leverage, username: truncateAddress(dydxAddress), isLong: data.isLong, isCross: data.isCross, @@ -37,6 +37,7 @@ export const useSharePnlImage = (data: SharePNLAnalyticsDialogProps) => { exitPx: data.exitPrice ?? undefined, liquidationPx: data.liquidationPrice ?? undefined, markPx: data.oraclePrice ?? undefined, + closeType: data.closeType ?? undefined, }; const response = await fetch(pnlImageApi, { diff --git a/src/lib/tradeHistoryHelpers.ts b/src/lib/tradeHistoryHelpers.ts new file mode 100644 index 0000000000..2afdc06d8e --- /dev/null +++ b/src/lib/tradeHistoryHelpers.ts @@ -0,0 +1,58 @@ +import { TradeAction } from '@/bonsai/types/summaryTypes'; + +import { STRING_KEYS } from '@/constants/localization'; + +import type { useStringGetter } from '@/hooks/useStringGetter'; + +export function getTradeActionDisplayInfo( + action: TradeAction, + stringGetter: ReturnType +) { + switch (action) { + case TradeAction.OPEN_LONG: + return { + label: stringGetter({ key: STRING_KEYS.OPEN_LONG }), + color: 'var(--color-positive)', + }; + case TradeAction.OPEN_SHORT: + return { + label: stringGetter({ key: STRING_KEYS.OPEN_SHORT }), + color: 'var(--color-negative)', + }; + case TradeAction.CLOSE_LONG: + return { + label: stringGetter({ key: STRING_KEYS.CLOSE_LONG }), + color: 'var(--color-negative)', + }; + case TradeAction.CLOSE_SHORT: + return { + label: stringGetter({ key: STRING_KEYS.CLOSE_SHORT }), + color: 'var(--color-negative)', + }; + case TradeAction.PARTIAL_CLOSE_LONG: + return { + label: stringGetter({ key: STRING_KEYS.PARTIAL_CLOSE_LONG }), + color: 'var(--color-negative)', + }; + case TradeAction.PARTIAL_CLOSE_SHORT: + return { + label: stringGetter({ key: STRING_KEYS.PARTIAL_CLOSE_SHORT }), + color: 'var(--color-negative)', + }; + case TradeAction.ADD_TO_LONG: + return { + label: stringGetter({ key: STRING_KEYS.ADD_TO_LONG }), + color: 'var(--color-positive)', + }; + case TradeAction.ADD_TO_SHORT: + return { + label: stringGetter({ key: STRING_KEYS.ADD_TO_SHORT }), + color: 'var(--color-negative)', + }; + default: + return { + label: stringGetter({ key: STRING_KEYS.OPEN_LONG }), + color: 'var(--color-positive)', + }; + } +} diff --git a/src/pages/portfolio/Portfolio.tsx b/src/pages/portfolio/Portfolio.tsx index 4d03b524db..c5f8f55179 100644 --- a/src/pages/portfolio/Portfolio.tsx +++ b/src/pages/portfolio/Portfolio.tsx @@ -127,7 +127,6 @@ const PortfolioPage = () => { ] : [ TradeHistoryTableColumnKey.Market, - TradeHistoryTableColumnKey.Leverage, TradeHistoryTableColumnKey.Type, TradeHistoryTableColumnKey.Action, TradeHistoryTableColumnKey.Price, diff --git a/src/state/raw.ts b/src/state/raw.ts index 8fc27e3255..e0e170b1bf 100644 --- a/src/state/raw.ts +++ b/src/state/raw.ts @@ -31,7 +31,7 @@ import { } from '@/types/indexer/indexerApiGen'; import { IndexerCompositeFillResponse, - IndexerCompositeTradeResponse, + IndexerCompositeTradeHistoryResponse, IndexerSparklineResponseObject, } from '@/types/indexer/indexerManual'; @@ -105,7 +105,7 @@ export interface RawDataState { parentSubaccount: Loadable; fills: Loadable; orders: Loadable; - trades: Loadable; + tradeHistory: Loadable; transfers: Loadable; blockTradingRewards: Loadable; }; @@ -150,7 +150,7 @@ const initialState: RawDataState = { stakingTier: loadableIdle(), fills: loadableIdle(), orders: loadableIdle(), - trades: loadableIdle(), + tradeHistory: loadableIdle(), transfers: loadableIdle(), blockTradingRewards: loadableIdle(), }, @@ -230,9 +230,9 @@ export const rawSlice = createSlice({ }, setAccountTradesRaw: ( state, - action: PayloadAction> + action: PayloadAction> ) => { - state.account.trades = action.payload; + state.account.tradeHistory = action.payload; }, setAccountTransfersRaw: ( state, diff --git a/src/types/indexer/indexerChecks.ts b/src/types/indexer/indexerChecks.ts index 2a0ef4787a..37e273a464 100644 --- a/src/types/indexer/indexerChecks.ts +++ b/src/types/indexer/indexerChecks.ts @@ -24,7 +24,7 @@ import { IndexerWsParentSubaccountUpdateObject, IndexerWsPerpetualMarketResponse, IndexerWsTradesUpdateObject, - IndexerCompositeTradeResponse, + IndexerCompositeTradeHistoryResponse, } from './indexerManual'; export const isWsParentSubaccountSubscribed = @@ -58,4 +58,5 @@ export const isPerpetualMarketSparklineResponse = export const isIndexerHistoricalPnlResponse = typia.createAssert(); export const isIndexerHistoricalTradingRewardAggregationResponse = typia.createAssert(); -export const isParentSubaccountTradeResponse = typia.createAssert(); +export const isParentSubaccountTradeHistoryResponse = + typia.createAssert(); diff --git a/src/types/indexer/indexerManual.ts b/src/types/indexer/indexerManual.ts index 6acd1bf01f..91bb26bd53 100644 --- a/src/types/indexer/indexerManual.ts +++ b/src/types/indexer/indexerManual.ts @@ -167,36 +167,36 @@ export enum IndexerTradeAction { CLOSE = 'CLOSE', PARTIAL_CLOSE = 'PARTIAL_CLOSE', EXTEND = 'EXTEND', + LIQUIDATION_CLOSE = 'LIQUIDATION_CLOSE', + LIQUIDATION_PARTIAL_CLOSE = 'LIQUIDATION_PARTIAL_CLOSE', } -// TODO: DWJ -- review this -export interface IndexerCompositeTradeObject { +export interface IndexerCompositeTradeHistoryObject { id?: string; marketId?: string; orderId?: string; - positionId?: string; - side?: IndexerOrderSide; - positionSide?: IndexerPositionSide; - type?: IndexerFillType; // or a new trade-specific type enum + side?: string | IndexerOrderSide; + positionSide?: string | IndexerPositionSide | null; + entryPrice?: string; executionPrice?: string; value?: string; prevSize?: string; additionalSize?: string; netFee?: string; time?: string; - // Fields the new endpoint will likely add: - action?: IndexerTradeAction; - leverage?: string; - marginMode?: string; // "CROSS" | "ISOLATED" - orderType?: string; - netRealizedPnl?: string; + action?: string | IndexerTradeAction; + marginMode?: string; + orderType?: string | IndexerOrderType; + netRealizedPnl?: string | null; + netRealizedPnlPercent?: string | null; + subaccountNumber?: number; } -export interface IndexerCompositeTradeResponse { +export interface IndexerCompositeTradeHistoryResponse { pageSize?: number; totalResults?: number; offset?: number; - trades?: IndexerCompositeTradeObject[]; + tradeHistory?: IndexerCompositeTradeHistoryObject[]; } export interface IndexerWsParentSubaccountSubscribedResponse { diff --git a/src/views/Lists/Trade/TradeHistoryList.tsx b/src/views/Lists/Trade/TradeHistoryList.tsx index dddef1b833..8d9104bd8a 100644 --- a/src/views/Lists/Trade/TradeHistoryList.tsx +++ b/src/views/Lists/Trade/TradeHistoryList.tsx @@ -1,11 +1,9 @@ import { useRef } from 'react'; import { BonsaiCore } from '@/bonsai/ontology'; -import { SubaccountFillType, SubaccountTrade, TradeAction } from '@/bonsai/types/summaryTypes'; import { defaultRangeExtractor, useVirtualizer } from '@tanstack/react-virtual'; import { STRING_KEYS } from '@/constants/localization'; -import { IndexerOrderSide, IndexerPositionSide } from '@/types/indexer/indexerApiGen'; import { useStringGetter } from '@/hooks/useStringGetter'; @@ -13,180 +11,19 @@ import { LoadingSpace } from '@/components/Loading/LoadingSpinner'; import { useAppSelector } from '@/state/appTypes'; -// import { TradeRow } from './TradeRow'; import { TradeHistoryRow } from './TradeHistoryRow'; const FILL_HEIGHT = 64; -const MOCK_TRADES: SubaccountTrade[] = [ - // --- L → Close scenario --- - { - id: 'trade-001', - market: 'ETH-USD', - side: IndexerOrderSide.SELL, - positionSide: IndexerPositionSide.LONG, - action: TradeAction.CLOSE_LONG, - price: '5000.00', - size: '10.05', - value: 50250, - fee: '0.10', - closedPnl: -20293.09, - closedPnlPercent: -0.0404, - createdAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(), - marginMode: 'CROSS', - orderType: SubaccountFillType.MARKET, - leverage: 10, - }, - { - id: 'trade-002', - market: 'ETH-USD', - side: IndexerOrderSide.BUY, - positionSide: IndexerPositionSide.LONG, - action: TradeAction.OPEN_LONG, - price: '3000.00', - size: '10.05', - value: 30150, - fee: '0.10', - closedPnl: undefined, - closedPnlPercent: undefined, - createdAt: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(), - marginMode: 'CROSS', - orderType: SubaccountFillType.LIMIT, - leverage: 10, - }, - // --- L → S (crossing 0) scenario --- - { - id: 'trade-003', - market: 'ETH-USD', - side: IndexerOrderSide.SELL, - positionSide: IndexerPositionSide.SHORT, - action: TradeAction.OPEN_SHORT, - price: '3000.00', - size: '10.05', - value: 30150, - fee: '0.10', - closedPnl: undefined, - closedPnlPercent: undefined, - createdAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(), - marginMode: 'CROSS', - orderType: SubaccountFillType.MARKET, - leverage: 10, - }, - { - id: 'trade-004', - market: 'ETH-USD', - side: IndexerOrderSide.SELL, - positionSide: IndexerPositionSide.LONG, - action: TradeAction.CLOSE_LONG, - price: '3000.00', - size: '10.05', - value: 30150, - fee: '0.10', - closedPnl: 20293.09, - closedPnlPercent: 0.0404, - createdAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(), - marginMode: 'CROSS', - orderType: SubaccountFillType.MARKET, - leverage: 10, - }, - { - id: 'trade-005', - market: 'ETH-USD', - side: IndexerOrderSide.BUY, - positionSide: IndexerPositionSide.LONG, - action: TradeAction.OPEN_LONG, - price: '3000.00', - size: '10.05', - value: 30150, - fee: '0.10', - closedPnl: undefined, - closedPnlPercent: undefined, - createdAt: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(), - marginMode: 'CROSS', - orderType: SubaccountFillType.LIMIT, - leverage: 10, - }, - // --- L → Partial Close scenario --- - { - id: 'trade-006', - market: 'ETH-USD', - side: IndexerOrderSide.SELL, - positionSide: IndexerPositionSide.LONG, - action: TradeAction.PARTIAL_CLOSE_LONG, - price: '5000.00', - size: '5.00', - value: 25000, - fee: '0.10', - closedPnl: 5293.09, - closedPnlPercent: 0.0212, - createdAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(), - marginMode: 'CROSS', - orderType: SubaccountFillType.MARKET, - leverage: 10, - }, - { - id: 'trade-007', - market: 'ETH-USD', - side: IndexerOrderSide.BUY, - positionSide: IndexerPositionSide.LONG, - action: TradeAction.OPEN_LONG, - price: '3000.00', - size: '10.00', - value: 30000, - fee: '0.10', - closedPnl: undefined, - closedPnlPercent: undefined, - createdAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(), - marginMode: 'CROSS', - orderType: SubaccountFillType.LIMIT, - leverage: 10, - }, - - // --- Add to Long scenario --- - { - id: 'trade-008', - market: 'ETH-USD', - side: IndexerOrderSide.BUY, - positionSide: IndexerPositionSide.LONG, - action: TradeAction.ADD_TO_LONG, - price: '3000.00', - size: '5.00', - value: 15000, - fee: '0.10', - closedPnl: undefined, - closedPnlPercent: undefined, - createdAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(), - marginMode: 'CROSS', - orderType: SubaccountFillType.LIMIT, - leverage: 10, - }, - { - id: 'trade-009', - market: 'ETH-USD', - side: IndexerOrderSide.BUY, - positionSide: IndexerPositionSide.LONG, - action: TradeAction.OPEN_LONG, - price: '3000.00', - size: '10.00', - value: 30000, - fee: '0.10', - closedPnl: undefined, - closedPnlPercent: undefined, - createdAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(), - marginMode: 'CROSS', - orderType: SubaccountFillType.LIMIT, - leverage: 10, - }, -]; - export const TradeHistoryList = () => { - const isLoading = useAppSelector(BonsaiCore.account.fills.loading) === 'pending'; - // const fills = useAppSelector(BonsaiCore.account.fills.data); - // const trades = useAppSelector(BonsaiCore.account.trades.data); - const trades = MOCK_TRADES; + const loadingStatus = useAppSelector(BonsaiCore.account.tradeHistory.loading); + const trades = useAppSelector(BonsaiCore.account.tradeHistory.data); const parentRef = useRef(null); const stringGetter = useStringGetter(); + const isLoading = loadingStatus === 'pending'; + const isError = loadingStatus === 'error'; + const rowVirtualizer = useVirtualizer({ count: trades.length, estimateSize: (_index: number) => FILL_HEIGHT, @@ -196,6 +33,19 @@ export const TradeHistoryList = () => { }, }); + if (isError) { + return ( +
+
+ {stringGetter({ + key: STRING_KEYS.SOMETHING_WENT_WRONG_WITH_MESSAGE, + params: { ERROR_MESSAGE: 'Failed to load trades' }, + })} +
+
+ ); + } + if (isLoading) { return ; } @@ -203,11 +53,11 @@ export const TradeHistoryList = () => { if (trades.length === 0) { return (
- {/* TODO: DWJ -- Replace with real trades empty state */} -
{stringGetter({ key: STRING_KEYS.FILLS_EMPTY_STATE })}
+
{stringGetter({ key: STRING_KEYS.TRADES_EMPTY_STATE })}
); } + return (
-) { - switch (action) { - case TradeAction.OPEN_LONG: - return { label: 'Open Long', color: 'var(--color-positive)' }; - case TradeAction.OPEN_SHORT: - return { label: 'Open Short', color: 'var(--color-negative)' }; - case TradeAction.CLOSE_LONG: - return { label: 'Close Long', color: 'var(--color-negative)' }; - case TradeAction.CLOSE_SHORT: - return { label: 'Close Short', color: 'var(--color-positive)' }; - case TradeAction.PARTIAL_CLOSE_LONG: - return { label: 'Partial Close Long', color: 'var(--color-negative)' }; - case TradeAction.PARTIAL_CLOSE_SHORT: - return { label: 'Partial Close Short', color: 'var(--color-positive)' }; - case TradeAction.ADD_TO_LONG: - return { label: 'Add to Long', color: 'var(--color-positive)' }; - case TradeAction.ADD_TO_SHORT: - return { label: 'Add to Short', color: 'var(--color-negative)' }; - default: - return { label: 'Open Long', color: 'var(--color-positive)' }; - } -} - export const TradeHistoryRow = ({ className, trade, @@ -55,26 +27,29 @@ export const TradeHistoryRow = ({ className?: string; trade: SubaccountTrade; }) => { - // const stringGetter = useStringGetter(); - const { market, side, action, price, size, closedPnl, createdAt } = trade; + const stringGetter = useStringGetter(); + const { marketId, side, action, price, size, closedPnl, createdAt } = trade; - const marketData = useAppSelectorWithArgs(BonsaiHelpers.markets.selectMarketSummaryById, market); + const marketData = useAppSelectorWithArgs( + BonsaiHelpers.markets.selectMarketSummaryById, + marketId + ); const assetInfo = useAppSelectorWithArgs( BonsaiHelpers.assets.selectAssetInfo, - mapIfPresent(market, getAssetFromMarketId) + mapIfPresent(marketId, getAssetFromMarketId) ); const { logo } = orEmptyObj(assetInfo); const { displayableAsset, stepSizeDecimals, tickSizeDecimals } = orEmptyObj(marketData); const { actionLabel, actionColor, sideColor } = useMemo(() => { - const display = getActionDisplayInfo(action); // , stringGetter); + const display = getTradeActionDisplayInfo(action, stringGetter); return { actionLabel: display.label, actionColor: display.color, sideColor: side === IndexerOrderSide.BUY ? 'var(--color-positive)' : 'var(--color-negative)', }; - }, [action, side]); + }, [action, side, stringGetter]); const miniIcon = (
- {closedPnl != null ? ( + {closedPnl != null && closedPnl !== 0 ? ( diff --git a/src/views/tables/PositionsTable/PositionsActionsCell.tsx b/src/views/tables/PositionsTable/PositionsActionsCell.tsx index bd90e1706a..970d64fd8e 100644 --- a/src/views/tables/PositionsTable/PositionsActionsCell.tsx +++ b/src/views/tables/PositionsTable/PositionsActionsCell.tsx @@ -39,7 +39,6 @@ type ElementProps = { entryPrice: Nullable; unrealizedPnl: Nullable; side: Nullable; - sideLabel: Nullable; isDisabled?: boolean; showClosePositionAction: boolean; }; @@ -53,7 +52,6 @@ export const PositionsActionsCell = ({ entryPrice, unrealizedPnl, side, - sideLabel, isDisabled, showClosePositionAction, }: ElementProps) => { @@ -86,24 +84,16 @@ export const PositionsActionsCell = ({ size: position?.value.toNumber() ?? 0, isLong: side === IndexerPositionSide.LONG, isCross: position?.marginMode === 'CROSS', - shareType: position?.status === 'OPEN' ? 'open' : 'close', leverage: leverage?.toNumber(), oraclePrice: oraclePrice?.toNumber(), entryPrice: entryPrice?.toNumber(), unrealizedPnl: unrealizedPnl?.toNumber(), pnl: position?.realizedPnl.toNumber(), - pnlPercentage: position?.updatedUnrealizedPnlPercent?.toNumber() ?? 0, + pnlPercentage: position?.updatedUnrealizedPnlPercent?.toNumber(), liquidationPrice: position?.liquidationPrice?.toNumber(), }; - dispatch( - openDialog( - DialogTypes.SharePNLAnalytics({ - ...sharePnlData, - sideLabel, - }) - ) - ); + dispatch(openDialog(DialogTypes.SharePNLAnalytics(sharePnlData))); }; return ( @@ -145,11 +135,9 @@ const $ActionsTableCell = styled(ActionsTableCell)` `; const $TriggersButton = styled(IconButton)` - --button-icon-size: 1.25em; + --button-icon-size: 1em; --button-textColor: var(--color-text-0); --button-hover-textColor: var(--color-text-1); - - --button-icon-size: 1em; `; const $CloseButtonToggle = styled(IconButton)` diff --git a/src/views/tables/TradeHistoryTable.tsx b/src/views/tables/TradeHistoryTable.tsx index b0c1ad913a..b9e32d1b97 100644 --- a/src/views/tables/TradeHistoryTable.tsx +++ b/src/views/tables/TradeHistoryTable.tsx @@ -1,19 +1,13 @@ import { forwardRef, useMemo } from 'react'; import { BonsaiCore } from '@/bonsai/ontology'; -import { - PerpetualMarketSummary, - SubaccountFillType, - SubaccountTrade, - TradeAction, -} from '@/bonsai/types/summaryTypes'; +import { PerpetualMarketSummary, SubaccountTrade } from '@/bonsai/types/summaryTypes'; import type { ColumnSize } from '@react-types/table'; import styled, { css } from 'styled-components'; import tw from 'twin.macro'; import { STRING_KEYS, type StringGetterFunction } from '@/constants/localization'; -import { NumberSign } from '@/constants/numbers'; -import { IndexerOrderSide, IndexerPositionSide } from '@/types/indexer/indexerApiGen'; +import { IndexerOrderSide } from '@/types/indexer/indexerApiGen'; import { useStringGetter } from '@/hooks/useStringGetter'; @@ -23,7 +17,6 @@ import { AssetIcon } from '@/components/AssetIcon'; import { Icon, IconName } from '@/components/Icon'; import { Output, OutputType, ShowSign } from '@/components/Output'; import { ColumnDef, Table } from '@/components/Table'; -// import { MarketSummaryTableCell } from '@/components/Table/MarketTableCell'; import { TableCell } from '@/components/Table/TableCell'; import { TableColumnHeader } from '@/components/Table/TableColumnHeader'; import { PageSize } from '@/components/Table/TablePaginationRow'; @@ -32,8 +25,8 @@ import { Tag } from '@/components/Tag'; import { useAppSelector } from '@/state/appTypes'; import { getMarginModeStringKey } from '@/lib/enumToStringKeyHelpers'; -import { getNumberSign, MustBigNumber } from '@/lib/numbers'; -// import { MustBigNumber } from '@/lib/numbers'; +import { MustBigNumber } from '@/lib/numbers'; +import { getTradeActionDisplayInfo } from '@/lib/tradeHistoryHelpers'; import { Nullable, orEmptyRecord } from '@/lib/typeUtils'; import { TradeHistoryActionsCell } from './TradeHistoryTable/TradeHistoryActionsCell'; @@ -41,7 +34,6 @@ import { TradeHistoryActionsCell } from './TradeHistoryTable/TradeHistoryActions export enum TradeHistoryTableColumnKey { Time = 'Time', Market = 'Market', - Leverage = 'Leverage', Type = 'Type', Action = 'Action', Side = 'Side', @@ -63,33 +55,9 @@ export type TradeTableRow = { tickSizeDecimals: number; } & SubaccountTrade; -function getActionDisplayInfo(action: TradeAction) { - switch (action) { - case TradeAction.OPEN_LONG: - return { label: 'Open Long', color: 'var(--color-positive)' }; - case TradeAction.OPEN_SHORT: - return { label: 'Open Short', color: 'var(--color-negative)' }; - case TradeAction.CLOSE_LONG: - return { label: 'Close Long', color: 'var(--color-negative)' }; - case TradeAction.CLOSE_SHORT: - return { label: 'Close Short', color: 'var(--color-positive)' }; - case TradeAction.PARTIAL_CLOSE_LONG: - return { label: 'Partial Close Long', color: 'var(--color-negative)' }; - case TradeAction.PARTIAL_CLOSE_SHORT: - return { label: 'Partial Close Short', color: 'var(--color-positive)' }; - case TradeAction.ADD_TO_LONG: - return { label: 'Add to Long', color: 'var(--color-positive)' }; - case TradeAction.ADD_TO_SHORT: - return { label: 'Add to Short', color: 'var(--color-negative)' }; - default: - return { label: '', color: 'var(--color-text-0)' }; - } -} - const getTradeHistoryTableColumnDef = ({ key, stringGetter, - symbol = '', width, }: { key: TradeHistoryTableColumnKey; @@ -133,24 +101,6 @@ const getTradeHistoryTableColumnDef = ({ ), }, - [TradeHistoryTableColumnKey.Leverage]: { - columnKey: 'leverage', - getCellValue: (row) => row.leverage, - label: stringGetter({ key: STRING_KEYS.LEVERAGE }), - renderCell: ({ leverage, positionSide }) => ( - - <$OutputSigned - sign={getNumberSign( - positionSide === IndexerPositionSide.SHORT ? -1 * (leverage ?? 0) : (leverage ?? 0) - )} - type={OutputType.Multiple} - value={leverage} - showSign={ShowSign.None} - /> - - ), - }, - [TradeHistoryTableColumnKey.Type]: { columnKey: 'type', getCellValue: (row) => row.marginMode, @@ -167,7 +117,7 @@ const getTradeHistoryTableColumnDef = ({ getCellValue: (row) => row.action, label: stringGetter({ key: STRING_KEYS.ACTION }), renderCell: ({ action }) => { - const { label, color } = getActionDisplayInfo(action); + const { label, color } = getTradeActionDisplayInfo(action, stringGetter); return {label}; }, }, @@ -198,11 +148,14 @@ const getTradeHistoryTableColumnDef = ({ [TradeHistoryTableColumnKey.Size]: { columnKey: 'size', - getCellValue: (row) => row.size, + getCellValue: (row) => row.additionalSize, label: stringGetter({ key: STRING_KEYS.AMOUNT }), - tag: symbol, - renderCell: ({ size, stepSizeDecimals }) => ( - + renderCell: ({ additionalSize, stepSizeDecimals }) => ( + ), }, @@ -231,10 +184,10 @@ const getTradeHistoryTableColumnDef = ({ [TradeHistoryTableColumnKey.ClosedPnl]: { columnKey: 'closedPnl', getCellValue: (row) => row.closedPnl, - label: stringGetter({ key: STRING_KEYS.PNL }), - renderCell: ({ closedPnl, closedPnlPercent }) => ( + label: stringGetter({ key: STRING_KEYS.CLOSED_PNL }), + renderCell: ({ closedPnl, netClosedPnlPercent }) => ( - {closedPnl != null ? ( + {closedPnl != null && closedPnl !== 0 ? ( <> <$HighlightOutput - isNegative={MustBigNumber(closedPnlPercent).isNegative()} + isNegative={MustBigNumber(netClosedPnlPercent).isNegative()} type={OutputType.Percent} - value={closedPnlPercent} + value={netClosedPnlPercent} showSign={ShowSign.Both} withParentheses withSignedValueColor @@ -259,7 +212,6 @@ const getTradeHistoryTableColumnDef = ({ }, // --- Tablet stacked columns --- - [TradeHistoryTableColumnKey.ActionAmount]: { columnKey: 'actionAmount', getCellValue: (row) => row.size, @@ -270,7 +222,7 @@ const getTradeHistoryTableColumnDef = ({ ), renderCell: ({ action, size, stepSizeDecimals, marketSummary }) => { - const { label, color } = getActionDisplayInfo(action); + const { label, color } = getTradeActionDisplayInfo(action, stringGetter); return ( { const stringGetter = useStringGetter(); - - const allTrades = MOCK_TRADES; // useAppSelector(BonsaiCore.account.trades.data); + const allTrades = useAppSelector(BonsaiCore.account.tradeHistory.data); + const tradesStatus = useAppSelector(BonsaiCore.account.tradeHistory.loading); + const isError = tradesStatus === 'error'; const marketSummaries = orEmptyRecord(useAppSelector(BonsaiCore.markets.markets.data)); const tradesData = useMemo( @@ -373,9 +326,9 @@ export const TradeHistoryTable = forwardRef( return ( <$Table - key="all-trades" + key="trade-history" label="Trades" - tableId="trades" + tableId="tradeHistory" data={tradesData} getRowKey={(row: TradeTableRow) => row.id} columns={columnKeys.map((key: TradeHistoryTableColumnKey) => @@ -386,10 +339,22 @@ export const TradeHistoryTable = forwardRef( }) )} slotEmpty={ - <> - -

{stringGetter({ key: STRING_KEYS.TRADES_EMPTY_STATE })}

- + isError ? ( + <> + +

+ {stringGetter({ + key: STRING_KEYS.SOMETHING_WENT_WRONG_WITH_MESSAGE, + params: { ERROR_MESSAGE: 'Failed to load trades' }, + })} +

+ + ) : ( + <> + +

{stringGetter({ key: STRING_KEYS.TRADES_EMPTY_STATE })}

+ + ) } initialPageSize={initialPageSize} withOuterBorder={withOuterBorder} @@ -419,230 +384,6 @@ const $Side = styled.span<{ side: Nullable }>` }[side]}; `; -const MOCK_TRADES: SubaccountTrade[] = [ - // --- L → Close scenario --- - { - id: 'trade-001', - marketId: 'ETH-USD', - side: IndexerOrderSide.SELL, - positionSide: IndexerPositionSide.LONG, - action: TradeAction.CLOSE_LONG, - price: '5000.00', - size: '10.05', - prevSize: '0.00', - additionalSize: '10.05', - value: 50250, - fee: '0.10', - closedPnl: 20293.09, - closedPnlPercent: 0.404, - createdAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(), - marginMode: 'CROSS', - orderType: SubaccountFillType.MARKET, - leverage: 10, - entryPrice: 3000.01, - liquidationPrice: 2000.01, - orderId: '', - positionId: '', - }, - { - id: 'trade-002', - marketId: 'ETH-USD', - side: IndexerOrderSide.BUY, - positionSide: IndexerPositionSide.LONG, - action: TradeAction.OPEN_LONG, - price: '3000.00', - size: '10.05', - prevSize: '0.00', - additionalSize: '10.05', - value: 30150, - fee: '0.10', - closedPnl: undefined, - closedPnlPercent: undefined, - createdAt: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(), - marginMode: 'CROSS', - orderType: SubaccountFillType.LIMIT, - leverage: 10, - entryPrice: 3000.01, - liquidationPrice: 2000.01, - orderId: '', - positionId: '', - }, - // --- L → S (crossing 0) scenario --- - { - id: 'trade-003', - marketId: 'ETH-USD', - side: IndexerOrderSide.SELL, - positionSide: IndexerPositionSide.SHORT, - action: TradeAction.OPEN_SHORT, - price: '3000.00', - size: '10.05', - prevSize: '0.00', - additionalSize: '10.05', - value: 30150, - fee: '0.10', - closedPnl: undefined, - closedPnlPercent: undefined, - createdAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(), - marginMode: 'CROSS', - orderType: SubaccountFillType.MARKET, - leverage: 10, - entryPrice: 3000.01, - liquidationPrice: 2000.01, - orderId: '', - positionId: '', - }, - { - id: 'trade-004', - marketId: 'ETH-USD', - side: IndexerOrderSide.SELL, - positionSide: IndexerPositionSide.LONG, - action: TradeAction.CLOSE_LONG, - price: '3000.00', - size: '10.05', - prevSize: '0.00', - additionalSize: '10.05', - value: 30150, - fee: '0.10', - closedPnl: -20293.09, - closedPnlPercent: -0.4469, - createdAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(), - marginMode: 'CROSS', - orderType: SubaccountFillType.MARKET, - leverage: 10, - entryPrice: 5000.01, - liquidationPrice: 2000.01, - orderId: '', - positionId: '', - }, - { - id: 'trade-005', - marketId: 'ETH-USD', - side: IndexerOrderSide.BUY, - positionSide: IndexerPositionSide.LONG, - action: TradeAction.OPEN_LONG, - price: '3000.00', - size: '10.05', - prevSize: '0.00', - additionalSize: '10.05', - value: 30150, - fee: '0.10', - closedPnl: undefined, - closedPnlPercent: undefined, - createdAt: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(), - marginMode: 'CROSS', - orderType: SubaccountFillType.LIMIT, - leverage: 10, - entryPrice: 3000.01, - liquidationPrice: 2000.01, - orderId: '', - positionId: '', - }, - // --- L → Partial Close scenario --- - { - id: 'trade-006', - marketId: 'ETH-USD', - side: IndexerOrderSide.SELL, - positionSide: IndexerPositionSide.LONG, - action: TradeAction.PARTIAL_CLOSE_LONG, - price: '5000.00', - size: '5.00', - prevSize: '6.00', - additionalSize: '-5.00', - value: 25000, - fee: '0.10', - closedPnl: 5293.09, - closedPnlPercent: 0.0212, - createdAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(), - marginMode: 'CROSS', - orderType: SubaccountFillType.MARKET, - leverage: 10, - entryPrice: 3000.01, - liquidationPrice: 2000.01, - orderId: '', - positionId: '', - }, - { - id: 'trade-007', - marketId: 'ETH-USD', - side: IndexerOrderSide.BUY, - positionSide: IndexerPositionSide.LONG, - action: TradeAction.OPEN_LONG, - price: '3000.00', - size: '10.00', - prevSize: '0.00', - additionalSize: '10.00', - value: 30000, - fee: '0.10', - closedPnl: undefined, - closedPnlPercent: undefined, - createdAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(), - marginMode: 'CROSS', - orderType: SubaccountFillType.LIMIT, - leverage: 10, - entryPrice: 3000.01, - liquidationPrice: 2000.01, - orderId: '', - positionId: '', - }, - - // --- Add to Long scenario --- - { - id: 'trade-008', - marketId: 'ETH-USD', - side: IndexerOrderSide.BUY, - positionSide: IndexerPositionSide.LONG, - action: TradeAction.ADD_TO_LONG, - price: '3000.00', - size: '5.00', - prevSize: '10.00', - additionalSize: '5.00', - value: 15000, - fee: '0.10', - closedPnl: undefined, - closedPnlPercent: undefined, - createdAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(), - marginMode: 'CROSS', - orderType: SubaccountFillType.LIMIT, - leverage: 10, - entryPrice: 3000.01, - liquidationPrice: 2000.01, - orderId: '', - positionId: '', - }, - { - id: 'trade-009', - marketId: 'ETH-USD', - side: IndexerOrderSide.BUY, - positionSide: IndexerPositionSide.LONG, - action: TradeAction.OPEN_LONG, - price: '3000.00', - size: '10.00', - prevSize: '0.00', - additionalSize: '10.00', - value: 30000, - fee: '0.10', - closedPnl: undefined, - closedPnlPercent: undefined, - createdAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(), - marginMode: 'CROSS', - orderType: SubaccountFillType.LIMIT, - leverage: 10, - entryPrice: 3000.01, - liquidationPrice: 2000.01, - orderId: '', - positionId: '', - }, -]; - -const $OutputSigned = styled(Output)<{ sign: NumberSign }>` - color: ${({ sign }) => - ({ - [NumberSign.Positive]: `var(--color-positive)`, - [NumberSign.Negative]: `var(--color-negative)`, - [NumberSign.Neutral]: `var(--color-text-2)`, - })[sign]}; -`; - const $HighlightOutput = styled(Output)<{ isNegative?: boolean }>` color: var(--color-text-1); --secondary-item-color: currentColor; diff --git a/src/views/tables/TradeHistoryTable/TradeHistoryActionsCell.tsx b/src/views/tables/TradeHistoryTable/TradeHistoryActionsCell.tsx index 37d7ce9f82..32dfe14e1a 100644 --- a/src/views/tables/TradeHistoryTable/TradeHistoryActionsCell.tsx +++ b/src/views/tables/TradeHistoryTable/TradeHistoryActionsCell.tsx @@ -4,7 +4,6 @@ import styled from 'styled-components'; import { ButtonShape, ButtonStyle } from '@/constants/buttons'; import { DialogTypes, SharePNLAnalyticsDialogProps } from '@/constants/dialogs'; import { STRING_KEYS } from '@/constants/localization'; -import { IndexerPositionSide } from '@/types/indexer/indexerApiGen'; import { useStringGetter } from '@/hooks/useStringGetter'; @@ -16,10 +15,9 @@ import { WithTooltip } from '@/components/WithTooltip'; import { useAppDispatch } from '@/state/appTypes'; import { openDialog } from '@/state/dialogs'; -import { getIndexerPositionSideStringKey } from '@/lib/enumToStringKeyHelpers'; import { MaybeBigNumber } from '@/lib/numbers'; -import { TradeTableRow } from '../TradeHistoryTable'; +import { type TradeTableRow } from '../TradeHistoryTable'; type ElementProps = { trade: TradeTableRow; @@ -30,12 +28,6 @@ export const TradeHistoryActionsCell = ({ trade, isDisabled }: ElementProps) => const dispatch = useAppDispatch(); const stringGetter = useStringGetter(); - const sideLabel = - trade.positionSide && - stringGetter({ - key: getIndexerPositionSideStringKey(trade.positionSide), - }); - const shareType = trade.action === TradeAction.OPEN_LONG || trade.action === TradeAction.OPEN_SHORT ? 'open' @@ -44,33 +36,34 @@ export const TradeHistoryActionsCell = ({ trade, isDisabled }: ElementProps) => ? 'partialClose' : trade.action === TradeAction.CLOSE_LONG || trade.action === TradeAction.CLOSE_SHORT ? 'close' - : undefined; + : trade.action === TradeAction.LIQUIDATION + ? 'liquidated' + : undefined; + + const prevSize = !!trade.prevSize && !!trade.price ? trade.prevSize * trade.price : undefined; const sharePnlData: SharePNLAnalyticsDialogProps = { assetId: trade.marketSummary?.assetId ?? '', marketId: trade.marketId, - size: trade.value ?? 0, - isLong: trade.positionSide === IndexerPositionSide.LONG, + size: trade.size ?? trade.value, + prevSize, + isLong: + trade.positionSide === 'LONG' || + trade.action === TradeAction.OPEN_LONG || + trade.action === TradeAction.CLOSE_LONG, isCross: trade.marginMode === 'CROSS', shareType, leverage: trade.leverage, oraclePrice: MaybeBigNumber(trade.marketSummary?.oraclePrice)?.toNumber(), - entryPrice: MaybeBigNumber(trade.entryPrice)?.toNumber(), - exitPrice: MaybeBigNumber(trade.price)?.toNumber(), + entryPrice: trade.entryPrice, + exitPrice: trade.price, pnl: trade.closedPnl, - pnlPercentage: trade.closedPnlPercent ?? 0, + pnlPercentage: trade.netClosedPnlPercent ?? 0, liquidationPrice: trade.liquidationPrice, }; const openShareDialog = () => { - dispatch( - openDialog( - DialogTypes.SharePNLAnalytics({ - ...sharePnlData, - sideLabel: sideLabel ?? undefined, - }) - ) - ); + dispatch(openDialog(DialogTypes.SharePNLAnalytics(sharePnlData))); }; return ( @@ -94,9 +87,7 @@ const $ActionsTableCell = styled(ActionsTableCell)` `; const $TriggersButton = styled(IconButton)` - --button-icon-size: 1.25em; + --button-icon-size: 1em; --button-textColor: var(--color-text-0); --button-hover-textColor: var(--color-text-1); - - --button-icon-size: 1em; `; From 49784bc8282b9597bd47adfda2a14deefc14e008 Mon Sep 17 00:00:00 2001 From: dwjanus Date: Wed, 25 Feb 2026 15:19:22 -0500 Subject: [PATCH 3/6] feedback --- pnpm-lock.yaml | 8 +-- src/bonsai/calculators/trades.ts | 17 ++++-- src/constants/dialogs.ts | 3 +- src/lib/tradeHistoryHelpers.ts | 39 +++++++++++- src/views/Lists/Trade/TradeHistoryRow.tsx | 31 +++++----- src/views/tables/TradeHistoryTable.tsx | 33 ++-------- .../TradeHistoryActionsCell.tsx | 61 ++++++++----------- 7 files changed, 101 insertions(+), 91 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 97c406f13c..b91fa6b96c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -30,8 +30,8 @@ dependencies: specifier: ^5.23.3 version: 5.35.1 '@dydxprotocol/v4-client-js': - specifier: 3.5.0 - version: 3.5.0 + specifier: 3.4.0 + version: 3.4.0 '@dydxprotocol/v4-localization': specifier: 1.1.390 version: 1.1.390 @@ -1671,8 +1671,8 @@ packages: '@datadog/browser-core': 5.35.1 dev: false - /@dydxprotocol/v4-client-js@3.5.0: - resolution: {integrity: sha512-s751Se22tax9cdWG7ZPpNgHMgrA9HHR/rZndSm/WlDHoQKc0FVmjWG5Vo1D8+tnPbA5CNyyQ8TLLN6IktmpqIQ==} + /@dydxprotocol/v4-client-js@3.4.0: + resolution: {integrity: sha512-dMEcqfJAreiDQjrgJyUNGqxUH3sh5J5LG5fKItCRIM7MsygAvf5/kpedQByh6nO6ov+tUw4zgT4OFc9vKN+cMQ==} dependencies: '@cosmjs/amino': 0.32.4 '@cosmjs/encoding': 0.32.4 diff --git a/src/bonsai/calculators/trades.ts b/src/bonsai/calculators/trades.ts index e0cfc554a4..adaeba3598 100644 --- a/src/bonsai/calculators/trades.ts +++ b/src/bonsai/calculators/trades.ts @@ -15,14 +15,23 @@ import { import { MustBigNumber, MustNumber } from '@/lib/numbers'; import { mergeObjects } from '../lib/mergeObjects'; +import { logBonsaiError } from '../logs'; import { SubaccountTrade, TradeAction } from '../types/summaryTypes'; export function calculateTrades( restTrades: IndexerCompositeTradeHistoryObject[] | undefined, liveTrades: IndexerCompositeTradeHistoryObject[] | undefined ): SubaccountTrade[] { - const getTradesById = (data: IndexerCompositeTradeHistoryObject[]) => - keyBy(data, (trade) => trade.id ?? ''); + const getTradesById = (data: IndexerCompositeTradeHistoryObject[]) => { + const tradesWithIds = data.filter((trade) => { + if (!trade.id) { + logBonsaiError('calculateTrades', 'Trade missing id, skipping', { trade }); + return false; + } + return true; + }); + return keyBy(tradesWithIds, (trade) => trade.id!); + }; const merged = mergeObjects( getTradesById(restTrades ?? EMPTY_ARR), @@ -44,9 +53,7 @@ const calculateTrade = weakMapMemoize( action: deriveTradeAction(base), price: Number(base.executionPrice), entryPrice: base.entryPrice ? Number(base.entryPrice) : undefined, - size: - Number(base.executionPrice) * - (MustNumber(base.additionalSize ?? 0) + MustNumber(base.prevSize ?? 0)), + size: MustNumber(base.additionalSize ?? 0), prevSize: base.prevSize ? Number(base.prevSize) : undefined, additionalSize: base.additionalSize ? Number(base.additionalSize) : undefined, value: MustNumber(base.value ?? 0), diff --git a/src/constants/dialogs.ts b/src/constants/dialogs.ts index ccaaec6859..f34e58dc8a 100644 --- a/src/constants/dialogs.ts +++ b/src/constants/dialogs.ts @@ -64,12 +64,13 @@ export type SelectMarginModeDialogProps = {}; export type SetMarketLeverageDialogProps = { marketId: string }; export type SetupPasskeyDialogProps = { onClose: () => void }; export type ShareAffiliateDialogProps = {}; +export type SharePNLShareType = 'open' | 'close' | 'liquidated' | 'partialClose' | 'extend'; export type SharePNLAnalyticsDialogProps = { assetId: string; marketId?: string; isLong: boolean; isCross: boolean; - shareType?: 'open' | 'close' | 'liquidated' | 'partialClose' | 'extend' | undefined; + shareType?: SharePNLShareType | undefined; leverage?: Nullable; size?: Nullable; prevSize?: Nullable; diff --git a/src/lib/tradeHistoryHelpers.ts b/src/lib/tradeHistoryHelpers.ts index 2afdc06d8e..95a86f9b69 100644 --- a/src/lib/tradeHistoryHelpers.ts +++ b/src/lib/tradeHistoryHelpers.ts @@ -1,9 +1,27 @@ import { TradeAction } from '@/bonsai/types/summaryTypes'; +import { SharePNLAnalyticsDialogProps } from '@/constants/dialogs'; import { STRING_KEYS } from '@/constants/localization'; +import { IndexerOrderSide } from '@/types/indexer/indexerApiGen'; import type { useStringGetter } from '@/hooks/useStringGetter'; +import { assertNever } from './assertNever'; + +type ShareType = NonNullable; + +export const TRADE_ACTION_TO_SHARE_TYPE_MAP: Record = { + [TradeAction.OPEN_LONG]: 'open', + [TradeAction.OPEN_SHORT]: 'open', + [TradeAction.CLOSE_LONG]: 'close', + [TradeAction.CLOSE_SHORT]: 'close', + [TradeAction.PARTIAL_CLOSE_LONG]: 'partialClose', + [TradeAction.PARTIAL_CLOSE_SHORT]: 'partialClose', + [TradeAction.ADD_TO_LONG]: 'extend', + [TradeAction.ADD_TO_SHORT]: 'extend', + [TradeAction.LIQUIDATION]: 'liquidated', +}; + export function getTradeActionDisplayInfo( action: TradeAction, stringGetter: ReturnType @@ -49,10 +67,25 @@ export function getTradeActionDisplayInfo( label: stringGetter({ key: STRING_KEYS.ADD_TO_SHORT }), color: 'var(--color-negative)', }; - default: + case TradeAction.LIQUIDATION: return { - label: stringGetter({ key: STRING_KEYS.OPEN_LONG }), - color: 'var(--color-positive)', + label: stringGetter({ key: STRING_KEYS.LIQUIDATED }), + color: 'var(--color-negative)', }; + default: + return assertNever(action); } } + +export function getOrderSideColor( + action: TradeAction, + side: IndexerOrderSide | undefined, + stringGetter: ReturnType +) { + const display = getTradeActionDisplayInfo(action, stringGetter); + return { + actionLabel: display.label, + actionColor: display.color, + sideColor: side === IndexerOrderSide.BUY ? 'var(--color-positive)' : 'var(--color-negative)', + }; +} diff --git a/src/views/Lists/Trade/TradeHistoryRow.tsx b/src/views/Lists/Trade/TradeHistoryRow.tsx index 679f3c16f6..800584f025 100644 --- a/src/views/Lists/Trade/TradeHistoryRow.tsx +++ b/src/views/Lists/Trade/TradeHistoryRow.tsx @@ -5,8 +5,6 @@ import { SubaccountTrade } from '@/bonsai/types/summaryTypes'; import styled from 'styled-components'; import tw from 'twin.macro'; -import { IndexerOrderSide } from '@/types/indexer/indexerApiGen'; - import { useAppSelectorWithArgs } from '@/hooks/useParameterizedSelector'; import { useStringGetter } from '@/hooks/useStringGetter'; @@ -15,7 +13,7 @@ import { Output, OutputType, ShowSign } from '@/components/Output'; import { getAssetFromMarketId } from '@/lib/assetUtils'; import { mapIfPresent } from '@/lib/do'; -import { getTradeActionDisplayInfo } from '@/lib/tradeHistoryHelpers'; +import { getOrderSideColor } from '@/lib/tradeHistoryHelpers'; import { orEmptyObj } from '@/lib/typeUtils'; import { DateContent } from '../DateContent'; @@ -42,20 +40,19 @@ export const TradeHistoryRow = ({ const { logo } = orEmptyObj(assetInfo); const { displayableAsset, stepSizeDecimals, tickSizeDecimals } = orEmptyObj(marketData); - const { actionLabel, actionColor, sideColor } = useMemo(() => { - const display = getTradeActionDisplayInfo(action, stringGetter); - return { - actionLabel: display.label, - actionColor: display.color, - sideColor: side === IndexerOrderSide.BUY ? 'var(--color-positive)' : 'var(--color-negative)', - }; - }, [action, side, stringGetter]); + const { actionLabel, actionColor, sideColor } = useMemo( + () => getOrderSideColor(action, side, stringGetter), + [action, side, stringGetter] + ); - const miniIcon = ( - + const miniIcon = useMemo( + () => ( + + ), + [sideColor] ); return ( @@ -87,7 +84,7 @@ export const TradeHistoryRow = ({ tw="text-color-text-2 font-mini-book" type={OutputType.Fiat} value={closedPnl} - showSign={closedPnl > 0 ? ShowSign.None : ShowSign.Negative} + showSign={closedPnl >= 0 ? ShowSign.None : ShowSign.Negative} withSignedValueColor /> ) : ( diff --git a/src/views/tables/TradeHistoryTable.tsx b/src/views/tables/TradeHistoryTable.tsx index b9e32d1b97..f51a2184ce 100644 --- a/src/views/tables/TradeHistoryTable.tsx +++ b/src/views/tables/TradeHistoryTable.tsx @@ -17,6 +17,8 @@ import { AssetIcon } from '@/components/AssetIcon'; import { Icon, IconName } from '@/components/Icon'; import { Output, OutputType, ShowSign } from '@/components/Output'; import { ColumnDef, Table } from '@/components/Table'; +import { DateAgeOutput } from '@/components/Table/DateAgeToggleHeader'; +import { MarketSummaryTableCell } from '@/components/Table/MarketTableCell'; import { TableCell } from '@/components/Table/TableCell'; import { TableColumnHeader } from '@/components/Table/TableColumnHeader'; import { PageSize } from '@/components/Table/TablePaginationRow'; @@ -36,7 +38,6 @@ export enum TradeHistoryTableColumnKey { Market = 'Market', Type = 'Type', Action = 'Action', - Side = 'Side', Price = 'Price', Size = 'Size', Value = 'Value', @@ -73,11 +74,9 @@ const getTradeHistoryTableColumnDef = ({ getCellValue: (row) => row.createdAt, label: stringGetter({ key: STRING_KEYS.TIME }), renderCell: ({ createdAt }) => ( - ), }, @@ -87,17 +86,7 @@ const getTradeHistoryTableColumnDef = ({ getCellValue: (row) => row.marketId, label: stringGetter({ key: STRING_KEYS.MARKET }), renderCell: ({ marketSummary }) => ( - - } - > - {marketSummary?.displayableAsset ?? ''} - + ), }, @@ -122,16 +111,6 @@ const getTradeHistoryTableColumnDef = ({ }, }, - [TradeHistoryTableColumnKey.Side]: { - columnKey: 'side', - getCellValue: (row) => row.side, - label: stringGetter({ key: STRING_KEYS.SIDE }), - renderCell: ({ side }) => - side != null ? ( - <$Side side={side}>{side === IndexerOrderSide.BUY ? 'Buy' : 'Sell'} - ) : null, - }, - [TradeHistoryTableColumnKey.Price]: { columnKey: 'price', getCellValue: (row) => row.price, diff --git a/src/views/tables/TradeHistoryTable/TradeHistoryActionsCell.tsx b/src/views/tables/TradeHistoryTable/TradeHistoryActionsCell.tsx index 32dfe14e1a..376583cb0e 100644 --- a/src/views/tables/TradeHistoryTable/TradeHistoryActionsCell.tsx +++ b/src/views/tables/TradeHistoryTable/TradeHistoryActionsCell.tsx @@ -1,3 +1,5 @@ +import { useCallback } from 'react'; + import { TradeAction } from '@/bonsai/types/summaryTypes'; import styled from 'styled-components'; @@ -15,7 +17,8 @@ import { WithTooltip } from '@/components/WithTooltip'; import { useAppDispatch } from '@/state/appTypes'; import { openDialog } from '@/state/dialogs'; -import { MaybeBigNumber } from '@/lib/numbers'; +import { MaybeBigNumber, MustNumber } from '@/lib/numbers'; +import { TRADE_ACTION_TO_SHARE_TYPE_MAP } from '@/lib/tradeHistoryHelpers'; import { type TradeTableRow } from '../TradeHistoryTable'; @@ -28,43 +31,33 @@ export const TradeHistoryActionsCell = ({ trade, isDisabled }: ElementProps) => const dispatch = useAppDispatch(); const stringGetter = useStringGetter(); - const shareType = - trade.action === TradeAction.OPEN_LONG || trade.action === TradeAction.OPEN_SHORT - ? 'open' - : trade.action === TradeAction.PARTIAL_CLOSE_LONG || - trade.action === TradeAction.PARTIAL_CLOSE_SHORT - ? 'partialClose' - : trade.action === TradeAction.CLOSE_LONG || trade.action === TradeAction.CLOSE_SHORT - ? 'close' - : trade.action === TradeAction.LIQUIDATION - ? 'liquidated' - : undefined; + const shareType = TRADE_ACTION_TO_SHARE_TYPE_MAP[trade.action] ?? undefined; const prevSize = !!trade.prevSize && !!trade.price ? trade.prevSize * trade.price : undefined; - const sharePnlData: SharePNLAnalyticsDialogProps = { - assetId: trade.marketSummary?.assetId ?? '', - marketId: trade.marketId, - size: trade.size ?? trade.value, - prevSize, - isLong: - trade.positionSide === 'LONG' || - trade.action === TradeAction.OPEN_LONG || - trade.action === TradeAction.CLOSE_LONG, - isCross: trade.marginMode === 'CROSS', - shareType, - leverage: trade.leverage, - oraclePrice: MaybeBigNumber(trade.marketSummary?.oraclePrice)?.toNumber(), - entryPrice: trade.entryPrice, - exitPrice: trade.price, - pnl: trade.closedPnl, - pnlPercentage: trade.netClosedPnlPercent ?? 0, - liquidationPrice: trade.liquidationPrice, - }; - - const openShareDialog = () => { + const openShareDialog = useCallback(() => { + const sharePnlData: SharePNLAnalyticsDialogProps = { + assetId: trade.marketSummary?.assetId ?? '', + marketId: trade.marketId, + size: Number(trade.price) * MustNumber(trade.additionalSize ?? 0) + (prevSize ?? 0), + prevSize, + isLong: + trade.positionSide === 'LONG' || + trade.action === TradeAction.OPEN_LONG || + trade.action === TradeAction.CLOSE_LONG, + isCross: trade.marginMode === 'CROSS', + shareType, + leverage: trade.leverage, + oraclePrice: MaybeBigNumber(trade.marketSummary?.oraclePrice)?.toNumber(), + entryPrice: trade.entryPrice, + exitPrice: trade.price, + pnl: trade.closedPnl, + pnlPercentage: trade.netClosedPnlPercent ?? 0, + liquidationPrice: trade.liquidationPrice, + }; + dispatch(openDialog(DialogTypes.SharePNLAnalytics(sharePnlData))); - }; + }, [dispatch, trade, prevSize, shareType]); return ( <$ActionsTableCell tw="mr-[-0.5rem]"> From 71bca901ff2692a37863af49722a2edf85d0fc95 Mon Sep 17 00:00:00 2001 From: dwjanus Date: Wed, 25 Feb 2026 15:34:38 -0500 Subject: [PATCH 4/6] rebase and fix type enum --- src/constants/dialogs.ts | 2 +- src/lib/tradeHistoryHelpers.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/constants/dialogs.ts b/src/constants/dialogs.ts index f34e58dc8a..56a6c238e8 100644 --- a/src/constants/dialogs.ts +++ b/src/constants/dialogs.ts @@ -64,7 +64,7 @@ export type SelectMarginModeDialogProps = {}; export type SetMarketLeverageDialogProps = { marketId: string }; export type SetupPasskeyDialogProps = { onClose: () => void }; export type ShareAffiliateDialogProps = {}; -export type SharePNLShareType = 'open' | 'close' | 'liquidated' | 'partialClose' | 'extend'; +export type SharePNLShareType = 'open' | 'close' | 'liquidated' | 'partialClose' | 'extended'; export type SharePNLAnalyticsDialogProps = { assetId: string; marketId?: string; diff --git a/src/lib/tradeHistoryHelpers.ts b/src/lib/tradeHistoryHelpers.ts index 95a86f9b69..27fd02c000 100644 --- a/src/lib/tradeHistoryHelpers.ts +++ b/src/lib/tradeHistoryHelpers.ts @@ -17,8 +17,8 @@ export const TRADE_ACTION_TO_SHARE_TYPE_MAP: Record Date: Wed, 25 Feb 2026 16:26:09 -0500 Subject: [PATCH 5/6] lockfile --- pnpm-lock.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b91fa6b96c..97c406f13c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -30,8 +30,8 @@ dependencies: specifier: ^5.23.3 version: 5.35.1 '@dydxprotocol/v4-client-js': - specifier: 3.4.0 - version: 3.4.0 + specifier: 3.5.0 + version: 3.5.0 '@dydxprotocol/v4-localization': specifier: 1.1.390 version: 1.1.390 @@ -1671,8 +1671,8 @@ packages: '@datadog/browser-core': 5.35.1 dev: false - /@dydxprotocol/v4-client-js@3.4.0: - resolution: {integrity: sha512-dMEcqfJAreiDQjrgJyUNGqxUH3sh5J5LG5fKItCRIM7MsygAvf5/kpedQByh6nO6ov+tUw4zgT4OFc9vKN+cMQ==} + /@dydxprotocol/v4-client-js@3.5.0: + resolution: {integrity: sha512-s751Se22tax9cdWG7ZPpNgHMgrA9HHR/rZndSm/WlDHoQKc0FVmjWG5Vo1D8+tnPbA5CNyyQ8TLLN6IktmpqIQ==} dependencies: '@cosmjs/amino': 0.32.4 '@cosmjs/encoding': 0.32.4 From 1080154dc7fda9d2c94eafcea8d9be22e68506ee Mon Sep 17 00:00:00 2001 From: dwjanus Date: Wed, 25 Feb 2026 17:10:34 -0500 Subject: [PATCH 6/6] more feedback --- package.json | 2 +- src/bonsai/calculators/trades.ts | 14 ++++++++------ src/bonsai/rest/tradeHistory.ts | 10 ++++------ .../TradeHistoryTable/TradeHistoryActionsCell.tsx | 9 +++++---- 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 60e67f0bad..5007aa53dc 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "@cosmjs/stargate": "^0.32.1", "@cosmjs/tendermint-rpc": "^0.32.1", "@datadog/browser-logs": "^5.23.3", - "@dydxprotocol/v4-client-js": "3.4.0", + "@dydxprotocol/v4-client-js": "3.5.0", "@dydxprotocol/v4-localization": "1.1.390", "@dydxprotocol/v4-proto": "^7.0.0-dev.0", "@emotion/is-prop-valid": "^1.3.0", diff --git a/src/bonsai/calculators/trades.ts b/src/bonsai/calculators/trades.ts index adaeba3598..b9e562e438 100644 --- a/src/bonsai/calculators/trades.ts +++ b/src/bonsai/calculators/trades.ts @@ -23,13 +23,15 @@ export function calculateTrades( liveTrades: IndexerCompositeTradeHistoryObject[] | undefined ): SubaccountTrade[] { const getTradesById = (data: IndexerCompositeTradeHistoryObject[]) => { - const tradesWithIds = data.filter((trade) => { - if (!trade.id) { - logBonsaiError('calculateTrades', 'Trade missing id, skipping', { trade }); - return false; + const tradesWithIds = data.filter( + (trade): trade is IndexerCompositeTradeHistoryObject & { id: string } => { + if (!trade.id) { + logBonsaiError('calculateTrades', 'Trade missing id, skipping', { trade }); + return false; + } + return true; } - return true; - }); + ); return keyBy(tradesWithIds, (trade) => trade.id!); }; diff --git a/src/bonsai/rest/tradeHistory.ts b/src/bonsai/rest/tradeHistory.ts index 971ed1d289..e1f8bc5f6b 100644 --- a/src/bonsai/rest/tradeHistory.ts +++ b/src/bonsai/rest/tradeHistory.ts @@ -3,8 +3,6 @@ import { isParentSubaccountTradeHistoryResponse } from '@/types/indexer/indexerC import { type RootStore } from '@/state/_store'; import { setAccountTradesRaw } from '@/state/raw'; -import { isTruthy } from '@/lib/isTruthy'; - import { refreshIndexerQueryOnAccountSocketRefresh } from '../accountRefreshSignal'; import { loadableIdle } from '../lib/loadable'; import { mapLoadableData } from '../lib/mapLoadable'; @@ -18,14 +16,14 @@ export function setUpTradeHistoryQuery(store: RootStore) { name: 'tradeHistory', selector: selectParentSubaccountInfo, getQueryKey: (data) => ['account', 'tradeHistory', data.wallet, data.subaccount], - getQueryFn: (indexerClient, data) => { - if (!isTruthy(data.wallet) || data.subaccount == null || data.isPerpsGeoRestricted) { + getQueryFn: (indexerClient, { wallet, subaccount, isPerpsGeoRestricted }) => { + if (wallet == null || subaccount == null || isPerpsGeoRestricted) { return null; } return () => indexerClient.account.getParentSubaccountNumberTradeHistory( - data.wallet!, - data.subaccount!, + wallet, + subaccount, undefined, undefined, undefined, diff --git a/src/views/tables/TradeHistoryTable/TradeHistoryActionsCell.tsx b/src/views/tables/TradeHistoryTable/TradeHistoryActionsCell.tsx index 376583cb0e..5b3af3ce86 100644 --- a/src/views/tables/TradeHistoryTable/TradeHistoryActionsCell.tsx +++ b/src/views/tables/TradeHistoryTable/TradeHistoryActionsCell.tsx @@ -33,14 +33,15 @@ export const TradeHistoryActionsCell = ({ trade, isDisabled }: ElementProps) => const shareType = TRADE_ACTION_TO_SHARE_TYPE_MAP[trade.action] ?? undefined; - const prevSize = !!trade.prevSize && !!trade.price ? trade.prevSize * trade.price : undefined; + const prevSizeDollar = + !!trade.prevSize && !!trade.price ? trade.prevSize * trade.price : undefined; const openShareDialog = useCallback(() => { const sharePnlData: SharePNLAnalyticsDialogProps = { assetId: trade.marketSummary?.assetId ?? '', marketId: trade.marketId, - size: Number(trade.price) * MustNumber(trade.additionalSize ?? 0) + (prevSize ?? 0), - prevSize, + size: Number(trade.price) * MustNumber(trade.additionalSize ?? 0) + (prevSizeDollar ?? 0), + prevSize: prevSizeDollar, isLong: trade.positionSide === 'LONG' || trade.action === TradeAction.OPEN_LONG || @@ -57,7 +58,7 @@ export const TradeHistoryActionsCell = ({ trade, isDisabled }: ElementProps) => }; dispatch(openDialog(DialogTypes.SharePNLAnalytics(sharePnlData))); - }, [dispatch, trade, prevSize, shareType]); + }, [dispatch, trade, prevSizeDollar, shareType]); return ( <$ActionsTableCell tw="mr-[-0.5rem]">