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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
31
|
-
|
|
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
|
-
},
|
|
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
|
|
49
|
-
|
|
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
|
};
|