Polling mode in OrderManager wasn't emitting fill events when orders were filled. It only emitted status change events, making it difficult for consumers to detect and track fills when WebSocket was unavailable.
The updateWatchedOrder() method only compared order status changes but ignored changes in filledSize. When an order's filled size increased (indicating a new fill), no fill event was emitted.
Location: packages/poly-sdk/src/services/order-manager.ts:718-773
Added fill detection logic:
- Compare
oldFilledSizevsnewFilledSize - Calculate fill delta when size increases
- Emit
order_partially_filledororder_filledevents based on completion status - Include fill details: size, price, tradeId, timestamp
Key Features:
- Fill Delta Calculation: Only emits the incremental fill, not cumulative
- Deduplication: Uses event key
fill_{orderId}_{newFilledSize}to prevent duplicates - Price Estimation: Uses order price as estimate (OpenOrder API doesn't provide avgFillPrice)
- TradeId Handling: Uses last associateTrade or generates synthetic
polling_{timestamp}
Location: packages/poly-sdk/src/services/order-manager.ts:783-788
Modified emitStatusChange() to accept fillAlreadyEmitted parameter:
- When polling detects a fill, it sets this flag to
true - Prevents duplicate fill events when status transitions to
FILLED - Maintains backward compatibility for WebSocket path
-
packages/poly-sdk/src/services/order-manager.ts
- Enhanced
updateWatchedOrder()with fill detection (lines 718-773) - Updated
emitStatusChange()signature with deduplication flag (lines 783-788)
- Enhanced
-
packages/poly-sdk/src/services/order-manager.test.ts (NEW)
- Comprehensive test suite covering fill detection
- Tests for partial fills, complete fills, multiple fills
- Edge case tests (no fills, instant fills, no tradeIds)
- Deduplication tests for hybrid mode
✅ Fill Detection
- Single partial fill detection
- Complete fill detection
- Multiple sequential fills
- Fill event deduplication
✅ Fill Event Details
- Correct delta calculation
- Price estimation from order
- TradeId extraction/generation
- Cumulative and remaining sizes
✅ Status + Fill Events
- Both events emit correctly
- No duplication between sources
✅ Edge Cases
- No fill changes (stable fillSize)
- Missing associateTrades array
- Instant complete fills (0 → 100%)
- Orders starting with existing fills
✅ WebSocket + Polling Deduplication
- Hybrid mode doesn't duplicate fills
- Event key deduplication works
Test Files 1 passed (1)
Tests 10 passed (10)
Duration 4.46s
All changes are backward compatible. The existing API surface remains unchanged:
// Existing event listeners work as before
orderManager.on('order_filled', (event: FillEvent) => {
console.log(`Filled: ${event.fill.size} @ ${event.fill.price}`);
});
orderManager.on('order_partially_filled', (event: FillEvent) => {
console.log(`Partial fill: ${event.fill.size} shares`);
});Before: Polling mode only emitted status changes
// Only received status_change events
orderManager.on('status_change', (event) => {
// Had to manually detect fills from status
});After: Polling mode emits both status and fill events
// Now receives dedicated fill events
orderManager.on('order_partially_filled', (event) => {
console.log(`New fill: ${event.fill.size} shares`);
console.log(`Total filled: ${event.cumulativeFilled}`);
console.log(`Remaining: ${event.remainingSize}`);
});The CLOB API's OpenOrder type doesn't include avgFillPrice, so we use the order's limit price as an estimate. This is acceptable because:
- Limit orders: Fill at or better than limit price
- Polling context: Price accuracy is less critical than WebSocket (which has actual trade data)
- Use case: Most consumers care about fill detection, not exact price in polling mode
For exact fill prices, consumers should:
- Use WebSocket mode (provides actual trade prices)
- Query trade history via
getTrades()method
Two-level deduplication:
- Intra-source: Within polling, uses
processedEventsSet with key formatfill_{orderId}_{filledSize} - Inter-source: Between polling and WebSocket, the same Set prevents duplicates in hybrid mode
Event keys persist in memory for the lifetime of the OrderManager instance.
No code changes required! Fill events now work automatically:
const orderManager = new OrderManager({
privateKey: '0x...',
rateLimiter,
cache,
mode: 'polling', // or 'hybrid'
pollingInterval: 5000,
});
// These now work in polling mode:
orderManager.on('order_partially_filled', handlePartialFill);
orderManager.on('order_filled', handleCompleteFill);No changes needed. Deduplication ensures fills are only emitted once, regardless of source.
- Minimal: Only adds two number comparisons per poll
- Memory: Adds one entry to
processedEventsSet per fill - Network: No additional API calls
- Average Fill Price: If CLOB API adds
avgFillPriceto OpenOrder, use it instead of limit price - Fill History: Consider storing fill events for replay/recovery
- Cleanup: Implement
processedEventsSet cleanup for long-running instances
- Resolves Issue #7: "Polling mode doesn't emit fill events"
- Improves parity between WebSocket and Polling modes
- Enhances DipArb strategy fill detection reliability