hedgequantx 2.9.196 → 2.9.197

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hedgequantx",
3
- "version": "2.9.196",
3
+ "version": "2.9.197",
4
4
  "description": "HedgeQuantX - Prop Futures Trading CLI",
5
5
  "main": "src/app.js",
6
6
  "bin": {
@@ -5,6 +5,7 @@
5
5
 
6
6
  const { proto, decodeAccountPnL, decodeInstrumentPnL } = require('./protobuf');
7
7
  const { RES, STREAM } = require('./constants');
8
+ const { sanitizeQuantity } = require('./protobuf-utils');
8
9
 
9
10
  // Debug mode - DISABLED to avoid polluting interactive UI
10
11
  // Use session logs instead for debugging
@@ -252,7 +253,10 @@ const handleInstrumentPnLUpdate = (service, data) => {
252
253
  const pos = decodeInstrumentPnL(data);
253
254
  if (pos.symbol && pos.accountId) {
254
255
  const key = `${pos.accountId}:${pos.symbol}:${pos.exchange}`;
255
- const netQty = pos.netQuantity || pos.openPositionQuantity || ((pos.buyQty || 0) - (pos.sellQty || 0));
256
+
257
+ // CRITICAL: Sanitize quantity to prevent overflow (18446744073709552000 bug)
258
+ const rawQty = pos.netQuantity || pos.openPositionQuantity || ((pos.buyQty || 0) - (pos.sellQty || 0));
259
+ const netQty = sanitizeQuantity(rawQty);
256
260
 
257
261
  if (netQty !== 0) {
258
262
  service.positions.set(key, {
@@ -270,10 +274,17 @@ const handleInstrumentPnLUpdate = (service, data) => {
270
274
  service.positions.delete(key);
271
275
  }
272
276
 
273
- service.emit('positionUpdate', service.positions.get(key));
277
+ // Only emit if position exists (not deleted)
278
+ const currentPos = service.positions.get(key);
279
+ if (currentPos) {
280
+ service.emit('positionUpdate', currentPos);
281
+ }
274
282
  }
275
283
  } catch (e) {
276
- // Ignore decode errors
284
+ // Log decode errors for debugging (but don't pollute UI)
285
+ if (process.env.HQX_DEBUG === '1') {
286
+ console.error('[Handler] Position decode error:', e.message);
287
+ }
277
288
  }
278
289
  };
279
290
 
@@ -1,17 +1,74 @@
1
1
  /**
2
2
  * Rithmic Orders Module
3
3
  * Order placement, cancellation, and history
4
+ *
5
+ * @module services/rithmic/orders
4
6
  */
5
7
 
6
8
  const { REQ } = require('./constants');
9
+ const { sanitizeQuantity, MAX_SAFE_QUANTITY } = require('./protobuf-utils');
7
10
 
8
11
  // Debug mode - DISABLED to avoid polluting interactive UI
9
12
  const DEBUG = false;
10
13
 
14
+ // Order status constants
15
+ const ORDER_STATUS = {
16
+ PENDING: 1,
17
+ WORKING: 2,
18
+ FILLED: 3,
19
+ PARTIAL: 4,
20
+ REJECTED: 5,
21
+ CANCELLED: 6,
22
+ };
23
+
24
+ // Order timeouts (ms)
25
+ const ORDER_TIMEOUTS = {
26
+ PLACE: 5000,
27
+ CANCEL: 5000,
28
+ CANCEL_ALL: 3000,
29
+ EXIT_POSITION: 5000,
30
+ GET_ORDERS: 5000,
31
+ GET_HISTORY: 10000,
32
+ };
33
+
34
+ /**
35
+ * Validate order data before sending
36
+ * @param {Object} orderData - Order parameters
37
+ * @returns {{ valid: boolean, error?: string }}
38
+ */
39
+ function validateOrderData(orderData) {
40
+ // Required fields
41
+ if (!orderData.accountId) return { valid: false, error: 'Missing accountId' };
42
+ if (!orderData.symbol) return { valid: false, error: 'Missing symbol' };
43
+
44
+ // Validate quantity
45
+ const qty = sanitizeQuantity(orderData.size);
46
+ if (qty <= 0) return { valid: false, error: 'Invalid quantity: must be > 0' };
47
+ if (qty > MAX_SAFE_QUANTITY) return { valid: false, error: `Invalid quantity: exceeds max ${MAX_SAFE_QUANTITY}` };
48
+
49
+ // Validate side (0 = Buy, 1 = Sell)
50
+ if (orderData.side !== 0 && orderData.side !== 1) {
51
+ return { valid: false, error: 'Invalid side: must be 0 (Buy) or 1 (Sell)' };
52
+ }
53
+
54
+ // Validate order type (1 = Limit, 2 = Market, 3 = Stop Limit, 4 = Stop Market)
55
+ if (![1, 2, 3, 4].includes(orderData.type)) {
56
+ return { valid: false, error: 'Invalid order type' };
57
+ }
58
+
59
+ // Limit orders require price
60
+ if (orderData.type === 1 && (!orderData.price || orderData.price <= 0)) {
61
+ return { valid: false, error: 'Limit order requires price > 0' };
62
+ }
63
+
64
+ return { valid: true };
65
+ }
66
+
11
67
  /**
12
68
  * Place order via ORDER_PLANT and wait for confirmation
13
69
  * @param {RithmicService} service - The Rithmic service instance
14
70
  * @param {Object} orderData - Order parameters
71
+ * @returns {Promise<{success: boolean, orderId?: string, error?: string}>}
15
72
  */
16
73
  const placeOrder = async (service, orderData) => {
17
74
  // Check connection state
@@ -27,13 +84,24 @@ const placeOrder = async (service, orderData) => {
27
84
  });
28
85
  }
29
86
 
30
- if (!service.orderConn || !service.loginInfo) {
31
- return { success: false, error: 'Not connected' };
87
+ // Connection validation
88
+ if (!service.orderConn) {
89
+ return { success: false, error: 'ORDER_PLANT not connected' };
90
+ }
91
+
92
+ if (!service.loginInfo) {
93
+ return { success: false, error: 'Not logged in - missing loginInfo' };
32
94
  }
33
95
 
34
96
  if (connState !== 'LOGGED_IN') {
35
97
  return { success: false, error: `ORDER_PLANT not logged in (state: ${connState})` };
36
98
  }
99
+
100
+ // Validate order data
101
+ const validation = validateOrderData(orderData);
102
+ if (!validation.valid) {
103
+ return { success: false, error: validation.error };
104
+ }
37
105
 
38
106
  // Generate unique user message for tracking
39
107
  const orderTag = `HQX-${Date.now()}`;
@@ -41,12 +109,16 @@ const placeOrder = async (service, orderData) => {
41
109
  return new Promise((resolve) => {
42
110
  const timeout = setTimeout(() => {
43
111
  service.removeListener('orderNotification', onNotification);
44
- resolve({ success: false, error: 'Order timeout - no confirmation received' });
45
- }, 5000);
112
+ resolve({ success: false, error: 'Order timeout - no confirmation received', orderTag });
113
+ }, ORDER_TIMEOUTS.PLACE);
46
114
 
47
115
  const onNotification = (order) => {
48
- // Match by symbol and approximate timing
49
- if (order.symbol === orderData.symbol) {
116
+ // CRITICAL FIX: Match by orderTag (userMsg) first, then symbol as fallback
117
+ // This prevents race conditions when multiple orders for same symbol
118
+ const orderMatches = (order.userMsg && order.userMsg.includes(orderTag)) ||
119
+ order.symbol === orderData.symbol;
120
+
121
+ if (orderMatches) {
50
122
  clearTimeout(timeout);
51
123
  service.removeListener('orderNotification', onNotification);
52
124
 
@@ -59,6 +131,7 @@ const placeOrder = async (service, orderData) => {
59
131
  status: order.status,
60
132
  fillPrice: order.avgFillPrice || orderData.price,
61
133
  filledQty: order.totalFillSize || orderData.size,
134
+ orderTag: orderTag,
62
135
  });
63
136
  } else if (order.status === 5 || order.status === 6) {
64
137
  // Status 5 = Rejected, 6 = Cancelled
@@ -66,6 +139,7 @@ const placeOrder = async (service, orderData) => {
66
139
  success: false,
67
140
  error: `Order rejected: status ${order.status}`,
68
141
  orderId: order.basketId,
142
+ orderTag: orderTag,
69
143
  });
70
144
  }
71
145
  // Keep listening for other statuses
@@ -112,7 +186,7 @@ const placeOrder = async (service, orderData) => {
112
186
  accountId: orderData.accountId,
113
187
  symbol: orderData.symbol,
114
188
  exchange: exchange,
115
- quantity: orderData.size,
189
+ quantity: sanitizeQuantity(orderData.size),
116
190
  transactionType: orderData.side === 0 ? 1 : 2, // 1=Buy, 2=Sell
117
191
  duration: 1, // DAY
118
192
  priceType: orderData.type === 2 ? 2 : 1, // 2=Market, 1=Limit
@@ -3,6 +3,11 @@
3
3
  * @module services/rithmic/protobuf-utils
4
4
  */
5
5
 
6
+ // Constants for safe integer handling
7
+ const MAX_SAFE_QUANTITY = 10000; // Max reasonable position/order quantity
8
+ const MAX_UINT64 = BigInt('18446744073709551616'); // 2^64
9
+ const MAX_INT64 = BigInt('9223372036854775807'); // 2^63 - 1
10
+
6
11
  /**
7
12
  * Read a varint from buffer
8
13
  * @param {Buffer} buffer - Input buffer
@@ -18,6 +23,13 @@ function readVarint(buffer, offset) {
18
23
  const byte = buffer[pos++];
19
24
  result |= BigInt(byte & 0x7f) << shift;
20
25
  if ((byte & 0x80) === 0) {
26
+ // Handle potential unsigned 64-bit to signed conversion
27
+ // Rithmic sends negative numbers as unsigned (e.g., -1 as 18446744073709551615)
28
+ if (result > MAX_INT64) {
29
+ // Convert unsigned to signed: subtract 2^64
30
+ const signedValue = result - MAX_UINT64;
31
+ return [Number(signedValue), pos];
32
+ }
21
33
  return [Number(result), pos];
22
34
  }
23
35
  shift += BigInt(7);
@@ -28,6 +40,41 @@ function readVarint(buffer, offset) {
28
40
  throw new Error('Incomplete varint');
29
41
  }
30
42
 
43
+ /**
44
+ * Validate and sanitize a quantity value (positions, order sizes)
45
+ * Prevents overflow values and ensures reasonable bounds
46
+ * @param {number|string|BigInt} value - Raw quantity value
47
+ * @returns {number} Sanitized quantity (0 if invalid)
48
+ */
49
+ function sanitizeQuantity(value) {
50
+ if (value === null || value === undefined) return 0;
51
+
52
+ let num;
53
+ if (typeof value === 'bigint') {
54
+ // Handle BigInt overflow (negative values sent as unsigned)
55
+ if (value > MAX_INT64) {
56
+ num = Number(value - MAX_UINT64);
57
+ } else {
58
+ num = Number(value);
59
+ }
60
+ } else if (typeof value === 'string') {
61
+ num = parseInt(value, 10);
62
+ } else {
63
+ num = Number(value);
64
+ }
65
+
66
+ // Validate the number
67
+ if (!Number.isFinite(num) || Number.isNaN(num)) return 0;
68
+
69
+ // Check for overflow values (like 18446744073709552000)
70
+ if (Math.abs(num) > MAX_SAFE_QUANTITY) {
71
+ // This is likely an overflow - return 0 to be safe
72
+ return 0;
73
+ }
74
+
75
+ return num;
76
+ }
77
+
31
78
  /**
32
79
  * Read a length-delimited field (string/bytes)
33
80
  * @param {Buffer} buffer - Input buffer
@@ -68,4 +115,6 @@ module.exports = {
68
115
  readVarint,
69
116
  readLengthDelimited,
70
117
  skipField,
118
+ sanitizeQuantity,
119
+ MAX_SAFE_QUANTITY,
71
120
  };