Skip to content

Commit

Permalink
add market close all positions
Browse files Browse the repository at this point in the history
  • Loading branch information
aforaleka committed Sep 25, 2024
1 parent cc1ec27 commit ead0936
Show file tree
Hide file tree
Showing 16 changed files with 335 additions and 25 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
"@cosmjs/stargate": "^0.32.1",
"@cosmjs/tendermint-rpc": "^0.32.1",
"@datadog/browser-logs": "^5.23.3",
"@dydxprotocol/v4-abacus": "1.11.13",
"@dydxprotocol/v4-abacus": "1.11.16",
"@dydxprotocol/v4-client-js": "1.3.7",
"@dydxprotocol/v4-localization": "^1.1.203",
"@dydxprotocol/v4-proto": "^6.0.1",
Expand Down
8 changes: 4 additions & 4 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions src/constants/abacus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,8 @@ export const AbacusMarginMode = Abacus.exchange.dydx.abacus.output.input.MarginM

export type HumanReadablePlaceOrderPayload =
Abacus.exchange.dydx.abacus.state.manager.HumanReadablePlaceOrderPayload;
export type HumanReadableCloseAllPositionsPayload =
Abacus.exchange.dydx.abacus.state.manager.HumanReadableCloseAllPositionsPayload;
export type HumanReadableCancelOrderPayload =
Abacus.exchange.dydx.abacus.state.manager.HumanReadableCancelOrderPayload;
export type HumanReadableTriggerOrdersPayload =
Expand Down
2 changes: 2 additions & 0 deletions src/constants/dialogs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export type AdjustIsolatedMarginDialogProps = {
};
export type AdjustTargetLeverageDialogProps = {};
export type ClosePositionDialogProps = {};
export type CloseAllPositionsConfirmationDialogProps = {};
export type CancelAllOrdersConfirmationDialogProps = { marketId?: string };
export type CancelPendingOrdersDialogProps = { marketId: string };
export type ComplianceConfigDialogProps = {};
Expand Down Expand Up @@ -101,6 +102,7 @@ export const DialogTypes = unionize(
AdjustTargetLeverage: ofType<AdjustTargetLeverageDialogProps>(),
CancelAllOrdersConfirmation: ofType<CancelAllOrdersConfirmationDialogProps>(),
CancelPendingOrders: ofType<CancelPendingOrdersDialogProps>(),
CloseAllPositionsConfirmation: ofType<CloseAllPositionsConfirmationDialogProps>(),
ClosePosition: ofType<ClosePositionDialogProps>(),
ComplianceConfig: ofType<ComplianceConfigDialogProps>(),
ConfirmPendingDeposit: ofType<ConfirmPendingDepositDialogProps>(),
Expand Down
7 changes: 7 additions & 0 deletions src/constants/trade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,3 +195,10 @@ export type LocalCancelAllData = {
failedOrderIds?: string[];
errorParams?: ErrorParams;
};

export type LocalCloseAllPositionsData = {
submittedOrderClientIds: string[];
filledOrderClientIds: string[];
failedOrderClientIds: string[];
errorParams?: ErrorParams;
};
28 changes: 28 additions & 0 deletions src/hooks/useNotificationTypes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { Output, OutputType } from '@/components/Output';
// eslint-disable-next-line import/no-cycle
import { BlockRewardNotification } from '@/views/notifications/BlockRewardNotification';
import { CancelAllNotification } from '@/views/notifications/CancelAllNotification';
import { CloseAllPositionsNotification } from '@/views/notifications/CloseAllPositionsNotification';
import { IncentiveSeasonDistributionNotification } from '@/views/notifications/IncentiveSeasonDistributionNotification';
import { MarketLaunchTrumpwinNotification } from '@/views/notifications/MarketLaunchTrumpwinNotification';
import { OrderCancelNotification } from '@/views/notifications/OrderCancelNotification';
Expand All @@ -58,6 +59,7 @@ import { openDialog } from '@/state/dialogs';
import {
getLocalCancelAlls,
getLocalCancelOrders,
getLocalCloseAllPositions,
getLocalPlaceOrders,
} from '@/state/localOrdersSelectors';
import { getAbacusNotifications, getCustomNotifications } from '@/state/notificationsSelectors';
Expand Down Expand Up @@ -664,6 +666,7 @@ export const notificationTypes: NotificationTypeConfig[] = [
const localPlaceOrders = useAppSelector(getLocalPlaceOrders, shallowEqual);
const localCancelOrders = useAppSelector(getLocalCancelOrders, shallowEqual);
const localCancelAlls = useAppSelector(getLocalCancelAlls, shallowEqual);
const localCloseAllPositions = useAppSelector(getLocalCloseAllPositions, shallowEqual);

const allOrders = useAppSelector(getSubaccountOrders, shallowEqual);
const stringGetter = useStringGetter();
Expand Down Expand Up @@ -754,6 +757,31 @@ export const notificationTypes: NotificationTypeConfig[] = [
);
}
}, [localCancelAlls]);

useEffect(() => {
if (!localCloseAllPositions) return;
const localCloseAllKey = localCloseAllPositions.submittedOrderClientIds.join('-');
// eslint-disable-next-line no-restricted-syntax
trigger(
localCloseAllKey,
{
icon: null,
title: stringGetter({ key: STRING_KEYS.CLOSE_ALL_POSITIONS }),
toastSensitivity: 'background',
groupKey: localCloseAllKey,
toastDuration: DEFAULT_TOAST_AUTO_CLOSE_MS,
renderCustomBody: ({ isToast, notification }) => (
<CloseAllPositionsNotification
isToast={isToast}
localCloseAllPositions={localCloseAllPositions}
notification={notification}
/>
),
},
[localCloseAllPositions],
true
);
}, [localCloseAllPositions]);
},
useNotificationAction: () => {
const dispatch = useAppDispatch();
Expand Down
32 changes: 32 additions & 0 deletions src/hooks/useSubaccount.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
cancelOrderFailed,
cancelOrderSubmitted,
clearLocalOrders,
closeAllPositionsSubmitted,
placeOrderFailed,
placeOrderSubmitted,
} from '@/state/localOrders';
Expand Down Expand Up @@ -601,6 +602,36 @@ const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: LocalWall
[dispatch, orders]
);

const closeAllPositions = useCallback(() => {
// this is for each single close position / place order transaction
const callback = (
success: boolean,
parsingError?: Nullable<ParsingError>,
data?: Nullable<HumanReadablePlaceOrderPayload>
) => {
if (!success) {
const errorParams = getValidErrorParamsFromParsingError(parsingError);
if (data?.clientId !== undefined) {
dispatch(
placeOrderFailed({
clientId: data.clientId,
errorParams,
})
);
}
}
};

const payload = abacusStateManager.closeAllPositions(callback);
if (payload) {
dispatch(
closeAllPositionsSubmitted(
payload.payloads.toArray().map((orderPayload) => orderPayload.clientId)
)
);
}
}, [dispatch]);

// ------ Trigger Orders Methods ------ //
const placeTriggerOrders = useCallback(
async ({
Expand Down Expand Up @@ -892,6 +923,7 @@ const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: LocalWall
// Trading Methods
placeOrder,
closePosition,
closeAllPositions,
cancelOrder,
cancelAllOrders,
placeTriggerOrders,
Expand Down
4 changes: 4 additions & 0 deletions src/layout/DialogManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { AdjustIsolatedMarginDialog } from '@/views/dialogs/AdjustIsolatedMargin
import { AdjustTargetLeverageDialog } from '@/views/dialogs/AdjustTargetLeverageDialog';
import { CancelAllOrdersConfirmationDialog } from '@/views/dialogs/CancelAllOrdersConfirmationDialog';
import { CancelPendingOrdersDialog } from '@/views/dialogs/CancelPendingOrdersDialog';
import { CloseAllPositionsConfirmationDialog } from '@/views/dialogs/CloseAllPositionsConfirmationDialog';
import { ClosePositionDialog } from '@/views/dialogs/ClosePositionDialog';
import { ComplianceConfigDialog } from '@/views/dialogs/ComplianceConfigDialog';
import { ConfirmPendingDepositDialog } from '@/views/dialogs/ConfirmPendingDepositDialog';
Expand Down Expand Up @@ -69,6 +70,9 @@ export const DialogManager = () => {
AdjustIsolatedMargin: (args) => <AdjustIsolatedMarginDialog {...args} {...modalProps} />,
AdjustTargetLeverage: (args) => <AdjustTargetLeverageDialog {...args} {...modalProps} />,
ClosePosition: (args) => <ClosePositionDialog {...args} {...modalProps} />,
CloseAllPositionsConfirmation: (args) => (
<CloseAllPositionsConfirmationDialog {...args} {...modalProps} />
),
CancelAllOrdersConfirmation: (args) => (
<CancelAllOrdersConfirmationDialog {...args} {...modalProps} />
),
Expand Down
10 changes: 10 additions & 0 deletions src/lib/abacus/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
HistoricalTradingRewardsPeriod,
HistoricalTradingRewardsPeriods,
HumanReadableCancelOrderPayload,
HumanReadableCloseAllPositionsPayload,
HumanReadablePlaceOrderPayload,
HumanReadableSubaccountTransferPayload,
HumanReadableTriggerOrdersPayload,
Expand Down Expand Up @@ -402,6 +403,15 @@ class AbacusStateManager {
) => void
): Nullable<HumanReadablePlaceOrderPayload> => this.stateManager.commitClosePosition(callback);

closeAllPositions = (
callback: (
success: boolean,
parsingError: Nullable<ParsingError>,
data: Nullable<HumanReadablePlaceOrderPayload>
) => void
): Nullable<HumanReadableCloseAllPositionsPayload> =>
this.stateManager.closeAllPositions(callback);

cancelOrder = (
orderId: string,
callback: (
Expand Down
47 changes: 42 additions & 5 deletions src/state/localOrders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import _ from 'lodash';

import { Nullable, SubaccountFill, SubaccountOrder } from '@/constants/abacus';
import { AbacusOrderStatus, Nullable, SubaccountFill, SubaccountOrder } from '@/constants/abacus';
import { DEFAULT_SOMETHING_WENT_WRONG_ERROR_PARAMS, ErrorParams } from '@/constants/errors';
import {
CANCEL_ALL_ORDERS_KEY,
CancelOrderStatuses,
LocalCancelAllData,
LocalCancelOrderData,
LocalCloseAllPositionsData,
LocalPlaceOrderData,
PlaceOrderStatuses,
TradeTypes,
Expand All @@ -21,13 +22,15 @@ export interface LocalOrdersState {
localPlaceOrders: LocalPlaceOrderData[];
localCancelOrders: LocalCancelOrderData[];
localCancelAlls: Record<string, LocalCancelAllData>;
localCloseAllPositions?: LocalCloseAllPositionsData;
latestOrder?: Nullable<SubaccountOrder>;
}

const initialState: LocalOrdersState = {
localPlaceOrders: [],
localCancelOrders: [],
localCancelAlls: {},
localCloseAllPositions: undefined,
latestOrder: undefined,
};

Expand All @@ -39,12 +42,19 @@ export const localOrdersSlice = createSlice({
return initialState;
},
updateOrders: (state, action: PayloadAction<SubaccountOrder[]>) => {
const canceledOrderIdsInPayload = action.payload
.filter((order) => isOrderStatusCanceled(order.status))
.map((order) => order.id);
const { payload: orders } = action;
let { localCloseAllPositions } = state;
const canceledOrders = orders.filter((order) => isOrderStatusCanceled(order.status));
const filledOrderClientIds = orders
.filter((order) => order.status === AbacusOrderStatus.Filled)
.map((order) => order.clientId)
.filter(isTruthy);

if (!canceledOrderIdsInPayload) return state;
// no relevant cancel or filled orders
if (!canceledOrders.length && (!localCloseAllPositions || !filledOrderClientIds.length))
return state;

const canceledOrderIdsInPayload = canceledOrders.map((order) => order.id);
// ignore locally canceled orders since it's intentional and already handled
// by local cancel tracking and notification
const isOrderCanceledByBackend = (orderId: string) =>
Expand All @@ -56,6 +66,23 @@ export const localOrdersSlice = createSlice({
return _.uniq([...(batch.canceledOrderIds ?? []), ...newCanceledOrderIds]);
};

if (localCloseAllPositions) {
localCloseAllPositions = {
...localCloseAllPositions,
filledOrderClientIds: _.uniq([
..._.intersection(localCloseAllPositions.submittedOrderClientIds, filledOrderClientIds),
...localCloseAllPositions.filledOrderClientIds,
]),
failedOrderClientIds: _.uniq([
..._.intersection(
localCloseAllPositions.submittedOrderClientIds,
canceledOrders.map((order) => order.clientId)?.filter(isTruthy)
),
...localCloseAllPositions.failedOrderClientIds,
]),
};
}

return {
...state,
localPlaceOrders: state.localPlaceOrders.map((order) =>
Expand All @@ -77,6 +104,7 @@ export const localOrdersSlice = createSlice({
...batch,
canceledOrderIds: getNewCanceledOrderIds(batch),
})),
localCloseAllPositions,
};
},
updateFilledOrders: (state, action: PayloadAction<SubaccountFill[]>) => {
Expand Down Expand Up @@ -212,6 +240,13 @@ export const localOrdersSlice = createSlice({
updateCancelAllOrderIds(state, order.id, 'failedOrderIds', order.marketId);
if (errorParams) cancelOrderFailed({ orderId: order.id, errorParams });
},
closeAllPositionsSubmitted: (state, action: PayloadAction<string[]>) => {
state.localCloseAllPositions = {
submittedOrderClientIds: action.payload,
filledOrderClientIds: [],
failedOrderClientIds: [],
};
},
},
});

Expand All @@ -232,6 +267,8 @@ export const {

cancelAllSubmitted,
cancelAllOrderFailed,

closeAllPositionsSubmitted,
} = localOrdersSlice.actions;

// helper functions
Expand Down
6 changes: 6 additions & 0 deletions src/state/localOrdersSelectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ export const getLocalCancelOrders = (state: RootState) => state.localOrders.loca
*/
export const getLocalCancelAlls = (state: RootState) => state.localOrders.localCancelAlls;

/**
* @returns the local close all positions data for the current FE session
*/
export const getLocalCloseAllPositions = (state: RootState) =>
state.localOrders.localCloseAllPositions;

/**
* @returns whether the subaccount has uncommitted orders (local orders that are only submitted and not placed)
*/
Expand Down
34 changes: 34 additions & 0 deletions src/views/dialogs/CloseAllPositionsConfirmationDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { useCallback } from 'react';

import { ButtonAction, ButtonType } from '@/constants/buttons';
import { CloseAllPositionsConfirmationDialogProps, DialogProps } from '@/constants/dialogs';
import { STRING_KEYS } from '@/constants/localization';

import { useStringGetter } from '@/hooks/useStringGetter';
import { useSubaccount } from '@/hooks/useSubaccount';

import { Button } from '@/components/Button';
import { Dialog } from '@/components/Dialog';

export const CloseAllPositionsConfirmationDialog = ({
setIsOpen,
}: DialogProps<CloseAllPositionsConfirmationDialogProps>) => {
const stringGetter = useStringGetter();
const { closeAllPositions } = useSubaccount();

const onSubmit = useCallback(() => {
closeAllPositions();
setIsOpen?.(false);
}, [closeAllPositions, setIsOpen]);

return (
<Dialog isOpen setIsOpen={setIsOpen} title={stringGetter({ key: STRING_KEYS.CONFIRM })}>
<form onSubmit={onSubmit} tw="flex flex-col gap-0.75">
<div>{stringGetter({ key: STRING_KEYS.ARE_YOU_SURE_CLOSE_MULTI_POSITION_ALL })}</div>
<Button action={ButtonAction.Destroy} type={ButtonType.Submit} tw="w-full">
{stringGetter({ key: STRING_KEYS.CLOSE_ALL_POSITIONS })}
</Button>
</form>
</Dialog>
);
};
Loading

0 comments on commit ead0936

Please sign in to comment.