Force Close
When a partyA has requested to close a LIMIT position but the hedger (partyB) never filled it, after cooldowns the partyA can force it closed at a Muon-oracle-attested price. The SDK wraps the whole flow behind one function that takes just a quoteId.
Force close applies only to a CLOSE_PENDING LIMIT position and is a majors / Rasa feature. It routes forceClosePosition through the AccountLayer _call proxy (the sub-account’s owner signs).
When to use it — the scenario
Force close is the escape hatch for an unresponsive hedger. A partyB may be offline, degraded, or simply not filling your close — the position is stuck CLOSE_PENDING with no counterparty. Force close lets you exit at an oracle price without the hedger, once the protocol cooldowns have passed.
The recommended flow is a two-step “request, then force”:
- Send a limit close at a price worse than the current market — a lower requested close price for a LONG (you’re selling), a higher one for a SHORT — via
limitCloseAuto/useLimitCloseAuto. Setting a worse-than-market price makes the force-close price condition trivially satisfiable: the market has already traded through your price, so step 3 is never blocked byFORCE_CLOSE_PRICE_NOT_REACHED. - Wait for a valid price window. The quote sits
CLOSE_PENDINGuntil a signable Muon window exists —now ≥ statusModifyTimestamp + firstCooldown + secondCooldown + minSigPeriod(the window[statusModifyTimestamp + firstCooldown, now - secondCooldown]must be at leastminSigPeriodlong) — and still beforedeadline - secondCooldown, else the request expires (cancel and retry).useForceCloseEligibilitycounts this down and only enables the button then, so the price fetch never fires against an empty window. - Force close with
forceCloseAuto/useForceClose. The Muon oracle attests the price window; because you set a worse price, the gap check clears.
You don’t actually eat the worse price. The execution price is bounded by the oracle average, not your
intentionally-conservative limit: max(R × (1 + penalty), averagePrice) for a LONG, min(R × (1 - penalty), averagePrice) for a SHORT (see previewForceClosePrice). So the requested price only gates
eligibility — the close still executes near the market average.
Import
import {
forceCloseAuto,
forceClosePosition,
getForceCloseParams,
checkForceCloseEligibility,
findForceCloseWindow,
checkForceClosePriceReached,
previewForceClosePrice,
type ForceCloseParams,
type ForceCloseEligibility,
} from "@symmio/trading-core";forceCloseAuto
The one-call, end-to-end flow — pass a quoteId, get a tx hash:
const hash = await forceCloseAuto(config, { account: "0xsub…", quoteId: 42n });Under the hood it:
- reads the quote (
getQuote) + the protocol force-close params (getForceCloseParams, one multicall); - gates on eligibility (
checkForceCloseEligibility) — throwsFORCE_CLOSE_NOT_ELIGIBLE(not-close-pending/not-limit/cooldown/expired); - fetches reference-exchange (Binance) candles over the valid window and finds a bar where the price reached the force-close level (
findForceCloseWindow) → the Muon time window; throwsFORCE_CLOSE_PRICE_NOT_REACHEDwhen none qualifies; - fetches the Muon
HighLowPriceSigfor that window (getForceClosePriceSig); - preflights the on-chain gap check (
checkForceClosePriceReached) — throwsFORCE_CLOSE_PRICE_NOT_REACHEDif the sig doesn’t clear the gap; - sends
forceClosePosition(quoteId, sig).
Cross-mode partyB is out of scope for now. A partyB running cross-margin settlement reverts ForceActionsFacet: Cross partyB mode enabled on the single call; the 3-step initializeForceClose / finalizeForceClose flow is a
follow-up. Rasa’s hedger is non-cross.
getForceCloseParams
Reads the protocol force-close params for a market in one multicall — the inputs to the gate and the price checks:
const { firstCooldown, secondCooldown, pricePenalty, minSigPeriod, gapRatio } = await getForceCloseParams(config, {
symbolId: 1n,
});| Field | Meaning |
|---|---|
firstCooldown | Seconds after statusModifyTimestamp before a force close is allowed. |
secondCooldown | The sig window must end this far before now, and now before deadline. |
pricePenalty | Penalty applied to the requested close price (1e18). |
minSigPeriod | Minimum Muon signature window length (seconds). |
gapRatio | Per-symbol gap the market must exceed the requested price by (1e18). |
Pure helpers
Framework-free, no network — reuse them to build a custom flow or a UI gate:
checkForceCloseEligibility({ quote, firstCooldown, secondCooldown, minSigPeriod, now })→{ eligible, reason?, cooldownRemaining }. The button gate + countdown. The cooldown clears only once a valid window exists (firstCooldown + secondCooldown + minSigPeriod), so it never enables the flow during the empty-window gap.findForceCloseWindow({ quote, candles, gapRatio, firstCooldown, secondCooldown, now, intervalMs })→{ t0, t1 } | null. Scans candles for the first in-window bar that reached the price (ported from Vibe-ui).checkForceClosePriceReached({ sig, positionType, requestedClosePrice, gapRatio })→boolean. Exact on-chain preflight: LONG needshighest ≥ R×(1+gap), SHORTlowest ≤ R×(1-gap).previewForceClosePrice({ sig, positionType, requestedClosePrice, pricePenalty })→bigint. LONGmax(R×(1+pen), avg), SHORTmin(R×(1-pen), avg).
forceClosePosition
The low-level single write, for callers that already have the Muon sig (forceCloseAuto calls it last):
const hash = await forceClosePosition(config, { account: "0xsub…", quoteId: 42n, sig });Relaying it without gas
Both forceClosePosition and forceCloseAuto accept gasless, so a force close can be relayed rather than paid for:
const hash = await forceCloseAuto(config, { account: "0xsub…", quoteId: 42n, gasless: true });The write already routes through the AccountLayer _call proxy, so it relays as one operation targeting the core
diamond, billed to the same account the position belongs to. See
relayable writes for the mode, the fallback policy and the per-call options. It is
deliberately not in GASLESS_SESSION_KEY_SELECTORS — relayable, but owner-signed.
Query / Mutation options
getForceCloseParamsQueryOptions(config, { symbolId })— TanStack query bag.forceCloseAutoMutationOptions(config)/forceClosePositionMutationOptions(config)— TanStack mutation bags.
Related
- React
useForceClose— the hook wrapper (+useForceCloseEligibilityfor the gate). - Muon
getForceClosePriceSig— theHighLowPriceSigattestation. - Cancel a close — the other way out of a
CLOSE_PENDING.