diff --git a/src/abacus-ts/calculators/orders.ts b/src/abacus-ts/calculators/orders.ts index c1dd7807f..02902fb8d 100644 --- a/src/abacus-ts/calculators/orders.ts +++ b/src/abacus-ts/calculators/orders.ts @@ -1,47 +1,87 @@ import { IndexerBestEffortOpenedStatus, IndexerOrderStatus } from '@/types/indexer/indexerApiGen'; import { IndexerCompositeOrderObject } from '@/types/indexer/indexerManual'; -import { maxBy, pickBy } from 'lodash'; - -import { SubaccountOrder } from '@/constants/abacus'; +import { HeightResponse } from '@dydxprotocol/v4-client-js'; +import { mapValues, maxBy, pickBy } from 'lodash'; import { assertNever } from '@/lib/assertNever'; -import { MustBigNumber } from '@/lib/numbers'; +import { getDisplayableTickerFromMarket } from '@/lib/assetUtils'; +import { mapIfPresent } from '@/lib/do'; +import { MaybeBigNumber, MustBigNumber } from '@/lib/numbers'; import { Loadable } from '../lib/loadable'; -import { mapLoadableData } from '../lib/mapLoadable'; +import { mapLoadableData, mergeLoadableData } from '../lib/mapLoadable'; import { mergeObjects } from '../lib/mergeObjects'; import { OrdersData } from '../rawTypes'; -import { OrderStatus } from '../summaryTypes'; - -// todo these are calculating the same thing twice pasically -function calculateOpenOrders(liveOrders: Loadable, restOrders: Loadable) { - const getOpenOrders = (data: Loadable) => - mapLoadableData(data, (d) => - pickBy( - d, - (order) => - getSimpleOrderStatus(calculateOrderStatus(order) ?? OrderStatus.Open) === OrderStatus.Open - ) - ); - return calculateMergedOrders(getOpenOrders(liveOrders), getOpenOrders(restOrders)); +import { OrderStatus, SubaccountOrder, SubaccountOrdersData } from '../summaryTypes'; + +export function calculateOpenOrders(orders: Loadable) { + return mapLoadableData(orders, (d) => + pickBy( + d, + (order) => getSimpleOrderStatus(order.status ?? OrderStatus.Open) === OrderStatus.Open + ) + ); +} + +export function calculateOrderHistory(orders: Loadable) { + return mapLoadableData(orders, (d) => + pickBy( + d, + (order) => getSimpleOrderStatus(order.status ?? OrderStatus.Open) !== OrderStatus.Open + ) + ); } -function calculateOrderHistory(liveOrders: Loadable, restOrders: Loadable) { - const getNonOpenOrders = (data: Loadable) => - mapLoadableData(data, (d) => - pickBy( - d, - (order) => - getSimpleOrderStatus(calculateOrderStatus(order) ?? OrderStatus.Open) !== OrderStatus.Open - ) - ); - return calculateMergedOrders(getNonOpenOrders(liveOrders), getNonOpenOrders(restOrders)); +export function calculateAllOrders( + liveOrders: Loadable, + restOrders: Loadable, + height: HeightResponse +): Loadable { + const merged = mergeLoadableData(liveOrders, restOrders); + const actuallyMerged = mapLoadableData(merged, ([a, b]) => + calculateMergedOrders(a ?? {}, b ?? {}) + ); + const mapped = mapLoadableData(actuallyMerged, (d) => + mapValues(d, (order) => calculateSubaccountOrder(order, height)) + ); + return mapped; } function calculateSubaccountOrder( - order: IndexerCompositeOrderObject, - protocolHeight: number -): SubaccountOrder {} + base: IndexerCompositeOrderObject, + protocolHeight: HeightResponse +): SubaccountOrder { + let order: SubaccountOrder = { + marketId: base.ticker, + status: calculateBaseOrderStatus(base), + displayId: getDisplayableTickerFromMarket(base.ticker), + expiresAtMilliseconds: mapIfPresent(base.goodTilBlockTime, (u) => new Date(u).valueOf()), + updatedAtMilliseconds: mapIfPresent(base.updatedAt, (u) => new Date(u).valueOf()), + updatedAtHeight: MaybeBigNumber(base.updatedAtHeight)?.toNumber(), + marginMode: undefined, + subaccountNumber: base.subaccountNumber, + id: base.id, + clientId: base.clientId, + type: base.type, + side: base.side, + timeInForce: base.timeInForce, + clobPairId: MaybeBigNumber(base.clobPairId)?.toNumber(), + orderFlags: base.orderFlags, + price: MustBigNumber(base.price), + triggerPrice: MaybeBigNumber(base.triggerPrice), + size: MustBigNumber(base.size), + totalFilled: MustBigNumber(base.totalFilled), + goodTilBlock: MaybeBigNumber(base.goodTilBlock)?.toNumber(), + goodTilBlockTime: mapIfPresent(base.goodTilBlockTime, (u) => new Date(u).valueOf()), + createdAtHeight: MaybeBigNumber(base.createdAtHeight)?.toNumber(), + postOnly: !!base.postOnly, + reduceOnly: !!base.reduceOnly, + remainingSize: MustBigNumber(base.size).minus(MustBigNumber(base.totalFilled)), + removalReason: base.removalReason, + }; + order = maybeUpdateOrderIfExpired(order, protocolHeight); + return order; +} function getSimpleOrderStatus(status: OrderStatus) { switch (status) { @@ -63,6 +103,44 @@ function getSimpleOrderStatus(status: OrderStatus) { } } +function maybeUpdateOrderIfExpired( + order: SubaccountOrder, + height: HeightResponse +): SubaccountOrder { + if (order.status == null) { + return order; + } + // todo: why not handle Open? + if ( + ![OrderStatus.Pending, OrderStatus.Canceling, OrderStatus.PartiallyFilled].includes( + order.status + ) + ) { + return order; + } + + // Check if order has expired based on goodTilBlock + if (order.goodTilBlock && order.goodTilBlock !== 0 && height.height >= order.goodTilBlock) { + let status = OrderStatus.Canceled; + + // Check for partial fills + if (order.totalFilled != null && order.totalFilled.gt(0)) { + const remainingSize = order.size.minus(order.totalFilled); + if (order.totalFilled.gt(0) && remainingSize.gt(0)) { + status = OrderStatus.PartiallyCanceled; + } + } + + return { + ...order, + status, + updatedAtMilliseconds: new Date(height.time).valueOf(), + }; + } + + return order; +} + function calculateBaseOrderStatus(order: IndexerCompositeOrderObject): OrderStatus | undefined { const status = order.status; if (status == null) { @@ -118,9 +196,7 @@ function calculateBaseOrderStatus(order: IndexerCompositeOrderObject): OrderStat } } -function calculateMergedOrders(liveOrders: Loadable, restOrders: Loadable) { - const liveData = liveOrders.data ?? {}; - const restData = restOrders.data ?? {}; +function calculateMergedOrders(liveData: OrdersData, restData: OrdersData) { return mergeObjects( liveData, restData, diff --git a/src/abacus-ts/calculators/subaccount.ts b/src/abacus-ts/calculators/subaccount.ts index 190ee7a9a..98e5a7c2a 100644 --- a/src/abacus-ts/calculators/subaccount.ts +++ b/src/abacus-ts/calculators/subaccount.ts @@ -10,7 +10,7 @@ import { mapValues } from 'lodash'; import { NUM_PARENT_SUBACCOUNTS } from '@/constants/account'; import { calc } from '@/lib/do'; -import { MaybeBigNumber, MustBigNumber, ToBigNumber } from '@/lib/numbers'; +import { BIG_NUMBERS, MaybeBigNumber, MustBigNumber, ToBigNumber } from '@/lib/numbers'; import { isPresent } from '@/lib/typeUtils'; import { ChildSubaccountData, MarketsData, ParentSubaccountData } from '../rawTypes'; @@ -25,9 +25,6 @@ import { SubaccountSummaryDerived, } from '../summaryTypes'; -const BN_0 = MustBigNumber(0); -const BN_1 = MustBigNumber(1); - export function calculateParentSubaccountPositions( parent: Omit, markets: MarketsData @@ -61,7 +58,7 @@ export function calculateParentSubaccountSummary( equity: Object.values(summaries) .filter(isPresent) .map((s) => s.equity) - .reduce((a, b) => a.plus(b), BN_0), + .reduce((a, b) => a.plus(b), BIG_NUMBERS.ZERO), }; } @@ -82,7 +79,7 @@ function calculateSubaccountSummaryCore( ): SubaccountSummaryCore { const quoteBalance = calc(() => { const usdcPosition = subaccountData.assetPositions.USDC; - if (!usdcPosition?.size) return BN_0; + if (!usdcPosition?.size) return BIG_NUMBERS.ZERO; const size = MustBigNumber(usdcPosition.size); return usdcPosition.side === IndexerPositionSide.LONG ? size : size.negated(); @@ -111,10 +108,10 @@ function calculateSubaccountSummaryCore( }; }, { - valueTotal: BN_0, - notionalTotal: BN_0, - initialRiskTotal: BN_0, - maintenanceRiskTotal: BN_0, + valueTotal: BIG_NUMBERS.ZERO, + notionalTotal: BIG_NUMBERS.ZERO, + initialRiskTotal: BIG_NUMBERS.ZERO, + maintenanceRiskTotal: BIG_NUMBERS.ZERO, } ); @@ -138,7 +135,7 @@ function calculateSubaccountSummaryDerived(core: SubaccountSummaryCore): Subacco if (equity.gt(0)) { leverage = notionalTotal.div(equity); - marginUsage = BN_1.minus(freeCollateral.div(equity)); + marginUsage = BIG_NUMBERS.ONE.minus(freeCollateral.div(equity)); } return { @@ -185,12 +182,14 @@ function calculateDerivedPositionCore( ): SubaccountPositionDerivedCore { const marginMode = position.subaccountNumber < NUM_PARENT_SUBACCOUNTS ? 'CROSS' : 'ISOLATED'; const effectiveImf = - market != null ? getMarketEffectiveInitialMarginForMarket(market) ?? BN_0 : BN_0; - const effectiveMmf = MaybeBigNumber(market?.maintenanceMarginFraction) ?? BN_0; + market != null + ? getMarketEffectiveInitialMarginForMarket(market) ?? BIG_NUMBERS.ZERO + : BIG_NUMBERS.ZERO; + const effectiveMmf = MaybeBigNumber(market?.maintenanceMarginFraction) ?? BIG_NUMBERS.ZERO; // indexer position size is already signed I think but we will be extra sure const unsignedSize = position.size.abs(); - const oracle = MaybeBigNumber(market?.oraclePrice) ?? BN_0; + const oracle = MaybeBigNumber(market?.oraclePrice) ?? BIG_NUMBERS.ZERO; const signedSize = position.side === IndexerPositionSide.SHORT ? unsignedSize.negated() : unsignedSize; @@ -211,7 +210,7 @@ function calculateDerivedPositionCore( if (effectiveImf.isZero()) { return null; } - return BN_1.div(effectiveImf); + return BIG_NUMBERS.ONE.div(effectiveImf); }), baseEntryPrice: position.entryPrice, baseNetFunding: position.netFunding, @@ -254,7 +253,9 @@ function calculatePositionDerivedExtra( const entryValue = signedSize.multipliedBy(MustBigNumber(position.baseEntryPrice)); const unrealizedPnlInner = value.minus(entryValue).plus(MustBigNumber(position.baseNetFunding)); - const scaledLeverage = leverage ? BigNumber.max(leverage.abs(), BN_1) : BN_1; + const scaledLeverage = leverage + ? BigNumber.max(leverage.abs(), BIG_NUMBERS.ONE) + : BIG_NUMBERS.ONE; const unrealizedPnlPercentInner = !entryValue.isZero() ? unrealizedPnlInner.dividedBy(entryValue.abs()).multipliedBy(scaledLeverage) diff --git a/src/abacus-ts/lib/mapLoadable.ts b/src/abacus-ts/lib/mapLoadable.ts index b41c2e3ec..a47479fbc 100644 --- a/src/abacus-ts/lib/mapLoadable.ts +++ b/src/abacus-ts/lib/mapLoadable.ts @@ -6,3 +6,15 @@ export function mapLoadableData(load: Loadable, map: (obj: T) => R): Lo data: load.data != null ? map(load.data) : undefined, } as Loadable; } + +export function mergeLoadableData( + one: Loadable, + two: Loadable +): Loadable<[T | undefined, R | undefined]> { + const priority = ['pending', 'error', 'success', 'idle'] as const; + return { + status: priority[Math.min(priority.indexOf(one.status), priority.indexOf(two.status))]!, + error: (one as any).error ?? (two as any).error ?? undefined, + data: [one.data, two.data], + } as any; +} diff --git a/src/abacus-ts/summaryTypes.ts b/src/abacus-ts/summaryTypes.ts index 7af08164a..aecfa86c1 100644 --- a/src/abacus-ts/summaryTypes.ts +++ b/src/abacus-ts/summaryTypes.ts @@ -102,30 +102,29 @@ export enum OrderStatus { export type SubaccountOrder = { subaccountNumber: number; id: string; - clientId: string | null; + clientId: string | undefined; type: IndexerOrderType; side: IndexerOrderSide; - status: OrderStatus; - timeInForce: IndexerAPITimeInForce | null; + status: OrderStatus | undefined; + timeInForce: IndexerAPITimeInForce | undefined; marketId: string; displayId: string; - clobPairId: number | null; - orderFlags: string | null; + clobPairId: number | undefined; + orderFlags: string | undefined; price: BigNumber; - triggerPrice: BigNumber | null; - trailingPercent: BigNumber | null; + triggerPrice: BigNumber | undefined; size: BigNumber; - remainingSize: BigNumber | null; - totalFilled: BigNumber | null; - goodTilBlock: number | null; - goodTilBlockTime: number | null; - createdAtHeight: number | null; - createdAtMilliseconds: number | null; - unfillableAtMilliseconds: number | null; - expiresAtMilliseconds: number | null; - updatedAtMilliseconds: number | null; + remainingSize: BigNumber | undefined; + totalFilled: BigNumber | undefined; + goodTilBlock: number | undefined; + goodTilBlockTime: number | undefined; + createdAtHeight: number | undefined; + expiresAtMilliseconds: number | undefined; + updatedAtMilliseconds: number | undefined; + updatedAtHeight: number | undefined; postOnly: boolean; reduceOnly: boolean; - cancelReason: string | null; - marginMode: MarginMode | null; + removalReason: string | undefined; + marginMode: MarginMode | undefined; }; +export type SubaccountOrdersData = { [orderId: string]: SubaccountOrder }; diff --git a/src/types/indexer/indexerManual.ts b/src/types/indexer/indexerManual.ts index 5592f2f50..70c1fefcd 100644 --- a/src/types/indexer/indexerManual.ts +++ b/src/types/indexer/indexerManual.ts @@ -25,15 +25,15 @@ export interface IndexerCompositeFillResponse { } export interface IndexerCompositeOrderObject { - id?: string; + id: string; subaccountId?: string; clientId?: string; clobPairId?: string; - side?: IndexerOrderSide; - size?: string; - totalFilled?: string; - price?: string; - type?: IndexerOrderType; + side: IndexerOrderSide; + size: string; + totalFilled: string; + price: string; + type: IndexerOrderType; reduceOnly?: boolean; orderFlags?: string; goodTilBlock?: string | null; @@ -44,10 +44,10 @@ export interface IndexerCompositeOrderObject { timeInForce?: IndexerAPITimeInForce; status?: IndexerAPIOrderStatus; postOnly?: boolean; - ticker?: string; + ticker: string; updatedAt?: IndexerIsoString | null; updatedAtHeight?: string | null; - subaccountNumber?: number; + subaccountNumber: number; removalReason?: string; totalOptimisticFilled?: string; }