UNPKG

@orderly.network/ui-positions

Version:

1,521 lines (1,515 loc) 188 kB
'use strict'; var ui = require('@orderly.network/ui'); var React2 = require('react'); var i18n = require('@orderly.network/i18n'); var types = require('@orderly.network/types'); var utils = require('@orderly.network/utils'); var jsxRuntime = require('react/jsx-runtime'); var hooks = require('@orderly.network/hooks'); var reactApp = require('@orderly.network/react-app'); var perp = require('@orderly.network/perp'); var uiConnector = require('@orderly.network/ui-connector'); var uiLeverage = require('@orderly.network/ui-leverage'); var uiShare = require('@orderly.network/ui-share'); var uiTpsl = require('@orderly.network/ui-tpsl'); var dateFns = require('date-fns'); function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; } var React2__default = /*#__PURE__*/_interopDefault(React2); // src/index.ts var ConfirmHeader = (props) => { const { hideCloseIcon = false } = props; return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "oui-relative oui-w-full oui-border-b oui-border-line-4 oui-pb-3", children: [ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "base", children: props.title }), !hideCloseIcon && /* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsx(ui.CloseIcon, { size: 18, color: "white" }) } ) ] }); }; var ConfirmFooter = (props) => { const { t } = i18n.useTranslation(); return /* @__PURE__ */ jsxRuntime.jsxs( ui.Flex, { id: "oui-positions-confirm-footer", gap: 2, width: "100%", className: "oui-mt-3 oui-pb-1", children: [ /* @__PURE__ */ jsxRuntime.jsx( ui.Button, { id: "oui-positions-confirm-footer-cancel-button", color: "secondary", fullWidth: true, onClick: props.onCancel, size: "md", children: t("common.cancel") } ), /* @__PURE__ */ jsxRuntime.jsx( ui.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 } = i18n.useTranslation(); const total = React2.useMemo(() => { if (price && quantity) { return new utils.Decimal(price).mul(quantity).toFixed(quoteDp, utils.Decimal.ROUND_DOWN); } return "--"; }, [price, quantity]); return /* @__PURE__ */ jsxRuntime.jsxs( ui.Flex, { direction: "column", gap: 1, width: "100%", className: "oui-text-sm oui-text-base-contrast-54", py: 5, children: [ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { justify: "between", width: "100%", gap: 1, children: [ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: t("common.qty") }), /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { color: side === types.OrderSide.BUY ? "success" : "danger", children: quantity }) ] }), /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { justify: "between", width: "100%", gap: 1, children: [ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: t("common.price") }), /* @__PURE__ */ jsxRuntime.jsx( ui.Text.formatted, { intensity: 98, suffix: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { intensity: 54, children: "USDC" }), children: price } ) ] }), /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { justify: "between", width: "100%", gap: 1, children: [ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: t("common.notional") }), /* @__PURE__ */ jsxRuntime.jsx( ui.Text.formatted, { intensity: 98, suffix: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { intensity: 54, children: "USDC" }), children: total } ) ] }) ] } ); }; var MarketCloseConfirm = (props) => { const { t } = i18n.useTranslation(); const onCancel = () => { const func = props?.onClose ?? props.close; func?.(); }; return /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { direction: "column", className: props.classNames?.root, children: [ /* @__PURE__ */ jsxRuntime.jsx( ConfirmHeader, { onClose: onCancel, title: t("positions.marketClose"), hideCloseIcon: props.hideCloseIcon } ), /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { intensity: 54, size: "sm", className: "oui-my-5", children: t("positions.marketClose.description", { quantity: utils.commifyOptional(props.quantity), base: props.base }) }), /* @__PURE__ */ jsxRuntime.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 } = i18n.useTranslation(); const onCancel = () => { props.onClose?.(); }; return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [ /* @__PURE__ */ jsxRuntime.jsx( ConfirmHeader, { onClose: onCancel, title: t("positions.limitClose"), hideCloseIcon: props.hideCloseIcon } ), /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { mt: 5, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { intensity: 54, size: "sm", children: t("positions.limitClose.description", { quantity: utils.commify(props.quantity), base: props.base }) }) }), /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { gap: 2, mb: 4, mt: 5, justify: "between", children: [ /* @__PURE__ */ jsxRuntime.jsx( ui.Text.formatted, { rule: "symbol", formatString: "base-type", size: "base", showIcon: true, children: order.symbol } ), /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { gap: 1, children: [ /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { color: "neutral", size: "xs", children: t("orderEntry.orderType.limit") }), /* @__PURE__ */ jsxRuntime.jsx( ui.Badge, { color: side === types.OrderSide.BUY ? "success" : "danger", size: "xs", children: side === types.OrderSide.BUY ? t("common.buy") : t("common.sell") } ) ] }) ] }), /* @__PURE__ */ jsxRuntime.jsx(ui.Divider, { className: "oui-w-full" }), /* @__PURE__ */ jsxRuntime.jsx( OrderDetail, { className: "oui-text-sm", price, quantity, side: order.side, quoteDp: quoteDp ?? 2 } ), /* @__PURE__ */ jsxRuntime.jsx( ConfirmFooter, { onCancel, onConfirm: props.onConfirm, submitting: props.submitting } ) ] }); }; var PositionsRowContext = React2.createContext( {} ); var usePositionsRowContext = () => { return React2.useContext(PositionsRowContext); }; function useEndReached(sentinelRef, onEndReached) { const observer = React2.useRef(); const cb = React2.useRef(onEndReached); cb.current = onEndReached; React2.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(); }; }, []); React2.useEffect(() => { observer.current?.observe(sentinelRef.current); }, []); } var EndReachedBox = (props) => { const sentinelRef = React2.useRef(null); const { onEndReached } = props; useEndReached(sentinelRef, () => { onEndReached?.(); }); return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [ props.children, /* @__PURE__ */ jsxRuntime.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 } = i18n.useTranslation(); const { isMobile } = ui.useScreen(); const { isLoading, data, setSize } = hooks.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 = React2.useCallback(() => { setSize((prev) => { return prev + 1; }); }, [setSize]); const flattenData = React2.useMemo(() => { if (!Array.isArray(data)) return []; return data.flat().map((item) => { return { ...item, funding_fee: -item.funding_fee }; }); }, [data]); const listView = React2.useMemo(() => { if (isMobile) { return /* @__PURE__ */ jsxRuntime.jsx( HistoryDataListViewSimple, { data: flattenData ?? types.EMPTY_LIST, isLoading, loadMore } ); } return /* @__PURE__ */ jsxRuntime.jsx( HistoryDataListView, { data: flattenData ?? types.EMPTY_LIST, isLoading, loadMore } ); }, [isMobile, flattenData, isLoading]); return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [ /* @__PURE__ */ jsxRuntime.jsxs( ui.Grid, { cols: 2, gapX: 3, className: "oui-sticky oui-top-0 oui-z-10 oui-bg-base-8 oui-py-4", children: [ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "oui-rounded-lg oui-border oui-border-line-6 oui-bg-base-9 oui-p-3", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { direction: "column", gap: 1, itemAlign: "start", children: [ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "oui-text-2xs oui-text-base-contrast-36", children: t("common.symbol") }), /* @__PURE__ */ jsxRuntime.jsx( ui.Text.formatted, { rule: "symbol", className: "oui-font-semibold", intensity: 98, children: symbol } ) ] }) }), /* @__PURE__ */ jsxRuntime.jsx("div", { className: "oui-rounded-lg oui-border oui-border-line-6 oui-bg-base-9 oui-p-3", children: /* @__PURE__ */ jsxRuntime.jsx( ui.Statistic, { label: isMobile ? /* @__PURE__ */ jsxRuntime.jsx( FundingFeeLabelButton, { label: `${t("funding.fundingFee")} (USDC)`, tooltip: t("positions.fundingFee.tooltip"), size: 14 } ) : /* @__PURE__ */ jsxRuntime.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 } = i18n.useTranslation(); return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "oui-flex oui-items-center oui-gap-1", children: [ /* @__PURE__ */ jsxRuntime.jsx("span", { children: label }), /* @__PURE__ */ jsxRuntime.jsx( "button", { className: "oui-flex oui-items-center", onClick: () => { ui.modal.alert({ message: tooltip, title: t("positions.fundingFee.title") }); }, children: /* @__PURE__ */ jsxRuntime.jsx( ui.ExclamationFillIcon, { className: "oui-cursor-pointer oui-text-base-contrast-54", size } ) } ) ] }); }; var FundingFeeLabel = ({ label, tooltip, size }) => { return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "oui-flex oui-items-center oui-gap-1", children: [ /* @__PURE__ */ jsxRuntime.jsx("span", { children: label }), /* @__PURE__ */ jsxRuntime.jsx(ui.Tooltip, { content: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "oui-w-64", children: tooltip }), children: /* @__PURE__ */ jsxRuntime.jsx( ui.ExclamationFillIcon, { className: "oui-cursor-pointer oui-text-base-contrast-54", size } ) }) ] }); }; var HistoryDataListView = ({ isLoading, data, loadMore }) => { const { t } = i18n.useTranslation(); const columns = React2.useMemo(() => { return [ { title: t("common.time"), dataIndex: "created_time", width: 120, render: (value) => { return /* @__PURE__ */ jsxRuntime.jsx(ui.Text.formatted, { rule: "date", children: value }); } }, { title: /* @__PURE__ */ jsxRuntime.jsx( FundingFeeLabel, { label: t("funding.fundingRate"), tooltip: t("positions.fundingRate.tooltip"), size: 12 } ), dataIndex: "funding_rate", formatter: (value) => new utils.Decimal(value).mul(100).toString(), render: (value) => { return /* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsx("span", { children: value }) }, { title: `${t("funding.fundingFee")} (USDC)`, dataIndex: "funding_fee", render: (value) => { return /* @__PURE__ */ jsxRuntime.jsx(ui.Text.numeral, { rule: "price", coloring: true, showIdentifier: true, ignoreDP: true, children: value }); } } ]; }, [t]); return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "oui-h-[calc(80vh_-_132px_-_8px)] oui-overflow-y-auto", children: /* @__PURE__ */ jsxRuntime.jsx(EndReachedBox, { onEndReached: loadMore, children: /* @__PURE__ */ jsxRuntime.jsx( ui.DataTable, { classNames: { root: ui.cn("oui-h-auto oui-bg-base-8 oui-text-sm") }, columns, dataSource: data ?? types.EMPTY_LIST, loading: isLoading } ) }) }); }; var HistoryDataListViewSimple = ({ data, isLoading, loadMore }) => { const renderItem = React2.useCallback((item) => { return /* @__PURE__ */ jsxRuntime.jsx(FundingFeeItem, { item }); }, []); return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "oui-h-[calc(80vh_-_104px)] oui-overflow-y-auto", children: /* @__PURE__ */ jsxRuntime.jsx( ui.ListView, { dataSource: data, renderItem, isLoading, contentClassName: "oui-space-y-0", loadMore } ) }); }; var FundingFeeItem = ({ item }) => { const { t } = i18n.useTranslation(); return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "oui-flex oui-flex-col oui-space-y-2 oui-border-t oui-border-line-6 oui-py-2", children: [ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { justify: "between", children: [ /* @__PURE__ */ jsxRuntime.jsx( ui.Statistic, { label: /* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsx( ui.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__ */ jsxRuntime.jsxs(ui.Flex, { justify: "between", children: [ /* @__PURE__ */ jsxRuntime.jsx( ui.Text.formatted, { rule: "date", className: "oui-text-base-contrast-36", size: "2xs", children: item.created_time } ), /* @__PURE__ */ jsxRuntime.jsx(ui.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 } = i18n.useTranslation(); const [isOpen, { setTrue, setFalse }] = hooks.useBoolean(false); const { isMobile } = ui.useScreen(); return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [ /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: setTrue, children: /* @__PURE__ */ jsxRuntime.jsx( ui.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__ */ jsxRuntime.jsx( ui.SimpleSheet, { open: isOpen, onOpenChange: setFalse, title: t("funding.fundingFee"), classNames: { body: "oui-max-h-[80vh] oui-py-0" }, children: /* @__PURE__ */ jsxRuntime.jsx( FundingFeeHistoryUI, { total: fee, symbol, start_t, end_t } ) } ) : /* @__PURE__ */ jsxRuntime.jsx( ui.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__ */ jsxRuntime.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 = perp.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 = perp.positions.MMR({ baseMMR, baseIMR, IMRFactor: account2?.imr_factor[item.symbol] ?? 0, positionNotional: notional, IMR_factor_power: 4 / 5 }); const mm = perp.positions.maintenanceMargin({ positionQty: item.position_qty, markPrice: item.mark_price, MMR }); const unrealPnl = perp.positions.unrealizedPnL({ qty: item.position_qty, openPrice: item?.average_open_price, markPrice: item.mark_price }); const maxLeverage = item.leverage || 1; const imr = perp.account.IMR({ maxLeverage, baseIMR, IMR_Factor: account2?.imr_factor[item.symbol] ?? 0, positionNotional: notional, ordersNotional: 0, IMR_factor_power: 4 / 5 }); const unrealPnlROI = perp.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 = perp.positions.unrealizedPnL({ qty: item.position_qty, openPrice: item?.average_open_price, markPrice: item.index_price }); unrealPnlROI_index = perp.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 } = hooks.findPositionTPSLFromOrders(tpslOrders, symbol); const full_tp_sl = fullPositionOrder ? hooks.findTPSLFromOrder(fullPositionOrder) : void 0; const partialPossitionOrder = partialPositionOrders && partialPositionOrders.length ? partialPositionOrders[0] : void 0; const partial_tp_sl = partialPossitionOrder ? hooks.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 = hooks.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, utils.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 } = hooks.useAccount(); const middleware = Array.isArray(options?.use) ? options?.use ?? [] : []; const ids = Array.isArray(accountId) ? accountId : [accountId]; const shouldFetch = ids.filter(Boolean).length && (state.status >= types.AccountStatusEnum.EnableTrading || state.status === types.AccountStatusEnum.EnableTradingWithoutConnected); return hooks.useSWR( () => shouldFetch ? [query, ids] : null, (url, init) => { return hooks.fetcher(url, init, { formatter }); }, { ...swrOptions, use: [...middleware, signatureMiddleware(account2, ids)], onError: () => { } } ); }; function useSubAccountTPSL(subAccountIds) { const ee = hooks.useEventEmitter(); const { data: algoOrdersResponse, mutate: mutateTPSLOrders } = useSubAccountQuery( `/v1/algo/orders?size=100&page=1&status=${types.OrderStatus.INCOMPLETE}`, { accountId: subAccountIds, formatter: (data) => data, revalidateOnFocus: false } ); const tpslOrders = React2.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) => [types.AlgoOrderRootType.POSITIONAL_TP_SL, types.AlgoOrderRootType.TP_SL].includes( order.algo_type ) ); }, [algoOrdersResponse, subAccountIds]); const refresh = hooks.useDebouncedCallback(() => { mutateTPSLOrders(); }, 200); React2.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 } = ui.usePagination({ pageSize: 50 }); const symbolsInfo = hooks.useSymbolsInfo(); const { state } = hooks.useAccount(); const [mainAccountPositions, , { isLoading }] = hooks.usePositionStream(symbol, { calcMode, includedPendingOrder }); const { data: newPositions = [], isLoading: isPositionLoading, mutate: mutatePositions } = hooks.usePrivateQuery("/v1/client/aggregate/positions", { errorRetryCount: 3 }); const { allAccountIds, subAccountIds } = React2.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 = React2.useMemo(() => { return calculatePositions( newPositions.filter((item) => item.account_id !== state.mainAccountId), symbolsInfo, accountInfo, tpslOrders ); }, [newPositions, symbolsInfo, accountInfo, state.mainAccountId, tpslOrders]); const allPositions = React2.useMemo(() => { return [...mainAccountPositions?.rows, ...subAccountPositions].filter( (item) => item.position_qty !== 0 ); }, [mainAccountPositions, subAccountPositions]); const dataSource = reactApp.useDataTap(allPositions) ?? []; const filtered = React2.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 = React2.useMemo(() => { return groupDataByAccount(filtered, { mainAccountId: state.mainAccountId, subAccounts: state.subAccounts }); }, [filtered, state.mainAccountId, state.subAccounts]); const loading = isLoading || isPositionLoading || isAccountInfoLoading; React2.useEffect(() => { setPage(1); }, [symbol]); const mutateList = React2.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.i18n.t("common.mainAccount") : findSubAccount?.description || ui.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] = React2.useState(initialSort); const onSort = React2.useCallback((options) => { const nextSort = options ? { sortKey: options.sortKey, sortOrder: options.sort } : void 0; setSort(nextSort); onSortChange?.(nextSort); }, []); const getSortedList = React2.useCallback( (list) => sortList(list, sort), [sort] ); return { sort, onSort, getSortedList }; } var OrderInfoCard = ({ title, side, leverage, qty, baseDp, estLiqPrice, estPnL, pnlNotionalDecimalPrecision = 2, className }) => { const { t } = i18n.useTranslation(); return /* @__PURE__ */ jsxRuntime.jsxs( ui.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__ */ jsxRuntime.jsxs(ui.Flex, { justify: "between", itemAlign: "center", gap: 2, children: [ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "sm", weight: "semibold", intensity: 98, children: title }), /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { itemAlign: "center", gap: 1, children: [ /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { color: side === types.OrderSide.SELL ? "sell" : "buy", size: "xs", children: side === types.OrderSide.SELL ? t("common.sell") : t("common.buy") }), /* @__PURE__ */ jsxRuntime.jsxs(ui.Badge, { color: "neutral", size: "xs", children: [ leverage, "X" ] }) ] }) ] }), /* @__PURE__ */ jsxRuntime.jsxs( ui.Flex, { direction: "column", justify: "between", itemAlign: "center", width: "100%", gap: 1, children: [ /* @__PURE__ */ jsxRuntime.jsxs( ui.Flex, { direction: "row", justify: "between", itemAlign: "center", width: "100%", gap: 1, children: [ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "sm", intensity: 54, children: t("common.qty") }), /* @__PURE__ */ jsxRuntime.jsx(ui.Text.numeral, { dp: baseDp, size: "sm", color: "danger", padding: false, children: qty }) ] } ), /* @__PURE__ */ jsxRuntime.jsxs( ui.Flex, { direction: "row", justify: "between", itemAlign: "center", width: "100%", gap: 1, children: [ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "sm", intensity: 54, children: t("common.price") }), /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "sm", weight: "semibold", children: t("common.market") }) ] } ), estPnL && /* @__PURE__ */ jsxRuntime.jsxs( ui.Flex, { direction: "row", justify: "between", itemAlign: "center", width: "100%", gap: 1, children: [ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "sm", intensity: 54, children: t("orderEntry.estPnL") }), /* @__PURE__ */ jsxRuntime.jsx( ui.Text.pnl, { dp: pnlNotionalDecimalPrecision, coloring: true, size: "sm", weight: "semibold", children: estPnL } ) ] } ), estLiqPrice && /* @__PURE__ */ jsxRuntime.jsxs( ui.Flex, { direction: "row", justify: "between", itemAlign: "center", width: "100%", gap: 1, children: [ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "sm", intensity: 54, children: t("orderEntry.estLiqPrice") }), /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "sm", weight: "semibold", children: estLiqPrice }) ] } ) ] } ) ] } ); }; var ReversePosition = (props) => { const { displayInfo, validationError, className, style, onConfirm, onCancel } = props; const { t } = i18n.useTranslation(); if (!displayInfo) { return null; } const { symbol, base, quote, baseDp, quoteDp, positionQty, reverseQty, markPrice, leverage, isLong, unrealizedPnL, pnlNotionalDecimalPrecision } = displayInfo; const closeSide = !isLong ? types.OrderSide.SELL : types.OrderSide.BUY; const openSide = isLong ? types.OrderSide.SELL : types.OrderSide.BUY; const closeAction = isLong ? t("positions.reverse.marketCloseLong") : t("positions.reverse.marketCloseShort"); const openAction = openSide === types.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__ */ jsxRuntime.jsx(ui.ArrowDownShortIcon, { size: 16, color: "danger", opacity: 1 }) : /* @__PURE__ */ jsxRuntime.jsx(ui.ArrowUpShortIcon, { size: 16, color: "success", opacity: 1 }); const priceLabel = /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { itemAlign: "center", gap: 1, children: [ /* @__PURE__ */ jsxRuntime.jsx(ui.Text.numeral, { dp: quoteDp, size: "sm", intensity: 80, children: markPrice }), /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "sm", intensity: 36, children: quote }) ] }); const showBelowMinError = validationError === "belowMin"; return /* @__PURE__ */ jsxRuntime.jsxs( ui.Flex, { direction: "column", className, style, gap: 4, width: "100%", children: [ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { direction: "column", gap: 2, width: "100%", children: [ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { justify: "between", itemAlign: "center", width: "100%", children: [ /* @__PURE__ */ jsxRuntime.jsx( ui.Text.formatted, { size: "base", weight: "semibold", rule: "symbol", formatString: "base-type", intensity: 98, showIcon: true, children: symbol } ), /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { color: isLong ? "sell" : "buy", size: "xs", children: reverseTo }) ] }), /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { justify: "between", itemAlign: "center", width: "100%", children: [ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "sm", intensity: 54, children: t("common.markPrice") }), priceLabel ] }) ] }), /* @__PURE__ */ jsxRuntime.jsx(ui.Divider, { intensity: 4, className: "oui-w-full" }), /* @__PURE__ */ jsxRuntime.jsx( OrderInfoCard, { title: closeAction, side: closeSide, leverage, qty: positionQty, baseDp, estPnL: unrealizedPnL?.toString(), pnlNotionalDecimalPrecision } ), /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { direction: "row", itemAlign: "center", width: "100%", children: [ /* @__PURE__ */ jsxRuntime.jsx(ui.Divider, { intensity: 8, className: "oui-w-full" }), /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { className: "oui-px-4 oui-py-[3px] oui-border oui-border-base-contrast-12 oui-rounded-full oui-shrink-0", children: [ reverseToIcon, /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "2xs", color: isLong ? "danger" : "success", children: reverseTo }) ] }), /* @__PURE__ */ jsxRuntime.jsx(ui.Divider, { intensity: 8, className: "oui-w-full" }) ] }), /* @__PURE__ */ jsxRuntime.jsx( OrderInfoCard, { title: openAction, side: openSide, leverage, qty: reverseQty, baseDp } ), showBelowMinError ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "2xs", color: "danger", weight: "semibold", children: t("positions.reverse.error.belowMin") }) : /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "2xs", color: "warning", weight: "semibold", children: t("positions.reverse.description") }) ] } ); }; var MAX_BATCH_ORDER_SIZE = 20; var useReversePositionEnabled = () => { const { isMobile, isDesktop } = ui.useScreen(); const [desktopEnabled, setDesktopEnabled] = hooks.useLocalStorage( "orderly_reverse_position_enabled_desktop", true ); const [mobileEnabled, setMobileEnabled] = hooks.useLocalStorage( "orderly_reverse_position_enabled_mobile", false ); const isEnabled = React2.useMemo(() => { if (isMobile) { return mobileEnabled; } if (isDesktop) { return desktopEnabled; } return false; }, [isMobile, isDesktop, desktopEnabled, mobileEnabled]); const setEnabled = React2.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] = hooks.useLocalStorage( "pnlNotionalDecimalPrecision", 2 ); const symbolsInfo = hooks.useSymbolsInfo(); const symbol = position?.symbol || ""; const { data: symbolMarketPrice } = hooks.useMarkPrice(symbol); const symbolInfo = symbolsInfo?.[symbol]; const calcMode = options?.calcMode || "markPrice"; const [positionData] = hooks.usePositionStream(symbol, { calcMode, includedPendingOrder: false }); const rawPositionRows = reactApp.useDataTap(positionData?.rows, { fallbackData: [] }); const unrealizedPnL = React2.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 = React2.useMemo(() => { if (!symbolInfo) return 0; return symbolInfo("base_min") || 0; }, [symbolInfo]); const baseMax = React2.useMemo(() => { if (!symbolInfo) return 0; return symbolInfo("base_max") || 0; }, [symbolInfo]); const baseDp = React2.useMemo(() => { if (!symbolInfo) return 6; return symbolInfo("base_dp") || 6; }, [symbolInfo]); const positionQty = React2.useMemo(() => { if (!position) return 0; return Math.abs(position.position_qty); }, [position]); const isLong = React2.useMemo(() => { if (!position) return false; return position.position_qty > 0; }, [position]); const reverseQty = positionQty; const validationError = React2.useMemo(() => { if (!position || !symbolInfo) return null; if (baseMin > 0 && reverseQty < baseMin) { return "belowMin"; } return null; }, [position, symbolInfo, reverseQty, baseMin]); const splitOrders = React2.useMemo(() => { if (!position || !symbolInfo || baseMax <= 0 || reverseQty <= 0) { return { needsSplit: false, orders: [] }; } const buildOrder = (qty, side, reduceOnly) => { return { symbol: position.symbol, order_type: types.OrderType.MARKET, side, order_quantity: new utils.Decimal(qty).todp(baseDp).toString(), reduce_only: reduceOnly }; }; const closeSide = isLong ? types.OrderSide.SELL : types.OrderSide.BUY; const openSide = isLong ? types.OrderSide.SELL : types.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] = React2.useState(false); const [doBatchCreateOrder] = hooks.useSubAccountMutation( "/v1/batch-order", "POST", { accountId: position?.account_id } ); const reversePosition = React2.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 = React2.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 utils.Decimal(positionQty).todp(baseDp2).toString(), reverseQty: new utils.Decimal(reverseQty).todp(baseDp2).toString(), markPrice: symbolMarketPrice ? new utils.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 } = i18n.useTranslation(); const { visible, hide, onOpenChange } = ui.useModal(); const state = useReversePositionScript({ position, onSuccess: () => { resolve?.(true); hide(); }, onError: (error) => { reject?.(error); hide(); } }); const actions = React2.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__ */ jsxRuntime.jsx( ui.SimpleDialog, { open: visible, onOpenChange, size: "sm", title: i18n.i18n.t("positions.reverse.title"), classNames: { content: "oui-border oui-border-line-6" }, actions, closable: !state.isReversing, children: /* @__PURE__ */ jsxRuntime.jsx(ReversePosition, { ...state }) } ); }; var ReversePositionDialogId = "ReversePositionDialogId"; ui.registerSimpleDialog( ReversePositionDialogId, ReversePositionWidget, { size: "sm", classNames: { content: "oui-border oui-border-line-6" }, title: () => i18n.i18n.t("positions.reverse.title") } ); var PositionsTabName = /* @__PURE__ */ ((PositionsTabName2) => { PositionsTabName2["Positions"] = "positions"; PositionsTabName2["PositionHistory"] = "positionHistory"; return PositionsTabName2; })(PositionsTabName || {}); function useTabSort(options) { const [tabSort, setTabSort] = hooks.useSessionStorage(options.storageKey, { ["positions" /* Positions */]: { sortKey: "unrealized_pnl", sortOrder: "desc" }, ["positionHistory" /* PositionHistory */]: { sortKey: "close_timestamp", sortOrder: "desc" } }); const onTabSort = React2.useCallback( (