@catalyst-team/poly-sdk 0.4.6 → 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/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 +6 -5
- 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 +162 -6
- package/dist/src/services/order-manager.d.ts.map +1 -1
- package/dist/src/services/order-manager.js +419 -30
- package/dist/src/services/order-manager.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
|
@@ -187,6 +187,219 @@ import { createUnifiedCache } from '../core/unified-cache.js';
|
|
|
187
187
|
import { OrderStatus } from '../core/types.js';
|
|
188
188
|
import { isTerminalStatus, isValidStatusTransition } from '../core/order-status.js';
|
|
189
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
|
+
}
|
|
190
403
|
// ============================================================================
|
|
191
404
|
// OrderManager Implementation
|
|
192
405
|
// ============================================================================
|
|
@@ -209,7 +422,10 @@ export class OrderManager extends EventEmitter {
|
|
|
209
422
|
this.config = {
|
|
210
423
|
...config,
|
|
211
424
|
chainId: config.chainId ?? 137,
|
|
212
|
-
|
|
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',
|
|
213
429
|
pollingInterval: config.pollingInterval ?? 5000,
|
|
214
430
|
polygonRpcUrl: config.polygonRpcUrl ?? 'https://polygon-rpc.com',
|
|
215
431
|
};
|
|
@@ -307,8 +523,13 @@ export class OrderManager extends EventEmitter {
|
|
|
307
523
|
// Submit order via TradingService
|
|
308
524
|
const result = await this.tradingService.createLimitOrder(params);
|
|
309
525
|
if (result.success && result.orderId) {
|
|
310
|
-
// Auto-watch the order
|
|
311
|
-
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
|
+
});
|
|
312
533
|
// Emit order_created event
|
|
313
534
|
this.emit('order_created', {
|
|
314
535
|
id: result.orderId,
|
|
@@ -365,10 +586,15 @@ export class OrderManager extends EventEmitter {
|
|
|
365
586
|
// Submit market order via TradingService
|
|
366
587
|
const result = await this.tradingService.createMarketOrder(params);
|
|
367
588
|
if (result.success && result.orderId) {
|
|
368
|
-
// Auto-watch the order
|
|
589
|
+
// Auto-watch the order with initial order info (Bug 24 fix)
|
|
369
590
|
this.watchOrder(result.orderId, {
|
|
370
591
|
...metadata,
|
|
371
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,
|
|
372
598
|
});
|
|
373
599
|
// Emit order_created event
|
|
374
600
|
this.emit('order_created', {
|
|
@@ -400,6 +626,21 @@ export class OrderManager extends EventEmitter {
|
|
|
400
626
|
}
|
|
401
627
|
return result;
|
|
402
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
|
+
}
|
|
403
644
|
/**
|
|
404
645
|
* Get order details
|
|
405
646
|
* Fetches from TradingService (CLOB API)
|
|
@@ -424,36 +665,93 @@ export class OrderManager extends EventEmitter {
|
|
|
424
665
|
return result;
|
|
425
666
|
}
|
|
426
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
|
+
// ============================================================================
|
|
427
721
|
// Public API - Order Monitoring
|
|
428
722
|
// ============================================================================
|
|
429
723
|
/**
|
|
430
|
-
*
|
|
431
|
-
* Used for monitoring externally-created orders
|
|
724
|
+
* Watch an order by ID
|
|
432
725
|
*
|
|
433
|
-
* @param orderId Order ID to watch
|
|
434
|
-
* @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
|
|
435
730
|
*/
|
|
436
|
-
watchOrder(orderId, metadata) {
|
|
731
|
+
watchOrder(orderId, metadata, initialOrderInfo) {
|
|
437
732
|
if (this.watchedOrders.has(orderId)) {
|
|
438
733
|
return; // Already watching
|
|
439
734
|
}
|
|
440
735
|
// Create initial watched state
|
|
736
|
+
// Bug 24 fix: Use initialOrderInfo if provided to ensure correct tokenId
|
|
441
737
|
const watched = {
|
|
442
738
|
orderId,
|
|
443
739
|
order: {
|
|
444
740
|
id: orderId,
|
|
445
741
|
status: OrderStatus.PENDING,
|
|
446
|
-
tokenId: '',
|
|
447
|
-
side: 'BUY',
|
|
448
|
-
price: 0,
|
|
449
|
-
originalSize: 0,
|
|
742
|
+
tokenId: initialOrderInfo?.tokenId || '',
|
|
743
|
+
side: initialOrderInfo?.side || 'BUY',
|
|
744
|
+
price: initialOrderInfo?.price || 0,
|
|
745
|
+
originalSize: initialOrderInfo?.size || 0,
|
|
450
746
|
filledSize: 0,
|
|
451
|
-
remainingSize: 0,
|
|
747
|
+
remainingSize: initialOrderInfo?.size || 0,
|
|
452
748
|
associateTrades: [],
|
|
453
749
|
createdAt: Date.now(),
|
|
454
750
|
},
|
|
455
751
|
metadata,
|
|
456
752
|
lastStatus: OrderStatus.PENDING,
|
|
753
|
+
// Bug 24 fix: Store initialTokenId to protect from polling overwrites
|
|
754
|
+
initialTokenId: initialOrderInfo?.tokenId,
|
|
457
755
|
};
|
|
458
756
|
this.watchedOrders.set(orderId, watched);
|
|
459
757
|
// Start monitoring based on mode
|
|
@@ -551,17 +849,29 @@ export class OrderManager extends EventEmitter {
|
|
|
551
849
|
// (We use dynamic import to avoid circular dependencies)
|
|
552
850
|
const { RealtimeServiceV2 } = await import('./realtime-service-v2.js');
|
|
553
851
|
this.realtimeService = new RealtimeServiceV2({ autoReconnect: true });
|
|
554
|
-
// 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
|
|
555
854
|
if (this.realtimeService) {
|
|
556
|
-
|
|
855
|
+
console.log(`[OrderManager] Connecting to WebSocket...`);
|
|
856
|
+
await this.realtimeService.connect();
|
|
857
|
+
console.log(`[OrderManager] WebSocket connected successfully`);
|
|
557
858
|
}
|
|
558
859
|
// Subscribe to user events (requires credentials)
|
|
860
|
+
// Now safe to subscribe since connection is established
|
|
559
861
|
const credentials = this.tradingService.getCredentials();
|
|
560
862
|
if (credentials && this.realtimeService) {
|
|
561
|
-
|
|
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, {
|
|
562
871
|
onOrder: this.handleUserOrder.bind(this),
|
|
563
872
|
onTrade: this.handleUserTrade.bind(this),
|
|
564
873
|
});
|
|
874
|
+
console.log(`[OrderManager] User events subscription complete`);
|
|
565
875
|
}
|
|
566
876
|
}
|
|
567
877
|
/**
|
|
@@ -580,17 +890,17 @@ export class OrderManager extends EventEmitter {
|
|
|
580
890
|
// Update order state
|
|
581
891
|
watched.order.price = userOrder.price;
|
|
582
892
|
watched.order.originalSize = userOrder.originalSize;
|
|
583
|
-
watched.order.filledSize = userOrder.
|
|
584
|
-
watched.order.remainingSize = userOrder.originalSize - userOrder.
|
|
893
|
+
watched.order.filledSize = userOrder.sizeMatched;
|
|
894
|
+
watched.order.remainingSize = userOrder.originalSize - userOrder.sizeMatched;
|
|
585
895
|
// Infer new status
|
|
586
896
|
let newStatus = watched.lastStatus;
|
|
587
897
|
if (userOrder.eventType === 'PLACEMENT') {
|
|
588
898
|
newStatus = OrderStatus.OPEN;
|
|
589
899
|
}
|
|
590
900
|
else if (userOrder.eventType === 'UPDATE') {
|
|
591
|
-
if (userOrder.
|
|
901
|
+
if (userOrder.sizeMatched > 0) {
|
|
592
902
|
newStatus =
|
|
593
|
-
userOrder.
|
|
903
|
+
userOrder.sizeMatched >= userOrder.originalSize
|
|
594
904
|
? OrderStatus.FILLED
|
|
595
905
|
: OrderStatus.PARTIALLY_FILLED;
|
|
596
906
|
}
|
|
@@ -606,31 +916,105 @@ export class OrderManager extends EventEmitter {
|
|
|
606
916
|
/**
|
|
607
917
|
* Handle USER_TRADE WebSocket event (Polymarket-specific)
|
|
608
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
|
|
609
923
|
*/
|
|
610
924
|
async handleUserTrade(userTrade) {
|
|
611
|
-
|
|
612
|
-
|
|
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`);
|
|
613
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
|
+
});
|
|
614
989
|
// Deduplicate events
|
|
615
|
-
|
|
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}`;
|
|
616
994
|
if (this.processedEvents.has(eventKey))
|
|
617
995
|
return;
|
|
618
996
|
this.processedEvents.add(eventKey);
|
|
619
997
|
// Emit fill event
|
|
620
|
-
|
|
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;
|
|
621
1005
|
const fillEvent = {
|
|
622
1006
|
orderId: watched.orderId,
|
|
623
1007
|
order: watched.order,
|
|
624
1008
|
fill: {
|
|
625
1009
|
tradeId: userTrade.tradeId,
|
|
626
|
-
size:
|
|
627
|
-
price:
|
|
1010
|
+
size: fillSize,
|
|
1011
|
+
price: fillPrice,
|
|
628
1012
|
fee: 0, // Fee info not in UserTrade
|
|
629
1013
|
timestamp: userTrade.timestamp,
|
|
630
1014
|
transactionHash: userTrade.transactionHash,
|
|
631
1015
|
},
|
|
632
|
-
cumulativeFilled: watched.order.filledSize +
|
|
633
|
-
remainingSize: watched.order.originalSize - (watched.order.filledSize +
|
|
1016
|
+
cumulativeFilled: watched.order.filledSize + fillSize,
|
|
1017
|
+
remainingSize: watched.order.originalSize - (watched.order.filledSize + fillSize),
|
|
634
1018
|
isCompleteFill,
|
|
635
1019
|
};
|
|
636
1020
|
if (isCompleteFill) {
|
|
@@ -697,7 +1081,12 @@ export class OrderManager extends EventEmitter {
|
|
|
697
1081
|
// Only emit if this is an actual increase from a previously known state
|
|
698
1082
|
if (newFilledSize > oldFilledSize && oldFilledSize >= 0) {
|
|
699
1083
|
const fillDelta = newFilledSize - oldFilledSize;
|
|
700
|
-
|
|
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;
|
|
701
1090
|
// Deduplicate fill events using fill size as key
|
|
702
1091
|
const eventKey = `fill_${orderId}_${newFilledSize}`;
|
|
703
1092
|
if (!this.processedEvents.has(eventKey)) {
|