From e7d5a2a084601511d2990d00f8b01e781420038b Mon Sep 17 00:00:00 2001 From: Tyler Date: Fri, 13 Dec 2024 16:16:21 -0500 Subject: [PATCH] order calculations --- src/abacus-ts/calculators/orders.ts | 159 ++++++++++++++++++ .../calculators/{account.ts => subaccount.ts} | 4 +- src/abacus-ts/rawTypes.ts | 3 +- src/abacus-ts/summaryTypes.ts | 53 +++++- src/state/raw.ts | 14 +- 5 files changed, 219 insertions(+), 14 deletions(-) create mode 100644 src/abacus-ts/calculators/orders.ts rename src/abacus-ts/calculators/{account.ts => subaccount.ts} (98%) diff --git a/src/abacus-ts/calculators/orders.ts b/src/abacus-ts/calculators/orders.ts new file mode 100644 index 000000000..0bf838d7d --- /dev/null +++ b/src/abacus-ts/calculators/orders.ts @@ -0,0 +1,159 @@ +import { IndexerBestEffortOpenedStatus, IndexerOrderStatus } from '@/types/indexer/indexerApiGen'; +import { IndexerCompositeOrderObject } from '@/types/indexer/indexerManual'; +import { maxBy, pickBy } from 'lodash'; + +import { SubaccountOrder } from '@/constants/abacus'; + +import { assertNever } from '@/lib/assertNever'; +import { MustBigNumber } from '@/lib/numbers'; + +import { Loadable } from '../lib/loadable'; +import { OrdersData } from '../rawTypes'; +import { OrderStatus } from '../summaryTypes'; + +const BN_0 = MustBigNumber(0); +const BN_1 = MustBigNumber(1); + +// 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)); +} + +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)); +} + +function calculateSubaccountOrder( + order: IndexerCompositeOrderObject, + protocolHeight: number +): SubaccountOrder {} + +function getSimpleOrderStatus(status: OrderStatus) { + switch (status) { + case OrderStatus.Open: + case OrderStatus.Pending: + case OrderStatus.PartiallyFilled: + case OrderStatus.Untriggered: + case OrderStatus.Canceling: + return OrderStatus.Open; + case OrderStatus.Canceled: + case OrderStatus.PartiallyCanceled: + return OrderStatus.Canceled; + case OrderStatus.Filled: + return OrderStatus.Filled; + default: + assertNever(status); + return OrderStatus.Open; + } +} + +function calculateBaseOrderStatus(order: IndexerCompositeOrderObject): OrderStatus | undefined { + const status = order.status; + if (status == null) { + return undefined; + } + + if (status === IndexerBestEffortOpenedStatus.BESTEFFORTOPENED) { + return OrderStatus.Pending; + } + + // Calculate filled amounts + const size = MustBigNumber(order.size); + const totalFilled = MustBigNumber(order.totalFilled); + const remainingSize = size.minus(totalFilled); + const hasPartialFill = totalFilled.gt(0) && remainingSize.gt(0); + + // Handle partial fills + if (hasPartialFill) { + if (status === IndexerOrderStatus.OPEN) { + return OrderStatus.PartiallyFilled; + } + if (status === IndexerOrderStatus.CANCELED) { + return OrderStatus.PartiallyCanceled; + } + } + + // Handle short-term order edge case + const isShortTermOrder = order.orderFlags === '0'; + const isBestEffortCanceled = status === IndexerOrderStatus.BESTEFFORTCANCELED; + const isUserCanceled = + order.removalReason === 'USER_CANCELED' || + order.removalReason === 'ORDER_REMOVAL_REASON_USER_CANCELED'; + + if (isShortTermOrder && isBestEffortCanceled && !isUserCanceled) { + return OrderStatus.Pending; + } + + // Direct mapping for remaining cases + switch (status) { + case IndexerOrderStatus.OPEN: + return OrderStatus.Open; + case IndexerOrderStatus.FILLED: + return OrderStatus.Filled; + case IndexerOrderStatus.CANCELED: + return OrderStatus.Canceled; + case IndexerOrderStatus.BESTEFFORTCANCELED: + return OrderStatus.Canceling; + case IndexerOrderStatus.UNTRIGGERED: + return OrderStatus.Untriggered; + default: + assertNever(status); + return undefined; + } +} + +function calculateMergedOrders(liveOrders: Loadable, restOrders: Loadable) { + const liveData = liveOrders.data ?? {}; + const restData = restOrders.data ?? {}; + return mergeObjects( + liveData, + restData, + (a, b) => + maxBy([a, b], (o) => MustBigNumber(o.updatedAtHeight ?? o.createdAtHeight).toNumber())! + ); +} + +function mapLoadableData(load: Loadable, map: (obj: T) => R): Loadable { + return { + ...load, + data: load.data != null ? map(load.data) : undefined, + } as Loadable; +} + +type SimpleMap = { [key: string]: T }; +function mergeObjects(one: SimpleMap, two: SimpleMap, merge: (a: T, b: T) => T) { + const finalObj: SimpleMap = {}; + + [...Object.keys(one), ...Object.keys(two)].forEach((key) => { + if (finalObj[key] != null) { + return; + } + const obj = one[key]; + const otherObj = two[key]; + if (obj != null && otherObj != null) { + finalObj[key] = merge(obj, otherObj); + } else if (obj == null && otherObj == null) { + // do nothing + } else { + // we know one of them is non-null + finalObj[key] = (obj ?? otherObj)!; + } + }); + return finalObj; +} diff --git a/src/abacus-ts/calculators/account.ts b/src/abacus-ts/calculators/subaccount.ts similarity index 98% rename from src/abacus-ts/calculators/account.ts rename to src/abacus-ts/calculators/subaccount.ts index 6d2458e8e..190ee7a9a 100644 --- a/src/abacus-ts/calculators/account.ts +++ b/src/abacus-ts/calculators/subaccount.ts @@ -1,6 +1,7 @@ import { IndexerPerpetualMarketResponseObject, IndexerPerpetualPositionResponseObject, + IndexerPerpetualPositionStatus, IndexerPositionSide, } from '@/types/indexer/indexerApiGen'; import BigNumber from 'bignumber.js'; @@ -37,6 +38,7 @@ export function calculateParentSubaccountPositions( const subaccount = calculateSubaccountSummary(child, markets); return Object.values(child.openPerpetualPositions) .filter(isPresent) + .filter((p) => p.status === IndexerPerpetualPositionStatus.OPEN) .map((perp) => calculateSubaccountPosition(subaccount, perp, markets[perp.market])); }); } @@ -63,7 +65,7 @@ export function calculateParentSubaccountSummary( }; } -function calculateSubaccountSummary( +export function calculateSubaccountSummary( subaccountData: ChildSubaccountData, markets: MarketsData ): SubaccountSummary { diff --git a/src/abacus-ts/rawTypes.ts b/src/abacus-ts/rawTypes.ts index 975ad639d..b76ab671b 100644 --- a/src/abacus-ts/rawTypes.ts +++ b/src/abacus-ts/rawTypes.ts @@ -11,6 +11,7 @@ import { } from '@/types/indexer/indexerManual'; export type MarketsData = { [marketId: string]: IndexerPerpetualMarketResponseObject }; +export type OrdersData = { [orderId: string]: IndexerCompositeOrderObject }; export type OrderbookData = { bids: { [price: string]: string }; @@ -28,7 +29,7 @@ export interface ParentSubaccountData { ephemeral: { tradingRewards?: IndexerHistoricalBlockTradingReward[]; fills?: IndexerCompositeFillObject[]; - orders?: { [orderId: string]: IndexerCompositeOrderObject }; + orders?: OrdersData; transfers?: IndexerTransferResponseObject[]; }; } diff --git a/src/abacus-ts/summaryTypes.ts b/src/abacus-ts/summaryTypes.ts index a2a2f9e8f..7af08164a 100644 --- a/src/abacus-ts/summaryTypes.ts +++ b/src/abacus-ts/summaryTypes.ts @@ -1,4 +1,9 @@ -import { IndexerPerpetualPositionResponseObject } from '@/types/indexer/indexerApiGen'; +import { + IndexerAPITimeInForce, + IndexerOrderSide, + IndexerOrderType, + IndexerPerpetualPositionResponseObject, +} from '@/types/indexer/indexerApiGen'; import { type BigNumber } from 'bignumber.js'; type ReplaceBigNumberInUnion = T extends string ? BigNumber : T; @@ -47,8 +52,10 @@ export type SubaccountPositionBase = ConvertStringToBigNumber< | 'exitPrice' >; +export type MarginMode = 'ISOLATED' | 'CROSS'; + export type SubaccountPositionDerivedCore = { - marginMode: 'ISOLATED' | 'CROSS'; + marginMode: MarginMode; signedSize: BigNumber; // indexer size is signed by default but we make it obvious here unsignedSize: BigNumber; // always positive @@ -80,3 +87,45 @@ export type SubaccountPositionDerivedExtra = { export type SubaccountPosition = SubaccountPositionBase & SubaccountPositionDerivedCore & SubaccountPositionDerivedExtra; + +export enum OrderStatus { + Canceled = 'CANCELED', + Canceling = 'BEST_EFFORT_CANCELED', + Filled = 'FILLED', + Open = 'OPEN', + Pending = 'PENDING', + Untriggered = 'UNTRIGGERED', + PartiallyFilled = 'PARTIALLY_FILLED', + PartiallyCanceled = 'PARTIALLY_CANCELED', +} + +export type SubaccountOrder = { + subaccountNumber: number; + id: string; + clientId: string | null; + type: IndexerOrderType; + side: IndexerOrderSide; + status: OrderStatus; + timeInForce: IndexerAPITimeInForce | null; + marketId: string; + displayId: string; + clobPairId: number | null; + orderFlags: string | null; + price: BigNumber; + triggerPrice: BigNumber | null; + trailingPercent: BigNumber | null; + 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; + postOnly: boolean; + reduceOnly: boolean; + cancelReason: string | null; + marginMode: MarginMode | null; +}; diff --git a/src/state/raw.ts b/src/state/raw.ts index a514699f7..df03eb04a 100644 --- a/src/state/raw.ts +++ b/src/state/raw.ts @@ -1,13 +1,10 @@ import { Loadable, loadableIdle } from '@/abacus-ts/lib/loadable'; -import { MarketsData, OrderbookData, ParentSubaccountData } from '@/abacus-ts/rawTypes'; +import { MarketsData, OrderbookData, OrdersData, ParentSubaccountData } from '@/abacus-ts/rawTypes'; import { IndexerHistoricalBlockTradingRewardsResponse, IndexerParentSubaccountTransferResponse, } from '@/types/indexer/indexerApiGen'; -import { - IndexerCompositeFillResponse, - IndexerCompositeOrderObject, -} from '@/types/indexer/indexerManual'; +import { IndexerCompositeFillResponse } from '@/types/indexer/indexerManual'; import { createSlice, PayloadAction } from '@reduxjs/toolkit'; export interface RawDataState { @@ -18,7 +15,7 @@ export interface RawDataState { account: { parentSubaccount: Loadable; fills: Loadable; - orders: Loadable<{ [id: string]: IndexerCompositeOrderObject }>; + orders: Loadable; transfers: Loadable; blockTradingRewards: Loadable; }; @@ -66,10 +63,7 @@ export const rawSlice = createSlice({ ) => { state.account.blockTradingRewards = action.payload; }, - setAccountOrdersRaw: ( - state, - action: PayloadAction> - ) => { + setAccountOrdersRaw: (state, action: PayloadAction>) => { state.account.orders = action.payload; }, },