@0xmonaco/core 1.0.45 → 1.0.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -34,6 +34,7 @@ export declare const perpRoutes: {
34
34
  pageSize?: number;
35
35
  }) => string;
36
36
  readonly getTwap: (twapOrderId: string) => string;
37
+ readonly getTwapTca: (twapOrderId: string) => string;
37
38
  readonly cancelTwap: (twapOrderId: string) => string;
38
39
  };
39
40
  readonly delegatedAgents: {
@@ -32,6 +32,7 @@ export const perpRoutes = {
32
32
  createTwap: () => `${API_V1}/orders/twap`,
33
33
  listTwap: (params) => withQuery(`${API_V1}/orders/twap`, params),
34
34
  getTwap: (twapOrderId) => `${API_V1}/orders/twap/${encodeSegment(twapOrderId)}`,
35
+ getTwapTca: (twapOrderId) => `${API_V1}/orders/twap/${encodeSegment(twapOrderId)}/tca`,
35
36
  cancelTwap: (twapOrderId) => `${API_V1}/orders/twap/${encodeSegment(twapOrderId)}/cancel`,
36
37
  },
37
38
  delegatedAgents: {
@@ -21,7 +21,7 @@
21
21
  * );
22
22
  * ```
23
23
  */
24
- import type { BatchCancelOrdersResponse, BatchCreateOrderParams, BatchCreateOrdersResponse, BatchReplaceOrderParams, BatchReplaceOrdersResponse, CancelConditionalOrderResponse, CancelOrderResponse, CancelTwapOrderResponse, ConditionalOrder, CreateOrderResponse, CreateTwapOrderOptions, CreateTwapOrderResponse, GetOrderResponse, GetPaginatedOrdersParams, GetPaginatedOrdersResponse, ListConditionalOrdersParams, ListConditionalOrdersResponse, ListTwapOrdersParams, ListTwapOrdersResponse, MarginMode, OrderSide, ParentTpSlLegParams, PositionSide, ReplaceOrderResponse, TimeInForce, TradingAPI, TradingMode, TwapOrder } from "@0xmonaco/types";
24
+ import type { BatchCancelOrdersResponse, BatchCreateOrderParams, BatchCreateOrdersResponse, BatchReplaceOrderParams, BatchReplaceOrdersResponse, CancelConditionalOrderResponse, CancelOrderResponse, CancelTwapOrderResponse, ConditionalOrder, CreateOrderResponse, CreateTwapOrderOptions, CreateTwapOrderResponse, GetOrderResponse, GetPaginatedOrdersParams, GetPaginatedOrdersResponse, GetTwapOrderTcaResponse, ListConditionalOrdersParams, ListConditionalOrdersResponse, ListTwapOrdersParams, ListTwapOrdersResponse, MarginMode, OrderSide, ParentTpSlLegParams, PositionSide, ReplaceOrderResponse, TimeInForce, TradingAPI, TradingMode, TwapOrder } from "@0xmonaco/types";
25
25
  import { BaseAPI } from "../base";
26
26
  export declare class TradingAPIImpl extends BaseAPI implements TradingAPI {
27
27
  /**
@@ -444,6 +444,28 @@ export declare class TradingAPIImpl extends BaseAPI implements TradingAPI {
444
444
  * ```
445
445
  */
446
446
  getTwapOrder(twapOrderId: string): Promise<TwapOrder>;
447
+ /**
448
+ * Gets the post-execution TCA (transaction cost analysis) report for a
449
+ * TERMINAL TWAP parent order.
450
+ *
451
+ * Available once the parent is COMPLETED or CANCELLED (a non-terminal
452
+ * parent returns 400). Reports realized average price against the arrival
453
+ * price and the window-TWAP benchmark over the parent's active window.
454
+ * Sign convention: positive bps = execution beat the reference. Benchmark
455
+ * fields are null when no price series covered the window sufficiently.
456
+ *
457
+ * @param twapOrderId - The TWAP parent order UUID
458
+ * @returns Promise resolving to the TCA report
459
+ * @throws {APIError} When API communication fails, the order is not found,
460
+ * or the order has not reached a terminal state
461
+ *
462
+ * @example
463
+ * ```typescript
464
+ * const tca = await tradingAPI.getTwapOrderTca(twapOrderId);
465
+ * console.log(`vs benchmark: ${tca.performanceVsBenchmarkBps} bps (${tca.benchmarkSource})`);
466
+ * ```
467
+ */
468
+ getTwapOrderTca(twapOrderId: string): Promise<GetTwapOrderTcaResponse>;
447
469
  /**
448
470
  * Lists TWAP parent orders for the authenticated user.
449
471
  *
@@ -720,6 +720,33 @@ export class TradingAPIImpl extends BaseAPI {
720
720
  method: "GET",
721
721
  });
722
722
  }
723
+ /**
724
+ * Gets the post-execution TCA (transaction cost analysis) report for a
725
+ * TERMINAL TWAP parent order.
726
+ *
727
+ * Available once the parent is COMPLETED or CANCELLED (a non-terminal
728
+ * parent returns 400). Reports realized average price against the arrival
729
+ * price and the window-TWAP benchmark over the parent's active window.
730
+ * Sign convention: positive bps = execution beat the reference. Benchmark
731
+ * fields are null when no price series covered the window sufficiently.
732
+ *
733
+ * @param twapOrderId - The TWAP parent order UUID
734
+ * @returns Promise resolving to the TCA report
735
+ * @throws {APIError} When API communication fails, the order is not found,
736
+ * or the order has not reached a terminal state
737
+ *
738
+ * @example
739
+ * ```typescript
740
+ * const tca = await tradingAPI.getTwapOrderTca(twapOrderId);
741
+ * console.log(`vs benchmark: ${tca.performanceVsBenchmarkBps} bps (${tca.benchmarkSource})`);
742
+ * ```
743
+ */
744
+ async getTwapOrderTca(twapOrderId) {
745
+ validate(CancelTwapOrderSchema, { twapOrderId });
746
+ return await this.makeAuthenticatedRequest(perpRoutes.orders.getTwapTca(twapOrderId), {
747
+ method: "GET",
748
+ });
749
+ }
723
750
  /**
724
751
  * Lists TWAP parent orders for the authenticated user.
725
752
  *
@@ -1,4 +1,4 @@
1
- import type { AccountEvent, ConditionalOrderEvent, Interval, LiquidationEvent, MarketStatsAllEvent, MarketStatsEvent, MonacoWebSocketConfig, OHLCVEvent, OHLCVPriceType, OrderbookEvent, OrderbookQuotationMode, OrderEvent, PositionEvent, SessionCredentials, TradeEvent, TradingMode, TwapOrderEvent, UserBalanceEvent, UserMovementEvent, WebSocketStatus } from "@0xmonaco/types";
1
+ import type { AccountEvent, ConditionalOrderEvent, InstrumentEvent, Interval, LiquidationEvent, MarketStatsAllEvent, MarketStatsEvent, MonacoWebSocketConfig, OHLCVEvent, OHLCVPriceType, OrderbookEvent, OrderbookQuotationMode, OrderEvent, PositionEvent, SessionCredentials, TradeEvent, TradingMode, TwapOrderEvent, UserBalanceEvent, UserMovementEvent, WebSocketStatus } from "@0xmonaco/types";
2
2
  export type { ResyncHandler, StatusHandler, WebSocketResyncInfo } from "@0xmonaco/types";
3
3
  export type MessageHandler<T> = (data: T) => void;
4
4
  /**
@@ -46,6 +46,8 @@ export interface MonacoWebSocket {
46
46
  liquidations: (handler: MessageHandler<LiquidationEvent>, tradingPairId?: string) => () => void;
47
47
  /** Subscribe to account-level margin health updates for the authenticated user (requires authentication) */
48
48
  account: (handler: MessageHandler<AccountEvent>) => () => void;
49
+ /** Subscribe to market lifecycle + config changes (public); omit `tradingPairId` for all markets */
50
+ instruments: (handler: MessageHandler<InstrumentEvent>, tradingPairId?: string) => () => void;
49
51
  /** Subscribe to per-market statistics for one trading pair (public) */
50
52
  marketStats: (tradingPairId: string, handler: MessageHandler<MarketStatsEvent>) => () => void;
51
53
  /** Subscribe to the all-markets statistics summary (public) */
@@ -13,6 +13,7 @@ const CONDITIONAL_ORDER_TRIGGER_SOURCES = ["MARK_PRICE"];
13
13
  const CONDITIONAL_ORDER_STATES = ["PENDING_PARENT", "ACTIVE", "TRIGGERING", "TRIGGERED", "CANCELLED", "EXPIRED", "FAILED"];
14
14
  const CONDITIONAL_ORDER_ASSOCIATION_TYPES = ["POSITION", "PARENT_ORDER"];
15
15
  const TWAP_ORDER_STATES = ["PENDING", "ACTIVE", "COMPLETED", "CANCELLED"];
16
+ const INSTRUMENT_CHANGE_KINDS = ["listing", "delisting", "halt", "unhalt", "config_change"];
16
17
  function isRecord(value) {
17
18
  return typeof value === "object" && value !== null && !Array.isArray(value);
18
19
  }
@@ -48,6 +49,13 @@ function readOptionalNumber(source, field) {
48
49
  }
49
50
  return value;
50
51
  }
52
+ function readStringArray(source, field) {
53
+ const value = source[field];
54
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
55
+ throw new Error(`Invalid WebSocket event: ${field} must be an array of strings`);
56
+ }
57
+ return value;
58
+ }
51
59
  function readEnum(source, field, allowed) {
52
60
  const value = readString(source, field);
53
61
  if (!allowed.includes(value)) {
@@ -269,6 +277,42 @@ function parseMarketStatsAllEvent(rawData) {
269
277
  markets: rawData.markets.map(parseMarketStatsSummary),
270
278
  };
271
279
  }
280
+ function parseInstrumentEventData(raw) {
281
+ if (!isRecord(raw)) {
282
+ throw new Error("Invalid instrument event: data must be an object");
283
+ }
284
+ const data = keysToCamelCase(raw);
285
+ return {
286
+ change: readEnum(data, "change", INSTRUMENT_CHANGE_KINDS),
287
+ tradingPairId: readString(data, "tradingPairId"),
288
+ symbol: readString(data, "symbol"),
289
+ isActive: readBoolean(data, "isActive"),
290
+ tickSize: readString(data, "tickSize"),
291
+ quantityStepSize: readString(data, "quantityStepSize"),
292
+ minOrderSize: readString(data, "minOrderSize"),
293
+ maxOrderSize: readString(data, "maxOrderSize"),
294
+ marketType: readString(data, "marketType"),
295
+ category: readString(data, "category"),
296
+ maxLeverage: readOptionalString(data, "maxLeverage"),
297
+ initialMarginRate: readOptionalString(data, "initialMarginRate"),
298
+ maintenanceMarginRate: readOptionalString(data, "maintenanceMarginRate"),
299
+ // Values are snake_case wire field NAMES (e.g. "tick_size"); only object
300
+ // keys are camelCased, so they pass through untouched.
301
+ changedFields: readStringArray(data, "changedFields"),
302
+ updatedAt: readString(data, "updatedAt"),
303
+ };
304
+ }
305
+ function parseInstrumentEvent(rawData) {
306
+ if (!isRecord(rawData)) {
307
+ throw new Error("Invalid instrument event: payload must be an object");
308
+ }
309
+ // The frame's top-level `trading_pair_id` is not inside `data`, so it stays
310
+ // snake_case (only `data` is camelCased by parseInstrumentEventData).
311
+ return {
312
+ tradingPairId: readString(rawData, "trading_pair_id"),
313
+ data: parseInstrumentEventData(rawData.data),
314
+ };
315
+ }
272
316
  /**
273
317
  * Parse a `twap_orders` channel frame into a typed event. Only the envelope
274
318
  * and the snapshot's identity/state fields are validated strictly — the rest
@@ -337,6 +381,23 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
337
381
  let pendingResyncCode = null;
338
382
  // Handler storage: channel -> set of handlers (supports multiple subscribers per channel)
339
383
  const handlers = new Map();
384
+ // Subscribe-time Snapshot frame handlers, keyed by the RAW subscribed
385
+ // channel: the server echoes that exact channel on the Snapshot frame
386
+ // (unlike live Event frames, which can arrive on a more specific channel and
387
+ // are prefix-matched). Only subscriptions that opt in receive them — a
388
+ // snapshot's data shape differs from the channel's Event shape, so blindly
389
+ // reusing the Event handlers would feed every other channel's parser a frame
390
+ // it must reject.
391
+ const snapshotHandlers = new Map();
392
+ // Latest known full-config frame per market for each subscribed instrument
393
+ // channel, seeded by the subscribe-time Snapshot and superseded by every
394
+ // live frame (instrument frames are terminal-state, never deltas). The
395
+ // server sends a snapshot only for a genuinely new wire subscription, so a
396
+ // late local subscriber on an already-held channel bootstraps from this
397
+ // cache instead of starting blind. Dropped when the channel's last local
398
+ // subscriber unsubscribes — the next first subscriber triggers a fresh wire
399
+ // Subscribe whose snapshot is authoritative, never the stale cache.
400
+ const instrumentBaseline = new Map();
340
401
  const getStatus = () => {
341
402
  if (!ws)
342
403
  return reconnectTimer ? "reconnecting" : "disconnected";
@@ -431,6 +492,11 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
431
492
  }
432
493
  };
433
494
  const resubscribeAll = () => {
495
+ // Every re-sent Subscribe is genuinely new to the server (new
496
+ // connection), so fresh authoritative snapshots are inbound: the
497
+ // pre-outage instrument cache is stale — markets may have been delisted
498
+ // during the gap — and must not be replayed to late subscribers.
499
+ instrumentBaseline.clear();
434
500
  const channels = Array.from(handlers.keys());
435
501
  if (channels.length > 0) {
436
502
  send({ type: "Subscribe", channels });
@@ -460,7 +526,17 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
460
526
  return frameChannel === subscriptionChannel || frameChannel.startsWith(`${subscriptionChannel}:`);
461
527
  };
462
528
  const routeMessage = (msg) => {
463
- if (msg.type !== "Event" || !msg.channel)
529
+ if (!msg.channel)
530
+ return;
531
+ if (msg.type === "Snapshot") {
532
+ // Exact match only: the Snapshot frame's channel echoes the raw
533
+ // subscribed channel, never a more specific one.
534
+ for (const handler of snapshotHandlers.get(msg.channel) ?? []) {
535
+ handler(msg.data);
536
+ }
537
+ return;
538
+ }
539
+ if (msg.type !== "Event")
464
540
  return;
465
541
  // Find matching handlers and call all of them
466
542
  for (const [subscriptionChannel, channelHandlers] of handlers) {
@@ -579,23 +655,38 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
579
655
  stopHeartbeat();
580
656
  pendingResyncCode = null;
581
657
  handlers.clear();
658
+ snapshotHandlers.clear();
659
+ instrumentBaseline.clear();
582
660
  ws?.close(1000, "Client disconnect");
583
661
  ws = null;
584
662
  options.onStatusChange?.("disconnected");
585
663
  };
586
- const subscribe = (channel, handler) => {
664
+ const subscribe = (channel, handler, snapshotHandler) => {
587
665
  const isFirstSubscriber = !handlers.has(channel);
588
666
  // Add handler to the set for this channel
589
667
  if (!handlers.has(channel)) {
590
668
  handlers.set(channel, new Set());
591
669
  }
592
670
  handlers.get(channel)?.add(handler);
671
+ if (snapshotHandler) {
672
+ if (!snapshotHandlers.has(channel)) {
673
+ snapshotHandlers.set(channel, new Set());
674
+ }
675
+ snapshotHandlers.get(channel)?.add(snapshotHandler);
676
+ }
593
677
  // Only send Subscribe message on first subscriber for this channel
594
678
  if (isFirstSubscriber && ws?.readyState === WebSocket.OPEN) {
595
679
  send({ type: "Subscribe", channels: [channel] });
596
680
  }
597
681
  // Return unsubscribe function
598
682
  return () => {
683
+ if (snapshotHandler) {
684
+ const channelSnapshotHandlers = snapshotHandlers.get(channel);
685
+ channelSnapshotHandlers?.delete(snapshotHandler);
686
+ if (channelSnapshotHandlers?.size === 0) {
687
+ snapshotHandlers.delete(channel);
688
+ }
689
+ }
599
690
  const channelHandlers = handlers.get(channel);
600
691
  if (channelHandlers) {
601
692
  channelHandlers.delete(handler);
@@ -952,6 +1043,110 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
952
1043
  }
953
1044
  });
954
1045
  };
1046
+ /**
1047
+ * Subscribe to market lifecycle + configuration changes (public): listings,
1048
+ * delistings, and edits to tick size, step size, order-size bounds, or (for
1049
+ * perp markets) leverage/margin-rate bounds. Omit `tradingPairId` for all
1050
+ * markets, or pass one to filter. Every frame carries the market's FULL
1051
+ * current config as a terminal-state snapshot (never an additive delta);
1052
+ * `changedFields` names what the mutation touched. A subscribe-time snapshot
1053
+ * of the current active-pair list arrives before the live diffs; a
1054
+ * subscriber joining a channel this client already holds is replayed the
1055
+ * current baseline from a local cache instead (the server snapshots only
1056
+ * genuinely new wire subscriptions). Lag policy is Conflate: a lagging
1057
+ * subscriber is fast-forwarded, no gap signal.
1058
+ */
1059
+ const subscribeInstruments = (handler, tradingPairId) => {
1060
+ if (!handler) {
1061
+ throw new Error("instruments subscription requires a handler");
1062
+ }
1063
+ if (tradingPairId !== undefined && tradingPairId.length === 0) {
1064
+ throw new Error("instruments tradingPairId cannot be empty");
1065
+ }
1066
+ // Every live frame is published on instrument:<pairId>; a bare
1067
+ // `instrument` subscription receives all of them via routeMessage's prefix
1068
+ // match, so unlike market_stats there is no distinct all-markets frame
1069
+ // shape to guard against.
1070
+ const channel = tradingPairId ? `instrument:${tradingPairId}` : "instrument";
1071
+ const cacheEvent = (event) => {
1072
+ let byPair = instrumentBaseline.get(channel);
1073
+ if (!byPair) {
1074
+ byPair = new Map();
1075
+ instrumentBaseline.set(channel, byPair);
1076
+ }
1077
+ byPair.set(event.tradingPairId, event);
1078
+ };
1079
+ // A subscriber joining a channel this connection already holds gets no
1080
+ // fresh server snapshot (the wire Subscribe is suppressed, and the server
1081
+ // snapshots only genuinely new subscriptions), so replay the cached
1082
+ // baseline to it instead. Asynchronously, matching how a real snapshot
1083
+ // arrives — never during the subscribe call itself.
1084
+ const isLateSubscriber = handlers.has(channel);
1085
+ let unsubscribed = false;
1086
+ const unsubscribe = subscribe(channel, (rawData) => {
1087
+ try {
1088
+ const event = parseInstrumentEvent(rawData);
1089
+ cacheEvent(event);
1090
+ handler(event);
1091
+ }
1092
+ catch (err) {
1093
+ console.error("WebSocket: Error processing instrument event", err);
1094
+ }
1095
+ },
1096
+ // The subscribe-time baseline arrives as one Snapshot frame whose data is
1097
+ // a flat InstrumentEventData[] (a `listing` per active market, no
1098
+ // per-item envelope). Fan it out as one event per market so the handler
1099
+ // sees the same shape for baseline and live frames.
1100
+ (rawData) => {
1101
+ try {
1102
+ if (!Array.isArray(rawData)) {
1103
+ throw new Error("Invalid instrument snapshot: data must be an array");
1104
+ }
1105
+ const events = rawData.map(parseInstrumentEventData).map((item) => ({ tradingPairId: item.tradingPairId, data: item }));
1106
+ // A snapshot is the authoritative active-market set: replace the
1107
+ // channel's cache wholesale rather than upserting, so markets absent
1108
+ // from it (delisted while disconnected) don't linger.
1109
+ instrumentBaseline.set(channel, new Map(events.map((event) => [event.tradingPairId, event])));
1110
+ for (const event of events) {
1111
+ handler(event);
1112
+ }
1113
+ }
1114
+ catch (err) {
1115
+ console.error("WebSocket: Error processing instrument snapshot", err);
1116
+ }
1117
+ });
1118
+ // Replay only while the socket is open: during abnormal-close backoff the
1119
+ // handlers and cache survive but the state is pre-outage — the reconnect's
1120
+ // resubscribe clears it and a fresh authoritative snapshot follows, so a
1121
+ // backoff-time subscriber must wait for that instead.
1122
+ if (isLateSubscriber && ws?.readyState === WebSocket.OPEN) {
1123
+ const cached = instrumentBaseline.get(channel);
1124
+ if (cached && cached.size > 0) {
1125
+ const replay = [...cached.values()];
1126
+ queueMicrotask(() => {
1127
+ if (unsubscribed)
1128
+ return;
1129
+ for (const event of replay) {
1130
+ try {
1131
+ handler(event);
1132
+ }
1133
+ catch (err) {
1134
+ console.error("WebSocket: Error processing instrument baseline replay", err);
1135
+ }
1136
+ }
1137
+ });
1138
+ }
1139
+ }
1140
+ return () => {
1141
+ unsubscribed = true;
1142
+ unsubscribe();
1143
+ // Last local subscriber gone: the wire Unsubscribe was sent, so the
1144
+ // cache would only go stale from here — drop it.
1145
+ if (!handlers.has(channel)) {
1146
+ instrumentBaseline.delete(channel);
1147
+ }
1148
+ };
1149
+ };
955
1150
  /**
956
1151
  * Subscribe to per-market statistics for a single trading pair (public). Each
957
1152
  * frame carries the market's full current stats as a terminal-state snapshot.
@@ -1029,6 +1224,7 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
1029
1224
  positions: subscribePositions,
1030
1225
  liquidations: subscribeLiquidations,
1031
1226
  account: subscribeAccount,
1227
+ instruments: subscribeInstruments,
1032
1228
  marketStats: subscribeMarketStats,
1033
1229
  marketStatsAll: subscribeMarketStatsAll,
1034
1230
  };
@@ -13,6 +13,7 @@ export declare const COVERED: {
13
13
  cancel_twap_order: string;
14
14
  create_twap_order: string;
15
15
  get_twap_order: string;
16
+ get_twap_order_tca: string;
16
17
  list_twap_orders: string;
17
18
  close_position: string;
18
19
  create_challenge: string;
package/dist/coverage.js CHANGED
@@ -13,6 +13,7 @@ export const COVERED = {
13
13
  cancel_twap_order: "cancelTwapOrder",
14
14
  create_twap_order: "createTwapOrder",
15
15
  get_twap_order: "getTwapOrder",
16
+ get_twap_order_tca: "getTwapOrderTca",
16
17
  list_twap_orders: "listTwapOrders",
17
18
  close_position: "closePosition",
18
19
  create_challenge: "createChallenge",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@0xmonaco/core",
3
- "version": "1.0.45",
3
+ "version": "1.0.46",
4
4
  "type": "module",
5
5
  "homepage": "https://docs.0xmonaco.com/sdk/typescript",
6
6
  "main": "./dist/index.js",
@@ -19,8 +19,8 @@
19
19
  "viem": "^2.45.2"
20
20
  },
21
21
  "dependencies": {
22
- "@0xmonaco/contracts": "1.0.45",
23
- "@0xmonaco/types": "1.0.45",
22
+ "@0xmonaco/contracts": "1.0.46",
23
+ "@0xmonaco/types": "1.0.46",
24
24
  "@noble/curves": "^1.9.1",
25
25
  "@noble/hashes": "^1.8.0",
26
26
  "http-status-codes": "^2.3.0"