Skip to Content
Symmio Trading-SDK — the SDK surface for builders on HyperEVM
ReactUnified Quotes

Unified Quotes hooks

Live, reconciled quote list — the primary abstraction UI code binds to. Composes on-chain reads, pending instant-opens, pending instant-closes, and solver notifications into one reactive stream of UnifiedQuote[].

Mostly reads. The writes that produce the pending rows (useInstantOpen*, useInstantClose*) live under Solvers; the exception on this page is Cancelling limit orders — the cancel / force-cancel writes are documented here next to the useLimitOrders list they act on.

Import

import { useManagedQuotes, useLimitOrders, useGroupedQuotes, useOptimisticQuotesStore, useRequestToCancelQuote, useForceCancelQuote, useCoolDownsOfMA, useRequestToCancelCloseRequest, useForceCancelCloseRequest, useForceClose, useForceCloseEligibility, useForceCloseParams, useQuoteUpnlAndPnl, useQuotePlatformFee, useAccountLiquidationPrice, useQuoteGroupMarginRisk, useQuotePriceHistory, } from "@symmio/trading-react";

useManagedQuotes

The orchestrator. Handles:

  1. ReadsgetPartyAOpenPositions, getPartyAPendingQuotes, getInstantOpens, getInstantCloses on a chain-scoped poll.
  2. Notifications — subscribes via useNotifications, folds each frame through applyNotificationToQuotes.
  3. Reconciliation — merges every source via reconcileQuotes with the useOptimisticQuotesStore ledger.
  4. Polling acceleration — switches to a tighter interval (~1.5 s) while any row is mid-transition (shouldAccelerate(quotes) === true); otherwise idles at ~5 s.
  5. Invalidation on notification — debounces + invalidates the on-chain reads so the next poll returns fresh data.
  6. Confirm holds — after a frame that reports a change the chain has not shown yet, re-reads on a doubling backoff until it has. Covers an open anchor (the solver emits it when it broadcasts the transaction, so the one invalidation it triggers loses the race), a close fill (the quote lingers OPENED while the settle lags), and a cancel request — the case with no second frame at all, since the solver’s acceptCancelRequest is never announced. Each hold releases itself the moment the chain catches up. While a cancel hold is live the balance reads are re-read too, since the accept is what returns the fee and the reserved margin.
const managed = useManagedQuotes({ partyA: subAccount, live: true, // subscribe to notifications (default: true) }); for (const quote of managed.quotes) { render(quote); }

Parameters

NameTypeDefaultNotes
partyAAddressrequiredSubAccount whose quotes to manage.
livebooleantrueSubscribe to the notifications WS; false for poll-only.
extraAccountsAddress[]?[]Extra addresses to include in the on-chain read set (VAs).
chainIdnumber?config defaultOptional chain override.

Return type

{ quotes: UnifiedQuote[]; isLoading: boolean; isFetching: boolean; socketStatus: SocketStatus; error: SymmioRequestError | null; }

The React hook is the recommended entry point — it wires stores, polling, and WS reconciliation you would otherwise have to compose by hand.

useLimitOrders

The single-account, order-type-scoped view of useManagedQuotes — one reconciled list of a partyA’s resting LIMIT orders across off-chain and on-chain sources. A just-sent order shows instantly as an offchain row (from the hedger’s pending instant-open feed), then flips to onchain when it anchors. Both feed the same pure reconcileQuotes primitive useManagedQuotes uses, so a row transitions OPTIMISTIC → WRITE_ONCHAIN → ONCHAIN (its origin flipping offchain → onchain) without flicker.

Reach for it when you render a limit-order book / open-orders panel for one subaccount; reach for useManagedQuotes when you need the full multi-account position + pending picture.

const { quotes, isLoading, socketStatus } = useLimitOrders({ partyA: subAccount }); for (const order of quotes) { // order.origin: "offchain" | "onchain"; order.lifecycle drives the badge. render(order); }

Limit orders are a majors / Rasa feature (cross-margin), so partyA is the subaccount itself — its notification frames arrive on it directly, with no virtual account.

Parameters

NameTypeDefaultNotes
partyAAddress?SubAccount whose orders to read. No reads until it is set.
liveboolean?trueSubscribe to notifications; burst-refetch on an anchor or cancel frame.
orderTypeOrderType?OrderType.LIMITWhich order type to return.
chainIdnumber?config defaultOptional chain override.
queryobjectTanStack overrides (enabled, refetchInterval).

Return type

{ quotes: UnifiedQuote[]; // offchain then onchain, tagged via `origin` / `lifecycle` isLoading: boolean; isError: boolean; error: SymmioRequestError | null; refetch: () => void; socketStatus: SocketStatus; }

Only a row with origin === "onchain" carries a real quoteId — gate cancel / force-cancel actions on that.

Cancelling limit orders

The scenario. You placed a resting limit order. While it’s PENDING (no partyB yet) you can cancel it instantly. Once a partyB locks it (LOCKED), cancelling isn’t immediate — requestToCancelQuote puts it in CANCEL_PENDING and waits for partyB to acknowledge. If partyB stalls past the force-cancel cooldown, you force-cancel it yourself (forceCancelQuote) — the escape hatch, exactly like force close is for a stuck close.

PENDING ──requestToCancelQuote──▶ CANCELED (instant) LOCKED ──requestToCancelQuote──▶ CANCEL_PENDING ──(cooldown + partyB stalls)──▶ forceCancelQuote ──▶ CANCELED

Three write / read hooks act on the useLimitOrders list. All route the diamond call through the AccountLayer _call proxy (the subaccount’s owner wallet signs; no session key), and on success invalidate the pending reads, the quote’s own getQuote hydration (the read that carries quoteStatus), and the subaccount balance reads — a cancel refunds the open trading fee and releases the margin the order had reserved.

Cancelling a LOCKED order is a two-transaction flow of which only the first is announced. Your requestToCancelQuote moves the quote to CANCEL_PENDING and it stays in partyAPendingQuotes; the solver’s later acceptCancelRequest is what sets CANCELED and removes it — and that transaction publishes no notification frame. useLimitOrders and useManagedQuotes re-read the chain until the quote is gone, so a list fed by either drops the row on its own. A consumer reading getPartyAPendingQuotes directly has to poll, or it will render the cancelled order indefinitely.

useRequestToCancelQuote

Cancel a resting limit order (requestToCancelQuote). A PENDING (unlocked) quote cancels instantly and leaves the list on the receipt. A LOCKED one (a partyB is committed) only enters CANCEL_PENDING — the solver still has to accept, or the force-cancel cooldown has to run out. The mutation resolves on the receipt of the request, so its onSuccess is not the moment the order is gone.

const cancel = useRequestToCancelQuote(); cancel.mutate({ account: subAccount, quoteId }); // quoteId from an onchain row

useForceCancelQuote

Force-cancel a stalled quote (forceCancelQuote). Only valid once the quote is CANCEL_PENDING (you called requestToCancelQuote on a LOCKED order and partyB never acted) and the force-cancel cooldown has elapsed.

const force = useForceCancelQuote(); force.mutate({ account: subAccount, quoteId });

useCoolDownsOfMA

Read the protocol cooldown periods (coolDownsOfMA) as a 4-tuple of seconds. Index 1 is the force-cancel cooldown — the delay after a quote enters CANCEL_PENDING before useForceCancelQuote becomes eligible. Combine with the quote’s statusModifyTimestamp:

const { data: coolDowns } = useCoolDownsOfMA(); const forceReadyAt = Number(quote.statusModifyTimestamp) + Number(coolDowns?.[1] ?? 0n); const canForce = Date.now() / 1000 >= forceReadyAt;

Cancelling a close

The close-side analog of the cancel hooks: back out of a resting close (e.g. a limit close) while the quote is CLOSE_PENDING. Same _call routing; on success both invalidate the subaccount’s open-positions read so the row’s status refreshes.

useRequestToCancelCloseRequest

Cancel a pending close (requestToCancelCloseRequest). If partyB accepts, the quote returns to OPENED; if it stalls, it enters CANCEL_CLOSE_PENDING.

const cancelClose = useRequestToCancelCloseRequest(); cancelClose.mutate({ account: subAccount, quoteId });

useForceCancelCloseRequest

Force-cancel a stalled close (forceCancelCloseRequest). Only valid once the quote is CANCEL_CLOSE_PENDING and the force-cancel-close cooldown — useCoolDownsOfMA index 2 — has elapsed. The quote returns to OPENED.

const { data: coolDowns } = useCoolDownsOfMA(); const forceReadyAt = Number(quote.statusModifyTimestamp) + Number(coolDowns?.[2] ?? 0n); const forceClose = useForceCancelCloseRequest(); if (Date.now() / 1000 >= forceReadyAt) forceClose.mutate({ account: subAccount, quoteId });

Force closing a limit position

When a partyA requested to close a LIMIT position but the hedger never filled it, after cooldowns it can be force-closed at a Muon-attested price — distinct from cancelling the close. See core Force Close for the flow.

useForceClose

The one-call, end-to-end force close from a quoteId (wraps forceCloseAuto): reads the quote + params, gates on eligibility, finds a price window from reference candles, fetches the Muon sig, preflights the gap, and sends. onSuccess invalidates the subaccount’s open positions.

const forceClose = useForceClose(); forceClose.mutate({ account: subAccount, quoteId });

Throws (normalized) on ineligibility (FORCE_CLOSE_NOT_ELIGIBLE) or the market not hitting the price (FORCE_CLOSE_PRICE_NOT_REACHED). Gate the button first with useForceCloseEligibility.

useForceCloseEligibility

The button gate + cooldown countdown for a quote. Reads the market’s force-close params (useForceCloseParams) and runs the pure checkForceCloseEligibility against a 1-second clock, so cooldownRemaining ticks and eligible flips on when the cooldown elapses.

const { eligible, reason, cooldownRemaining } = useForceCloseEligibility({ quote }); // reason: "not-close-pending" | "not-limit" | "cooldown" | "expired"

useForceCloseParams

Read the raw force-close params (cooldowns, price penalty, min sig period, symbol gap ratio) for a market — the inputs the eligibility gate composes.

const { data } = useForceCloseParams({ symbolId });

useGroupedQuotes

Everything useManagedQuotes returns, with the active positions folded into QuoteGroups and the resting limit orders kept flat.

const { groups, pending, quotes } = useGroupedQuotes({ partyA: subAccount, strategy: SubAccountIsolationType.MARKET_DIRECTION, // the default });

It takes the same parameters as useManagedQuotes — it reads the quotes itself, you do not pass them in — plus:

  • strategySubAccountIsolationType.MARKET_DIRECTION (the default, one group per market + side) or a custom { keyOf }. Any other isolation type throws UNSUPPORTED_GROUPING_ISOLATION; see groupQuotes. Memoize a custom strategy; a fresh object every render regroups every render.
  • groupSort — group ordering override. Defaults to newest-first.

Each group carries pure QuoteGroupMetrics: open size, weighted average open price, frozen at-open notional, summed locked margin, and blended leverage. Price-dependent figures are not included — inject a mark price yourself.

Note that a group can span Virtual Accounts, since the VA is not part of the group key. Any write that fans out over group.quotes must use each child’s own vaAddress.

The isolation check

Grouped positions only exist under MARKET_DIRECTION isolation (see supportsQuoteGrouping for why). The hook enforces that against the chain, not just against your argument: it resolves the isolation of the sub-account that owns partyA — walking up from a Virtual Account to its parentAccount when partyA is a VA — and refuses to fold when the answer is anything else.

const { groups, quotes, pending, isolationType, isGroupingSupported, groupingError } = useGroupedQuotes({ partyA }); if (!isGroupingSupported) { // groups is [] — render the flat rows instead return <FlatPositions rows={quotes} note={groupingError?.message} />; }
  • isolationType — the resolved isolation, or undefined while the read is in flight, when partyA is neither a sub-account nor a VA, and when a custom { keyOf } skipped the lookup.
  • isGroupingSupportedfalse only when the isolation is known and unsupported. It stays true while unresolved, so a consumer gating on it never flashes an error during the read.
  • groupingError — the SymmioRequestError behind a false, carrying code UNSUPPORTED_GROUPING_ISOLATION; null otherwise.

quotes and pending are never blanked, so the flat views stay usable for accounts that cannot group. Both isolation reads are fixed-at-creation data and cached for the session, so the check costs nothing after the first resolve. A custom { keyOf } opts out of it entirely — that caller has already chosen a dimension of their own.

useCloseQuoteGroup

Close an exact quantity across a grouped position.

const { close, status, progressPercent, steps } = useCloseQuoteGroup(); const summary = await close({ group, targetQuantity: parseUnits("2.8", 18), minAcceptableQuoteValue: market.minAcceptableQuoteValue, slippage: 5, }); if (!summary.ok) showError(summary.planFailure ?? summary.error);

The run plans the allocation with planGroupClose — largest leg first, every partially closed leg keeping the symbol’s dust floor, summing to the target exactly or failing without closing anything — then submits every allocation in one bulk request. A submitted leg stays closing until its close-fill notification arrives, which advances closedQuantity and progressPercent.

close() never rejects; inspect the returned summary. An infeasible plan comes back as summary.planFailure with nothing closed.

For take profit and stop loss across the same grouped position, see Grouped TP/SL.

useOptimisticQuotesStore

Module-level Zustand store — the “pending opens the reconciler still remembers about” ledger. See Stores.

Written by mutations (useInstantOpen* push an entry) and cleared by reconciliation once the row is anchored on-chain. Consumers rarely read the store directly; useManagedQuotes composes it in for you.

Direct access via a selector when you need to render optimistic UI outside the managed list:

const pending = useOptimisticQuotesStore((state) => state.pending);

Per-quote decorators

Small derived hooks that compose reads on a specific quote:

useQuoteUpnlAndPnl

{ upnl, upnlPercent, markPrice } snapshot for a quote — composes on-chain quote + mark price + Muon UPNL.

const { upnl, upnlPercent, markPrice } = useQuoteUpnlAndPnl({ quote });

useQuotePlatformFee

Open + close fee for a quote based on useFeeForUser + quote size.

const { openFee, closeFee } = useQuotePlatformFee({ quote });

useAccountLiquidationPrice

Liquidation price for an account, computed from its balance info + open positions via calculateLiquidationPrice. Pass the position’s VA (quote.vaAddress ?? quote.partyA); lowcap isolates each position into its own single-position VA, so the account’s liq price is that position’s.

const { liquidationPrice, isLoading } = useAccountLiquidationPrice({ account: quote.vaAddress ?? quote.partyA });

liquidationPrice is 18-decimal wei bigint0n when unavailable (no balance / no positions).

useQuoteGroupMarginRisk

Margin and liquidation risk for a whole QuoteGroup: resolves the group’s Virtual Account, folds its children’s unrealized PnL at the mark price with aggregateGroupUpnl, and runs both through calculateMarginRisk. The account’s liquidation price comes from useAccountLiquidationPrice.

const { metrics, upnl, liquidationPrice, isMultiAccount } = useQuoteGroupMarginRisk({ group }); if (!metrics) return <Skeleton />; // The margin legs are uPnL-independent — render them immediately. // Gate equity and the buffer on `upnl.isComplete`.

The same upnl carries the group’s return as well as its amount: upnl.upnlPercent is the leveraged return on the margin behind the open size — the figure to show beside the PnL — and upnl.returnPercent is the unleveraged move on the position itself. Both are 18-decimal fixed-point percents, and undefined rather than 0n when there is no basis to divide by. See QuoteGroupUpnl.

Mark price: injected wins. Pass markPrice (18-decimal wei) and no price subscription is opened; omit it and the hook subscribes to the group’s market itself. Inject it when the surrounding screen already holds the price, so the two cannot disagree by a tick.

metrics is withheld when the group spans several accounts. A group normally maps 1:1 to a Virtual Account, because the built-in strategy mirrors the sub-account’s on-chain isolation type — and useGroupedQuotes refuses to group at all when it does not. When a group still spans accounts — a custom keyOf, e.g. folding a MARKET_DIRECTION account by market with keyQuoteByMarketisMultiAccount is true and metrics is undefined rather than blended: each account is liquidated independently, so one averaged buffer would read as safe while one of them is about to go. Fan out with useAccountMarginRisk over the returned accounts.

equity = allocatedBalance + upnl mixes an account-wide balance with the group’s uPnL. That is exact for the MARKET_DIRECTION fold, where a group is its Virtual Account. Build a custom keyOf that slices an account into several groups and each group is a subset of its account, which understates equity — the hook cannot detect that without a second read.

Parameters

NameTypeDefaultNotes
groupQuoteGroupThe merged position to describe.
markPricebigint?subscribedMark price override, wei. Omit to subscribe to the group’s market.
accountAddress?group’s VAAccount override. The uPnL fold narrows to it, so equity stays consistent.
liveboolean?trueRefetch the balance when an open/close settles on-chain.
enabledboolean?trueGate the underlying reads (e.g. while the panel is collapsed).
chainIdnumber?config defaultOptional chain override.
configConfig?provider valueOptional config override.

Return type

interface UseQuoteGroupMarginRiskReturnType { /** `undefined` while the balance loads, or whenever `isMultiAccount` is true. */ metrics?: MarginRiskMetrics; /** The group's aggregated unrealized PnL. Check `upnl.isComplete` before trusting it. */ upnl: QuoteGroupUpnl; /** Liquidation price of the resolved account, wei; `0n` when unavailable. */ liquidationPrice: bigint; /** The mark price actually used, wei; `undefined` before the first tick. */ markPrice?: bigint; /** The account `metrics` and `liquidationPrice` describe. */ account?: Address; /** Distinct liquidation domains among the group's children. */ accounts: Address[]; isMultiAccount: boolean; isLoading: boolean; error: SymmioRequestError | null; }

useQuotePriceHistory

Subgraph-backed open-price history for a quote → { rows, hasMore }. Each row is a settle-uPnL recompute or funding tick (PRICE_HISTORY_EVENT_TYPES) with newPrice / prevPrice (wei) and a timestamp (seconds). Needs the on-chain quoteId; pass orderDirection: "asc" for an oldest-first timeline.

const { data } = useQuotePriceHistory({ quoteId: 42n, orderDirection: "asc", query: { enabled: quoteId != null } }); const priceChanges = (data?.rows ?? []).filter((r) => r.newPrice !== undefined);
Last updated on