hedgequantx 2.9.247 → 2.9.249

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.247",
3
+ "version": "2.9.249",
4
4
  "description": "HedgeQuantX - Prop Futures Trading CLI",
5
5
  "main": "src/app.js",
6
6
  "bin": {
@@ -118,6 +118,32 @@ const executeAlgo = async ({ service, account, contract, config, strategy: strat
118
118
  }
119
119
  ui.addLog('system', 'Connecting to market data...');
120
120
 
121
+ // Listen for position updates from Rithmic (external closes, manual trades)
122
+ const accId = account.rithmicAccountId || account.accountId;
123
+ service.on('positionUpdate', (pos) => {
124
+ // Match by account and symbol
125
+ const posSymbol = pos.contractId || pos.symbol || '';
126
+ const matchesSymbol = posSymbol.includes(contract.name) || posSymbol.includes(contractId) ||
127
+ posSymbol === symbolCode || contractId.includes(posSymbol);
128
+ const matchesAccount = pos.accountId === accId || pos.accountId === account.accountId;
129
+
130
+ if (matchesSymbol && matchesAccount) {
131
+ const qty = parseInt(pos.quantity) || 0;
132
+ if (!isNaN(qty) && Math.abs(qty) < 1000 && qty !== currentPosition) {
133
+ const oldPos = currentPosition;
134
+ currentPosition = qty;
135
+ if (qty === 0 && oldPos !== 0) {
136
+ ui.addLog('trade', `Position closed externally (was ${oldPos})`);
137
+ sessionLogger.log('POSITION', `External close: ${oldPos} -> 0`);
138
+ pendingOrder = false; // Reset pending order flag
139
+ } else if (qty !== 0 && oldPos === 0) {
140
+ ui.addLog('trade', `Position opened externally: ${qty}`);
141
+ sessionLogger.log('POSITION', `External open: 0 -> ${qty}`);
142
+ }
143
+ }
144
+ }
145
+ });
146
+
121
147
  strategy.on('signal', async (signal) => {
122
148
  const dir = signal.direction?.toUpperCase() || 'UNKNOWN';
123
149
  const signalLog = smartLogs.getSignalLog(dir, symbolCode, (signal.confidence || 0) * 100, strategyName);
@@ -45,6 +45,9 @@ class DaemonProxyService extends EventEmitter {
45
45
 
46
46
  /** @type {Object|null} Credentials for direct mode */
47
47
  this.credentials = null;
48
+
49
+ /** @type {Map<string, Object>} Cached P&L data for daemon mode */
50
+ this._pnlCache = new Map();
48
51
  }
49
52
 
50
53
  /**
@@ -192,7 +195,20 @@ class DaemonProxyService extends EventEmitter {
192
195
 
193
196
  client.on('orderUpdate', (data) => this.emit('orderUpdate', data));
194
197
  client.on('positionUpdate', (data) => this.emit('positionUpdate', data));
195
- client.on('pnlUpdate', (data) => this.emit('pnlUpdate', data));
198
+
199
+ // Cache P&L data for sync getAccountPnL calls
200
+ client.on('pnlUpdate', (data) => {
201
+ if (data && data.accountId) {
202
+ this._pnlCache.set(data.accountId, {
203
+ pnl: data.dayPnl ?? data.pnl ?? null,
204
+ openPnl: data.openPositionPnl ?? data.openPnl ?? null,
205
+ closedPnl: data.closedPositionPnl ?? data.closedPnl ?? null,
206
+ balance: data.accountBalance ?? data.balance ?? null,
207
+ });
208
+ }
209
+ this.emit('pnlUpdate', data);
210
+ });
211
+
196
212
  client.on('fill', (data) => this.emit('fill', data));
197
213
  client.on('marketData', (data) => this.emit('marketData', data));
198
214
  client.on('rithmicDisconnected', (data) => this.emit('disconnected', data));
@@ -248,8 +264,15 @@ class DaemonProxyService extends EventEmitter {
248
264
  if (!this._backend) return { pnl: null, openPnl: null, closedPnl: null, balance: null };
249
265
 
250
266
  if (this._mode === 'daemon') {
251
- // For daemon, we need to make async call but this is sync interface
252
- // Return cached value or null
267
+ // Return cached P&L from pnlUpdate events
268
+ const cached = this._pnlCache.get(accountId);
269
+ if (cached) return cached;
270
+
271
+ // Try without account ID (some events don't include it)
272
+ if (this._pnlCache.size === 1) {
273
+ return this._pnlCache.values().next().value;
274
+ }
275
+
253
276
  return { pnl: null, openPnl: null, closedPnl: null, balance: null };
254
277
  }
255
278
  return this._backend.getAccountPnL(accountId);
@@ -215,32 +215,56 @@ const handleShowOrdersResponse = (service, data) => {
215
215
 
216
216
  /**
217
217
  * Handle new order response (template 313)
218
+ * rpCode[0] = '0' means accepted, anything else = rejected by gateway
218
219
  */
219
220
  const handleNewOrderResponse = (service, data) => {
220
221
  try {
221
222
  const res = proto.decode('ResponseNewOrder', data);
222
- debug('New order response:', JSON.stringify(res));
223
223
 
224
- // Emit as orderNotification for the placeOrder listener
225
- // Even if no basketId, emit for rejection handling
226
- const isRejected = res.rpCode?.[0] !== '0';
224
+ const isAccepted = res.rpCode?.[0] === '0';
225
+
226
+ // Build rejection reason from rpCode array
227
+ // rpCode is typically ['code', 'message'] or just ['code']
228
+ let rejectReason = null;
229
+ if (!isAccepted) {
230
+ // Try to get the error message from rpCode[1] or rqHandlerRpCode[1]
231
+ const rpMsg = res.rpCode?.slice(1).join(' ') || '';
232
+ const rqMsg = res.rqHandlerRpCode?.slice(1).join(' ') || '';
233
+ rejectReason = rpMsg || rqMsg || null;
234
+
235
+ // If no message, interpret the code
236
+ if (!rejectReason && res.rpCode?.[0]) {
237
+ const codeMap = {
238
+ '1': 'Invalid request',
239
+ '2': 'Invalid account',
240
+ '3': 'Invalid symbol',
241
+ '4': 'Invalid quantity',
242
+ '5': 'Insufficient buying power',
243
+ '6': 'Market closed',
244
+ '7': 'Order rejected by risk system',
245
+ '8': 'Position limit exceeded',
246
+ '9': 'Rate limit exceeded',
247
+ };
248
+ rejectReason = codeMap[res.rpCode[0]] || `Gateway rejected (code: ${res.rpCode[0]})`;
249
+ }
250
+
251
+ console.log('[Rithmic] Gateway rejection:', rejectReason, '| rpCode:', res.rpCode, '| rqHandlerRpCode:', res.rqHandlerRpCode);
252
+ }
253
+
227
254
  const order = {
228
- basketId: res.basketId || res.orderId || res.userMsg?.[0] || 'unknown',
229
- accountId: res.accountId,
255
+ basketId: res.basketId || null,
230
256
  symbol: res.symbol,
231
257
  exchange: res.exchange || 'CME',
232
- status: isRejected ? 5 : 2, // 2=Working, 5=Rejected
233
- notifyType: isRejected ? 0 : 1, // 1=Accepted
258
+ notifyType: isAccepted ? 1 : 6, // 1=STATUS (accepted), 6=REJECT
259
+ status: isAccepted ? 2 : 6,
260
+ text: rejectReason,
234
261
  rpCode: res.rpCode,
235
- rqHandlerRpCode: res.rqHandlerRpCode, // Also include request handler response code
236
262
  userMsg: res.userMsg,
237
263
  };
238
264
  service.emit('orderNotification', order);
239
-
240
- // Also emit specific event
241
265
  service.emit('newOrderResponse', res);
242
266
  } catch (e) {
243
- debug('Error decoding ResponseNewOrder:', e.message);
267
+ console.error('Error decoding ResponseNewOrder:', e.message);
244
268
  }
245
269
  };
246
270
 
@@ -369,12 +393,50 @@ const handleAccountRmsInfo = (service, data) => {
369
393
 
370
394
  /**
371
395
  * Handle exchange order notification (fills/trades)
372
- * NotifyType: 5 = FILL
396
+ * NotifyType: 1=STATUS, 2=MODIFY, 3=CANCEL, 4=TRIGGER, 5=FILL, 6=REJECT
373
397
  */
374
398
  const handleExchangeNotification = (service, data) => {
375
399
  try {
376
400
  const res = proto.decode('ExchangeOrderNotification', data);
377
- debug('Exchange notification:', res.notifyType, res.symbol);
401
+ debug('Exchange notification:', res.notifyType, res.symbol, res.status);
402
+
403
+ // Build rejection text from available fields
404
+ // Priority: text > reportText > remarks > status code interpretation
405
+ let rejectText = res.text || res.reportText || res.remarks || null;
406
+ if (!rejectText && res.notifyType === 6) {
407
+ // Map common Rithmic status codes to human-readable messages
408
+ const statusMap = {
409
+ '1': 'Order pending',
410
+ '2': 'Order working',
411
+ '3': 'Order filled',
412
+ '4': 'Order partially filled',
413
+ '5': 'Order rejected by exchange',
414
+ '6': 'Order cancelled',
415
+ '7': 'Order expired',
416
+ '8': 'Order suspended',
417
+ };
418
+ rejectText = statusMap[res.status] || `Exchange rejected (code: ${res.status || 'unknown'})`;
419
+ console.log('[Rithmic] Exchange rejection:', rejectText, '| status:', res.status, '| symbol:', res.symbol);
420
+ }
421
+
422
+ // Emit orderNotification for placeOrder listener
423
+ // This is critical for order tracking
424
+ const orderNotif = {
425
+ basketId: res.basketId,
426
+ accountId: res.accountId,
427
+ symbol: res.symbol,
428
+ exchange: res.exchange || 'CME',
429
+ notifyType: res.notifyType, // 1=STATUS, 5=FILL, 6=REJECT
430
+ status: res.notifyType === 5 ? 3 : res.notifyType === 6 ? 6 : 2,
431
+ fillPrice: res.fillPrice ? parseFloat(res.fillPrice) : null,
432
+ fillSize: res.fillSize ? parseInt(res.fillSize) : null,
433
+ avgFillPrice: res.avgFillPrice ? parseFloat(res.avgFillPrice) : null,
434
+ totalFillSize: res.totalFillSize ? parseInt(res.totalFillSize) : null,
435
+ confirmedSize: res.confirmedSize ? parseInt(res.confirmedSize) : null,
436
+ text: rejectText,
437
+ userMsg: res.userTag ? [res.userTag] : null,
438
+ };
439
+ service.emit('orderNotification', orderNotif);
378
440
 
379
441
  // notifyType 5 = FILL (trade executed)
380
442
  if (res.notifyType === 5 && res.fillPrice && res.fillSize) {
@@ -125,72 +125,83 @@ const placeOrder = async (service, orderData) => {
125
125
  }, ORDER_TIMEOUTS.PLACE);
126
126
 
127
127
  const onNotification = (order) => {
128
- // HFT: Fast match by orderTag first (most specific)
129
- const orderMatches = (order.userMsg && order.userMsg.includes(orderTag)) ||
130
- order.symbol === orderData.symbol;
128
+ // Match by symbol
129
+ if (order.symbol !== orderData.symbol) return;
131
130
 
132
- if (orderMatches) {
131
+ const notifyType = order.notifyType;
132
+
133
+ // FILL (notifyType 5) - Order executed
134
+ if (notifyType === 5) {
133
135
  clearTimeout(timeout);
134
136
  service.removeListener('orderNotification', onNotification);
135
-
136
- // HFT: Combined status check (2=Working, 3=Filled, 15=Complete)
137
- const status = order.status;
138
- if (status === 2 || status === 3 || order.notifyType === 15) {
139
- resolve({
140
- success: true,
141
- orderId: order.basketId,
142
- status,
143
- fillPrice: order.avgFillPrice || orderData.price,
144
- filledQty: order.totalFillSize || orderData.size,
145
- orderTag,
146
- });
147
- } else if (status === 5 || status === 6) {
148
- // Extract rejection reason from rpCode[1] or rqHandlerRpCode[1]
149
- const rpCode = order.rpCode;
150
- const rqCode = order.rqHandlerRpCode;
151
- let errorMsg = `Order rejected: status ${status}`;
152
-
153
- // Try rpCode first, then rqHandlerRpCode
154
- if (rpCode && Array.isArray(rpCode) && rpCode.length > 1 && rpCode[1]) {
155
- errorMsg = `Rejected: ${rpCode[1]}`;
156
- } else if (rqCode && Array.isArray(rqCode) && rqCode.length > 1 && rqCode[1]) {
157
- errorMsg = `Rejected: ${rqCode[1]}`;
158
- } else if (rpCode && Array.isArray(rpCode) && rpCode[0] && rpCode[0] !== '0') {
159
- errorMsg = `Rejected: code ${rpCode[0]}`;
160
- }
161
-
162
- resolve({
163
- success: false,
164
- error: errorMsg,
165
- orderId: order.basketId,
166
- orderTag,
167
- rpCode,
168
- rqHandlerRpCode: rqCode,
169
- });
137
+ resolve({
138
+ success: true,
139
+ orderId: order.basketId,
140
+ status: 3,
141
+ fillPrice: order.avgFillPrice || order.fillPrice || orderData.price,
142
+ filledQty: order.totalFillSize || order.fillSize || orderData.size,
143
+ orderTag,
144
+ });
145
+ return;
146
+ }
147
+
148
+ // REJECT (notifyType 6) - Order rejected
149
+ if (notifyType === 6) {
150
+ clearTimeout(timeout);
151
+ service.removeListener('orderNotification', onNotification);
152
+ resolve({
153
+ success: false,
154
+ error: order.text || 'Order rejected',
155
+ orderId: order.basketId,
156
+ orderTag,
157
+ });
158
+ return;
159
+ }
160
+
161
+ // STATUS (notifyType 1) with basketId - Order accepted by gateway
162
+ if (notifyType === 1 && order.basketId) {
163
+ // For market orders, wait a bit more for fill notification
164
+ // For limit orders, this is success (working)
165
+ if (orderData.type === 2) {
166
+ // Market order - don't resolve yet, wait for fill
167
+ return;
170
168
  }
169
+ clearTimeout(timeout);
170
+ service.removeListener('orderNotification', onNotification);
171
+ resolve({
172
+ success: true,
173
+ orderId: order.basketId,
174
+ status: 2,
175
+ fillPrice: orderData.price,
176
+ filledQty: 0,
177
+ orderTag,
178
+ });
179
+ return;
171
180
  }
172
181
  };
173
182
 
174
183
  service.on('orderNotification', onNotification);
175
184
 
176
- // HFT: Inline order request construction (no try-catch in hot path)
177
185
  const exchange = orderData.exchange || 'CME';
178
186
 
179
- // Get trade route from cache
187
+ // CRITICAL: Get trade route - orders WILL BE REJECTED without it
180
188
  let tradeRoute = null;
181
189
  const routes = service.tradeRoutes;
182
190
  if (routes && routes.size > 0) {
183
191
  const routeInfo = routes.get(exchange);
184
- tradeRoute = routeInfo ? routeInfo.tradeRoute :
185
- routes.values().next().value?.tradeRoute || null;
192
+ tradeRoute = routeInfo?.tradeRoute || routes.values().next().value?.tradeRoute;
186
193
  }
187
194
 
188
- // Warn if no trade route - Rithmic will reject the order
195
+ // FAIL FAST if no trade route
189
196
  if (!tradeRoute) {
190
- DEBUG && console.log('[Orders] WARNING: No trade route for', exchange, '- order may be rejected');
197
+ clearTimeout(timeout);
198
+ service.removeListener('orderNotification', onNotification);
199
+ console.log('[Orders] ERROR: No trade route for', exchange);
200
+ resolve({ success: false, error: `No trade route for ${exchange}. Login may be incomplete.`, orderTag });
201
+ return;
191
202
  }
192
203
 
193
- // HFT: Reuse template and mutate (faster than object spread)
204
+ // Build order request
194
205
  ORDER_REQUEST_TEMPLATE.userMsg[0] = orderTag;
195
206
  ORDER_REQUEST_TEMPLATE.fcmId = service.loginInfo.fcmId;
196
207
  ORDER_REQUEST_TEMPLATE.ibId = service.loginInfo.ibId;