Skip to main content

Monaco Protocol SDK v1.0.66

This release adds trailing stops for perp positions and fixes two WebSocket client gaps. A trailing stop is a reduce-only market close whose trigger follows the best mark reached since it armed: attach one to an open position with sdk.positions.attachPositionTpSl(positionId, { trailingStop }), or to a margin entry order with trailingStop on placeLimitOrder / placeMarketOrder. Position TP/SL attach now draws from the order-creation rate-limit budget, and Position.version now advances only on a position whose content actually changed. On the socket, the client stops reconnecting in a loop after the server ends a connection for lost authentication — it calls the new onReauthenticationRequired option instead — and onError now names the channel a per-channel Error frame concerns. Action items: pass onReauthenticationRequired if your app holds authenticated WebSocket subscriptions, so it can supply a fresh session when one is revoked or expires; and if you attach TP/SL at high frequency, budget each attach as one order-creation item.

Added

Trailing stops

sdk.positions.attachPositionTpSl (POST /api/v1/positions/{position_id}/tp-sl; gRPC PositionsService.AttachPositionTpSl) accepts a trailingStop: TrailingStopLeg, alone or alongside takeProfit / stopLoss, and the response gains trailingStopOrderId. sdk.trading.placeLimitOrder / placeMarketOrder (POST /api/v1/orders; gRPC OrdersService.CreateOrder) accept the same leg as options.trailingStop (ParentTrailingStopLegParams) on margin entry orders that are not reduce-only, and CreateOrderResponse gains trailingStopOrderId — present only when the trailing stop materializes: the order fully fills on arrival or leaves a resting remainder. A partial fill whose remainder is cancelled (an IOC or MARKET order) creates no trailing stop and omits the id. The leg takes:
  • trailBps — integer, 10 to 2000 basis points behind the watermark
  • activationPrice? — the mark level that starts tracking and seeds the watermark; rejected if the mark has already reached it. Omitted, tracking starts at the current mark
  • quantity? / closePosition? — the same sizing contract as a TP/SL leg, mutually exclusive
  • slippageToleranceBps? — 0 to 10000
  • expiresAt? — ISO 8601
For a long, trigger = watermark × (1 − trailBps/10000) and it fires when the mark falls to or below it; a short mirrors it. The watermark is the best mark since arming, so the trigger only ever moves in the position’s favour. It fires through the standard conditional market close (1,200 bps band, optional slippage cap).
  • One per position. A second attach is rejected with 409 (gRPC ALREADY_EXISTS) — as is an attach while a resting entry order on the same trading pair and risk bucket carries a pending trailing stop — never replaced, and a trailing stop never supersedes or is superseded by a full-close TP/SL leg. It coexists with fixed TP/SL and never joins oco.
  • No modify. Cancel it and attach a new one.
  • Entry-order attach arms only when the order fully fills, and is cancelled with the order otherwise.
  • Reads. ConditionalOrder (getConditionalOrder, listConditionalOrders, and the conditional detail of order history) reads conditionType: "TRAILING_STOP" and gains trailBps, activationPrice, watermarkPrice and trailArmedAt. An entry-attached stop waits as PENDING_PARENT until its order fully fills; from then on (and from the start for a position attach) the row stays state: "ACTIVE" while waiting for activation and while tracking; trailArmedAt is absent until tracking begins, and triggerPrice is provisional until the first mark after arming.
  • Streaming. sdk.ws.conditionalOrders frames (snapshot and live) carry the same four fields, read leniently. Live frames carry reason: "armed" when tracking begins and reason: "ratcheted" on every trigger move, with the new triggerPrice and watermarkPrice.
The SDK rejects a trailBps outside 10–2000 or not an integer, quantity together with closePosition, and a trailing stop on a SPOT or reduce-only order before any request is made. @0xmonaco/types adds TrailingStopLeg, ParentTrailingStopLegParams, "TRAILING_STOP" on ConditionalOrderConditionType, "armed" / "ratcheted" on ConditionalOrderEventReason, and the matching Zod schemas. See Trailing Stop and Positions.

Changed

Position TP/SL attach is metered by the order-creation rate limit

attachPositionTpSl (POST /api/v1/positions/{positionId}/tp-sl; gRPC PositionsService.AttachPositionTpSl) now draws one item per call from the order-creation budget it shares with order creates, replaces, batch items and TWAP creation — whatever legs the call carries. The account-family tier, the PrivateLink column and the X-RateLimit-* headers apply exactly as on the order endpoints. Over budget it returns 429 RATE_LIMIT_EXCEEDED with Retry-After on REST and RESOURCE_EXHAUSTED with RetryInfo on gRPC. Position closes stay unmetered. Like the other order budgets, enforcement is warn-only until it is turned on for the environment, so handle 429 / RESOURCE_EXHAUSTED now and back off on the server’s retry hint rather than waiting for a rejection. See Rate Limits.

Position.version advances only when the position changed

version on position reads now advances exactly when that row’s versioned content changed at a sequence (including a change undone within one persistence batch), instead of on every open position of the margin account whenever a sequenced command touched it. An unchanged position keeps its version. A ranked live position_update frame for an untouched sibling of a filled position can therefore carry a higher version than the REST row while describing identical content — rank it as newer; nothing changes. The per-field-group merge rule is unchanged, and there is no field or type change. See Positions.

Fixed

The WebSocket client stops reconnecting after authentication is lost

After a SESSION_INVALID Error frame followed by a 1008 close (the session was revoked or expired), or an AUTH_FAILED frame after the connection sent Authenticate, the client now stops reconnecting instead of retrying in a loop. It drops the rejected session keypair, keeps your subscription handlers, reports "disconnected", and calls the new onReauthenticationRequired({ code, message, closeCode }) option once. Supplying a fresh session (sdk.login, or setSessionKeypair on a createMonacoWebSocket client) reconnects, authenticates, resubscribes every channel you still hold, and fires onResync. Every other abnormal close — including a 1008 for the inbound message-rate cap and a 1013 slow-client shed — keeps the backoff reconnect. @0xmonaco/types exports ReauthenticationRequiredHandler and WebSocketReauthenticationInfo. See Session loss.

onError names the channel a per-channel failure concerns

onError now delivers { code, message, channel } for a per-channel Error frame (INVALID_SUBSCRIPTION, AUTH_REQUIRED, SUBSCRIPTION_LIMIT, SNAPSHOT_UNAVAILABLE), with channel echoed exactly as you sent it — so an app can tell which channel of a batched or reconnect Subscribe failed, and refetch only that one, without matching the human-readable message. A connection-wide failure is still delivered as exactly { code, message }. WebSocketErrorInfo gains the optional channel?: string. See Server error frames.

Upgrade

Every change is additive for TypeScript callers — no signature changes to adopt. Two behaviours differ: an authenticated WebSocket connection whose session is revoked or expired now stays disconnected until you supply a fresh session (handle onReauthenticationRequired), and each attachPositionTpSl call now counts against the order-creation budget.