hedgequantx 2.7.11 → 2.7.13

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.
@@ -17,6 +17,7 @@ const {
17
17
  getPositions,
18
18
  } = require('./accounts');
19
19
  const { placeOrder, cancelOrder, getOrders, getOrderHistory, getOrderHistoryDates, getTradeHistoryFull, closePosition } = require('./orders');
20
+ const { fillsToRoundTrips, calculateTradeStats } = require('./trades');
20
21
  const { getContracts, searchContracts } = require('./contracts');
21
22
  const { TIMEOUTS } = require('../../config/settings');
22
23
  const { logger } = require('../../utils/logger');
@@ -242,53 +243,50 @@ class RithmicService extends EventEmitter {
242
243
  async getUser() { return this.user; }
243
244
 
244
245
  /**
245
- * Get trade history from Rithmic API
246
+ * Get trade history from Rithmic API as round-trips
246
247
  * @param {string} accountId - Optional account filter
247
248
  * @param {number} days - Number of days to look back (default 30)
248
249
  */
249
250
  async getTradeHistory(accountId, days = 30) {
250
- // Fetch from API
251
+ // Fetch fills from API
251
252
  const result = await getTradeHistoryFull(this, days);
252
253
 
253
254
  if (!result.success) {
254
255
  return { success: false, trades: [] };
255
256
  }
256
257
 
257
- let trades = result.trades || [];
258
+ let fills = result.trades || [];
258
259
 
259
260
  // Filter by account if specified
260
261
  if (accountId) {
261
- trades = trades.filter(t => t.accountId === accountId);
262
+ fills = fills.filter(t => t.accountId === accountId);
262
263
  }
263
264
 
264
- // Add timestamp from fillDate/fillTime if not present
265
- trades = trades.map(t => ({
266
- ...t,
267
- timestamp: t.timestamp || this._parseDateTime(t.fillDate, t.fillTime),
268
- }));
265
+ // Convert fills to round-trips with P&L
266
+ const roundTrips = fillsToRoundTrips(fills);
269
267
 
270
- // Sort by timestamp descending (newest first)
271
- trades.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
272
-
273
- return { success: true, trades };
268
+ return { success: true, trades: roundTrips };
274
269
  }
275
270
 
276
271
  /**
277
- * Parse Rithmic date/time to timestamp
278
- * @private
272
+ * Get raw fills (not matched to round-trips)
273
+ * @param {string} accountId - Optional account filter
274
+ * @param {number} days - Number of days to look back (default 30)
279
275
  */
280
- _parseDateTime(dateStr, timeStr) {
281
- if (!dateStr) return Date.now();
282
- try {
283
- // dateStr format: YYYYMMDD, timeStr format: HH:MM:SS
284
- const year = dateStr.slice(0, 4);
285
- const month = dateStr.slice(4, 6);
286
- const day = dateStr.slice(6, 8);
287
- const time = timeStr || '00:00:00';
288
- return new Date(`${year}-${month}-${day}T${time}Z`).getTime();
289
- } catch (e) {
290
- return Date.now();
276
+ async getRawFills(accountId, days = 30) {
277
+ const result = await getTradeHistoryFull(this, days);
278
+
279
+ if (!result.success) {
280
+ return { success: false, fills: [] };
281
+ }
282
+
283
+ let fills = result.trades || [];
284
+
285
+ if (accountId) {
286
+ fills = fills.filter(t => t.accountId === accountId);
291
287
  }
288
+
289
+ return { success: true, fills };
292
290
  }
293
291
 
294
292
  /**
@@ -297,54 +295,12 @@ class RithmicService extends EventEmitter {
297
295
  async getLifetimeStats(accountId) {
298
296
  const { trades } = await this.getTradeHistory(accountId, 365);
299
297
 
300
- if (trades.length === 0) {
298
+ if (!trades || trades.length === 0) {
301
299
  return { success: true, stats: null };
302
300
  }
303
301
 
304
- // Calculate stats from trades
305
- let totalTrades = trades.length;
306
- let winningTrades = 0;
307
- let losingTrades = 0;
308
- let totalProfit = 0;
309
- let totalLoss = 0;
310
- let longTrades = 0;
311
- let shortTrades = 0;
312
- let totalVolume = 0;
313
-
314
- // Group fills by basketId to calculate P&L per trade
315
- const tradeGroups = new Map();
316
- for (const trade of trades) {
317
- const key = trade.basketId || trade.id;
318
- if (!tradeGroups.has(key)) {
319
- tradeGroups.set(key, []);
320
- }
321
- tradeGroups.get(key).push(trade);
322
- }
323
-
324
- for (const [, fills] of tradeGroups) {
325
- const firstFill = fills[0];
326
- totalVolume += fills.reduce((sum, f) => sum + f.size, 0);
327
-
328
- if (firstFill.side === 1) longTrades++;
329
- else if (firstFill.side === 2) shortTrades++;
330
-
331
- // P&L calculation requires entry/exit matching which needs position tracking
332
- // For now, count trades
333
- totalTrades = tradeGroups.size;
334
- }
335
-
336
- const stats = {
337
- totalTrades,
338
- winningTrades,
339
- losingTrades,
340
- winRate: totalTrades > 0 ? ((winningTrades / totalTrades) * 100).toFixed(2) : 0,
341
- totalProfit,
342
- totalLoss,
343
- netPnL: totalProfit - totalLoss,
344
- longTrades,
345
- shortTrades,
346
- totalVolume,
347
- };
302
+ // Calculate stats from round-trips
303
+ const stats = calculateTradeStats(trades);
348
304
 
349
305
  return { success: true, stats };
350
306
  }
@@ -5,6 +5,9 @@
5
5
 
6
6
  const { REQ } = require('./constants');
7
7
 
8
+ // Debug mode
9
+ const DEBUG = process.env.HQX_DEBUG === '1';
10
+
8
11
  /**
9
12
  * Place order via ORDER_PLANT
10
13
  * @param {RithmicService} service - The Rithmic service instance
@@ -120,14 +123,17 @@ const getOrders = async (service) => {
120
123
 
121
124
  /**
122
125
  * Get available order history dates
126
+ * RequestShowOrderHistoryDates (318) does NOT require account_id
123
127
  * @param {RithmicService} service - The Rithmic service instance
124
128
  * @returns {Promise<{success: boolean, dates: string[]}>}
125
129
  */
126
130
  const getOrderHistoryDates = async (service) => {
127
- if (!service.orderConn || !service.loginInfo || service.accounts.length === 0) {
131
+ if (!service.orderConn || !service.loginInfo) {
128
132
  return { success: false, dates: [] };
129
133
  }
130
134
 
135
+ const { proto } = require('./protobuf');
136
+
131
137
  return new Promise((resolve) => {
132
138
  const dates = [];
133
139
  const timeout = setTimeout(() => {
@@ -136,19 +142,30 @@ const getOrderHistoryDates = async (service) => {
136
142
  }, 5000);
137
143
 
138
144
  const handler = (msg) => {
145
+ // msg contains { templateId, data }
139
146
  if (msg.templateId === 319) {
140
- // ResponseShowOrderHistoryDates returns dates
141
- if (msg.date) {
142
- if (Array.isArray(msg.date)) {
143
- dates.push(...msg.date);
144
- } else {
145
- dates.push(msg.date);
147
+ try {
148
+ const res = proto.decode('ResponseShowOrderHistoryDates', msg.data);
149
+ DEBUG && console.log('[OrderHistory] 319 response:', JSON.stringify(res));
150
+
151
+ // Dates come as repeated string field
152
+ if (res.date) {
153
+ const dateList = Array.isArray(res.date) ? res.date : [res.date];
154
+ for (const d of dateList) {
155
+ if (d && !dates.includes(d)) {
156
+ dates.push(d);
157
+ }
158
+ }
146
159
  }
147
- }
148
- if (msg.rpCode && msg.rpCode[0] === '0') {
149
- clearTimeout(timeout);
150
- service.orderConn.removeListener('message', handler);
151
- resolve({ success: true, dates });
160
+
161
+ // Check for completion (rpCode = '0')
162
+ if (res.rpCode && res.rpCode.length > 0 && res.rpCode[0] === '0') {
163
+ clearTimeout(timeout);
164
+ service.orderConn.removeListener('message', handler);
165
+ resolve({ success: true, dates });
166
+ }
167
+ } catch (e) {
168
+ DEBUG && console.log('[OrderHistory] Error decoding 319:', e.message);
152
169
  }
153
170
  }
154
171
  };
@@ -156,16 +173,12 @@ const getOrderHistoryDates = async (service) => {
156
173
  service.orderConn.on('message', handler);
157
174
 
158
175
  try {
159
- // Send for each account
160
- for (const acc of service.accounts) {
161
- service.orderConn.send('RequestShowOrderHistoryDates', {
162
- templateId: REQ.SHOW_ORDER_HISTORY_DATES,
163
- userMsg: ['HQX'],
164
- fcmId: acc.fcmId || service.loginInfo.fcmId,
165
- ibId: acc.ibId || service.loginInfo.ibId,
166
- accountId: acc.accountId,
167
- });
168
- }
176
+ // Request 318 does NOT need account_id - just template_id and user_msg
177
+ service.orderConn.send('RequestShowOrderHistoryDates', {
178
+ templateId: REQ.SHOW_ORDER_HISTORY_DATES,
179
+ userMsg: ['HQX'],
180
+ });
181
+ DEBUG && console.log('[OrderHistory] Sent request 318 (ShowOrderHistoryDates)');
169
182
  } catch (e) {
170
183
  clearTimeout(timeout);
171
184
  service.orderConn.removeListener('message', handler);
@@ -176,86 +189,103 @@ const getOrderHistoryDates = async (service) => {
176
189
 
177
190
  /**
178
191
  * Get order history for a specific date using show_order_history_summary
192
+ * RequestShowOrderHistorySummary (324) returns ExchangeOrderNotification (352) with is_snapshot=true
179
193
  * @param {RithmicService} service - The Rithmic service instance
180
194
  * @param {string} date - Date in YYYYMMDD format
181
195
  * @returns {Promise<{success: boolean, orders: Array}>}
182
196
  */
183
197
  const getOrderHistory = async (service, date) => {
184
- if (!service.orderConn || !service.loginInfo) {
198
+ if (!service.orderConn || !service.loginInfo || service.accounts.length === 0) {
185
199
  return { success: true, orders: [] };
186
200
  }
187
201
 
202
+ const { proto } = require('./protobuf');
188
203
  const dateStr = date || new Date().toISOString().slice(0, 10).replace(/-/g, '');
189
204
 
190
205
  return new Promise((resolve) => {
191
206
  const orders = [];
192
- let receivedEnd = false;
207
+ let receivedCount = 0;
208
+ const expectedAccounts = service.accounts.length;
209
+ const requestId = `HQX-${Date.now()}`;
193
210
 
194
211
  const timeout = setTimeout(() => {
195
- service.removeListener('exchangeNotification', handler);
212
+ service.orderConn.removeListener('message', handler);
213
+ DEBUG && console.log(`[OrderHistory] Timeout. Got ${orders.length} orders`);
196
214
  resolve({ success: true, orders });
197
215
  }, 10000);
198
216
 
199
- const handler = (notification) => {
200
- // ExchangeOrderNotification with isSnapshot=true contains history
201
- if (notification && notification.symbol) {
202
- orders.push({
203
- id: notification.fillId || notification.basketId || `${Date.now()}`,
204
- accountId: notification.accountId,
205
- symbol: notification.symbol,
206
- exchange: notification.exchange || 'CME',
207
- side: notification.transactionType, // 1=BUY, 2=SELL
208
- quantity: parseInt(notification.quantity) || 0,
209
- price: parseFloat(notification.price) || 0,
210
- fillPrice: parseFloat(notification.fillPrice) || 0,
211
- fillSize: parseInt(notification.fillSize) || 0,
212
- fillTime: notification.fillTime,
213
- fillDate: notification.fillDate,
214
- avgFillPrice: parseFloat(notification.avgFillPrice) || 0,
215
- totalFillSize: parseInt(notification.totalFillSize) || 0,
216
- status: notification.status,
217
- notifyType: notification.notifyType,
218
- isSnapshot: notification.isSnapshot,
219
- profitAndLoss: 0, // Will be calculated from fills
220
- pnl: 0,
221
- });
217
+ const handler = (msg) => {
218
+ // Response comes as template 352 (ExchangeOrderNotification) with is_snapshot=true
219
+ if (msg.templateId === 352) {
220
+ try {
221
+ const notification = proto.decode('ExchangeOrderNotification', msg.data);
222
+
223
+ // Only process snapshot data (historical orders)
224
+ if (notification.isSnapshot) {
225
+ DEBUG && console.log('[OrderHistory] 352 snapshot:', notification.symbol, notification.notifyType);
226
+
227
+ if (notification.symbol) {
228
+ orders.push({
229
+ id: notification.fillId || notification.basketId || `${Date.now()}-${orders.length}`,
230
+ accountId: notification.accountId,
231
+ symbol: notification.symbol,
232
+ exchange: notification.exchange || 'CME',
233
+ side: notification.transactionType, // 1=BUY, 2=SELL
234
+ quantity: parseInt(notification.quantity) || 0,
235
+ price: parseFloat(notification.price) || 0,
236
+ fillPrice: parseFloat(notification.fillPrice) || 0,
237
+ fillSize: parseInt(notification.fillSize) || 0,
238
+ fillTime: notification.fillTime,
239
+ fillDate: notification.fillDate,
240
+ avgFillPrice: parseFloat(notification.avgFillPrice) || 0,
241
+ totalFillSize: parseInt(notification.totalFillSize) || 0,
242
+ status: notification.status,
243
+ notifyType: notification.notifyType,
244
+ isSnapshot: true,
245
+ });
246
+ }
247
+ }
248
+ } catch (e) {
249
+ DEBUG && console.log('[OrderHistory] Error decoding 352:', e.message);
250
+ }
222
251
  }
223
252
 
224
- // Check for end of snapshot (rpCode = '0')
225
- if (notification && notification.rpCode && notification.rpCode[0] === '0') {
226
- receivedEnd = true;
227
- clearTimeout(timeout);
228
- service.removeListener('exchangeNotification', handler);
229
- resolve({ success: true, orders });
253
+ // Template 325 signals completion of order history summary
254
+ if (msg.templateId === 325) {
255
+ try {
256
+ const res = proto.decode('ResponseShowOrderHistorySummary', msg.data);
257
+ DEBUG && console.log('[OrderHistory] 325 response:', JSON.stringify(res));
258
+ receivedCount++;
259
+
260
+ if (receivedCount >= expectedAccounts) {
261
+ clearTimeout(timeout);
262
+ service.orderConn.removeListener('message', handler);
263
+ resolve({ success: true, orders });
264
+ }
265
+ } catch (e) {
266
+ DEBUG && console.log('[OrderHistory] Error decoding 325:', e.message);
267
+ }
230
268
  }
231
269
  };
232
270
 
233
- service.on('exchangeNotification', handler);
271
+ service.orderConn.on('message', handler);
234
272
 
235
273
  try {
236
- // Use template 324 (RequestShowOrderHistorySummary)
274
+ // Send request 324 for each account
237
275
  for (const acc of service.accounts) {
276
+ DEBUG && console.log(`[OrderHistory] Sending 324 for account ${acc.accountId}, date ${dateStr}`);
238
277
  service.orderConn.send('RequestShowOrderHistorySummary', {
239
278
  templateId: REQ.SHOW_ORDER_HISTORY,
240
- userMsg: ['HQX'],
279
+ userMsg: [requestId],
241
280
  fcmId: acc.fcmId || service.loginInfo.fcmId,
242
281
  ibId: acc.ibId || service.loginInfo.ibId,
243
282
  accountId: acc.accountId,
244
283
  date: dateStr,
245
284
  });
246
285
  }
247
-
248
- // Wait for responses
249
- setTimeout(() => {
250
- if (!receivedEnd) {
251
- clearTimeout(timeout);
252
- service.removeListener('exchangeNotification', handler);
253
- resolve({ success: true, orders });
254
- }
255
- }, 5000);
256
286
  } catch (e) {
257
287
  clearTimeout(timeout);
258
- service.removeListener('exchangeNotification', handler);
288
+ service.orderConn.removeListener('message', handler);
259
289
  resolve({ success: false, error: e.message, orders: [] });
260
290
  }
261
291
  });
@@ -0,0 +1,20 @@
1
+
2
+ package rti;
3
+
4
+ message RequestAccountRmsInfo
5
+ {
6
+ // PB_OFFSET = 100000, is the offset added for each MNM field id
7
+
8
+ enum UserType {
9
+ USER_TYPE_FCM = 1;
10
+ USER_TYPE_IB = 2;
11
+ USER_TYPE_TRADER = 3;
12
+ }
13
+
14
+ required int32 template_id = 154467; // PB_OFFSET + MNM_TEMPLATE_ID
15
+ repeated string user_msg = 132760; // PB_OFFSET + MNM_USER_MSG
16
+
17
+ optional string fcm_id = 154013; // PB_OFFSET + MNM_FCM_ID
18
+ optional string ib_id = 154014; // PB_OFFSET + MNM_IB_ID
19
+ optional UserType user_type = 154036; // PB_OFFSET + MNM_USER_TYPE
20
+ }
@@ -0,0 +1,61 @@
1
+
2
+ package rti;
3
+
4
+ message ResponseAccountRmsInfo
5
+ {
6
+ // PB_OFFSET = 100000, is the offset added for each MNM field id
7
+
8
+ // below enum is just for reference only, not used in this message
9
+ enum PresenceBits {
10
+ BUY_LIMIT = 1;
11
+ SELL_LIMIT = 2;
12
+ LOSS_LIMIT = 4;
13
+ MAX_ORDER_QUANTITY = 8;
14
+ MIN_ACCOUNT_BALANCE = 16;
15
+ MIN_MARGIN_BALANCE = 32;
16
+ ALGORITHM = 64;
17
+ STATUS = 128;
18
+ CURRENCY = 256;
19
+ DEFAULT_COMMISSION = 512;
20
+ }
21
+
22
+ enum AutoLiquidateFlag {
23
+ ENABLED = 1;
24
+ DISABLED = 2;
25
+ }
26
+
27
+
28
+ required int32 template_id = 154467; // PB_OFFSET + MNM_TEMPLATE_ID
29
+ repeated string user_msg = 132760; // PB_OFFSET + MNM_USER_MSG
30
+ repeated string rq_handler_rp_code = 132764; // PB_OFFSET + MNM_REQUEST_HANDLER_RESPONSE_CODE
31
+ repeated string rp_code = 132766; // PB_OFFSET + MNM_RESPONSE_CODE
32
+
33
+ optional uint32 presence_bits = 153622; // PB_OFFSET + MNM_FIELD_PRESENCE_BITS
34
+
35
+ optional string fcm_id = 154013; // PB_OFFSET + MNM_FCM_ID
36
+ optional string ib_id = 154014; // PB_OFFSET + MNM_IB_ID
37
+ optional string account_id = 154008; // PB_OFFSET + MNM_ACCOUNT_ID
38
+
39
+ optional string currency = 154383; // PB_OFFSET + MNM_ACCOUNT_CURRENCY
40
+ optional string status = 154003; // PB_OFFSET + MNM_ACCOUNT_STATUS
41
+ optional string algorithm = 150142; // PB_OFFSET + MNM_ALGORITHM
42
+
43
+ optional string auto_liquidate_criteria = 131036; // PB_OFFSET + MNM_ACCOUNT_AUTO_LIQUIDATE_CRITERIA
44
+
45
+ optional AutoLiquidateFlag auto_liquidate = 131035; // PB_OFFSET + MNM_ACCOUNT_AUTO_LIQUIDATE
46
+ optional AutoLiquidateFlag disable_on_auto_liquidate = 131038; // PB_OFFSET + MNM_DISABLE_ON_AUTO_LIQUIDATE_FLAG
47
+
48
+ optional double auto_liquidate_threshold = 131037; // PB_OFFSET + MNM_ACCOUNT_AUTO_LIQUIDATE_THRESHOLD
49
+ optional double auto_liquidate_max_min_account_balance = 131039; // PB_OFFSET + MNM_AUTO_LIQ_MAX_MIN_ACCOUNT_BALANCE
50
+
51
+ optional double loss_limit = 154019; // PB_OFFSET + MNM_LOSS_LIMIT
52
+ optional double min_account_balance = 156968; // PB_OFFSET + MNM_MINIMUM_ACCOUNT_BALANCE
53
+ optional double min_margin_balance = 156976; // PB_OFFSET + MNM_MIN_MARGIN_BALANCE
54
+ optional double default_commission = 153368; // PB_OFFSET + MNM_DEFAULT_COMMISSION
55
+
56
+ optional int32 buy_limit = 154009; // PB_OFFSET + MNM_BUY_LIMIT
57
+ optional int32 max_order_quantity = 110105; // PB_OFFSET + MNM_MAX_LIMIT_QUAN
58
+ optional int32 sell_limit = 154035; // PB_OFFSET + MNM_SELL_LIMIT
59
+
60
+ optional bool check_min_account_balance = 156972; // PB_OFFSET + MNM_CHECK_MIN_ACCT_BALANCE
61
+ }
@@ -3,9 +3,9 @@ package rti;
3
3
 
4
4
  message ResponseShowOrderHistorySummary
5
5
  {
6
- // Based on async_rithmic proto definitions
7
- optional int32 template_id = 156580;
8
- repeated string user_msg = 142744;
9
- repeated string rp_code = 142750;
10
- optional int32 fills_collected = 156569;
6
+ // Field IDs from rundef/async_rithmic (verified Jan 2026)
7
+ // Same field IDs as other messages (PB_OFFSET = 100000)
8
+ optional int32 template_id = 154467;
9
+ repeated string user_msg = 132760;
10
+ repeated string rp_code = 132766;
11
11
  }