@orderly.network/ui-positions
Version:
1,556 lines (1,551 loc) • 178 kB
JavaScript
import { registerSimpleDialog, useModal, SimpleDialog, Flex, Text, Badge, Divider, CloseIcon, Button, ThrottledButton, useScreen, ArrowDownShortIcon, ArrowUpShortIcon, Grid, Statistic, ExclamationFillIcon, modal, Tooltip, DataTable, cn, ListView, SimpleSheet, usePagination, DataFilter, toast, formatAddress, Box, HoverCard, ArrowLeftRightIcon, capitalizeFirstLetter, ShareIcon, EditIcon, Input, inputFormatter, Select, PopoverRoot, PopoverTrigger, PopoverContent, Slider } from '@orderly.network/ui';
import React2, { createContext, useMemo, useState, useCallback, useContext, useEffect, useRef } from 'react';
import { i18n, useTranslation } from '@orderly.network/i18n';
import { OrderSide, OrderType, EMPTY_LIST, AccountStatusEnum, TrackerEventName, OrderStatus, AlgoOrderRootType, PositionType, AlgoOrderType } from '@orderly.network/types';
import { commifyOptional, Decimal, formatNum, getTimestamp, commify } from '@orderly.network/utils';
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
import { useLocalStorage, useSymbolsInfo, useMarkPrice, usePositionStream, useSubAccountMutation, usePrivateInfiniteQuery, useBoolean, useSessionStorage, useAccount, usePrivateQuery, useTrack, usePositionClose, useSWR, fetcher, useEventEmitter, useDebouncedCallback, useGetRwaSymbolOpenStatus, useTpslPriceChecker, useConfig, findPositionTPSLFromOrders, findTPSLFromOrder, useReferralInfo, useAccountInfo, useLeverageBySymbol, utils, useMaxLeverage } from '@orderly.network/hooks';
import { useDataTap, useOrderEntryFormErrorMsg } from '@orderly.network/react-app';
import { positions, account } from '@orderly.network/perp';
import { AuthGuardDataTable } from '@orderly.network/ui-connector';
import { SymbolLeverageDialogId, SymbolLeverageSheetId } from '@orderly.network/ui-leverage';
import { SharePnLDialogId, SharePnLBottomSheetId } from '@orderly.network/ui-share';
import { CloseToLiqPriceIcon, TPSLSheetId, TPSLDialogId, TPSLDetailSheetId, TPSLDetailDialogId } from '@orderly.network/ui-tpsl';
import { subDays, differenceInDays } from 'date-fns';
// src/index.ts
var ConfirmHeader = (props) => {
const { hideCloseIcon = false } = props;
return /* @__PURE__ */ jsxs("div", { className: "oui-relative oui-w-full oui-border-b oui-border-line-4 oui-pb-3", children: [
/* @__PURE__ */ jsx(Text, { size: "base", children: props.title }),
!hideCloseIcon && /* @__PURE__ */ jsx(
"button",
{
onClick: props.onClose,
className: "oui-absolute oui-right-0 oui-top-0 oui-p-2 oui-text-base-contrast-54 hover:oui-text-base-contrast-80",
children: /* @__PURE__ */ jsx(CloseIcon, { size: 18, color: "white" })
}
)
] });
};
var ConfirmFooter = (props) => {
const { t } = useTranslation();
return /* @__PURE__ */ jsxs(
Flex,
{
id: "oui-positions-confirm-footer",
gap: 2,
width: "100%",
className: "oui-mt-3 oui-pb-1",
children: [
/* @__PURE__ */ jsx(
Button,
{
id: "oui-positions-confirm-footer-cancel-button",
color: "secondary",
fullWidth: true,
onClick: props.onCancel,
size: "md",
children: t("common.cancel")
}
),
/* @__PURE__ */ jsx(
ThrottledButton,
{
id: "oui-positions-confirm-footer-confirm-button",
onClick: props.onConfirm,
fullWidth: true,
loading: props.submitting,
disabled: props.disabled,
size: "md",
children: t("common.confirm")
}
)
]
}
);
};
var OrderDetail = (props) => {
const { quantity, price, quoteDp, side } = props;
const { t } = useTranslation();
const total = useMemo(() => {
if (price && quantity) {
return new Decimal(price).mul(quantity).toFixed(quoteDp, Decimal.ROUND_DOWN);
}
return "--";
}, [price, quantity]);
return /* @__PURE__ */ jsxs(
Flex,
{
direction: "column",
gap: 1,
width: "100%",
className: "oui-text-sm oui-text-base-contrast-54",
py: 5,
children: [
/* @__PURE__ */ jsxs(Flex, { justify: "between", width: "100%", gap: 1, children: [
/* @__PURE__ */ jsx(Text, { children: t("common.qty") }),
/* @__PURE__ */ jsx(Text, { color: side === OrderSide.BUY ? "success" : "danger", children: quantity })
] }),
/* @__PURE__ */ jsxs(Flex, { justify: "between", width: "100%", gap: 1, children: [
/* @__PURE__ */ jsx(Text, { children: t("common.price") }),
/* @__PURE__ */ jsx(
Text.formatted,
{
intensity: 98,
suffix: /* @__PURE__ */ jsx(Text, { intensity: 54, children: "USDC" }),
children: price
}
)
] }),
/* @__PURE__ */ jsxs(Flex, { justify: "between", width: "100%", gap: 1, children: [
/* @__PURE__ */ jsx(Text, { children: t("common.notional") }),
/* @__PURE__ */ jsx(
Text.formatted,
{
intensity: 98,
suffix: /* @__PURE__ */ jsx(Text, { intensity: 54, children: "USDC" }),
children: total
}
)
] })
]
}
);
};
var MarketCloseConfirm = (props) => {
const { t } = useTranslation();
const onCancel = () => {
const func = props?.onClose ?? props.close;
func?.();
};
return /* @__PURE__ */ jsxs(Flex, { direction: "column", className: props.classNames?.root, children: [
/* @__PURE__ */ jsx(
ConfirmHeader,
{
onClose: onCancel,
title: t("positions.marketClose"),
hideCloseIcon: props.hideCloseIcon
}
),
/* @__PURE__ */ jsx(Text, { intensity: 54, size: "sm", className: "oui-my-5", children: t("positions.marketClose.description", {
quantity: commifyOptional(props.quantity),
base: props.base
}) }),
/* @__PURE__ */ jsx(
ConfirmFooter,
{
onCancel,
onConfirm: async () => {
await props.onConfirm?.();
onCancel();
},
submitting: props.submitting
}
)
] });
};
var LimitConfirmDialog = (props) => {
const { order, quoteDp, quantity, price } = props;
const { side } = order;
const { t } = useTranslation();
const onCancel = () => {
props.onClose?.();
};
return /* @__PURE__ */ jsxs(Fragment, { children: [
/* @__PURE__ */ jsx(
ConfirmHeader,
{
onClose: onCancel,
title: t("positions.limitClose"),
hideCloseIcon: props.hideCloseIcon
}
),
/* @__PURE__ */ jsx(Box, { mt: 5, children: /* @__PURE__ */ jsx(Text, { intensity: 54, size: "sm", children: t("positions.limitClose.description", {
quantity: commify(props.quantity),
base: props.base
}) }) }),
/* @__PURE__ */ jsxs(Flex, { gap: 2, mb: 4, mt: 5, justify: "between", children: [
/* @__PURE__ */ jsx(
Text.formatted,
{
rule: "symbol",
formatString: "base-type",
size: "base",
showIcon: true,
children: order.symbol
}
),
/* @__PURE__ */ jsxs(Flex, { gap: 1, children: [
/* @__PURE__ */ jsx(Badge, { color: "neutral", size: "xs", children: t("orderEntry.orderType.limit") }),
/* @__PURE__ */ jsx(
Badge,
{
color: side === OrderSide.BUY ? "success" : "danger",
size: "xs",
children: side === OrderSide.BUY ? t("common.buy") : t("common.sell")
}
)
] })
] }),
/* @__PURE__ */ jsx(Divider, { className: "oui-w-full" }),
/* @__PURE__ */ jsx(
OrderDetail,
{
className: "oui-text-sm",
price,
quantity,
side: order.side,
quoteDp: quoteDp ?? 2
}
),
/* @__PURE__ */ jsx(
ConfirmFooter,
{
onCancel,
onConfirm: props.onConfirm,
submitting: props.submitting
}
)
] });
};
var PositionsRowContext = createContext(
{}
);
var usePositionsRowContext = () => {
return useContext(PositionsRowContext);
};
function useEndReached(sentinelRef, onEndReached) {
const observer = useRef();
const cb = useRef(onEndReached);
cb.current = onEndReached;
useEffect(() => {
const options = {
root: null,
rootMargin: "0px",
threshold: 0
};
const handleObserver = (entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
cb.current?.();
}
});
};
observer.current = new IntersectionObserver(handleObserver, options);
return () => {
observer.current?.disconnect();
};
}, []);
useEffect(() => {
observer.current?.observe(sentinelRef.current);
}, []);
}
var EndReachedBox = (props) => {
const sentinelRef = useRef(null);
const { onEndReached } = props;
useEndReached(sentinelRef, () => {
onEndReached?.();
});
return /* @__PURE__ */ jsxs(Fragment, { children: [
props.children,
/* @__PURE__ */ jsx(
"div",
{
ref: sentinelRef,
className: "oui-relative oui-invisible oui-h-[25px] oui-bg-red-400 oui-top-[-300px]"
}
)
] });
};
var PAGE_SIZE = 60;
var FundingFeeHistoryUI = ({ total, symbol, start_t, end_t }) => {
const { t } = useTranslation();
const { isMobile } = useScreen();
const { isLoading, data, setSize } = usePrivateInfiniteQuery(
(pageIndex, previousPageData) => {
if ((!previousPageData || (previousPageData.length ?? 0) < PAGE_SIZE) && pageIndex > 0)
return null;
return `/v1/funding_fee/history?page=${pageIndex + 1}&size=${PAGE_SIZE}&symbol=${symbol}&start_t=${start_t}&end_t=${end_t}`;
},
{
revalidateFirstPage: false
}
);
const loadMore = useCallback(() => {
setSize((prev) => {
return prev + 1;
});
}, [setSize]);
const flattenData = useMemo(() => {
if (!Array.isArray(data))
return [];
return data.flat().map((item) => {
return {
...item,
funding_fee: -item.funding_fee
};
});
}, [data]);
const listView = useMemo(() => {
if (isMobile) {
return /* @__PURE__ */ jsx(
HistoryDataListViewSimple,
{
data: flattenData ?? EMPTY_LIST,
isLoading,
loadMore
}
);
}
return /* @__PURE__ */ jsx(
HistoryDataListView,
{
data: flattenData ?? EMPTY_LIST,
isLoading,
loadMore
}
);
}, [isMobile, flattenData, isLoading]);
return /* @__PURE__ */ jsxs("div", { children: [
/* @__PURE__ */ jsxs(
Grid,
{
cols: 2,
gapX: 3,
className: "oui-sticky oui-top-0 oui-z-10 oui-bg-base-8 oui-py-4",
children: [
/* @__PURE__ */ jsx("div", { className: "oui-rounded-lg oui-border oui-border-line-6 oui-bg-base-9 oui-p-3", children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 1, itemAlign: "start", children: [
/* @__PURE__ */ jsx("span", { className: "oui-text-2xs oui-text-base-contrast-36", children: t("common.symbol") }),
/* @__PURE__ */ jsx(
Text.formatted,
{
rule: "symbol",
className: "oui-font-semibold",
intensity: 98,
children: symbol
}
)
] }) }),
/* @__PURE__ */ jsx("div", { className: "oui-rounded-lg oui-border oui-border-line-6 oui-bg-base-9 oui-p-3", children: /* @__PURE__ */ jsx(
Statistic,
{
label: isMobile ? /* @__PURE__ */ jsx(
FundingFeeLabelButton,
{
label: `${t("funding.fundingFee")} (USDC)`,
tooltip: t("positions.fundingFee.tooltip"),
size: 14
}
) : /* @__PURE__ */ jsx(
FundingFeeLabel,
{
label: `${t("funding.fundingFee")} (USDC)`,
tooltip: t("positions.fundingFee.tooltip"),
size: 14
}
),
valueProps: {
ignoreDP: true,
coloring: true,
showIdentifier: true
},
children: total
}
) })
]
}
),
listView
] });
};
var FundingFeeLabelButton = ({ label, tooltip, size }) => {
const { t } = useTranslation();
return /* @__PURE__ */ jsxs("div", { className: "oui-flex oui-items-center oui-gap-1", children: [
/* @__PURE__ */ jsx("span", { children: label }),
/* @__PURE__ */ jsx(
"button",
{
className: "oui-flex oui-items-center",
onClick: () => {
modal.alert({
message: tooltip,
title: t("positions.fundingFee.title")
});
},
children: /* @__PURE__ */ jsx(
ExclamationFillIcon,
{
className: "oui-cursor-pointer oui-text-base-contrast-54",
size
}
)
}
)
] });
};
var FundingFeeLabel = ({
label,
tooltip,
size
}) => {
return /* @__PURE__ */ jsxs("div", { className: "oui-flex oui-items-center oui-gap-1", children: [
/* @__PURE__ */ jsx("span", { children: label }),
/* @__PURE__ */ jsx(Tooltip, { content: /* @__PURE__ */ jsx("div", { className: "oui-w-64", children: tooltip }), children: /* @__PURE__ */ jsx(
ExclamationFillIcon,
{
className: "oui-cursor-pointer oui-text-base-contrast-54",
size
}
) })
] });
};
var HistoryDataListView = ({ isLoading, data, loadMore }) => {
const { t } = useTranslation();
const columns = useMemo(() => {
return [
{
title: t("common.time"),
dataIndex: "created_time",
width: 120,
render: (value) => {
return /* @__PURE__ */ jsx(Text.formatted, { rule: "date", children: value });
}
},
{
title: /* @__PURE__ */ jsx(
FundingFeeLabel,
{
label: t("funding.fundingRate"),
tooltip: t("positions.fundingRate.tooltip"),
size: 12
}
),
dataIndex: "funding_rate",
formatter: (value) => new Decimal(value).mul(100).toString(),
render: (value) => {
return /* @__PURE__ */ jsx("span", { children: `${value}%` });
}
},
{
title: t("funding.paymentType"),
dataIndex: "payment_type",
formatter: (value) => {
return value === "Pay" ? t("funding.paymentType.paid") : t("funding.paymentType.received");
},
render: (value) => /* @__PURE__ */ jsx("span", { children: value })
},
{
title: `${t("funding.fundingFee")} (USDC)`,
dataIndex: "funding_fee",
render: (value) => {
return /* @__PURE__ */ jsx(Text.numeral, { rule: "price", coloring: true, showIdentifier: true, ignoreDP: true, children: value });
}
}
];
}, [t]);
return /* @__PURE__ */ jsx("div", { className: "oui-h-[calc(80vh_-_132px_-_8px)] oui-overflow-y-auto", children: /* @__PURE__ */ jsx(EndReachedBox, { onEndReached: loadMore, children: /* @__PURE__ */ jsx(
DataTable,
{
classNames: {
root: cn("oui-h-auto oui-bg-base-8 oui-text-sm")
},
columns,
dataSource: data ?? EMPTY_LIST,
loading: isLoading
}
) }) });
};
var HistoryDataListViewSimple = ({
data,
isLoading,
loadMore
}) => {
const renderItem = useCallback((item) => {
return /* @__PURE__ */ jsx(FundingFeeItem, { item });
}, []);
return /* @__PURE__ */ jsx("div", { className: "oui-h-[calc(80vh_-_104px)] oui-overflow-y-auto", children: /* @__PURE__ */ jsx(
ListView,
{
dataSource: data,
renderItem,
isLoading,
contentClassName: "oui-space-y-0",
loadMore
}
) });
};
var FundingFeeItem = ({ item }) => {
const { t } = useTranslation();
return /* @__PURE__ */ jsxs("div", { className: "oui-flex oui-flex-col oui-space-y-2 oui-border-t oui-border-line-6 oui-py-2", children: [
/* @__PURE__ */ jsxs(Flex, { justify: "between", children: [
/* @__PURE__ */ jsx(
Statistic,
{
label: /* @__PURE__ */ jsx(
FundingFeeLabelButton,
{
label: t("funding.fundingRate"),
tooltip: t("positions.fundingRate.tooltip"),
size: 12
}
),
classNames: {
label: "oui-text-2xs"
},
valueProps: {
ignoreDP: true,
rule: "percentages",
className: "oui-text-xs"
},
children: item.funding_rate
}
),
/* @__PURE__ */ jsx(
Statistic,
{
label: t("common.amount"),
className: "oui-items-end",
classNames: {
label: "oui-text-2xs"
},
valueProps: {
ignoreDP: true,
coloring: true,
as: "div",
className: "oui-text-xs",
showIdentifier: true
},
children: item.funding_fee
}
)
] }),
/* @__PURE__ */ jsxs(Flex, { justify: "between", children: [
/* @__PURE__ */ jsx(
Text.formatted,
{
rule: "date",
className: "oui-text-base-contrast-36",
size: "2xs",
children: item.created_time
}
),
/* @__PURE__ */ jsx(Text, { size: "sm", intensity: 80, children: item.payment_type === "Pay" ? t("funding.paymentType.paid") : t("funding.paymentType.received") })
] })
] });
};
var FundingFeeButton = ({ fee, symbol, start_t, end_t }) => {
const { t } = useTranslation();
const [isOpen, { setTrue, setFalse }] = useBoolean(false);
const { isMobile } = useScreen();
return /* @__PURE__ */ jsxs(Fragment, { children: [
/* @__PURE__ */ jsx("button", { onClick: setTrue, children: /* @__PURE__ */ jsx(
Text.numeral,
{
rule: "price",
coloring: true,
showIdentifier: true,
ignoreDP: true,
className: "oui-border-b oui-border-line-16 oui-border-dashed oui-py-0.5",
children: fee
}
) }),
isMobile ? /* @__PURE__ */ jsx(
SimpleSheet,
{
open: isOpen,
onOpenChange: setFalse,
title: t("funding.fundingFee"),
classNames: {
body: "oui-max-h-[80vh] oui-py-0"
},
children: /* @__PURE__ */ jsx(
FundingFeeHistoryUI,
{
total: fee,
symbol,
start_t,
end_t
}
)
}
) : /* @__PURE__ */ jsx(
SimpleDialog,
{
open: isOpen,
onOpenChange: setFalse,
title: t("funding.fundingFee"),
classNames: {
content: "lg:oui-max-w-[640px]",
body: "oui-max-h-[80vh] oui-bg-base-8 lg:oui-py-0"
},
children: /* @__PURE__ */ jsx(
FundingFeeHistoryUI,
{
total: fee,
symbol,
start_t,
end_t
}
)
}
)
] });
};
var calculatePositions = (positions2, symbolsInfo, accountInfo, tpslOrders) => {
return positions2.map((item) => {
const info = symbolsInfo[item.symbol];
const notional = positions.notional(item.position_qty, item.mark_price);
const account2 = accountInfo.find(
(acc) => acc.account_id === item.account_id
);
const baseMMR = info?.("base_mmr");
const baseIMR = info?.("base_imr");
if (!baseMMR || !baseIMR) {
return item;
}
const MMR = positions.MMR({
baseMMR,
baseIMR,
IMRFactor: account2?.imr_factor[item.symbol] ?? 0,
positionNotional: notional,
IMR_factor_power: 4 / 5
});
const mm = positions.maintenanceMargin({
positionQty: item.position_qty,
markPrice: item.mark_price,
MMR
});
const unrealPnl = positions.unrealizedPnL({
qty: item.position_qty,
openPrice: item?.average_open_price,
markPrice: item.mark_price
});
const maxLeverage = item.leverage || 1;
const imr = account.IMR({
maxLeverage,
baseIMR,
IMR_Factor: account2?.imr_factor[item.symbol] ?? 0,
positionNotional: notional,
ordersNotional: 0,
IMR_factor_power: 4 / 5
});
const unrealPnlROI = positions.unrealizedPnLROI({
positionQty: item.position_qty,
openPrice: item.average_open_price,
IMR: imr,
unrealizedPnL: unrealPnl
});
let unrealPnl_index = 0;
let unrealPnlROI_index = 0;
if (item.index_price) {
unrealPnl_index = positions.unrealizedPnL({
qty: item.position_qty,
openPrice: item?.average_open_price,
markPrice: item.index_price
});
unrealPnlROI_index = positions.unrealizedPnLROI({
positionQty: item.position_qty,
openPrice: item.average_open_price,
IMR: imr,
unrealizedPnL: unrealPnl_index
});
}
const filteredTPSLOrders = tpslOrders.filter(
(tpslOrder) => tpslOrder.account_id === item.account_id
);
const tpsl = formatTPSL(filteredTPSLOrders, item.symbol);
return {
...item,
...tpsl,
mmr: MMR,
mm,
notional,
unrealized_pnl: unrealPnl,
unrealized_pnl_ROI: unrealPnlROI,
unrealized_pnl_ROI_index: unrealPnlROI_index
};
});
};
function formatTPSL(tpslOrders, symbol) {
if (Array.isArray(tpslOrders) && tpslOrders.length) {
const { fullPositionOrder, partialPositionOrders } = findPositionTPSLFromOrders(tpslOrders, symbol);
const full_tp_sl = fullPositionOrder ? findTPSLFromOrder(fullPositionOrder) : void 0;
const partialPossitionOrder = partialPositionOrders && partialPositionOrders.length ? partialPositionOrders[0] : void 0;
const partial_tp_sl = partialPossitionOrder ? findTPSLFromOrder(partialPossitionOrder) : void 0;
return {
full_tp_sl: {
tp_trigger_price: full_tp_sl?.tp_trigger_price,
sl_trigger_price: full_tp_sl?.sl_trigger_price,
algo_order: fullPositionOrder
},
partial_tp_sl: {
order_num: partialPositionOrders?.length ?? 0,
tp_trigger_price: partial_tp_sl?.tp_trigger_price,
sl_trigger_price: partial_tp_sl?.sl_trigger_price,
algo_order: partialPossitionOrder
}
};
}
}
var signatureMiddleware = (account2, accountId) => {
const apiBaseUrl = useConfig("apiBaseUrl");
return (useSWRNext) => {
return (key, fetcher2, config) => {
try {
const extendedFetcher = async (args) => {
const url = Array.isArray(args) ? args[0] : args;
const fullUrl = `${apiBaseUrl}${url}`;
const signer = account2.signer;
const payload = { method: "GET", url };
const signature = await signer.sign(payload, getTimestamp());
const ids = Array.isArray(accountId) ? accountId : [accountId];
return Promise.all(
ids.map((id) => {
return fetcher2(fullUrl, {
headers: {
...signature,
"orderly-account-id": id
}
});
})
);
};
return useSWRNext(key, extendedFetcher, config);
} catch (e) {
throw e;
}
};
};
};
var useSubAccountQuery = (query, options) => {
const { formatter, accountId, ...swrOptions } = options || {};
const { state, account: account2 } = useAccount();
const middleware = Array.isArray(options?.use) ? options?.use ?? [] : [];
const ids = Array.isArray(accountId) ? accountId : [accountId];
const shouldFetch = ids.filter(Boolean).length && (state.status >= AccountStatusEnum.EnableTrading || state.status === AccountStatusEnum.EnableTradingWithoutConnected);
return useSWR(
() => shouldFetch ? [query, ids] : null,
(url, init) => {
return fetcher(url, init, { formatter });
},
{
...swrOptions,
use: [...middleware, signatureMiddleware(account2, ids)],
onError: () => {
}
}
);
};
function useSubAccountTPSL(subAccountIds) {
const ee = useEventEmitter();
const { data: algoOrdersResponse, mutate: mutateTPSLOrders } = useSubAccountQuery(
`/v1/algo/orders?size=100&page=1&status=${OrderStatus.INCOMPLETE}`,
{
accountId: subAccountIds,
formatter: (data) => data,
revalidateOnFocus: false
}
);
const tpslOrders = useMemo(() => {
if (!Array.isArray(algoOrdersResponse)) {
return [];
}
const algoOrders = algoOrdersResponse?.map(
(item, index) => item.rows.map((order) => ({
...order,
account_id: subAccountIds[index]
}))
)?.flat();
return algoOrders?.filter(
(order) => [AlgoOrderRootType.POSITIONAL_TP_SL, AlgoOrderRootType.TP_SL].includes(
order.algo_type
)
);
}, [algoOrdersResponse, subAccountIds]);
const refresh = useDebouncedCallback(() => {
mutateTPSLOrders();
}, 200);
useEffect(() => {
const handler = (position) => {
if (position.account_id) {
refresh();
}
};
ee.on("tpsl:updateOrder", handler);
return () => {
ee.off("tpsl:updateOrder", handler);
};
}, [ee]);
return { tpslOrders, mutateTPSLOrders };
}
// src/components/positions/combinePositions.script.ts
var useCombinePositionsScript = (props) => {
const {
symbol,
calcMode,
includedPendingOrder,
pnlNotionalDecimalPrecision,
sharePnLConfig,
onSymbolChange,
selectedAccount
} = props;
const { pagination, setPage } = usePagination({ pageSize: 50 });
const symbolsInfo = useSymbolsInfo();
const { state } = useAccount();
const [mainAccountPositions, , { isLoading }] = usePositionStream(symbol, {
calcMode,
includedPendingOrder
});
const {
data: newPositions = [],
isLoading: isPositionLoading,
mutate: mutatePositions
} = usePrivateQuery("/v1/client/aggregate/positions", {
errorRetryCount: 3
});
const { allAccountIds, subAccountIds } = useMemo(() => {
const uniqueIds = new Set(
newPositions.filter((item) => item.account_id).map((item) => item.account_id).filter(Boolean)
);
const allAccountIds2 = Array.from(uniqueIds);
const subAccountIds2 = allAccountIds2.filter(
(item) => item !== state.mainAccountId
);
return { allAccountIds: allAccountIds2, subAccountIds: subAccountIds2 };
}, [newPositions, state.mainAccountId]);
const { data: accountInfo = [], isLoading: isAccountInfoLoading } = useSubAccountQuery("/v1/client/info", {
accountId: allAccountIds,
revalidateOnFocus: false
});
const { tpslOrders, mutateTPSLOrders } = useSubAccountTPSL(subAccountIds);
const subAccountPositions = useMemo(() => {
return calculatePositions(
newPositions.filter((item) => item.account_id !== state.mainAccountId),
symbolsInfo,
accountInfo,
tpslOrders
);
}, [newPositions, symbolsInfo, accountInfo, state.mainAccountId, tpslOrders]);
const allPositions = useMemo(() => {
return [...mainAccountPositions?.rows, ...subAccountPositions].filter(
(item) => item.position_qty !== 0
);
}, [mainAccountPositions, subAccountPositions]);
const dataSource = useDataTap(allPositions) ?? [];
const filtered = useMemo(() => {
if (!selectedAccount || selectedAccount === "All accounts" /* ALL */) {
return dataSource;
}
return dataSource.filter((item) => {
if (selectedAccount === "Main accounts" /* MAIN */) {
return item.account_id === state.mainAccountId || !item.account_id;
} else {
return item.account_id === selectedAccount;
}
});
}, [dataSource, selectedAccount, state.mainAccountId]);
const groupDataSource = useMemo(() => {
return groupDataByAccount(filtered, {
mainAccountId: state.mainAccountId,
subAccounts: state.subAccounts
});
}, [filtered, state.mainAccountId, state.subAccounts]);
const loading = isLoading || isPositionLoading || isAccountInfoLoading;
useEffect(() => {
setPage(1);
}, [symbol]);
const mutateList = useCallback(() => {
mutatePositions();
mutateTPSLOrders();
}, []);
return {
tableData: groupDataSource,
isLoading: loading,
pnlNotionalDecimalPrecision,
sharePnLConfig,
symbol,
onSymbolChange,
pagination,
mutatePositions: mutateList
};
};
var groupDataByAccount = (data, options) => {
const { mainAccountId = "", subAccounts = [] } = options;
const map = /* @__PURE__ */ new Map();
for (const item of data) {
const accountId = item.account_id || mainAccountId;
const findSubAccount = subAccounts.find((item2) => item2.id === accountId);
if (map.has(accountId)) {
map.get(accountId)?.children?.push(item);
} else {
map.set(accountId, {
account_id: accountId,
description: accountId === mainAccountId ? i18n.t("common.mainAccount") : findSubAccount?.description || formatAddress(findSubAccount?.id || ""),
children: [item]
});
}
}
return {
expanded: Array.from(map.keys()),
dataSource: Array.from(map.values())
};
};
// src/constants.ts
var TRADING_POSITIONS_SORT_STORAGE_KEY = "orderly_trading_positions_sort";
var LIQ_DISTANCE_THRESHOLD = 10;
var compareValues = (aValue, bValue) => {
if (aValue == null && bValue == null)
return 0;
if (aValue == null)
return 1;
if (bValue == null)
return -1;
const aStr = String(aValue).trim();
const bStr = String(bValue).trim();
const aNum = Number(aStr);
const bNum = Number(bStr);
const aIsNumber = !isNaN(aNum) && isFinite(aNum) && /^-?\d*\.?\d+([eE][+-]?\d+)?$/.test(aStr);
const bIsNumber = !isNaN(bNum) && isFinite(bNum) && /^-?\d*\.?\d+([eE][+-]?\d+)?$/.test(bStr);
if (aIsNumber && bIsNumber) {
return aNum - bNum;
}
const aIsDate = /^\d{4}-\d{2}-\d{2}/.test(aStr) || /^\d{13}$/.test(aStr);
const bIsDate = /^\d{4}-\d{2}-\d{2}/.test(bStr) || /^\d{13}$/.test(bStr);
if (aIsDate && bIsDate) {
const aDate = new Date(aValue);
const bDate = new Date(bValue);
if (!isNaN(aDate.getTime()) && !isNaN(bDate.getTime())) {
return aDate.getTime() - bDate.getTime();
}
}
return aStr.localeCompare(bStr, void 0, {
sensitivity: "base",
numeric: false
});
};
function sortList(list, sort) {
const { sortKey, sortOrder } = sort || {};
const sortedList = [...list || []];
if (sortKey && sortOrder) {
sortedList.sort((a, b) => {
const comparison = compareValues(a[sortKey], b[sortKey]);
return sortOrder === "desc" ? -comparison : comparison;
});
}
return sortedList;
}
function useSort(initialSort, onSortChange) {
const [sort, setSort] = useState(initialSort);
const onSort = useCallback((options) => {
const nextSort = options ? {
sortKey: options.sortKey,
sortOrder: options.sort
} : void 0;
setSort(nextSort);
onSortChange?.(nextSort);
}, []);
const getSortedList = useCallback(
(list) => sortList(list, sort),
[sort]
);
return {
sort,
onSort,
getSortedList
};
}
var OrderInfoCard = ({
title,
side,
leverage,
qty,
baseDp,
estLiqPrice,
estPnL,
pnlNotionalDecimalPrecision = 2,
className
}) => {
const { t } = useTranslation();
return /* @__PURE__ */ jsxs(
Flex,
{
direction: "column",
gap: 3,
itemAlign: "start",
className: `oui-bg-base-6 oui-rounded-lg oui-p-3 oui-w-full oui-font-weight-semibold ${className || ""}`,
children: [
/* @__PURE__ */ jsxs(Flex, { justify: "between", itemAlign: "center", gap: 2, children: [
/* @__PURE__ */ jsx(Text, { size: "sm", weight: "semibold", intensity: 98, children: title }),
/* @__PURE__ */ jsxs(Flex, { itemAlign: "center", gap: 1, children: [
/* @__PURE__ */ jsx(Badge, { color: side === OrderSide.SELL ? "sell" : "buy", size: "xs", children: side === OrderSide.SELL ? t("common.sell") : t("common.buy") }),
/* @__PURE__ */ jsxs(Badge, { color: "neutral", size: "xs", children: [
leverage,
"X"
] })
] })
] }),
/* @__PURE__ */ jsxs(
Flex,
{
direction: "column",
justify: "between",
itemAlign: "center",
width: "100%",
gap: 1,
children: [
/* @__PURE__ */ jsxs(
Flex,
{
direction: "row",
justify: "between",
itemAlign: "center",
width: "100%",
gap: 1,
children: [
/* @__PURE__ */ jsx(Text, { size: "sm", intensity: 54, children: t("common.qty") }),
/* @__PURE__ */ jsx(Text.numeral, { dp: baseDp, size: "sm", color: "danger", padding: false, children: qty })
]
}
),
/* @__PURE__ */ jsxs(
Flex,
{
direction: "row",
justify: "between",
itemAlign: "center",
width: "100%",
gap: 1,
children: [
/* @__PURE__ */ jsx(Text, { size: "sm", intensity: 54, children: t("common.price") }),
/* @__PURE__ */ jsx(Text, { size: "sm", weight: "semibold", children: t("common.market") })
]
}
),
estPnL && /* @__PURE__ */ jsxs(
Flex,
{
direction: "row",
justify: "between",
itemAlign: "center",
width: "100%",
gap: 1,
children: [
/* @__PURE__ */ jsx(Text, { size: "sm", intensity: 54, children: t("orderEntry.estPnL") }),
/* @__PURE__ */ jsx(
Text.pnl,
{
dp: pnlNotionalDecimalPrecision,
coloring: true,
size: "sm",
weight: "semibold",
children: estPnL
}
)
]
}
),
estLiqPrice && /* @__PURE__ */ jsxs(
Flex,
{
direction: "row",
justify: "between",
itemAlign: "center",
width: "100%",
gap: 1,
children: [
/* @__PURE__ */ jsx(Text, { size: "sm", intensity: 54, children: t("orderEntry.estLiqPrice") }),
/* @__PURE__ */ jsx(Text, { size: "sm", weight: "semibold", children: estLiqPrice })
]
}
)
]
}
)
]
}
);
};
var ReversePosition = (props) => {
const {
displayInfo,
validationError,
className,
style,
onConfirm,
onCancel
} = props;
const { t } = useTranslation();
if (!displayInfo) {
return null;
}
const {
symbol,
base,
quote,
baseDp,
quoteDp,
positionQty,
reverseQty,
markPrice,
leverage,
isLong,
unrealizedPnL,
pnlNotionalDecimalPrecision
} = displayInfo;
const closeSide = !isLong ? OrderSide.SELL : OrderSide.BUY;
const openSide = isLong ? OrderSide.SELL : OrderSide.BUY;
const closeAction = isLong ? t("positions.reverse.marketCloseLong") : t("positions.reverse.marketCloseShort");
const openAction = openSide === OrderSide.BUY ? t("positions.reverse.marketOpenLong") : t("positions.reverse.marketOpenShort");
const reverseTo = isLong ? t("positions.reverse.reverseToShort") : t("positions.reverse.reverseToLong");
const reverseToIcon = isLong ? /* @__PURE__ */ jsx(ArrowDownShortIcon, { size: 16, color: "danger", opacity: 1 }) : /* @__PURE__ */ jsx(ArrowUpShortIcon, { size: 16, color: "success", opacity: 1 });
const priceLabel = /* @__PURE__ */ jsxs(Flex, { itemAlign: "center", gap: 1, children: [
/* @__PURE__ */ jsx(Text.numeral, { dp: quoteDp, size: "sm", intensity: 80, children: markPrice }),
/* @__PURE__ */ jsx(Text, { size: "sm", intensity: 36, children: quote })
] });
const showBelowMinError = validationError === "belowMin";
return /* @__PURE__ */ jsxs(
Flex,
{
direction: "column",
className,
style,
gap: 4,
width: "100%",
children: [
/* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 2, width: "100%", children: [
/* @__PURE__ */ jsxs(Flex, { justify: "between", itemAlign: "center", width: "100%", children: [
/* @__PURE__ */ jsx(
Text.formatted,
{
size: "base",
weight: "semibold",
rule: "symbol",
formatString: "base-type",
intensity: 98,
showIcon: true,
children: symbol
}
),
/* @__PURE__ */ jsx(Badge, { color: isLong ? "sell" : "buy", size: "xs", children: reverseTo })
] }),
/* @__PURE__ */ jsxs(Flex, { justify: "between", itemAlign: "center", width: "100%", children: [
/* @__PURE__ */ jsx(Text, { size: "sm", intensity: 54, children: t("common.markPrice") }),
priceLabel
] })
] }),
/* @__PURE__ */ jsx(Divider, { intensity: 4, className: "oui-w-full" }),
/* @__PURE__ */ jsx(
OrderInfoCard,
{
title: closeAction,
side: closeSide,
leverage,
qty: positionQty,
baseDp,
estPnL: unrealizedPnL?.toString(),
pnlNotionalDecimalPrecision
}
),
/* @__PURE__ */ jsxs(Flex, { direction: "row", itemAlign: "center", width: "100%", children: [
/* @__PURE__ */ jsx(Divider, { intensity: 8, className: "oui-w-full" }),
/* @__PURE__ */ jsxs(Flex, { className: "oui-px-4 oui-py-[3px] oui-border oui-border-base-contrast-12 oui-rounded-full oui-shrink-0", children: [
reverseToIcon,
/* @__PURE__ */ jsx(Text, { size: "2xs", color: isLong ? "danger" : "success", children: reverseTo })
] }),
/* @__PURE__ */ jsx(Divider, { intensity: 8, className: "oui-w-full" })
] }),
/* @__PURE__ */ jsx(
OrderInfoCard,
{
title: openAction,
side: openSide,
leverage,
qty: reverseQty,
baseDp
}
),
showBelowMinError ? /* @__PURE__ */ jsx(Text, { size: "2xs", color: "danger", weight: "semibold", children: t("positions.reverse.error.belowMin") }) : /* @__PURE__ */ jsx(Text, { size: "2xs", color: "warning", weight: "semibold", children: t("positions.reverse.description") })
]
}
);
};
var MAX_BATCH_ORDER_SIZE = 20;
var useReversePositionEnabled = () => {
const { isMobile, isDesktop } = useScreen();
const [desktopEnabled, setDesktopEnabled] = useLocalStorage(
"orderly_reverse_position_enabled_desktop",
true
);
const [mobileEnabled, setMobileEnabled] = useLocalStorage(
"orderly_reverse_position_enabled_mobile",
false
);
const isEnabled = useMemo(() => {
if (isMobile) {
return mobileEnabled;
}
if (isDesktop) {
return desktopEnabled;
}
return false;
}, [isMobile, isDesktop, desktopEnabled, mobileEnabled]);
const setEnabled = useCallback(
(enabled) => {
if (isMobile) {
setMobileEnabled(enabled);
} else if (isDesktop) {
setDesktopEnabled(enabled);
}
},
[isMobile, isDesktop, setMobileEnabled, setDesktopEnabled]
);
return {
isEnabled,
setEnabled
};
};
var useReversePositionScript = (options) => {
const { position, onSuccess, onError } = options || {};
const { isEnabled, setEnabled } = useReversePositionEnabled();
const [pnlNotionalDecimalPrecision] = useLocalStorage(
"pnlNotionalDecimalPrecision",
2
);
const symbolsInfo = useSymbolsInfo();
const symbol = position?.symbol || "";
const { data: symbolMarketPrice } = useMarkPrice(symbol);
const symbolInfo = symbolsInfo?.[symbol];
const calcMode = options?.calcMode || "markPrice";
const [positionData] = usePositionStream(symbol, {
calcMode,
includedPendingOrder: false
});
const rawPositionRows = useDataTap(positionData?.rows, { fallbackData: [] });
const unrealizedPnL = useMemo(() => {
if (!rawPositionRows || !symbol)
return position?.unrealized_pnl;
const currentPosition = rawPositionRows.find(
(p) => p.symbol === symbol && p.position_qty !== 0
);
return currentPosition?.unrealized_pnl ?? position?.unrealized_pnl;
}, [rawPositionRows, symbol]);
const baseMin = useMemo(() => {
if (!symbolInfo)
return 0;
return symbolInfo("base_min") || 0;
}, [symbolInfo]);
const baseMax = useMemo(() => {
if (!symbolInfo)
return 0;
return symbolInfo("base_max") || 0;
}, [symbolInfo]);
const baseDp = useMemo(() => {
if (!symbolInfo)
return 6;
return symbolInfo("base_dp") || 6;
}, [symbolInfo]);
const positionQty = useMemo(() => {
if (!position)
return 0;
return Math.abs(position.position_qty);
}, [position]);
const isLong = useMemo(() => {
if (!position)
return false;
return position.position_qty > 0;
}, [position]);
const reverseQty = positionQty;
const validationError = useMemo(() => {
if (!position || !symbolInfo)
return null;
if (baseMin > 0 && reverseQty < baseMin) {
return "belowMin";
}
return null;
}, [position, symbolInfo, reverseQty, baseMin]);
const splitOrders = useMemo(() => {
if (!position || !symbolInfo || baseMax <= 0 || reverseQty <= 0) {
return { needsSplit: false, orders: [] };
}
const buildOrder = (qty, side, reduceOnly) => {
return {
symbol: position.symbol,
order_type: OrderType.MARKET,
side,
order_quantity: new Decimal(qty).todp(baseDp).toString(),
reduce_only: reduceOnly
};
};
const closeSide = isLong ? OrderSide.SELL : OrderSide.BUY;
const openSide = isLong ? OrderSide.SELL : OrderSide.BUY;
if (reverseQty <= baseMax) {
return {
needsSplit: false,
orders: [
buildOrder(reverseQty, closeSide, true),
buildOrder(reverseQty, openSide, false)
]
};
}
const orders = [];
const perOrderQty = baseMax;
const numOrders = Math.ceil(reverseQty / baseMax);
for (let i = 0; i < numOrders - 1; i++) {
orders.push(buildOrder(perOrderQty, closeSide, true));
}
orders.push(
buildOrder(reverseQty - perOrderQty * (numOrders - 1), closeSide, true)
);
for (let i = 0; i < numOrders - 1; i++) {
orders.push(buildOrder(perOrderQty, openSide, false));
}
orders.push(
buildOrder(reverseQty - perOrderQty * (numOrders - 1), openSide, false)
);
return {
needsSplit: true,
orders
};
}, [position, symbolInfo, reverseQty, baseMax, baseDp]);
const [isReversing, setIsReversing] = useState(false);
const [doBatchCreateOrder] = useSubAccountMutation(
"/v1/batch-order",
"POST",
{
accountId: position?.account_id
}
);
const reversePosition = useCallback(async () => {
if (!position || positionQty === 0) {
return false;
}
if (validationError) {
return false;
}
setIsReversing(true);
try {
const ordersArray = splitOrders.orders;
if (ordersArray.length > MAX_BATCH_ORDER_SIZE) {
for (let i = 0; i < ordersArray.length; i += MAX_BATCH_ORDER_SIZE) {
const batch = ordersArray.slice(i, i + MAX_BATCH_ORDER_SIZE);
const result = await doBatchCreateOrder({
orders: batch,
symbol: position.symbol
});
await new Promise(
(resolve) => setTimeout(resolve, batch.length * 110)
);
if (!result || result.error) {
throw result?.error || new Error("Batch order failed");
}
}
} else {
const result = await doBatchCreateOrder({
orders: ordersArray,
symbol: position.symbol
});
if (!result || result.error) {
throw result?.error || new Error("Batch order failed");
}
}
onSuccess?.();
return true;
} catch (error) {
onError?.(error);
return false;
} finally {
setIsReversing(false);
}
}, [
position,
positionQty,
reverseQty,
isLong,
doBatchCreateOrder,
splitOrders,
symbolInfo,
validationError,
onSuccess,
onError
]);
const displayInfo = useMemo(() => {
if (!position || !symbolInfo) {
return null;
}
const base = symbolInfo("base");
const quote = symbolInfo("quote");
const baseDp2 = symbolInfo("base_dp");
const quoteDp = symbolInfo("quote_dp");
const leverage = position.leverage || 1;
return {
symbol: position.symbol,
base,
quote,
baseDp: baseDp2,
quoteDp,
positionQty: new Decimal(positionQty).todp(baseDp2).toString(),
reverseQty: new Decimal(reverseQty).todp(baseDp2).toString(),
markPrice: symbolMarketPrice ? new Decimal(symbolMarketPrice).todp(quoteDp).toString() : "--",
leverage,
isLong,
unrealizedPnL,
pnlNotionalDecimalPrecision
};
}, [
position,
symbolMarketPrice,
symbolInfo,
positionQty,
reverseQty,
isLong,
unrealizedPnL,
pnlNotionalDecimalPrecision
]);
return {
isEnabled,
setEnabled,
reversePosition,
isReversing,
displayInfo,
positionQty,
reverseQty,
isLong,
validationError,
splitOrders
};
};
var ReversePositionWidget = (props) => {
const { position, close, resolve, reject } = props;
const { t } = useTranslation();
const { visible, hide, onOpenChange } = useModal();
const state = useReversePositionScript({
position,
onSuccess: () => {
resolve?.(true);
hide();
},
onError: (error) => {
reject?.(error);
hide();
}
});
const actions = useMemo(() => {
const hasValidationError = !!state.validationError;
return {
primary: {
label: t("common.confirm"),
onClick: async () => {
try {
const result = await state.reversePosition();
if (result) {
resolve?.(true);
hide();
return true;
} else {
reject?.(false);
return false;
}
} catch (error) {
reject?.(error);
throw error;
}
},
loading: state.isReversing,
disabled: state.isReversing || !state.displayInfo || hasValidationError
},
secondary: {
label: t("common.cancel"),
disabled: state.isReversing,
onClick: async () => {
reject?.("cancel");
hide();
return false;
}
}
};
}, [state, t, resolve, reject, hide]);
return /* @__PURE__ */ jsx(
SimpleDialog,
{
open: visible,
onOpenChange,
size: "sm",
title: i18n.t("positions.reverse.title"),
classNames: {
content: "oui-border oui-border-line-6"
},
actions,
closable: !state.isReversing,
children: /* @__PURE__ */ jsx(ReversePosition, { ...state })
}
);
};
var ReversePositionDialogId = "ReversePositionDialogId";
registerSimpleDialog(
ReversePositionDialogId,
ReversePositionWidget,
{
size: "sm",
classNames: {
content: "oui-border oui-border-line-6"
},
title: () => i18n.t("positions.reverse.title")
}
);
var PositionsTabName = /* @__PURE__ */ ((PositionsTabName2) => {
PositionsTabName2["Positions"] = "positions";
PositionsTabName2["PositionHistory"] = "positionHistory";
return PositionsTabName2;
})(PositionsTabName || {});
function useTabSort(options) {
const [tabSort, setTabSort] = useSessionStorage(options.storageKey, {
["positions" /* Positions */]: {
sortKey: "unrealized_pnl",
sortOrder: "desc"
},
["positionHistory" /* PositionHistory */]: {
sortKey: "close_timestamp",
sortOrder: "desc"
}
});
const onTabSort = useCallback(
(type) => (sort) => {
setTabSort({ ...tabSort, [type]: sort });
},
[tabSort, setTabSort]
);
return {
tabSort,
onTabSort
};
}
// src/components/positions/positions.script.ts
var usePositionsScript = (props) => {
const {
symbol,
calcMode,
includedPendingOrder,
pnlNotionalDecimalPrecision,
sharePnLConfig,
onSymbolChange,
enableSortingStorage = true
// Default to true for backward compatibility
} = props;
const { pagination, setPage } = usePagination({ pageSize: 50 });
const { isEnabled: positionReverse } = useReversePositionEnabled();
const { tabSort, onTabSort } = useTabSort({
storageKey: TRADING_POSITIONS_SORT_STORAGE_KEY
});
const { onSort, getSortedList, sort } = useSort(
enableSortingStorage ? tabSort?.["positions" /* Positions */] : void 0,
enableSortingStorage ? onTabSort("positions" /* Positions */) : void 0
);
React2.useEffect(() => {
setPage(1);
}, [symbol]);
const [data, , { isLoading }] = usePositionStream(symbol, {
calcMode,
includedPendingOrder
});
const rawDataSource = useDataTap(data?.rows, { fallbackData: [] }) ?? void 0;
const dataSource = getSortedList(rawDataSource || []);