@catalyst-team/poly-sdk 0.4.5 → 0.4.7
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.
- package/README.md +18 -9
- package/README.zh-CN.md +18 -9
- package/dist/src/clients/bridge-client.d.ts +131 -1
- package/dist/src/clients/bridge-client.d.ts.map +1 -1
- package/dist/src/clients/bridge-client.js +143 -0
- package/dist/src/clients/bridge-client.js.map +1 -1
- package/dist/src/clients/data-api.d.ts +25 -0
- package/dist/src/clients/data-api.d.ts.map +1 -1
- package/dist/src/clients/data-api.js +57 -0
- package/dist/src/clients/data-api.js.map +1 -1
- package/dist/src/core/types.d.ts +55 -0
- package/dist/src/core/types.d.ts.map +1 -1
- package/dist/src/index.d.ts +7 -6
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/realtime/index.d.ts +18 -0
- package/dist/src/realtime/index.d.ts.map +1 -0
- package/dist/src/realtime/index.js +14 -0
- package/dist/src/realtime/index.js.map +1 -0
- package/dist/src/realtime/realtime-data-client.d.ts +274 -0
- package/dist/src/realtime/realtime-data-client.d.ts.map +1 -0
- package/dist/src/realtime/realtime-data-client.js +771 -0
- package/dist/src/realtime/realtime-data-client.js.map +1 -0
- package/dist/src/realtime/types.d.ts +485 -0
- package/dist/src/realtime/types.d.ts.map +1 -0
- package/dist/src/realtime/types.js +36 -0
- package/dist/src/realtime/types.js.map +1 -0
- package/dist/src/services/arbitrage-service.d.ts.map +1 -1
- package/dist/src/services/arbitrage-service.js +2 -1
- package/dist/src/services/arbitrage-service.js.map +1 -1
- package/dist/src/services/dip-arb-service.d.ts.map +1 -1
- package/dist/src/services/dip-arb-service.js +3 -19
- package/dist/src/services/dip-arb-service.js.map +1 -1
- package/dist/src/services/market-service.d.ts +93 -11
- package/dist/src/services/market-service.d.ts.map +1 -1
- package/dist/src/services/market-service.js +189 -22
- package/dist/src/services/market-service.js.map +1 -1
- package/dist/src/services/order-handle.test.d.ts +15 -0
- package/dist/src/services/order-handle.test.d.ts.map +1 -0
- package/dist/src/services/order-handle.test.js +333 -0
- package/dist/src/services/order-handle.test.js.map +1 -0
- package/dist/src/services/order-manager.d.ts +325 -10
- package/dist/src/services/order-manager.d.ts.map +1 -1
- package/dist/src/services/order-manager.js +633 -32
- package/dist/src/services/order-manager.js.map +1 -1
- package/dist/src/services/order-manager.test.d.ts +2 -0
- package/dist/src/services/order-manager.test.d.ts.map +1 -1
- package/dist/src/services/order-manager.test.js +274 -0
- package/dist/src/services/order-manager.test.js.map +1 -1
- package/dist/src/services/realtime-service-v2.d.ts +122 -6
- package/dist/src/services/realtime-service-v2.d.ts.map +1 -1
- package/dist/src/services/realtime-service-v2.js +475 -70
- package/dist/src/services/realtime-service-v2.js.map +1 -1
- package/dist/src/services/trading-service.d.ts +129 -1
- package/dist/src/services/trading-service.d.ts.map +1 -1
- package/dist/src/services/trading-service.js +198 -5
- package/dist/src/services/trading-service.js.map +1 -1
- package/package.json +1 -2
- package/dist/src/services/ctf-detector.d.ts +0 -215
- package/dist/src/services/ctf-detector.d.ts.map +0 -1
- package/dist/src/services/ctf-detector.js +0 -420
- package/dist/src/services/ctf-detector.js.map +0 -1
|
@@ -8,7 +8,125 @@
|
|
|
8
8
|
* - Auto-validates orders before submission (market state, balance, precision)
|
|
9
9
|
* - Auto-watches orders after creation
|
|
10
10
|
*
|
|
11
|
-
*
|
|
11
|
+
* ============================================================================
|
|
12
|
+
* ORDER TYPES AND LIFECYCLE
|
|
13
|
+
* ============================================================================
|
|
14
|
+
*
|
|
15
|
+
* Polymarket supports 4 order types, each with different lifecycle patterns:
|
|
16
|
+
*
|
|
17
|
+
* ┌─────────────────────────────────────────────────────────────────────────┐
|
|
18
|
+
* │ LIMIT ORDERS (createOrder) │
|
|
19
|
+
* ├─────────────────────────────────────────────────────────────────────────┤
|
|
20
|
+
* │ │
|
|
21
|
+
* │ GTC (Good Till Cancelled) - Default limit order │
|
|
22
|
+
* │ ─────────────────────────────────────────────────────────────────────── │
|
|
23
|
+
* │ Lifecycle: PENDING → OPEN → PARTIALLY_FILLED* → FILLED or CANCELLED │
|
|
24
|
+
* │ Duration: seconds → minutes → hours → days (until filled/cancelled) │
|
|
25
|
+
* │ │
|
|
26
|
+
* │ ┌─────────┐ placed ┌──────┐ partial ┌──────────────────┐ │
|
|
27
|
+
* │ │ PENDING │ ───────────→ │ OPEN │ ──────────→ │ PARTIALLY_FILLED │ │
|
|
28
|
+
* │ └─────────┘ └──────┘ └──────────────────┘ │
|
|
29
|
+
* │ │ │ │ │
|
|
30
|
+
* │ │ reject │ cancel │ fill/cancel│
|
|
31
|
+
* │ ↓ ↓ ↓ │
|
|
32
|
+
* │ ┌──────────┐ ┌───────────┐ ┌────────┐ │
|
|
33
|
+
* │ │ REJECTED │ │ CANCELLED │ │ FILLED │ │
|
|
34
|
+
* │ └──────────┘ └───────────┘ └────────┘ │
|
|
35
|
+
* │ │
|
|
36
|
+
* │ GTD (Good Till Date) - Limit order with expiration │
|
|
37
|
+
* │ ───────────────────────────────────────────────────────────────────── │
|
|
38
|
+
* │ Lifecycle: Same as GTC, but auto-expires at specified time │
|
|
39
|
+
* │ Duration: seconds → specified expiration time │
|
|
40
|
+
* │ Additional terminal state: EXPIRED (when expiration time reached) │
|
|
41
|
+
* │ │
|
|
42
|
+
* ├─────────────────────────────────────────────────────────────────────────┤
|
|
43
|
+
* │ MARKET ORDERS (createMarketOrder) │
|
|
44
|
+
* ├─────────────────────────────────────────────────────────────────────────┤
|
|
45
|
+
* │ │
|
|
46
|
+
* │ FOK (Fill Or Kill) - Must fill completely or cancel immediately │
|
|
47
|
+
* │ ───────────────────────────────────────────────────────────────────────│
|
|
48
|
+
* │ Lifecycle: PENDING → FILLED (success) or PENDING → CANCELLED (fail) │
|
|
49
|
+
* │ Duration: milliseconds (instant execution) │
|
|
50
|
+
* │ Key: NO partial fills - all or nothing │
|
|
51
|
+
* │ │
|
|
52
|
+
* │ ┌─────────┐ full fill ┌────────┐ │
|
|
53
|
+
* │ │ PENDING │ ─────────────→ │ FILLED │ │
|
|
54
|
+
* │ └─────────┘ └────────┘ │
|
|
55
|
+
* │ │ │
|
|
56
|
+
* │ │ cannot fill completely │
|
|
57
|
+
* │ ↓ │
|
|
58
|
+
* │ ┌───────────┐ │
|
|
59
|
+
* │ │ CANCELLED │ (filledSize = 0) │
|
|
60
|
+
* │ └───────────┘ │
|
|
61
|
+
* │ │
|
|
62
|
+
* │ FAK (Fill And Kill) - Fill what's available, cancel the rest │
|
|
63
|
+
* │ ───────────────────────────────────────────────────────────────────────│
|
|
64
|
+
* │ Lifecycle: PENDING → FILLED or PENDING → CANCELLED (with partial fill)│
|
|
65
|
+
* │ Duration: milliseconds (instant execution) │
|
|
66
|
+
* │ Key: May have partial fills, remainder is cancelled immediately │
|
|
67
|
+
* │ │
|
|
68
|
+
* │ ┌─────────┐ full fill ┌────────┐ │
|
|
69
|
+
* │ │ PENDING │ ─────────────→ │ FILLED │ │
|
|
70
|
+
* │ └─────────┘ └────────┘ │
|
|
71
|
+
* │ │ │
|
|
72
|
+
* │ │ partial fill + cancel rest │
|
|
73
|
+
* │ ↓ │
|
|
74
|
+
* │ ┌───────────┐ │
|
|
75
|
+
* │ │ CANCELLED │ (filledSize > 0, cancelledSize > 0) │
|
|
76
|
+
* │ └───────────┘ │
|
|
77
|
+
* │ │
|
|
78
|
+
* └─────────────────────────────────────────────────────────────────────────┘
|
|
79
|
+
*
|
|
80
|
+
* ============================================================================
|
|
81
|
+
* WHY ORDERMANAGER SUPPORTS ALL ORDER TYPES
|
|
82
|
+
* ============================================================================
|
|
83
|
+
*
|
|
84
|
+
* The key insight is that despite different lifecycles, all order types share
|
|
85
|
+
* the same underlying status model and event stream:
|
|
86
|
+
*
|
|
87
|
+
* 1. UNIFIED STATUS MODEL
|
|
88
|
+
* All orders use the same OrderStatus enum: PENDING, OPEN, PARTIALLY_FILLED,
|
|
89
|
+
* FILLED, CANCELLED, EXPIRED. The difference is which transitions are valid
|
|
90
|
+
* and how quickly they occur.
|
|
91
|
+
*
|
|
92
|
+
* 2. SAME WEBSOCKET/POLLING EVENTS
|
|
93
|
+
* RealtimeServiceV2's `clob_user` topic emits the same USER_ORDER and
|
|
94
|
+
* USER_TRADE events for all order types. The eventType field indicates:
|
|
95
|
+
* - PLACEMENT: Order entered the system
|
|
96
|
+
* - UPDATE: Order was modified (fill, partial fill)
|
|
97
|
+
* - CANCELLATION: Order was cancelled
|
|
98
|
+
*
|
|
99
|
+
* 3. STATUS TRANSITION VALIDATION
|
|
100
|
+
* The isValidStatusTransition() function handles all valid paths:
|
|
101
|
+
* - GTC/GTD: PENDING → OPEN → PARTIALLY_FILLED → FILLED/CANCELLED
|
|
102
|
+
* - FOK: PENDING → FILLED or PENDING → CANCELLED (no PARTIALLY_FILLED)
|
|
103
|
+
* - FAK: PENDING → FILLED or PENDING → CANCELLED (filledSize may be > 0)
|
|
104
|
+
*
|
|
105
|
+
* 4. TERMINAL STATE HANDLING
|
|
106
|
+
* All order types eventually reach a terminal state (FILLED, CANCELLED,
|
|
107
|
+
* EXPIRED). OrderManager auto-unwatches orders when they reach terminal
|
|
108
|
+
* states, regardless of order type.
|
|
109
|
+
*
|
|
110
|
+
* 5. FILL EVENT DETECTION
|
|
111
|
+
* Whether fills come rapidly (FOK/FAK) or slowly (GTC/GTD), the same
|
|
112
|
+
* fill detection logic works: compare filledSize changes and emit
|
|
113
|
+
* order_partially_filled or order_filled events.
|
|
114
|
+
*
|
|
115
|
+
* Key differences in behavior:
|
|
116
|
+
* ┌─────────────┬─────────────────────────────────────────────────────────┐
|
|
117
|
+
* │ Order Type │ Behavior │
|
|
118
|
+
* ├─────────────┼─────────────────────────────────────────────────────────┤
|
|
119
|
+
* │ GTC/GTD │ May emit many events over long time (hours/days) │
|
|
120
|
+
* │ │ watchOrder() monitors until filled/cancelled/expired │
|
|
121
|
+
* ├─────────────┼─────────────────────────────────────────────────────────┤
|
|
122
|
+
* │ FOK/FAK │ Emit 1-2 events almost instantly (milliseconds) │
|
|
123
|
+
* │ │ watchOrder() monitors briefly, auto-unwatches quickly │
|
|
124
|
+
* └─────────────┴─────────────────────────────────────────────────────────┘
|
|
125
|
+
*
|
|
126
|
+
* ============================================================================
|
|
127
|
+
* PLATFORM-SPECIFIC IMPLEMENTATION
|
|
128
|
+
* ============================================================================
|
|
129
|
+
*
|
|
12
130
|
* This is a Polymarket-specific implementation. The design allows future extraction
|
|
13
131
|
* to interfaces if supporting multiple prediction markets, but for now we focus on
|
|
14
132
|
* clean encapsulation with Polymarket details in private methods.
|
|
@@ -36,13 +154,29 @@
|
|
|
36
154
|
* console.log(`On-chain settled: ${event.transactionHash}`);
|
|
37
155
|
* });
|
|
38
156
|
*
|
|
39
|
-
* // Create order (
|
|
40
|
-
* const
|
|
157
|
+
* // Create limit order (GTC by default)
|
|
158
|
+
* const limitResult = await orderMgr.createOrder({
|
|
41
159
|
* tokenId: '0x...',
|
|
42
160
|
* side: 'BUY',
|
|
43
161
|
* price: 0.52,
|
|
44
162
|
* size: 100,
|
|
45
163
|
* });
|
|
164
|
+
*
|
|
165
|
+
* // Create market order (FOK - fill completely or cancel)
|
|
166
|
+
* const fokResult = await orderMgr.createMarketOrder({
|
|
167
|
+
* tokenId: '0x...',
|
|
168
|
+
* side: 'BUY',
|
|
169
|
+
* amount: 50, // $50 USDC
|
|
170
|
+
* orderType: 'FOK',
|
|
171
|
+
* });
|
|
172
|
+
*
|
|
173
|
+
* // Create market order (FAK - fill what's available)
|
|
174
|
+
* const fakResult = await orderMgr.createMarketOrder({
|
|
175
|
+
* tokenId: '0x...',
|
|
176
|
+
* side: 'BUY',
|
|
177
|
+
* amount: 50,
|
|
178
|
+
* orderType: 'FAK',
|
|
179
|
+
* });
|
|
46
180
|
* ```
|
|
47
181
|
*/
|
|
48
182
|
import { EventEmitter } from 'events';
|
|
@@ -53,6 +187,219 @@ import { createUnifiedCache } from '../core/unified-cache.js';
|
|
|
53
187
|
import { OrderStatus } from '../core/types.js';
|
|
54
188
|
import { isTerminalStatus, isValidStatusTransition } from '../core/order-status.js';
|
|
55
189
|
import { PolymarketError, ErrorCode } from '../core/errors.js';
|
|
190
|
+
/**
|
|
191
|
+
* Internal implementation of OrderHandle.
|
|
192
|
+
*
|
|
193
|
+
* Lifecycle:
|
|
194
|
+
* 1. Constructor receives orderManager ref and an executor function
|
|
195
|
+
* 2. Status starts at 'created'
|
|
196
|
+
* 3. Executor is invoked immediately (fire-and-forget)
|
|
197
|
+
* 4. On success: stores orderId, subscribes to OrderManager events
|
|
198
|
+
* 5. On rejection: transitions to 'rejected', resolves promise
|
|
199
|
+
* 6. On terminal events: resolves promise, removes listeners
|
|
200
|
+
*/
|
|
201
|
+
export class OrderHandleImpl {
|
|
202
|
+
orderManager;
|
|
203
|
+
executor;
|
|
204
|
+
_orderId;
|
|
205
|
+
_status = 'created';
|
|
206
|
+
_fills = [];
|
|
207
|
+
_order = null;
|
|
208
|
+
_reason;
|
|
209
|
+
_resolve;
|
|
210
|
+
_promise;
|
|
211
|
+
_handlers = new Map();
|
|
212
|
+
_eventCleanup = null;
|
|
213
|
+
constructor(orderManager, executor) {
|
|
214
|
+
this.orderManager = orderManager;
|
|
215
|
+
this.executor = executor;
|
|
216
|
+
this._promise = new Promise((resolve) => {
|
|
217
|
+
this._resolve = resolve;
|
|
218
|
+
});
|
|
219
|
+
// Fire-and-forget execution
|
|
220
|
+
this.execute();
|
|
221
|
+
}
|
|
222
|
+
// ===== PromiseLike implementation =====
|
|
223
|
+
then(onfulfilled, onrejected) {
|
|
224
|
+
return this._promise.then(onfulfilled, onrejected);
|
|
225
|
+
}
|
|
226
|
+
// ===== Chainable lifecycle callbacks =====
|
|
227
|
+
onAccepted(handler) {
|
|
228
|
+
this.addHandler('accepted', handler);
|
|
229
|
+
return this;
|
|
230
|
+
}
|
|
231
|
+
onPartialFill(handler) {
|
|
232
|
+
this.addHandler('partial_fill', handler);
|
|
233
|
+
return this;
|
|
234
|
+
}
|
|
235
|
+
onFilled(handler) {
|
|
236
|
+
this.addHandler('filled', handler);
|
|
237
|
+
return this;
|
|
238
|
+
}
|
|
239
|
+
onRejected(handler) {
|
|
240
|
+
this.addHandler('rejected', handler);
|
|
241
|
+
return this;
|
|
242
|
+
}
|
|
243
|
+
onCancelled(handler) {
|
|
244
|
+
this.addHandler('cancelled', handler);
|
|
245
|
+
return this;
|
|
246
|
+
}
|
|
247
|
+
onExpired(handler) {
|
|
248
|
+
this.addHandler('expired', handler);
|
|
249
|
+
return this;
|
|
250
|
+
}
|
|
251
|
+
// ===== Operations =====
|
|
252
|
+
async cancel() {
|
|
253
|
+
if (!this._orderId)
|
|
254
|
+
return false;
|
|
255
|
+
if (this.isTerminal())
|
|
256
|
+
return false;
|
|
257
|
+
const result = await this.orderManager.cancelOrder(this._orderId);
|
|
258
|
+
return result.success;
|
|
259
|
+
}
|
|
260
|
+
// ===== State query =====
|
|
261
|
+
get orderId() {
|
|
262
|
+
return this._orderId;
|
|
263
|
+
}
|
|
264
|
+
get status() {
|
|
265
|
+
return this._status;
|
|
266
|
+
}
|
|
267
|
+
// ===== Private implementation =====
|
|
268
|
+
async execute() {
|
|
269
|
+
try {
|
|
270
|
+
const result = await this.executor();
|
|
271
|
+
if (!result.success || !result.orderId) {
|
|
272
|
+
// Order rejected by API or validation
|
|
273
|
+
this._status = 'rejected';
|
|
274
|
+
this._reason = result.errorMsg || 'Order rejected';
|
|
275
|
+
this.invokeHandlers('rejected', this._reason);
|
|
276
|
+
this._resolve({
|
|
277
|
+
status: 'rejected',
|
|
278
|
+
order: null,
|
|
279
|
+
fills: [],
|
|
280
|
+
reason: this._reason,
|
|
281
|
+
});
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
// Store orderId and subscribe to events
|
|
285
|
+
this._orderId = result.orderId;
|
|
286
|
+
this.subscribeToEvents();
|
|
287
|
+
}
|
|
288
|
+
catch (error) {
|
|
289
|
+
// Unexpected error during execution
|
|
290
|
+
this._status = 'rejected';
|
|
291
|
+
this._reason = error instanceof Error ? error.message : String(error);
|
|
292
|
+
this.invokeHandlers('rejected', this._reason);
|
|
293
|
+
this._resolve({
|
|
294
|
+
status: 'rejected',
|
|
295
|
+
order: null,
|
|
296
|
+
fills: [],
|
|
297
|
+
reason: this._reason,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
subscribeToEvents() {
|
|
302
|
+
const onOpened = (order) => {
|
|
303
|
+
if (order.id !== this._orderId)
|
|
304
|
+
return;
|
|
305
|
+
this._status = 'open';
|
|
306
|
+
this._order = order;
|
|
307
|
+
this.invokeHandlers('accepted', order);
|
|
308
|
+
};
|
|
309
|
+
const onPartialFill = (fill) => {
|
|
310
|
+
if (fill.orderId !== this._orderId)
|
|
311
|
+
return;
|
|
312
|
+
this._status = 'partially_filled';
|
|
313
|
+
this._order = fill.order;
|
|
314
|
+
this._fills.push(fill);
|
|
315
|
+
this.invokeHandlers('partial_fill', fill);
|
|
316
|
+
};
|
|
317
|
+
const onFilled = (fill) => {
|
|
318
|
+
if (fill.orderId !== this._orderId)
|
|
319
|
+
return;
|
|
320
|
+
this._status = 'filled';
|
|
321
|
+
this._order = fill.order;
|
|
322
|
+
this._fills.push(fill);
|
|
323
|
+
this.invokeHandlers('filled', fill);
|
|
324
|
+
this.resolveTerminal('filled');
|
|
325
|
+
};
|
|
326
|
+
const onCancelled = (event) => {
|
|
327
|
+
if (event.orderId !== this._orderId)
|
|
328
|
+
return;
|
|
329
|
+
this._status = 'cancelled';
|
|
330
|
+
this._order = event.order;
|
|
331
|
+
this.invokeHandlers('cancelled', event.order);
|
|
332
|
+
this.resolveTerminal('cancelled', 'Order cancelled');
|
|
333
|
+
};
|
|
334
|
+
const onExpired = (event) => {
|
|
335
|
+
if (event.orderId !== this._orderId)
|
|
336
|
+
return;
|
|
337
|
+
this._status = 'expired';
|
|
338
|
+
this._order = event.order;
|
|
339
|
+
this.invokeHandlers('expired', event.order);
|
|
340
|
+
this.resolveTerminal('expired', 'Order expired');
|
|
341
|
+
};
|
|
342
|
+
const onRejected = (event) => {
|
|
343
|
+
if (event.orderId !== this._orderId)
|
|
344
|
+
return;
|
|
345
|
+
this._status = 'rejected';
|
|
346
|
+
this._reason = event.reason;
|
|
347
|
+
this.invokeHandlers('rejected', event.reason);
|
|
348
|
+
this.resolveTerminal('rejected', event.reason);
|
|
349
|
+
};
|
|
350
|
+
this.orderManager.on('order_opened', onOpened);
|
|
351
|
+
this.orderManager.on('order_partially_filled', onPartialFill);
|
|
352
|
+
this.orderManager.on('order_filled', onFilled);
|
|
353
|
+
this.orderManager.on('order_cancelled', onCancelled);
|
|
354
|
+
this.orderManager.on('order_expired', onExpired);
|
|
355
|
+
this.orderManager.on('order_rejected', onRejected);
|
|
356
|
+
this._eventCleanup = () => {
|
|
357
|
+
this.orderManager.removeListener('order_opened', onOpened);
|
|
358
|
+
this.orderManager.removeListener('order_partially_filled', onPartialFill);
|
|
359
|
+
this.orderManager.removeListener('order_filled', onFilled);
|
|
360
|
+
this.orderManager.removeListener('order_cancelled', onCancelled);
|
|
361
|
+
this.orderManager.removeListener('order_expired', onExpired);
|
|
362
|
+
this.orderManager.removeListener('order_rejected', onRejected);
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
resolveTerminal(status, reason) {
|
|
366
|
+
this.cleanup();
|
|
367
|
+
this._resolve({
|
|
368
|
+
status,
|
|
369
|
+
order: this._order,
|
|
370
|
+
fills: this._fills,
|
|
371
|
+
reason,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
cleanup() {
|
|
375
|
+
if (this._eventCleanup) {
|
|
376
|
+
this._eventCleanup();
|
|
377
|
+
this._eventCleanup = null;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
isTerminal() {
|
|
381
|
+
return this._status === 'filled' || this._status === 'cancelled' || this._status === 'rejected' || this._status === 'expired';
|
|
382
|
+
}
|
|
383
|
+
addHandler(event, handler) {
|
|
384
|
+
if (!this._handlers.has(event)) {
|
|
385
|
+
this._handlers.set(event, []);
|
|
386
|
+
}
|
|
387
|
+
this._handlers.get(event).push(handler);
|
|
388
|
+
}
|
|
389
|
+
invokeHandlers(event, ...args) {
|
|
390
|
+
const handlers = this._handlers.get(event);
|
|
391
|
+
if (!handlers)
|
|
392
|
+
return;
|
|
393
|
+
for (const handler of handlers) {
|
|
394
|
+
try {
|
|
395
|
+
handler(...args);
|
|
396
|
+
}
|
|
397
|
+
catch {
|
|
398
|
+
// Swallow handler errors to avoid breaking the lifecycle
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
56
403
|
// ============================================================================
|
|
57
404
|
// OrderManager Implementation
|
|
58
405
|
// ============================================================================
|
|
@@ -75,7 +422,10 @@ export class OrderManager extends EventEmitter {
|
|
|
75
422
|
this.config = {
|
|
76
423
|
...config,
|
|
77
424
|
chainId: config.chainId ?? 137,
|
|
78
|
-
|
|
425
|
+
// Bug 24 fix: Default to 'websocket' mode instead of 'hybrid'
|
|
426
|
+
// Polling mechanism can update watched orders with incorrect tokenId,
|
|
427
|
+
// causing fill events to be processed with wrong token type
|
|
428
|
+
mode: config.mode ?? 'websocket',
|
|
79
429
|
pollingInterval: config.pollingInterval ?? 5000,
|
|
80
430
|
polygonRpcUrl: config.polygonRpcUrl ?? 'https://polygon-rpc.com',
|
|
81
431
|
};
|
|
@@ -173,8 +523,13 @@ export class OrderManager extends EventEmitter {
|
|
|
173
523
|
// Submit order via TradingService
|
|
174
524
|
const result = await this.tradingService.createLimitOrder(params);
|
|
175
525
|
if (result.success && result.orderId) {
|
|
176
|
-
// Auto-watch the order
|
|
177
|
-
this.watchOrder(result.orderId, metadata
|
|
526
|
+
// Auto-watch the order with initial order info (Bug 24 fix)
|
|
527
|
+
this.watchOrder(result.orderId, metadata, {
|
|
528
|
+
tokenId: params.tokenId,
|
|
529
|
+
side: params.side,
|
|
530
|
+
price: params.price,
|
|
531
|
+
size: params.size,
|
|
532
|
+
});
|
|
178
533
|
// Emit order_created event
|
|
179
534
|
this.emit('order_created', {
|
|
180
535
|
id: result.orderId,
|
|
@@ -191,6 +546,73 @@ export class OrderManager extends EventEmitter {
|
|
|
191
546
|
}
|
|
192
547
|
return result;
|
|
193
548
|
}
|
|
549
|
+
/**
|
|
550
|
+
* Create and submit a market order (FOK or FAK)
|
|
551
|
+
*
|
|
552
|
+
* Market order lifecycle:
|
|
553
|
+
* - FOK (Fill Or Kill): Must fill completely or cancels immediately
|
|
554
|
+
* PENDING → FILLED (success) or PENDING → CANCELLED (failed to fill)
|
|
555
|
+
* - FAK (Fill And Kill): Fills what it can, cancels the rest
|
|
556
|
+
* PENDING → PARTIALLY_FILLED + CANCELLED (partial) or PENDING → FILLED (complete)
|
|
557
|
+
*
|
|
558
|
+
* Auto-validates (Polymarket-specific):
|
|
559
|
+
* - Minimum value ($1)
|
|
560
|
+
*
|
|
561
|
+
* Auto-watches: Starts monitoring immediately after creation
|
|
562
|
+
* Note: Market orders typically complete very quickly (within seconds)
|
|
563
|
+
*
|
|
564
|
+
* @param params Market order parameters
|
|
565
|
+
* @param metadata Optional metadata for tracking
|
|
566
|
+
* @returns Order result with orderId or error
|
|
567
|
+
*/
|
|
568
|
+
async createMarketOrder(params, metadata) {
|
|
569
|
+
this.ensureInitialized();
|
|
570
|
+
// Validate market order (Polymarket-specific checks)
|
|
571
|
+
try {
|
|
572
|
+
await this.validateMarketOrder(params);
|
|
573
|
+
}
|
|
574
|
+
catch (error) {
|
|
575
|
+
const rejectEvent = {
|
|
576
|
+
params: params, // MarketOrderParams is compatible enough
|
|
577
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
578
|
+
timestamp: Date.now(),
|
|
579
|
+
};
|
|
580
|
+
this.emit('order_rejected', rejectEvent);
|
|
581
|
+
return {
|
|
582
|
+
success: false,
|
|
583
|
+
errorMsg: rejectEvent.reason,
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
// Submit market order via TradingService
|
|
587
|
+
const result = await this.tradingService.createMarketOrder(params);
|
|
588
|
+
if (result.success && result.orderId) {
|
|
589
|
+
// Auto-watch the order with initial order info (Bug 24 fix)
|
|
590
|
+
this.watchOrder(result.orderId, {
|
|
591
|
+
...metadata,
|
|
592
|
+
orderType: params.orderType || 'FOK', // Track order type for lifecycle handling
|
|
593
|
+
}, {
|
|
594
|
+
tokenId: params.tokenId,
|
|
595
|
+
side: params.side,
|
|
596
|
+
price: params.price,
|
|
597
|
+
size: params.amount,
|
|
598
|
+
});
|
|
599
|
+
// Emit order_created event
|
|
600
|
+
this.emit('order_created', {
|
|
601
|
+
id: result.orderId,
|
|
602
|
+
status: OrderStatus.PENDING,
|
|
603
|
+
tokenId: params.tokenId,
|
|
604
|
+
side: params.side,
|
|
605
|
+
price: params.price || 0, // Market orders may not have a fixed price
|
|
606
|
+
originalSize: params.amount, // For market orders, amount is in USDC
|
|
607
|
+
filledSize: 0,
|
|
608
|
+
remainingSize: params.amount,
|
|
609
|
+
associateTrades: [],
|
|
610
|
+
createdAt: Date.now(),
|
|
611
|
+
orderType: params.orderType || 'FOK',
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
return result;
|
|
615
|
+
}
|
|
194
616
|
/**
|
|
195
617
|
* Cancel an order
|
|
196
618
|
* Auto-unwatches after cancellation
|
|
@@ -204,6 +626,21 @@ export class OrderManager extends EventEmitter {
|
|
|
204
626
|
}
|
|
205
627
|
return result;
|
|
206
628
|
}
|
|
629
|
+
/**
|
|
630
|
+
* Cancel all open orders for this wallet.
|
|
631
|
+
* Useful for cleanup on strategy shutdown.
|
|
632
|
+
*/
|
|
633
|
+
async cancelAllOrders() {
|
|
634
|
+
this.ensureInitialized();
|
|
635
|
+
const result = await this.tradingService.cancelAllOrders();
|
|
636
|
+
if (result.success) {
|
|
637
|
+
// Unwatch all orders
|
|
638
|
+
for (const orderId of this.watchedOrders.keys()) {
|
|
639
|
+
this.unwatchOrder(orderId);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
return result;
|
|
643
|
+
}
|
|
207
644
|
/**
|
|
208
645
|
* Get order details
|
|
209
646
|
* Fetches from TradingService (CLOB API)
|
|
@@ -228,36 +665,93 @@ export class OrderManager extends EventEmitter {
|
|
|
228
665
|
return result;
|
|
229
666
|
}
|
|
230
667
|
// ============================================================================
|
|
668
|
+
// Public API - OrderHandle (Fluent Lifecycle)
|
|
669
|
+
// ============================================================================
|
|
670
|
+
/**
|
|
671
|
+
* Place a limit order and return a fluent OrderHandle.
|
|
672
|
+
*
|
|
673
|
+
* The handle is PromiseLike - await it for the terminal result.
|
|
674
|
+
* Chain lifecycle callbacks for real-time updates.
|
|
675
|
+
*
|
|
676
|
+
* @param params Limit order parameters
|
|
677
|
+
* @param metadata Optional metadata for tracking
|
|
678
|
+
* @returns OrderHandle - chainable, awaitable order lifecycle
|
|
679
|
+
*
|
|
680
|
+
* @example
|
|
681
|
+
* ```typescript
|
|
682
|
+
* const handle = orderManager.placeOrder({
|
|
683
|
+
* tokenId: '0x...',
|
|
684
|
+
* side: 'BUY',
|
|
685
|
+
* price: 0.52,
|
|
686
|
+
* size: 100,
|
|
687
|
+
* });
|
|
688
|
+
*
|
|
689
|
+
* handle
|
|
690
|
+
* .onAccepted((order) => console.log('Live:', order.id))
|
|
691
|
+
* .onFilled((fill) => console.log('Done!'));
|
|
692
|
+
*
|
|
693
|
+
* const result = await handle;
|
|
694
|
+
* ```
|
|
695
|
+
*/
|
|
696
|
+
placeOrder(params, metadata) {
|
|
697
|
+
return new OrderHandleImpl(this, () => this.createOrder(params, metadata));
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Place a market order (FOK/FAK) and return a fluent OrderHandle.
|
|
701
|
+
*
|
|
702
|
+
* @param params Market order parameters
|
|
703
|
+
* @param metadata Optional metadata for tracking
|
|
704
|
+
* @returns OrderHandle - chainable, awaitable order lifecycle
|
|
705
|
+
*
|
|
706
|
+
* @example
|
|
707
|
+
* ```typescript
|
|
708
|
+
* const result = await orderManager.placeMarketOrder({
|
|
709
|
+
* tokenId: '0x...',
|
|
710
|
+
* side: 'BUY',
|
|
711
|
+
* amount: 50,
|
|
712
|
+
* orderType: 'FOK',
|
|
713
|
+
* });
|
|
714
|
+
* console.log(result.status); // 'filled' or 'cancelled'
|
|
715
|
+
* ```
|
|
716
|
+
*/
|
|
717
|
+
placeMarketOrder(params, metadata) {
|
|
718
|
+
return new OrderHandleImpl(this, () => this.createMarketOrder(params, metadata));
|
|
719
|
+
}
|
|
720
|
+
// ============================================================================
|
|
231
721
|
// Public API - Order Monitoring
|
|
232
722
|
// ============================================================================
|
|
233
723
|
/**
|
|
234
|
-
*
|
|
235
|
-
* Used for monitoring externally-created orders
|
|
724
|
+
* Watch an order by ID
|
|
236
725
|
*
|
|
237
|
-
* @param orderId Order ID to watch
|
|
238
|
-
* @param metadata Optional metadata
|
|
726
|
+
* @param orderId - Order ID to watch
|
|
727
|
+
* @param metadata - Optional metadata for strategy context
|
|
728
|
+
* @param initialOrderInfo - Optional initial order info (tokenId, side, price, size)
|
|
729
|
+
* to avoid relying on polling for accurate token type
|
|
239
730
|
*/
|
|
240
|
-
watchOrder(orderId, metadata) {
|
|
731
|
+
watchOrder(orderId, metadata, initialOrderInfo) {
|
|
241
732
|
if (this.watchedOrders.has(orderId)) {
|
|
242
733
|
return; // Already watching
|
|
243
734
|
}
|
|
244
735
|
// Create initial watched state
|
|
736
|
+
// Bug 24 fix: Use initialOrderInfo if provided to ensure correct tokenId
|
|
245
737
|
const watched = {
|
|
246
738
|
orderId,
|
|
247
739
|
order: {
|
|
248
740
|
id: orderId,
|
|
249
741
|
status: OrderStatus.PENDING,
|
|
250
|
-
tokenId: '',
|
|
251
|
-
side: 'BUY',
|
|
252
|
-
price: 0,
|
|
253
|
-
originalSize: 0,
|
|
742
|
+
tokenId: initialOrderInfo?.tokenId || '',
|
|
743
|
+
side: initialOrderInfo?.side || 'BUY',
|
|
744
|
+
price: initialOrderInfo?.price || 0,
|
|
745
|
+
originalSize: initialOrderInfo?.size || 0,
|
|
254
746
|
filledSize: 0,
|
|
255
|
-
remainingSize: 0,
|
|
747
|
+
remainingSize: initialOrderInfo?.size || 0,
|
|
256
748
|
associateTrades: [],
|
|
257
749
|
createdAt: Date.now(),
|
|
258
750
|
},
|
|
259
751
|
metadata,
|
|
260
752
|
lastStatus: OrderStatus.PENDING,
|
|
753
|
+
// Bug 24 fix: Store initialTokenId to protect from polling overwrites
|
|
754
|
+
initialTokenId: initialOrderInfo?.tokenId,
|
|
261
755
|
};
|
|
262
756
|
this.watchedOrders.set(orderId, watched);
|
|
263
757
|
// Start monitoring based on mode
|
|
@@ -325,6 +819,22 @@ export class OrderManager extends EventEmitter {
|
|
|
325
819
|
// Note: Balance and market state checks would require additional service dependencies
|
|
326
820
|
// For now, we rely on TradingService validation
|
|
327
821
|
}
|
|
822
|
+
/**
|
|
823
|
+
* Validate market order parameters before submission
|
|
824
|
+
* Market orders have simpler validation than limit orders
|
|
825
|
+
*/
|
|
826
|
+
async validateMarketOrder(params) {
|
|
827
|
+
// 1. Minimum value validation (Polymarket: $1)
|
|
828
|
+
if (params.amount < 1) {
|
|
829
|
+
throw new PolymarketError(ErrorCode.ORDER_REJECTED, `Order amount must be at least $1 (got $${params.amount.toFixed(2)})`);
|
|
830
|
+
}
|
|
831
|
+
// 2. Validate order type if specified
|
|
832
|
+
if (params.orderType && !['FOK', 'FAK'].includes(params.orderType)) {
|
|
833
|
+
throw new PolymarketError(ErrorCode.ORDER_REJECTED, `Invalid market order type: ${params.orderType}. Must be FOK or FAK.`);
|
|
834
|
+
}
|
|
835
|
+
// Note: Price validation is optional for market orders
|
|
836
|
+
// If price is provided, it's used as a limit price (max for BUY, min for SELL)
|
|
837
|
+
}
|
|
328
838
|
// ============================================================================
|
|
329
839
|
// Private - WebSocket Monitoring (Polymarket-specific)
|
|
330
840
|
// ============================================================================
|
|
@@ -339,17 +849,29 @@ export class OrderManager extends EventEmitter {
|
|
|
339
849
|
// (We use dynamic import to avoid circular dependencies)
|
|
340
850
|
const { RealtimeServiceV2 } = await import('./realtime-service-v2.js');
|
|
341
851
|
this.realtimeService = new RealtimeServiceV2({ autoReconnect: true });
|
|
342
|
-
// Connect to WebSocket
|
|
852
|
+
// Connect to WebSocket and wait for connection to be established
|
|
853
|
+
// connect() is async and returns a Promise that resolves when connected
|
|
343
854
|
if (this.realtimeService) {
|
|
344
|
-
|
|
855
|
+
console.log(`[OrderManager] Connecting to WebSocket...`);
|
|
856
|
+
await this.realtimeService.connect();
|
|
857
|
+
console.log(`[OrderManager] WebSocket connected successfully`);
|
|
345
858
|
}
|
|
346
859
|
// Subscribe to user events (requires credentials)
|
|
860
|
+
// Now safe to subscribe since connection is established
|
|
347
861
|
const credentials = this.tradingService.getCredentials();
|
|
348
862
|
if (credentials && this.realtimeService) {
|
|
349
|
-
|
|
863
|
+
console.log(`[OrderManager] Subscribing to user events...`);
|
|
864
|
+
// Map ApiCredentials (key) to ClobApiKeyCreds (apiKey) format
|
|
865
|
+
const clobAuth = {
|
|
866
|
+
apiKey: credentials.key,
|
|
867
|
+
secret: credentials.secret,
|
|
868
|
+
passphrase: credentials.passphrase,
|
|
869
|
+
};
|
|
870
|
+
this.realtimeService.subscribeUserEvents(clobAuth, {
|
|
350
871
|
onOrder: this.handleUserOrder.bind(this),
|
|
351
872
|
onTrade: this.handleUserTrade.bind(this),
|
|
352
873
|
});
|
|
874
|
+
console.log(`[OrderManager] User events subscription complete`);
|
|
353
875
|
}
|
|
354
876
|
}
|
|
355
877
|
/**
|
|
@@ -368,17 +890,17 @@ export class OrderManager extends EventEmitter {
|
|
|
368
890
|
// Update order state
|
|
369
891
|
watched.order.price = userOrder.price;
|
|
370
892
|
watched.order.originalSize = userOrder.originalSize;
|
|
371
|
-
watched.order.filledSize = userOrder.
|
|
372
|
-
watched.order.remainingSize = userOrder.originalSize - userOrder.
|
|
893
|
+
watched.order.filledSize = userOrder.sizeMatched;
|
|
894
|
+
watched.order.remainingSize = userOrder.originalSize - userOrder.sizeMatched;
|
|
373
895
|
// Infer new status
|
|
374
896
|
let newStatus = watched.lastStatus;
|
|
375
897
|
if (userOrder.eventType === 'PLACEMENT') {
|
|
376
898
|
newStatus = OrderStatus.OPEN;
|
|
377
899
|
}
|
|
378
900
|
else if (userOrder.eventType === 'UPDATE') {
|
|
379
|
-
if (userOrder.
|
|
901
|
+
if (userOrder.sizeMatched > 0) {
|
|
380
902
|
newStatus =
|
|
381
|
-
userOrder.
|
|
903
|
+
userOrder.sizeMatched >= userOrder.originalSize
|
|
382
904
|
? OrderStatus.FILLED
|
|
383
905
|
: OrderStatus.PARTIALLY_FILLED;
|
|
384
906
|
}
|
|
@@ -394,31 +916,105 @@ export class OrderManager extends EventEmitter {
|
|
|
394
916
|
/**
|
|
395
917
|
* Handle USER_TRADE WebSocket event (Polymarket-specific)
|
|
396
918
|
* Triggered when: MATCHED, MINED, CONFIRMED
|
|
919
|
+
*
|
|
920
|
+
* Order lookup priority:
|
|
921
|
+
* 1. takerOrderId - if we placed a market order (FOK/FAK) or taker limit order
|
|
922
|
+
* 2. makerOrders[].orderId - if our limit order was matched as maker
|
|
397
923
|
*/
|
|
398
924
|
async handleUserTrade(userTrade) {
|
|
399
|
-
|
|
400
|
-
|
|
925
|
+
// Debug: Log all incoming USER_TRADE events
|
|
926
|
+
console.log(`[OrderManager] WebSocket USER_TRADE received:`, {
|
|
927
|
+
tradeId: userTrade.tradeId,
|
|
928
|
+
takerOrderId: userTrade.takerOrderId,
|
|
929
|
+
makerOrderIds: userTrade.makerOrders?.map(m => m.orderId),
|
|
930
|
+
size: userTrade.size,
|
|
931
|
+
price: userTrade.price,
|
|
932
|
+
status: userTrade.status,
|
|
933
|
+
watchedOrderCount: this.watchedOrders.size,
|
|
934
|
+
});
|
|
935
|
+
// Find the watched order using takerOrderId or makerOrders
|
|
936
|
+
let watched;
|
|
937
|
+
let makerInfo;
|
|
938
|
+
// Priority 1: Check if we're the taker
|
|
939
|
+
if (userTrade.takerOrderId) {
|
|
940
|
+
watched = this.watchedOrders.get(userTrade.takerOrderId);
|
|
941
|
+
}
|
|
942
|
+
// Priority 2: Check if we're a maker
|
|
943
|
+
if (!watched && userTrade.makerOrders) {
|
|
944
|
+
for (const maker of userTrade.makerOrders) {
|
|
945
|
+
watched = this.watchedOrders.get(maker.orderId);
|
|
946
|
+
if (watched) {
|
|
947
|
+
makerInfo = maker;
|
|
948
|
+
break;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
if (!watched) {
|
|
953
|
+
console.log(`[OrderManager] USER_TRADE not for any watched order, ignoring`);
|
|
401
954
|
return;
|
|
955
|
+
}
|
|
956
|
+
console.log(`[OrderManager] USER_TRADE matched watched order: ${watched.orderId}`);
|
|
957
|
+
// Bug 16 Fix: When we're the maker, use maker-specific matchedAmount and price
|
|
958
|
+
// userTrade.size is the TOTAL trade size (sum of all makers), not our individual fill
|
|
959
|
+
// Bug 20 Fix: Previously used wrong field name (matched_size instead of matched_amount)
|
|
960
|
+
// Now correctly reading matched_amount from Polymarket API
|
|
961
|
+
const rawMatchedAmount = makerInfo?.matchedAmount;
|
|
962
|
+
let fillSize = rawMatchedAmount ?? userTrade.size;
|
|
963
|
+
// Debug: Log raw values for verification
|
|
964
|
+
if (makerInfo) {
|
|
965
|
+
console.log(`[OrderManager] Maker fill debug:`, {
|
|
966
|
+
rawMatchedAmount,
|
|
967
|
+
makerOrdersLength: userTrade.makerOrders?.length,
|
|
968
|
+
willApplyFallback: fillSize === 0 || fillSize === undefined,
|
|
969
|
+
makerOrdersDetail: userTrade.makerOrders?.map(m => ({
|
|
970
|
+
orderId: m.orderId?.slice(0, 12),
|
|
971
|
+
matchedAmount: m.matchedAmount,
|
|
972
|
+
price: m.price,
|
|
973
|
+
})),
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
// Fallback: if matchedAmount is 0/undefined and we're a maker, use trade size
|
|
977
|
+
// This shouldn't happen now that we're reading the correct field, but kept as safety
|
|
978
|
+
if (makerInfo && (fillSize === 0 || fillSize === undefined)) {
|
|
979
|
+
console.log(`[OrderManager] Fallback applied: using userTrade.size ${userTrade.size} instead of ${fillSize}`);
|
|
980
|
+
fillSize = userTrade.size;
|
|
981
|
+
}
|
|
982
|
+
const fillPrice = makerInfo?.price ?? userTrade.price;
|
|
983
|
+
console.log(`[OrderManager] USER_TRADE fill details:`, {
|
|
984
|
+
isMaker: !!makerInfo,
|
|
985
|
+
fillSize,
|
|
986
|
+
fillPrice,
|
|
987
|
+
tradeTotalSize: userTrade.size,
|
|
988
|
+
});
|
|
402
989
|
// Deduplicate events
|
|
403
|
-
|
|
990
|
+
// When we're maker, include our orderId in the key to handle multiple makers in same trade
|
|
991
|
+
const eventKey = makerInfo
|
|
992
|
+
? `trade_${userTrade.tradeId}_${userTrade.status}_${watched.orderId}`
|
|
993
|
+
: `trade_${userTrade.tradeId}_${userTrade.status}_${userTrade.timestamp}`;
|
|
404
994
|
if (this.processedEvents.has(eventKey))
|
|
405
995
|
return;
|
|
406
996
|
this.processedEvents.add(eventKey);
|
|
407
997
|
// Emit fill event
|
|
408
|
-
|
|
998
|
+
// Calculate remaining size after this fill
|
|
999
|
+
const remainingAfterFill = watched.order.originalSize - (watched.order.filledSize + fillSize);
|
|
1000
|
+
// Complete fill if: sum >= originalSize OR remaining is zero/negative
|
|
1001
|
+
// Note: For market orders (FOK/FAK), originalSize is in USDC but filledSize is in shares,
|
|
1002
|
+
// causing remainingAfterFill to be negative. This is still a complete fill.
|
|
1003
|
+
const isCompleteFill = watched.order.filledSize + fillSize >= watched.order.originalSize ||
|
|
1004
|
+
remainingAfterFill <= 0;
|
|
409
1005
|
const fillEvent = {
|
|
410
1006
|
orderId: watched.orderId,
|
|
411
1007
|
order: watched.order,
|
|
412
1008
|
fill: {
|
|
413
1009
|
tradeId: userTrade.tradeId,
|
|
414
|
-
size:
|
|
415
|
-
price:
|
|
1010
|
+
size: fillSize,
|
|
1011
|
+
price: fillPrice,
|
|
416
1012
|
fee: 0, // Fee info not in UserTrade
|
|
417
1013
|
timestamp: userTrade.timestamp,
|
|
418
1014
|
transactionHash: userTrade.transactionHash,
|
|
419
1015
|
},
|
|
420
|
-
cumulativeFilled: watched.order.filledSize +
|
|
421
|
-
remainingSize: watched.order.originalSize - (watched.order.filledSize +
|
|
1016
|
+
cumulativeFilled: watched.order.filledSize + fillSize,
|
|
1017
|
+
remainingSize: watched.order.originalSize - (watched.order.filledSize + fillSize),
|
|
422
1018
|
isCompleteFill,
|
|
423
1019
|
};
|
|
424
1020
|
if (isCompleteFill) {
|
|
@@ -485,7 +1081,12 @@ export class OrderManager extends EventEmitter {
|
|
|
485
1081
|
// Only emit if this is an actual increase from a previously known state
|
|
486
1082
|
if (newFilledSize > oldFilledSize && oldFilledSize >= 0) {
|
|
487
1083
|
const fillDelta = newFilledSize - oldFilledSize;
|
|
488
|
-
|
|
1084
|
+
// Check for complete fill using status (preferred) or remainingSize
|
|
1085
|
+
// Note: For market orders (FOK/FAK), remainingSize may be negative due to unit mismatch
|
|
1086
|
+
// (originalSize is in USDC, filledSize is in shares), so we rely on status
|
|
1087
|
+
const isCompleteFill = freshOrder.status === OrderStatus.FILLED ||
|
|
1088
|
+
freshOrder.remainingSize === 0 ||
|
|
1089
|
+
freshOrder.remainingSize <= 0;
|
|
489
1090
|
// Deduplicate fill events using fill size as key
|
|
490
1091
|
const eventKey = `fill_${orderId}_${newFilledSize}`;
|
|
491
1092
|
if (!this.processedEvents.has(eventKey)) {
|