hedgequantx 1.3.0 → 1.3.1

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.
@@ -5,49 +5,50 @@
5
5
 
6
6
  const EventEmitter = require('events');
7
7
  const { RithmicConnection } = require('./connection');
8
- const { proto, decodeAccountPnL, decodeInstrumentPnL } = require('./protobuf');
9
- const { RITHMIC_ENDPOINTS, RITHMIC_SYSTEMS, REQ, RES, STREAM } = require('./constants');
8
+ const { RITHMIC_ENDPOINTS, RITHMIC_SYSTEMS } = require('./constants');
9
+ const { createOrderHandler, createPnLHandler } = require('./handlers');
10
+ const { fetchAccounts, getTradingAccounts, requestPnLSnapshot, subscribePnLUpdates, getPositions, hashAccountId } = require('./accounts');
11
+ const { placeOrder, cancelOrder, getOrders, getOrderHistory, closePosition } = require('./orders');
12
+
13
+ // PropFirm configurations
14
+ const PROPFIRM_CONFIGS = {
15
+ 'apex': { name: 'Apex Trader Funding', systemName: 'Apex', defaultBalance: 300000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
16
+ 'apex_rithmic': { name: 'Apex Trader Funding', systemName: 'Apex', defaultBalance: 300000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
17
+ 'topstep_r': { name: 'Topstep (Rithmic)', systemName: RITHMIC_SYSTEMS.TOPSTEP, defaultBalance: 150000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
18
+ 'bulenox_r': { name: 'Bulenox (Rithmic)', systemName: RITHMIC_SYSTEMS.BULENOX, defaultBalance: 150000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
19
+ 'earn2trade': { name: 'Earn2Trade', systemName: RITHMIC_SYSTEMS.EARN_2_TRADE, defaultBalance: 150000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
20
+ 'mescapital': { name: 'MES Capital', systemName: RITHMIC_SYSTEMS.MES_CAPITAL, defaultBalance: 150000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
21
+ 'tradefundrr': { name: 'TradeFundrr', systemName: RITHMIC_SYSTEMS.TRADEFUNDRR, defaultBalance: 150000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
22
+ 'thetradingpit': { name: 'The Trading Pit', systemName: RITHMIC_SYSTEMS.THE_TRADING_PIT, defaultBalance: 150000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
23
+ 'fundedfutures': { name: 'Funded Futures Network', systemName: RITHMIC_SYSTEMS.FUNDED_FUTURES_NETWORK, defaultBalance: 150000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
24
+ 'propshop': { name: 'PropShop Trader', systemName: RITHMIC_SYSTEMS.PROPSHOP_TRADER, defaultBalance: 150000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
25
+ '4proptrader': { name: '4PropTrader', systemName: RITHMIC_SYSTEMS.FOUR_PROP_TRADER, defaultBalance: 150000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
26
+ 'daytraders': { name: 'DayTraders.com', systemName: RITHMIC_SYSTEMS.DAY_TRADERS, defaultBalance: 150000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
27
+ '10xfutures': { name: '10X Futures', systemName: RITHMIC_SYSTEMS.TEN_X_FUTURES, defaultBalance: 150000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
28
+ 'lucidtrading': { name: 'Lucid Trading', systemName: RITHMIC_SYSTEMS.LUCID_TRADING, defaultBalance: 150000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
29
+ 'thrivetrading': { name: 'Thrive Trading', systemName: RITHMIC_SYSTEMS.THRIVE_TRADING, defaultBalance: 150000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
30
+ 'legendstrading': { name: 'Legends Trading', systemName: RITHMIC_SYSTEMS.LEGENDS_TRADING, defaultBalance: 150000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
31
+ };
10
32
 
11
33
  class RithmicService extends EventEmitter {
12
34
  constructor(propfirmKey) {
13
35
  super();
14
36
  this.propfirmKey = propfirmKey;
15
- this.propfirm = this.getPropFirmConfig(propfirmKey);
37
+ this.propfirm = PROPFIRM_CONFIGS[propfirmKey] || {
38
+ name: propfirmKey,
39
+ systemName: 'Rithmic Paper Trading',
40
+ defaultBalance: 150000,
41
+ gateway: RITHMIC_ENDPOINTS.PAPER
42
+ };
16
43
  this.orderConn = null;
17
44
  this.pnlConn = null;
18
45
  this.loginInfo = null;
19
46
  this.accounts = [];
20
- this.accountPnL = new Map(); // accountId -> pnl data
21
- this.positions = new Map(); // symbol -> position data (from InstrumentPnLPositionUpdate)
22
- this.orders = []; // Active orders
47
+ this.accountPnL = new Map();
48
+ this.positions = new Map();
49
+ this.orders = [];
23
50
  this.user = null;
24
- this.credentials = null; // Store for PNL connection
25
- }
26
-
27
- /**
28
- * Get PropFirm configuration
29
- * Note: Apex and other prop firms use the Chicago gateway (rprotocol.rithmic.com), NOT Paper Trading endpoint
30
- */
31
- getPropFirmConfig(key) {
32
- const propfirms = {
33
- 'apex': { name: 'Apex Trader Funding', systemName: 'Apex', defaultBalance: 300000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
34
- 'apex_rithmic': { name: 'Apex Trader Funding', systemName: 'Apex', defaultBalance: 300000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
35
- 'topstep_r': { name: 'Topstep (Rithmic)', systemName: RITHMIC_SYSTEMS.TOPSTEP, defaultBalance: 150000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
36
- 'bulenox_r': { name: 'Bulenox (Rithmic)', systemName: RITHMIC_SYSTEMS.BULENOX, defaultBalance: 150000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
37
- 'earn2trade': { name: 'Earn2Trade', systemName: RITHMIC_SYSTEMS.EARN_2_TRADE, defaultBalance: 150000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
38
- 'mescapital': { name: 'MES Capital', systemName: RITHMIC_SYSTEMS.MES_CAPITAL, defaultBalance: 150000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
39
- 'tradefundrr': { name: 'TradeFundrr', systemName: RITHMIC_SYSTEMS.TRADEFUNDRR, defaultBalance: 150000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
40
- 'thetradingpit': { name: 'The Trading Pit', systemName: RITHMIC_SYSTEMS.THE_TRADING_PIT, defaultBalance: 150000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
41
- 'fundedfutures': { name: 'Funded Futures Network', systemName: RITHMIC_SYSTEMS.FUNDED_FUTURES_NETWORK, defaultBalance: 150000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
42
- 'propshop': { name: 'PropShop Trader', systemName: RITHMIC_SYSTEMS.PROPSHOP_TRADER, defaultBalance: 150000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
43
- '4proptrader': { name: '4PropTrader', systemName: RITHMIC_SYSTEMS.FOUR_PROP_TRADER, defaultBalance: 150000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
44
- 'daytraders': { name: 'DayTraders.com', systemName: RITHMIC_SYSTEMS.DAY_TRADERS, defaultBalance: 150000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
45
- '10xfutures': { name: '10X Futures', systemName: RITHMIC_SYSTEMS.TEN_X_FUTURES, defaultBalance: 150000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
46
- 'lucidtrading': { name: 'Lucid Trading', systemName: RITHMIC_SYSTEMS.LUCID_TRADING, defaultBalance: 150000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
47
- 'thrivetrading': { name: 'Thrive Trading', systemName: RITHMIC_SYSTEMS.THRIVE_TRADING, defaultBalance: 150000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
48
- 'legendstrading': { name: 'Legends Trading', systemName: RITHMIC_SYSTEMS.LEGENDS_TRADING, defaultBalance: 150000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
49
- };
50
- return propfirms[key] || { name: key, systemName: 'Rithmic Paper Trading', defaultBalance: 150000, gateway: RITHMIC_ENDPOINTS.PAPER };
51
+ this.credentials = null;
51
52
  }
52
53
 
53
54
  /**
@@ -55,10 +56,7 @@ class RithmicService extends EventEmitter {
55
56
  */
56
57
  async login(username, password) {
57
58
  try {
58
- // Connect to ORDER_PLANT
59
59
  this.orderConn = new RithmicConnection();
60
-
61
- // Use propfirm-specific gateway (Chicago for Apex and most prop firms)
62
60
  const gateway = this.propfirm.gateway || RITHMIC_ENDPOINTS.CHICAGO;
63
61
 
64
62
  const config = {
@@ -71,12 +69,9 @@ class RithmicService extends EventEmitter {
71
69
  };
72
70
 
73
71
  await this.orderConn.connect(config);
72
+ this.orderConn.on('message', createOrderHandler(this));
74
73
 
75
- // Setup message handler for ORDER_PLANT
76
- this.orderConn.on('message', (msg) => this.handleOrderMessage(msg));
77
-
78
- // Login
79
- return new Promise((resolve, reject) => {
74
+ return new Promise((resolve) => {
80
75
  const timeout = setTimeout(() => {
81
76
  resolve({ success: false, error: 'Login timeout - server did not respond' });
82
77
  }, 30000);
@@ -86,14 +81,8 @@ class RithmicService extends EventEmitter {
86
81
  this.loginInfo = data;
87
82
  this.user = { userName: username, fcmId: data.fcmId, ibId: data.ibId };
88
83
 
89
- // Try to get accounts but don't fail if it doesn't work
90
- try {
91
- await this.fetchAccounts();
92
- } catch (e) {
93
- // Accounts fetch failed, ignore
94
- }
84
+ try { await fetchAccounts(this); } catch (e) {}
95
85
 
96
- // Create default account if none found
97
86
  if (this.accounts.length === 0) {
98
87
  this.accounts = [{
99
88
  accountId: username,
@@ -103,10 +92,8 @@ class RithmicService extends EventEmitter {
103
92
  }];
104
93
  }
105
94
 
106
- // Store credentials for PNL connection
107
95
  this.credentials = { username, password };
108
96
 
109
- // Format accounts for response
110
97
  const formattedAccounts = this.accounts.map(acc => ({
111
98
  accountId: acc.accountId,
112
99
  accountName: acc.accountName || acc.accountId,
@@ -116,11 +103,7 @@ class RithmicService extends EventEmitter {
116
103
  status: 0
117
104
  }));
118
105
 
119
- resolve({
120
- success: true,
121
- user: this.user,
122
- accounts: formattedAccounts
123
- });
106
+ resolve({ success: true, user: this.user, accounts: formattedAccounts });
124
107
  });
125
108
 
126
109
  this.orderConn.once('loginFailed', (data) => {
@@ -142,8 +125,6 @@ class RithmicService extends EventEmitter {
142
125
  async connectPnL(username, password) {
143
126
  try {
144
127
  this.pnlConn = new RithmicConnection();
145
-
146
- // Use propfirm-specific gateway (Chicago for Apex and most prop firms)
147
128
  const gateway = this.propfirm.gateway || RITHMIC_ENDPOINTS.CHICAGO;
148
129
 
149
130
  const config = {
@@ -156,9 +137,9 @@ class RithmicService extends EventEmitter {
156
137
  };
157
138
 
158
139
  await this.pnlConn.connect(config);
159
- this.pnlConn.on('message', (msg) => this.handlePnLMessage(msg));
140
+ this.pnlConn.on('message', createPnLHandler(this));
160
141
 
161
- return new Promise((resolve, reject) => {
142
+ return new Promise((resolve) => {
162
143
  const timeout = setTimeout(() => resolve(false), 10000);
163
144
 
164
145
  this.pnlConn.once('loggedIn', () => {
@@ -178,463 +159,29 @@ class RithmicService extends EventEmitter {
178
159
  }
179
160
  }
180
161
 
181
- /**
182
- * Fetch accounts from ORDER_PLANT
183
- * Note: Rithmic often fails to return accounts, so we use a short timeout
184
- */
185
- async fetchAccounts() {
186
- if (!this.orderConn || !this.loginInfo) {
187
- return [];
188
- }
189
-
190
- // Quick timeout - don't wait too long for accounts
191
- return new Promise((resolve) => {
192
- const accounts = [];
193
-
194
- const timeout = setTimeout(() => {
195
- this.accounts = accounts;
196
- resolve(accounts);
197
- }, 2000); // 2 seconds max
198
-
199
- this.once('accountReceived', (account) => {
200
- accounts.push(account);
201
- });
202
-
203
- this.once('accountListComplete', () => {
204
- clearTimeout(timeout);
205
- this.accounts = accounts;
206
- resolve(accounts);
207
- });
208
-
209
- // Request account list
210
- try {
211
- this.orderConn.send('RequestAccountList', {
212
- templateId: REQ.ACCOUNT_LIST,
213
- userMsg: ['HQX'],
214
- fcmId: this.loginInfo.fcmId,
215
- ibId: this.loginInfo.ibId,
216
- });
217
- } catch (e) {
218
- clearTimeout(timeout);
219
- resolve([]);
220
- }
221
- });
222
- }
223
-
224
- /**
225
- * Get trading accounts (formatted like ProjectX)
226
- */
227
- async getTradingAccounts() {
228
- // Only try to fetch if we don't have accounts yet
229
- if (this.accounts.length === 0 && this.orderConn && this.loginInfo) {
230
- try {
231
- await this.fetchAccounts();
232
- } catch (e) {
233
- // Ignore fetch errors
234
- }
235
- }
236
-
237
- let tradingAccounts = this.accounts.map((acc, index) => {
238
- const pnl = this.accountPnL.get(acc.accountId) || {};
239
- const balance = parseFloat(pnl.accountBalance || pnl.marginBalance || pnl.cashOnHand || 0) || this.propfirm.defaultBalance;
240
- const startingBalance = this.propfirm.defaultBalance;
241
- const profitAndLoss = balance - startingBalance;
242
-
243
- return {
244
- accountId: this.hashAccountId(acc.accountId),
245
- rithmicAccountId: acc.accountId,
246
- accountName: acc.accountName || acc.accountId,
247
- name: acc.accountName || acc.accountId,
248
- balance: balance,
249
- startingBalance: startingBalance,
250
- profitAndLoss: profitAndLoss,
251
- status: 0, // Active
252
- platform: 'Rithmic',
253
- propfirm: this.propfirm.name,
254
- };
255
- });
256
-
257
- // If no accounts but user is logged in, create a default account from login info
258
- if (tradingAccounts.length === 0 && this.user) {
259
- const userName = this.user.userName || 'Unknown';
260
- tradingAccounts = [{
261
- accountId: this.hashAccountId(userName),
262
- rithmicAccountId: userName,
263
- accountName: userName,
264
- name: userName,
265
- balance: this.propfirm.defaultBalance,
266
- startingBalance: this.propfirm.defaultBalance,
267
- profitAndLoss: 0,
268
- status: 0, // Active
269
- platform: 'Rithmic',
270
- propfirm: this.propfirm.name,
271
- }];
272
- }
273
-
274
- return { success: true, accounts: tradingAccounts };
275
- }
276
-
277
- /**
278
- * Request PnL snapshot for accounts
279
- */
280
- async requestPnLSnapshot() {
281
- if (!this.pnlConn || !this.loginInfo) return;
282
-
283
- for (const acc of this.accounts) {
284
- this.pnlConn.send('RequestPnLPositionSnapshot', {
285
- templateId: REQ.PNL_POSITION_SNAPSHOT,
286
- userMsg: ['HQX'],
287
- fcmId: acc.fcmId || this.loginInfo.fcmId,
288
- ibId: acc.ibId || this.loginInfo.ibId,
289
- accountId: acc.accountId,
290
- });
291
- }
292
-
293
- // Wait for responses
294
- await new Promise(resolve => setTimeout(resolve, 2000));
295
- }
296
-
297
- /**
298
- * Subscribe to PnL updates
299
- */
300
- subscribePnLUpdates() {
301
- if (!this.pnlConn || !this.loginInfo) return;
302
-
303
- for (const acc of this.accounts) {
304
- this.pnlConn.send('RequestPnLPositionUpdates', {
305
- templateId: REQ.PNL_POSITION_UPDATES,
306
- userMsg: ['HQX'],
307
- request: 1, // Subscribe
308
- fcmId: acc.fcmId || this.loginInfo.fcmId,
309
- ibId: acc.ibId || this.loginInfo.ibId,
310
- accountId: acc.accountId,
311
- });
312
- }
313
- }
314
-
315
- /**
316
- * Handle ORDER_PLANT messages
317
- */
318
- handleOrderMessage(msg) {
319
- const { templateId, data } = msg;
320
-
321
- switch (templateId) {
322
- case RES.LOGIN_INFO:
323
- this.onLoginInfo(data);
324
- break;
325
- case RES.ACCOUNT_LIST:
326
- this.onAccountList(data);
327
- break;
328
- case RES.TRADE_ROUTES:
329
- this.onTradeRoutes(data);
330
- break;
331
- case RES.SHOW_ORDERS:
332
- this.onShowOrdersResponse(data);
333
- break;
334
- case STREAM.EXCHANGE_NOTIFICATION:
335
- this.onExchangeNotification(data);
336
- break;
337
- case STREAM.ORDER_NOTIFICATION:
338
- this.onOrderNotification(data);
339
- break;
340
- }
341
- }
342
-
343
- onShowOrdersResponse(data) {
344
- try {
345
- const res = proto.decode('ResponseShowOrders', data);
346
- if (res.rpCode?.[0] === '0') {
347
- // End of orders list
348
- this.emit('ordersReceived');
349
- }
350
- } catch (e) {
351
- // Ignore
352
- }
353
- }
354
-
355
- /**
356
- * Handle PNL_PLANT messages
357
- */
358
- handlePnLMessage(msg) {
359
- const { templateId, data } = msg;
360
-
361
- switch (templateId) {
362
- case RES.PNL_POSITION_SNAPSHOT:
363
- case RES.PNL_POSITION_UPDATES:
364
- // OK response
365
- break;
366
- case STREAM.ACCOUNT_PNL_UPDATE:
367
- this.onAccountPnLUpdate(data);
368
- break;
369
- case STREAM.INSTRUMENT_PNL_UPDATE:
370
- this.onInstrumentPnLUpdate(data);
371
- break;
372
- }
373
- }
374
-
375
- onLoginInfo(data) {
376
- try {
377
- const res = proto.decode('ResponseLoginInfo', data);
378
- this.emit('loginInfoReceived', {
379
- fcmId: res.fcmId,
380
- ibId: res.ibId,
381
- firstName: res.firstName,
382
- lastName: res.lastName,
383
- userType: res.userType,
384
- });
385
- } catch (e) {
386
- // Ignore
387
- }
388
- }
389
-
390
- onAccountList(data) {
391
- try {
392
- const res = proto.decode('ResponseAccountList', data);
393
-
394
- if (res.rpCode?.[0] === '0') {
395
- // End of list
396
- this.emit('accountListComplete');
397
- } else if (res.accountId) {
398
- const account = {
399
- fcmId: res.fcmId,
400
- ibId: res.ibId,
401
- accountId: res.accountId,
402
- accountName: res.accountName,
403
- accountCurrency: res.accountCurrency,
404
- };
405
- this.accounts.push(account);
406
- this.emit('accountReceived', account);
407
- }
408
- } catch (e) {
409
- // Ignore
410
- }
411
- }
412
-
413
- onTradeRoutes(data) {
414
- try {
415
- const res = proto.decode('ResponseTradeRoutes', data);
416
- this.emit('tradeRoutes', res);
417
- } catch (e) {
418
- // Ignore
419
- }
420
- }
421
-
422
- onAccountPnLUpdate(data) {
423
- try {
424
- const pnl = decodeAccountPnL(data);
425
- if (pnl.accountId) {
426
- this.accountPnL.set(pnl.accountId, {
427
- accountBalance: parseFloat(pnl.accountBalance || 0),
428
- cashOnHand: parseFloat(pnl.cashOnHand || 0),
429
- marginBalance: parseFloat(pnl.marginBalance || 0),
430
- openPositionPnl: parseFloat(pnl.openPositionPnl || 0),
431
- closedPositionPnl: parseFloat(pnl.closedPositionPnl || 0),
432
- dayPnl: parseFloat(pnl.dayPnl || 0),
433
- });
434
- this.emit('pnlUpdate', pnl);
435
- }
436
- } catch (e) {
437
- // Ignore
438
- }
439
- }
440
-
441
- onInstrumentPnLUpdate(data) {
442
- // Handle instrument-level PnL - this contains position data
443
- try {
444
- const pos = decodeInstrumentPnL(data);
445
- if (pos.symbol && pos.accountId) {
446
- const key = `${pos.accountId}:${pos.symbol}:${pos.exchange}`;
447
- // Net quantity can come from netQuantity field or calculated from buy/sell
448
- const netQty = pos.netQuantity || pos.openPositionQuantity || ((pos.buyQty || 0) - (pos.sellQty || 0));
449
-
450
- if (netQty !== 0) {
451
- // We have an open position
452
- this.positions.set(key, {
453
- accountId: pos.accountId,
454
- symbol: pos.symbol,
455
- exchange: pos.exchange || 'CME',
456
- quantity: netQty,
457
- averagePrice: pos.avgOpenFillPrice || 0,
458
- openPnl: parseFloat(pos.openPositionPnl || pos.dayOpenPnl || 0),
459
- closedPnl: parseFloat(pos.closedPositionPnl || pos.dayClosedPnl || 0),
460
- dayPnl: parseFloat(pos.dayPnl || 0),
461
- isSnapshot: pos.isSnapshot || false,
462
- });
463
- } else {
464
- // Position closed
465
- this.positions.delete(key);
466
- }
467
-
468
- this.emit('positionUpdate', this.positions.get(key));
469
- }
470
- } catch (e) {
471
- // Ignore decode errors
472
- }
473
- }
474
-
475
- onExchangeNotification(data) {
476
- this.emit('exchangeNotification', data);
477
- }
478
-
479
- onOrderNotification(data) {
480
- this.emit('orderNotification', data);
481
- }
482
-
483
- /**
484
- * Hash account ID to numeric (for compatibility)
485
- */
486
- hashAccountId(str) {
487
- let hash = 0;
488
- for (let i = 0; i < str.length; i++) {
489
- const char = str.charCodeAt(i);
490
- hash = (hash << 5) - hash + char;
491
- hash = hash & hash;
492
- }
493
- return Math.abs(hash);
494
- }
495
-
496
- /**
497
- * Get user info
498
- */
499
- async getUser() {
500
- return this.user;
501
- }
502
-
503
- /**
504
- * Get positions via PNL_PLANT
505
- * Positions are streamed via InstrumentPnLPositionUpdate (template 450)
506
- */
507
- async getPositions() {
508
- // If PNL connection not established, try to connect
509
- if (!this.pnlConn && this.credentials) {
510
- await this.connectPnL(this.credentials.username, this.credentials.password);
511
- // Request snapshot to populate positions
512
- await this.requestPnLSnapshot();
513
- }
514
-
515
- // Return cached positions
516
- const positions = Array.from(this.positions.values()).map(pos => ({
517
- symbol: pos.symbol,
518
- exchange: pos.exchange,
519
- quantity: pos.quantity,
520
- averagePrice: pos.averagePrice,
521
- unrealizedPnl: pos.openPnl,
522
- realizedPnl: pos.closedPnl,
523
- side: pos.quantity > 0 ? 'LONG' : 'SHORT',
524
- }));
525
-
526
- return { success: true, positions };
527
- }
528
-
529
- /**
530
- * Get orders via ORDER_PLANT
531
- * Uses RequestShowOrders (template 320) -> ResponseShowOrders (template 321)
532
- */
533
- async getOrders() {
534
- if (!this.orderConn || !this.loginInfo) {
535
- return { success: true, orders: [] };
536
- }
537
-
538
- return new Promise((resolve) => {
539
- const orders = [];
540
- const timeout = setTimeout(() => {
541
- resolve({ success: true, orders });
542
- }, 3000);
162
+ // Delegate to modules
163
+ async getTradingAccounts() { return getTradingAccounts(this); }
164
+ async getPositions() { return getPositions(this); }
165
+ async getOrders() { return getOrders(this); }
166
+ async getOrderHistory(date) { return getOrderHistory(this, date); }
167
+ async placeOrder(orderData) { return placeOrder(this, orderData); }
168
+ async cancelOrder(orderId) { return cancelOrder(this, orderId); }
169
+ async closePosition(accountId, symbol) { return closePosition(this, accountId, symbol); }
543
170
 
544
- // Listen for order notifications
545
- const orderHandler = (notification) => {
546
- // RithmicOrderNotification contains order details
547
- if (notification.orderId) {
548
- orders.push({
549
- orderId: notification.orderId,
550
- symbol: notification.symbol,
551
- exchange: notification.exchange,
552
- side: notification.transactionType === 1 ? 'BUY' : 'SELL',
553
- quantity: notification.quantity,
554
- filledQuantity: notification.filledQuantity || 0,
555
- price: notification.price,
556
- orderType: notification.orderType,
557
- status: notification.status,
558
- });
559
- }
560
- };
561
-
562
- this.once('ordersReceived', () => {
563
- clearTimeout(timeout);
564
- this.removeListener('orderNotification', orderHandler);
565
- resolve({ success: true, orders });
566
- });
567
-
568
- this.on('orderNotification', orderHandler);
569
-
570
- // Send request
571
- try {
572
- for (const acc of this.accounts) {
573
- this.orderConn.send('RequestShowOrders', {
574
- templateId: REQ.SHOW_ORDERS,
575
- userMsg: ['HQX'],
576
- fcmId: acc.fcmId || this.loginInfo.fcmId,
577
- ibId: acc.ibId || this.loginInfo.ibId,
578
- accountId: acc.accountId,
579
- });
580
- }
581
- } catch (e) {
582
- clearTimeout(timeout);
583
- resolve({ success: false, error: e.message, orders: [] });
584
- }
585
- });
171
+ // Stubs for API compatibility
172
+ async getUser() { return this.user; }
173
+ async getLifetimeStats() { return { success: true, stats: null }; }
174
+ async getDailyStats() { return { success: true, stats: [] }; }
175
+ async getTradeHistory() { return { success: true, trades: [] }; }
176
+
177
+ async getMarketStatus() {
178
+ const status = this.checkMarketHours();
179
+ return { success: true, isOpen: status.isOpen, message: status.message };
586
180
  }
587
181
 
588
- /**
589
- * Get lifetime stats (stub for Rithmic - not available via API)
590
- */
591
- async getLifetimeStats(accountId) {
592
- return { success: true, stats: null };
593
- }
594
-
595
- /**
596
- * Get daily stats (stub for Rithmic - not available via API)
597
- */
598
- async getDailyStats(accountId) {
599
- return { success: true, stats: [] };
600
- }
182
+ getToken() { return this.loginInfo ? 'connected' : null; }
183
+ getPropfirm() { return this.propfirmKey || 'apex'; }
601
184
 
602
- /**
603
- * Get trade history (stub for Rithmic)
604
- */
605
- async getTradeHistory(accountId, days = 30) {
606
- return { success: true, trades: [] };
607
- }
608
-
609
- /**
610
- * Get market status
611
- */
612
- async getMarketStatus(accountId) {
613
- const marketHours = this.checkMarketHours();
614
- return {
615
- success: true,
616
- isOpen: marketHours.isOpen,
617
- message: marketHours.message,
618
- };
619
- }
620
-
621
- /**
622
- * Get token (stub - Rithmic uses WebSocket, not tokens)
623
- */
624
- getToken() {
625
- return this.loginInfo ? 'connected' : null;
626
- }
627
-
628
- /**
629
- * Get propfirm name
630
- */
631
- getPropfirm() {
632
- return this.propfirmKey || 'apex';
633
- }
634
-
635
- /**
636
- * Get Rithmic credentials for HQX Server
637
- */
638
185
  getRithmicCredentials() {
639
186
  if (!this.credentials) return null;
640
187
  return {
@@ -645,11 +192,7 @@ class RithmicService extends EventEmitter {
645
192
  };
646
193
  }
647
194
 
648
- /**
649
- * Search contracts (stub - would need TICKER_PLANT)
650
- */
651
195
  async searchContracts(searchText) {
652
- // Common futures contracts
653
196
  const contracts = [
654
197
  { symbol: 'ESH5', name: 'E-mini S&P 500 Mar 2025', exchange: 'CME' },
655
198
  { symbol: 'NQH5', name: 'E-mini NASDAQ-100 Mar 2025', exchange: 'CME' },
@@ -662,173 +205,26 @@ class RithmicService extends EventEmitter {
662
205
  return contracts.filter(c => c.symbol.includes(search) || c.name.toUpperCase().includes(search));
663
206
  }
664
207
 
665
- /**
666
- * Place order via ORDER_PLANT
667
- */
668
- async placeOrder(orderData) {
669
- if (!this.orderConn || !this.loginInfo) {
670
- return { success: false, error: 'Not connected' };
671
- }
672
-
673
- try {
674
- this.orderConn.send('RequestNewOrder', {
675
- templateId: REQ.NEW_ORDER,
676
- userMsg: ['HQX'],
677
- fcmId: this.loginInfo.fcmId,
678
- ibId: this.loginInfo.ibId,
679
- accountId: orderData.accountId,
680
- symbol: orderData.symbol,
681
- exchange: orderData.exchange || 'CME',
682
- quantity: orderData.size,
683
- transactionType: orderData.side === 0 ? 1 : 2, // 1=Buy, 2=Sell
684
- duration: 1, // DAY
685
- orderType: orderData.type === 2 ? 1 : 2, // 1=Market, 2=Limit
686
- price: orderData.price || 0,
687
- });
688
-
689
- return { success: true, message: 'Order submitted' };
690
- } catch (error) {
691
- return { success: false, error: error.message };
692
- }
693
- }
694
-
695
- /**
696
- * Cancel order
697
- */
698
- async cancelOrder(orderId) {
699
- if (!this.orderConn || !this.loginInfo) {
700
- return { success: false, error: 'Not connected' };
701
- }
702
-
703
- try {
704
- this.orderConn.send('RequestCancelOrder', {
705
- templateId: REQ.CANCEL_ORDER,
706
- userMsg: ['HQX'],
707
- fcmId: this.loginInfo.fcmId,
708
- ibId: this.loginInfo.ibId,
709
- orderId: orderId,
710
- });
711
-
712
- return { success: true };
713
- } catch (error) {
714
- return { success: false, error: error.message };
715
- }
716
- }
717
-
718
- /**
719
- * Close position (market order to flatten)
720
- */
721
- async closePosition(accountId, symbol) {
722
- // Get current position
723
- const positions = Array.from(this.positions.values());
724
- const position = positions.find(p => p.accountId === accountId && p.symbol === symbol);
725
-
726
- if (!position) {
727
- return { success: false, error: 'Position not found' };
728
- }
729
-
730
- // Place opposite order
731
- return this.placeOrder({
732
- accountId,
733
- symbol,
734
- exchange: position.exchange,
735
- size: Math.abs(position.quantity),
736
- side: position.quantity > 0 ? 1 : 0, // Sell if long, Buy if short
737
- type: 2, // Market
738
- });
739
- }
740
-
741
- /**
742
- * Get order history
743
- * Uses RequestShowOrderHistorySummary (template 324)
744
- */
745
- async getOrderHistory(date) {
746
- if (!this.orderConn || !this.loginInfo) {
747
- return { success: true, orders: [] };
748
- }
749
-
750
- // Default to today
751
- const dateStr = date || new Date().toISOString().slice(0, 10).replace(/-/g, '');
752
-
753
- return new Promise((resolve) => {
754
- const orders = [];
755
- const timeout = setTimeout(() => {
756
- resolve({ success: true, orders });
757
- }, 3000);
758
-
759
- try {
760
- for (const acc of this.accounts) {
761
- this.orderConn.send('RequestShowOrderHistorySummary', {
762
- templateId: REQ.SHOW_ORDER_HISTORY,
763
- userMsg: ['HQX'],
764
- fcmId: acc.fcmId || this.loginInfo.fcmId,
765
- ibId: acc.ibId || this.loginInfo.ibId,
766
- accountId: acc.accountId,
767
- date: dateStr,
768
- });
769
- }
770
-
771
- // Wait for response
772
- setTimeout(() => {
773
- clearTimeout(timeout);
774
- resolve({ success: true, orders });
775
- }, 2000);
776
- } catch (e) {
777
- clearTimeout(timeout);
778
- resolve({ success: false, error: e.message, orders: [] });
779
- }
780
- });
781
- }
782
-
783
- /**
784
- * Check market hours (same as ProjectX)
785
- */
786
208
  checkMarketHours() {
787
209
  const now = new Date();
788
210
  const utcDay = now.getUTCDay();
789
211
  const utcHour = now.getUTCHours();
790
- const utcMinute = now.getUTCMinutes();
791
- const utcTime = utcHour * 60 + utcMinute;
792
-
793
- // CME Futures: Sunday 5PM CT - Friday 4PM CT
794
- // CT = UTC-6 (CST) or UTC-5 (CDT)
795
- const ctOffset = this.isDST(now) ? 5 : 6;
212
+
213
+ const isDST = now.getTimezoneOffset() < Math.max(
214
+ new Date(now.getFullYear(), 0, 1).getTimezoneOffset(),
215
+ new Date(now.getFullYear(), 6, 1).getTimezoneOffset()
216
+ );
217
+ const ctOffset = isDST ? 5 : 6;
796
218
  const ctHour = (utcHour - ctOffset + 24) % 24;
797
219
  const ctDay = utcHour < ctOffset ? (utcDay + 6) % 7 : utcDay;
798
220
 
799
- // Market closed Saturday all day
800
- if (ctDay === 6) {
801
- return { isOpen: false, message: 'Market closed (Saturday)' };
802
- }
803
-
804
- // Sunday before 5PM CT
805
- if (ctDay === 0 && ctHour < 17) {
806
- return { isOpen: false, message: 'Market opens Sunday 5:00 PM CT' };
807
- }
808
-
809
- // Friday after 4PM CT
810
- if (ctDay === 5 && ctHour >= 16) {
811
- return { isOpen: false, message: 'Market closed (Friday after 4PM CT)' };
812
- }
813
-
814
- // Daily maintenance 4PM-5PM CT (Mon-Thu)
815
- if (ctHour === 16 && ctDay >= 1 && ctDay <= 4) {
816
- return { isOpen: false, message: 'Daily maintenance (4:00-5:00 PM CT)' };
817
- }
818
-
221
+ if (ctDay === 6) return { isOpen: false, message: 'Market closed (Saturday)' };
222
+ if (ctDay === 0 && ctHour < 17) return { isOpen: false, message: 'Market opens Sunday 5:00 PM CT' };
223
+ if (ctDay === 5 && ctHour >= 16) return { isOpen: false, message: 'Market closed (Friday after 4PM CT)' };
224
+ if (ctHour === 16 && ctDay >= 1 && ctDay <= 4) return { isOpen: false, message: 'Daily maintenance (4:00-5:00 PM CT)' };
819
225
  return { isOpen: true, message: 'Market is open' };
820
226
  }
821
227
 
822
- isDST(date) {
823
- const jan = new Date(date.getFullYear(), 0, 1);
824
- const jul = new Date(date.getFullYear(), 6, 1);
825
- const stdOffset = Math.max(jan.getTimezoneOffset(), jul.getTimezoneOffset());
826
- return date.getTimezoneOffset() < stdOffset;
827
- }
828
-
829
- /**
830
- * Disconnect all connections
831
- */
832
228
  async disconnect() {
833
229
  if (this.orderConn) {
834
230
  await this.orderConn.disconnect();