hedgequantx 2.9.248 → 2.9.250

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.248",
3
+ "version": "2.9.250",
4
4
  "description": "HedgeQuantX - Prop Futures Trading CLI",
5
5
  "main": "src/app.js",
6
6
  "bin": {
@@ -118,6 +118,35 @@ 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
+ // Only if service supports events (RithmicService or DaemonProxyService)
123
+ const accId = account.rithmicAccountId || account.accountId;
124
+ if (typeof service.on === 'function') {
125
+ service.on('positionUpdate', (pos) => {
126
+ // Match by account and symbol
127
+ const posSymbol = pos.contractId || pos.symbol || '';
128
+ const matchesSymbol = posSymbol.includes(contract.name) || posSymbol.includes(contractId) ||
129
+ posSymbol === symbolCode || contractId.includes(posSymbol);
130
+ const matchesAccount = pos.accountId === accId || pos.accountId === account.accountId;
131
+
132
+ if (matchesSymbol && matchesAccount) {
133
+ const qty = parseInt(pos.quantity) || 0;
134
+ if (!isNaN(qty) && Math.abs(qty) < 1000 && qty !== currentPosition) {
135
+ const oldPos = currentPosition;
136
+ currentPosition = qty;
137
+ if (qty === 0 && oldPos !== 0) {
138
+ ui.addLog('trade', `Position closed externally (was ${oldPos})`);
139
+ sessionLogger.log('POSITION', `External close: ${oldPos} -> 0`);
140
+ pendingOrder = false; // Reset pending order flag
141
+ } else if (qty !== 0 && oldPos === 0) {
142
+ ui.addLog('trade', `Position opened externally: ${qty}`);
143
+ sessionLogger.log('POSITION', `External open: 0 -> ${qty}`);
144
+ }
145
+ }
146
+ }
147
+ });
148
+ }
149
+
121
150
  strategy.on('signal', async (signal) => {
122
151
  const dir = signal.direction?.toUpperCase() || 'UNKNOWN';
123
152
  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);
@@ -222,11 +222,33 @@ const handleNewOrderResponse = (service, data) => {
222
222
  const res = proto.decode('ResponseNewOrder', data);
223
223
 
224
224
  const isAccepted = res.rpCode?.[0] === '0';
225
- const rejectReason = res.rpCode?.[1] || res.rqHandlerRpCode?.[1] || 'Unknown';
226
225
 
227
- // Log rejection details for debugging
226
+ // Build rejection reason from rpCode array
227
+ // rpCode is typically ['code', 'message'] or just ['code']
228
+ let rejectReason = null;
228
229
  if (!isAccepted) {
229
- console.log('[Rithmic] Order rejected:', rejectReason, '| rpCode:', res.rpCode, '| rqHandlerRpCode:', res.rqHandlerRpCode);
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);
230
252
  }
231
253
 
232
254
  const order = {
@@ -235,7 +257,7 @@ const handleNewOrderResponse = (service, data) => {
235
257
  exchange: res.exchange || 'CME',
236
258
  notifyType: isAccepted ? 1 : 6, // 1=STATUS (accepted), 6=REJECT
237
259
  status: isAccepted ? 2 : 6,
238
- text: isAccepted ? null : rejectReason,
260
+ text: rejectReason,
239
261
  rpCode: res.rpCode,
240
262
  userMsg: res.userMsg,
241
263
  };
@@ -378,6 +400,25 @@ const handleExchangeNotification = (service, data) => {
378
400
  const res = proto.decode('ExchangeOrderNotification', data);
379
401
  debug('Exchange notification:', res.notifyType, res.symbol, res.status);
380
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
+
381
422
  // Emit orderNotification for placeOrder listener
382
423
  // This is critical for order tracking
383
424
  const orderNotif = {
@@ -392,7 +433,7 @@ const handleExchangeNotification = (service, data) => {
392
433
  avgFillPrice: res.avgFillPrice ? parseFloat(res.avgFillPrice) : null,
393
434
  totalFillSize: res.totalFillSize ? parseInt(res.totalFillSize) : null,
394
435
  confirmedSize: res.confirmedSize ? parseInt(res.confirmedSize) : null,
395
- text: res.text || res.reportText,
436
+ text: rejectText,
396
437
  userMsg: res.userTag ? [res.userTag] : null,
397
438
  };
398
439
  service.emit('orderNotification', orderNotif);