hedgequantx 1.8.49 → 2.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.
Files changed (103) hide show
  1. package/README.md +13 -6
  2. package/bin/cli.js +13 -7
  3. package/dist/algo/copy-engine.js +3 -0
  4. package/dist/algo/copy-engine.jsc +0 -0
  5. package/dist/algo/engine.js +3 -0
  6. package/dist/algo/engine.jsc +0 -0
  7. package/dist/algo/market-data-rithmic.js +3 -0
  8. package/dist/algo/market-data-rithmic.jsc +0 -0
  9. package/dist/algo/market-data.js +3 -0
  10. package/dist/algo/market-data.jsc +0 -0
  11. package/dist/algo/rithmic/connection.js +3 -0
  12. package/dist/algo/rithmic/connection.jsc +0 -0
  13. package/dist/algo/rithmic/constants.js +3 -0
  14. package/dist/algo/rithmic/constants.jsc +0 -0
  15. package/dist/algo/rithmic/index.js +3 -0
  16. package/dist/algo/rithmic/index.jsc +0 -0
  17. package/dist/algo/rithmic/market-data.js +3 -0
  18. package/dist/algo/rithmic/market-data.jsc +0 -0
  19. package/dist/algo/rithmic/pnl.js +3 -0
  20. package/dist/algo/rithmic/pnl.jsc +0 -0
  21. package/dist/algo/rithmic/pool.js +3 -0
  22. package/dist/algo/rithmic/pool.jsc +0 -0
  23. package/dist/algo/rithmic/trading.js +3 -0
  24. package/dist/algo/rithmic/trading.jsc +0 -0
  25. package/dist/algo/rithmic-decoder.js +3 -0
  26. package/dist/algo/rithmic-decoder.jsc +0 -0
  27. package/dist/algo/strategies/ultra-scalping-v2.js +3 -0
  28. package/dist/algo/strategies/ultra-scalping-v2.jsc +0 -0
  29. package/dist/algo/strategies/ultra-scalping.js +3 -0
  30. package/dist/algo/strategies/ultra-scalping.jsc +0 -0
  31. package/dist/algo/trading-api-rithmic.js +3 -0
  32. package/dist/algo/trading-api-rithmic.jsc +0 -0
  33. package/dist/algo/trading-api.js +3 -0
  34. package/dist/algo/trading-api.jsc +0 -0
  35. package/dist/algo/utils/smart-logger.js +3 -0
  36. package/dist/algo/utils/smart-logger.jsc +0 -0
  37. package/dist/algo/utils/smart-logs.js +3 -0
  38. package/dist/algo/utils/smart-logs.jsc +0 -0
  39. package/package.json +33 -10
  40. package/protos/rithmic/account_pnl_position_update.proto +59 -0
  41. package/protos/rithmic/base.proto +7 -0
  42. package/protos/rithmic/best_bid_offer.proto +39 -0
  43. package/protos/rithmic/exchange_order_notification.proto +140 -0
  44. package/protos/rithmic/instrument_pnl_position_update.proto +50 -0
  45. package/protos/rithmic/last_trade.proto +53 -0
  46. package/protos/rithmic/request_account_list.proto +20 -0
  47. package/protos/rithmic/request_cancel_all_orders.proto +15 -0
  48. package/protos/rithmic/request_front_month_contract.proto +10 -0
  49. package/protos/rithmic/request_heartbeat.proto +13 -0
  50. package/protos/rithmic/request_login.proto +28 -0
  51. package/protos/rithmic/request_login_info.proto +10 -0
  52. package/protos/rithmic/request_logout.proto +10 -0
  53. package/protos/rithmic/request_market_data_update.proto +42 -0
  54. package/protos/rithmic/request_new_order.proto +84 -0
  55. package/protos/rithmic/request_pnl_position_snapshot.proto +14 -0
  56. package/protos/rithmic/request_pnl_position_updates.proto +20 -0
  57. package/protos/rithmic/request_product_codes.proto +9 -0
  58. package/protos/rithmic/request_rithmic_system_info.proto +8 -0
  59. package/protos/rithmic/request_show_order_history.proto +16 -0
  60. package/protos/rithmic/request_show_order_history_dates.proto +10 -0
  61. package/protos/rithmic/request_show_order_history_summary.proto +14 -0
  62. package/protos/rithmic/request_show_orders.proto +14 -0
  63. package/protos/rithmic/request_subscribe_for_order_updates.proto +14 -0
  64. package/protos/rithmic/request_tick_bar_replay.proto +48 -0
  65. package/protos/rithmic/request_trade_routes.proto +11 -0
  66. package/protos/rithmic/response_account_list.proto +18 -0
  67. package/protos/rithmic/response_front_month_contract.proto +13 -0
  68. package/protos/rithmic/response_heartbeat.proto +14 -0
  69. package/protos/rithmic/response_login.proto +18 -0
  70. package/protos/rithmic/response_login_info.proto +24 -0
  71. package/protos/rithmic/response_logout.proto +11 -0
  72. package/protos/rithmic/response_market_data_update.proto +9 -0
  73. package/protos/rithmic/response_new_order.proto +18 -0
  74. package/protos/rithmic/response_pnl_position_snapshot.proto +11 -0
  75. package/protos/rithmic/response_pnl_position_updates.proto +11 -0
  76. package/protos/rithmic/response_product_codes.proto +12 -0
  77. package/protos/rithmic/response_rithmic_system_info.proto +12 -0
  78. package/protos/rithmic/response_show_order_history.proto +11 -0
  79. package/protos/rithmic/response_show_order_history_dates.proto +13 -0
  80. package/protos/rithmic/response_show_order_history_summary.proto +11 -0
  81. package/protos/rithmic/response_show_orders.proto +11 -0
  82. package/protos/rithmic/response_subscribe_for_order_updates.proto +11 -0
  83. package/protos/rithmic/response_tick_bar_replay.proto +40 -0
  84. package/protos/rithmic/response_trade_routes.proto +19 -0
  85. package/protos/rithmic/rithmic_order_notification.proto +124 -0
  86. package/src/app.js +136 -89
  87. package/src/config/index.js +27 -8
  88. package/src/config/settings.js +155 -0
  89. package/src/pages/algo/copy-trading.js +293 -200
  90. package/src/pages/algo/one-account.js +1 -1
  91. package/src/security/encryption.js +81 -46
  92. package/src/security/index.js +12 -8
  93. package/src/security/rateLimit.js +68 -65
  94. package/src/security/validation.js +93 -79
  95. package/src/services/hqx-server.js +538 -206
  96. package/src/services/projectx/index.js +327 -204
  97. package/src/services/rithmic/index.js +288 -285
  98. package/src/services/session.js +184 -114
  99. package/src/services/tradovate/index.js +286 -297
  100. package/src/utils/http.js +236 -0
  101. package/src/utils/index.js +11 -2
  102. package/src/utils/logger.js +64 -33
  103. package/src/utils/prompts.js +79 -71
@@ -1,52 +1,71 @@
1
1
  /**
2
- * Rithmic Service
3
- * Main service for Rithmic prop firm connections
2
+ * @fileoverview Rithmic Service - Main service for Rithmic prop firm connections
3
+ * @module services/rithmic
4
+ *
5
+ * NO FAKE DATA - Only real values from Rithmic API
4
6
  */
5
7
 
6
8
  const EventEmitter = require('events');
7
9
  const { RithmicConnection } = require('./connection');
8
- const { RITHMIC_ENDPOINTS, RITHMIC_SYSTEMS, getSymbolInfo, getCategoryOrder } = require('./constants');
10
+ const { RITHMIC_ENDPOINTS, RITHMIC_SYSTEMS, getSymbolInfo } = require('./constants');
9
11
  const { createOrderHandler, createPnLHandler } = require('./handlers');
10
- const { fetchAccounts, getTradingAccounts, requestPnLSnapshot, subscribePnLUpdates, getPositions, hashAccountId } = require('./accounts');
12
+ const {
13
+ fetchAccounts,
14
+ getTradingAccounts,
15
+ requestPnLSnapshot,
16
+ subscribePnLUpdates,
17
+ getPositions,
18
+ hashAccountId,
19
+ } = require('./accounts');
11
20
  const { placeOrder, cancelOrder, getOrders, getOrderHistory, closePosition } = require('./orders');
12
- const { decodeFrontMonthContract, readVarint, readLengthDelimited, skipField } = require('./protobuf');
21
+ const { decodeFrontMonthContract } = require('./protobuf');
22
+ const { TIMEOUTS, CACHE } = require('../../config/settings');
23
+ const { logger } = require('../../utils/logger');
13
24
 
14
- // Debug mode
15
- const DEBUG = process.env.HQX_DEBUG === '1';
16
- const debug = (...args) => DEBUG && console.log('[Rithmic:Service]', ...args);
25
+ const log = logger.scope('Rithmic');
17
26
 
18
- // PropFirm configurations - NO FAKE DATA
27
+ /** PropFirm configurations */
19
28
  const PROPFIRM_CONFIGS = {
20
- 'apex': { name: 'Apex Trader Funding', systemName: 'Apex', gateway: RITHMIC_ENDPOINTS.CHICAGO },
21
- 'apex_rithmic': { name: 'Apex Trader Funding', systemName: 'Apex', gateway: RITHMIC_ENDPOINTS.CHICAGO },
22
- 'topstep_r': { name: 'Topstep (Rithmic)', systemName: RITHMIC_SYSTEMS.TOPSTEP, gateway: RITHMIC_ENDPOINTS.CHICAGO },
23
- 'bulenox_r': { name: 'Bulenox (Rithmic)', systemName: RITHMIC_SYSTEMS.BULENOX, gateway: RITHMIC_ENDPOINTS.CHICAGO },
24
- 'earn2trade': { name: 'Earn2Trade', systemName: RITHMIC_SYSTEMS.EARN_2_TRADE, gateway: RITHMIC_ENDPOINTS.CHICAGO },
25
- 'mescapital': { name: 'MES Capital', systemName: RITHMIC_SYSTEMS.MES_CAPITAL, gateway: RITHMIC_ENDPOINTS.CHICAGO },
26
- 'tradefundrr': { name: 'TradeFundrr', systemName: RITHMIC_SYSTEMS.TRADEFUNDRR, gateway: RITHMIC_ENDPOINTS.CHICAGO },
27
- 'thetradingpit': { name: 'The Trading Pit', systemName: RITHMIC_SYSTEMS.THE_TRADING_PIT, gateway: RITHMIC_ENDPOINTS.CHICAGO },
28
- 'fundedfutures': { name: 'Funded Futures Network', systemName: RITHMIC_SYSTEMS.FUNDED_FUTURES_NETWORK, gateway: RITHMIC_ENDPOINTS.CHICAGO },
29
- 'propshop': { name: 'PropShop Trader', systemName: RITHMIC_SYSTEMS.PROPSHOP_TRADER, gateway: RITHMIC_ENDPOINTS.CHICAGO },
29
+ apex: { name: 'Apex Trader Funding', systemName: 'Apex', gateway: RITHMIC_ENDPOINTS.CHICAGO },
30
+ apex_rithmic: { name: 'Apex Trader Funding', systemName: 'Apex', gateway: RITHMIC_ENDPOINTS.CHICAGO },
31
+ topstep_r: { name: 'Topstep (Rithmic)', systemName: RITHMIC_SYSTEMS.TOPSTEP, gateway: RITHMIC_ENDPOINTS.CHICAGO },
32
+ bulenox_r: { name: 'Bulenox (Rithmic)', systemName: RITHMIC_SYSTEMS.BULENOX, gateway: RITHMIC_ENDPOINTS.CHICAGO },
33
+ earn2trade: { name: 'Earn2Trade', systemName: RITHMIC_SYSTEMS.EARN_2_TRADE, gateway: RITHMIC_ENDPOINTS.CHICAGO },
34
+ mescapital: { name: 'MES Capital', systemName: RITHMIC_SYSTEMS.MES_CAPITAL, gateway: RITHMIC_ENDPOINTS.CHICAGO },
35
+ tradefundrr: { name: 'TradeFundrr', systemName: RITHMIC_SYSTEMS.TRADEFUNDRR, gateway: RITHMIC_ENDPOINTS.CHICAGO },
36
+ thetradingpit: { name: 'The Trading Pit', systemName: RITHMIC_SYSTEMS.THE_TRADING_PIT, gateway: RITHMIC_ENDPOINTS.CHICAGO },
37
+ fundedfutures: { name: 'Funded Futures Network', systemName: RITHMIC_SYSTEMS.FUNDED_FUTURES_NETWORK, gateway: RITHMIC_ENDPOINTS.CHICAGO },
38
+ propshop: { name: 'PropShop Trader', systemName: RITHMIC_SYSTEMS.PROPSHOP_TRADER, gateway: RITHMIC_ENDPOINTS.CHICAGO },
30
39
  '4proptrader': { name: '4PropTrader', systemName: RITHMIC_SYSTEMS.FOUR_PROP_TRADER, gateway: RITHMIC_ENDPOINTS.CHICAGO },
31
- 'daytraders': { name: 'DayTraders.com', systemName: RITHMIC_SYSTEMS.DAY_TRADERS, gateway: RITHMIC_ENDPOINTS.CHICAGO },
40
+ daytraders: { name: 'DayTraders.com', systemName: RITHMIC_SYSTEMS.DAY_TRADERS, gateway: RITHMIC_ENDPOINTS.CHICAGO },
32
41
  '10xfutures': { name: '10X Futures', systemName: RITHMIC_SYSTEMS.TEN_X_FUTURES, gateway: RITHMIC_ENDPOINTS.CHICAGO },
33
- 'lucidtrading': { name: 'Lucid Trading', systemName: RITHMIC_SYSTEMS.LUCID_TRADING, gateway: RITHMIC_ENDPOINTS.CHICAGO },
34
- 'thrivetrading': { name: 'Thrive Trading', systemName: RITHMIC_SYSTEMS.THRIVE_TRADING, gateway: RITHMIC_ENDPOINTS.CHICAGO },
35
- 'legendstrading': { name: 'Legends Trading', systemName: RITHMIC_SYSTEMS.LEGENDS_TRADING, gateway: RITHMIC_ENDPOINTS.CHICAGO },
42
+ lucidtrading: { name: 'Lucid Trading', systemName: RITHMIC_SYSTEMS.LUCID_TRADING, gateway: RITHMIC_ENDPOINTS.CHICAGO },
43
+ thrivetrading: { name: 'Thrive Trading', systemName: RITHMIC_SYSTEMS.THRIVE_TRADING, gateway: RITHMIC_ENDPOINTS.CHICAGO },
44
+ legendstrading: { name: 'Legends Trading', systemName: RITHMIC_SYSTEMS.LEGENDS_TRADING, gateway: RITHMIC_ENDPOINTS.CHICAGO },
36
45
  };
37
46
 
47
+ /**
48
+ * Rithmic Service for prop firm trading
49
+ */
38
50
  class RithmicService extends EventEmitter {
51
+ /**
52
+ * @param {string} propfirmKey - PropFirm identifier
53
+ */
39
54
  constructor(propfirmKey) {
40
55
  super();
41
56
  this.propfirmKey = propfirmKey;
42
- this.propfirm = PROPFIRM_CONFIGS[propfirmKey] || {
43
- name: propfirmKey,
44
- systemName: 'Rithmic Paper Trading',
45
- gateway: RITHMIC_ENDPOINTS.PAPER
57
+ this.propfirm = PROPFIRM_CONFIGS[propfirmKey] || {
58
+ name: propfirmKey,
59
+ systemName: 'Rithmic Paper Trading',
60
+ gateway: RITHMIC_ENDPOINTS.PAPER,
46
61
  };
62
+
63
+ // Connections
47
64
  this.orderConn = null;
48
65
  this.pnlConn = null;
49
- this.tickerConn = null; // TICKER_PLANT for symbol lookup
66
+ this.tickerConn = null;
67
+
68
+ // State
50
69
  this.loginInfo = null;
51
70
  this.accounts = [];
52
71
  this.accountPnL = new Map();
@@ -54,24 +73,31 @@ class RithmicService extends EventEmitter {
54
73
  this.orders = [];
55
74
  this.user = null;
56
75
  this.credentials = null;
57
- this.cachedContracts = null; // Cache contracts to avoid repeated API calls
76
+
77
+ // Cache
78
+ this._contractsCache = null;
79
+ this._contractsCacheTime = 0;
58
80
  }
59
81
 
82
+ // ==================== AUTH ====================
83
+
60
84
  /**
61
85
  * Login to Rithmic
86
+ * @param {string} username - Username
87
+ * @param {string} password - Password
88
+ * @returns {Promise<{success: boolean, user?: Object, accounts?: Array, error?: string}>}
62
89
  */
63
90
  async login(username, password) {
64
91
  try {
65
92
  this.orderConn = new RithmicConnection();
66
- const gateway = this.propfirm.gateway || RITHMIC_ENDPOINTS.CHICAGO;
67
93
 
68
94
  const config = {
69
- uri: gateway,
95
+ uri: this.propfirm.gateway || RITHMIC_ENDPOINTS.CHICAGO,
70
96
  systemName: this.propfirm.systemName,
71
97
  userId: username,
72
- password: password,
98
+ password,
73
99
  appName: 'HQX-CLI',
74
- appVersion: '1.0.0',
100
+ appVersion: '2.0.0',
75
101
  };
76
102
 
77
103
  await this.orderConn.connect(config);
@@ -80,88 +106,83 @@ class RithmicService extends EventEmitter {
80
106
  return new Promise((resolve) => {
81
107
  const timeout = setTimeout(() => {
82
108
  resolve({ success: false, error: 'Login timeout - server did not respond' });
83
- }, 30000);
109
+ }, TIMEOUTS.RITHMIC_LOGIN);
84
110
 
85
111
  this.orderConn.once('loggedIn', async (data) => {
86
112
  clearTimeout(timeout);
87
113
  this.loginInfo = data;
88
114
  this.user = { userName: username, fcmId: data.fcmId, ibId: data.ibId };
89
115
 
90
- try {
91
- await fetchAccounts(this);
92
- debug('Fetched accounts:', this.accounts.map(a => a.accountId));
93
- } catch (e) {
94
- debug('fetchAccounts error:', e.message);
116
+ // Fetch accounts
117
+ try {
118
+ await fetchAccounts(this);
119
+ log.debug('Fetched accounts', { count: this.accounts.length });
120
+ } catch (err) {
121
+ log.warn('Failed to fetch accounts', { error: err.message });
95
122
  }
96
123
 
97
- // NO FAKE ACCOUNTS - only real from Rithmic API
98
-
124
+ // Store credentials for reconnection
99
125
  this.credentials = { username, password };
100
126
 
101
- debug('Accounts found:', this.accounts.length);
102
- debug('Account IDs:', this.accounts.map(a => a.accountId));
103
-
104
- // Connect to PNL_PLANT for balance/P&L data
127
+ // Connect to PNL_PLANT
105
128
  try {
106
- debug('Connecting to PNL_PLANT...');
107
129
  const pnlConnected = await this.connectPnL(username, password);
108
- debug('PNL_PLANT connected:', pnlConnected, 'pnlConn:', !!this.pnlConn);
109
-
110
- if (this.pnlConn) {
111
- debug('Requesting P&L snapshot...');
130
+ if (pnlConnected && this.pnlConn) {
112
131
  await requestPnLSnapshot(this);
113
- debug('accountPnL map size after snapshot:', this.accountPnL.size);
114
132
  subscribePnLUpdates(this);
115
133
  }
116
- } catch (e) {
117
- debug('PnL connection failed:', e.message);
134
+ } catch (err) {
135
+ log.warn('PnL connection failed', { error: err.message });
118
136
  }
119
137
 
120
- // Get accounts with P&L data (if available)
121
138
  const result = await getTradingAccounts(this);
122
- debug('Final accounts:', result.accounts.length);
139
+ log.info('Login successful', { accounts: result.accounts.length });
123
140
 
124
141
  resolve({ success: true, user: this.user, accounts: result.accounts });
125
142
  });
126
143
 
127
144
  this.orderConn.once('loginFailed', (data) => {
128
145
  clearTimeout(timeout);
146
+ log.warn('Login failed', { error: data.message });
129
147
  resolve({ success: false, error: data.message || 'Login failed' });
130
148
  });
131
149
 
132
150
  this.orderConn.login('ORDER_PLANT');
133
151
  });
134
-
135
- } catch (error) {
136
- return { success: false, error: error.message };
152
+ } catch (err) {
153
+ log.error('Login error', { error: err.message });
154
+ return { success: false, error: err.message };
137
155
  }
138
156
  }
139
157
 
140
158
  /**
141
159
  * Connect to PNL_PLANT for balance data
160
+ * @param {string} username - Username
161
+ * @param {string} password - Password
162
+ * @returns {Promise<boolean>}
142
163
  */
143
164
  async connectPnL(username, password) {
144
165
  try {
145
166
  this.pnlConn = new RithmicConnection();
146
- const gateway = this.propfirm.gateway || RITHMIC_ENDPOINTS.CHICAGO;
147
-
167
+
148
168
  const config = {
149
- uri: gateway,
169
+ uri: this.propfirm.gateway || RITHMIC_ENDPOINTS.CHICAGO,
150
170
  systemName: this.propfirm.systemName,
151
171
  userId: username,
152
- password: password,
172
+ password,
153
173
  appName: 'HQX-CLI',
154
- appVersion: '1.0.0',
174
+ appVersion: '2.0.0',
155
175
  };
156
176
 
157
177
  await this.pnlConn.connect(config);
158
178
  this.pnlConn.on('message', createPnLHandler(this));
159
179
 
160
180
  return new Promise((resolve) => {
161
- const timeout = setTimeout(() => resolve(false), 10000);
181
+ const timeout = setTimeout(() => resolve(false), TIMEOUTS.RITHMIC_PNL);
162
182
 
163
183
  this.pnlConn.once('loggedIn', () => {
164
184
  clearTimeout(timeout);
185
+ log.debug('PNL_PLANT connected');
165
186
  resolve(true);
166
187
  });
167
188
 
@@ -172,247 +193,219 @@ class RithmicService extends EventEmitter {
172
193
 
173
194
  this.pnlConn.login('PNL_PLANT');
174
195
  });
175
- } catch (e) {
196
+ } catch (err) {
197
+ log.warn('PNL connection error', { error: err.message });
176
198
  return false;
177
199
  }
178
200
  }
179
201
 
180
- // Delegate to modules
181
- async getTradingAccounts() { return getTradingAccounts(this); }
182
- async getPositions() { return getPositions(this); }
183
- async getOrders() { return getOrders(this); }
184
- async getOrderHistory(date) { return getOrderHistory(this, date); }
185
- async placeOrder(orderData) { return placeOrder(this, orderData); }
186
- async cancelOrder(orderId) { return cancelOrder(this, orderId); }
187
- async closePosition(accountId, symbol) { return closePosition(this, accountId, symbol); }
188
-
189
- // Stubs for API compatibility
190
- async getUser() { return this.user; }
191
- async getLifetimeStats() { return { success: true, stats: null }; }
192
- async getDailyStats() { return { success: true, stats: [] }; }
193
- async getTradeHistory() { return { success: true, trades: [] }; }
194
-
195
- async getMarketStatus() {
196
- const status = this.checkMarketHours();
197
- return { success: true, isOpen: status.isOpen, message: status.message };
198
- }
199
-
200
- getToken() { return this.loginInfo ? 'connected' : null; }
201
- getPropfirm() { return this.propfirmKey || 'apex'; }
202
-
203
- getRithmicCredentials() {
204
- if (!this.credentials) return null;
205
- return {
206
- userId: this.credentials.username,
207
- password: this.credentials.password,
208
- systemName: this.propfirm.systemName,
209
- gateway: this.propfirm.gateway || 'wss://rprotocol.rithmic.com:443'
210
- };
211
- }
212
-
213
202
  /**
214
203
  * Connect to TICKER_PLANT for symbol lookup
204
+ * @param {string} username - Username
205
+ * @param {string} password - Password
206
+ * @returns {Promise<boolean>}
215
207
  */
216
208
  async connectTicker(username, password) {
217
209
  try {
218
210
  this.tickerConn = new RithmicConnection();
219
- const gateway = this.propfirm.gateway || RITHMIC_ENDPOINTS.CHICAGO;
220
-
211
+
221
212
  const config = {
222
- uri: gateway,
213
+ uri: this.propfirm.gateway || RITHMIC_ENDPOINTS.CHICAGO,
223
214
  systemName: this.propfirm.systemName,
224
215
  userId: username,
225
- password: password,
216
+ password,
226
217
  appName: 'HQX-CLI',
227
- appVersion: '1.0.0',
218
+ appVersion: '2.0.0',
228
219
  };
229
220
 
230
221
  await this.tickerConn.connect(config);
231
222
 
232
223
  return new Promise((resolve) => {
233
224
  const timeout = setTimeout(() => {
234
- debug('TICKER_PLANT login timeout');
225
+ log.debug('TICKER_PLANT timeout');
235
226
  resolve(false);
236
- }, 10000);
227
+ }, TIMEOUTS.RITHMIC_TICKER);
237
228
 
238
229
  this.tickerConn.once('loggedIn', () => {
239
230
  clearTimeout(timeout);
240
- debug('TICKER_PLANT connected');
231
+ log.debug('TICKER_PLANT connected');
241
232
  resolve(true);
242
233
  });
243
234
 
244
235
  this.tickerConn.once('loginFailed', () => {
245
236
  clearTimeout(timeout);
246
- debug('TICKER_PLANT login failed');
247
237
  resolve(false);
248
238
  });
249
239
 
250
240
  this.tickerConn.login('TICKER_PLANT');
251
241
  });
252
- } catch (e) {
253
- debug('TICKER_PLANT connection error:', e.message);
242
+ } catch (err) {
243
+ log.warn('TICKER connection error', { error: err.message });
254
244
  return false;
255
245
  }
256
246
  }
257
247
 
258
- /**
259
- * Get front month contracts for multiple symbols at once - batch request
260
- */
261
- async getFrontMonthsBatch(products) {
262
- if (!this.tickerConn) {
263
- throw new Error('TICKER_PLANT not connected');
264
- }
248
+ // ==================== DELEGATED METHODS ====================
265
249
 
266
- return new Promise((resolve) => {
267
- const results = new Map();
268
- const pending = new Set(products.map(p => p.productCode));
250
+ async getTradingAccounts() { return getTradingAccounts(this); }
251
+ async getPositions() { return getPositions(this); }
252
+ async getOrders() { return getOrders(this); }
253
+ async getOrderHistory(date) { return getOrderHistory(this, date); }
254
+ async placeOrder(orderData) { return placeOrder(this, orderData); }
255
+ async cancelOrder(orderId) { return cancelOrder(this, orderId); }
256
+ async closePosition(accountId, symbol) { return closePosition(this, accountId, symbol); }
269
257
 
270
- const handler = (msg) => {
271
- if (msg.templateId === 114) { // ResponseFrontMonthContract
272
- const decoded = decodeFrontMonthContract(msg.data);
273
- const baseSymbol = decoded.userMsg;
274
-
275
- if (pending.has(baseSymbol)) {
276
- pending.delete(baseSymbol);
277
-
278
- if (decoded.rpCode[0] === '0' && decoded.tradingSymbol) {
279
- results.set(baseSymbol, {
280
- symbol: decoded.tradingSymbol,
281
- baseSymbol: baseSymbol,
282
- exchange: decoded.exchange,
283
- });
284
- }
285
- }
286
- }
287
- };
258
+ // ==================== STUBS ====================
288
259
 
289
- this.tickerConn.on('message', handler);
260
+ async getUser() { return this.user; }
261
+ async getLifetimeStats() { return { success: true, stats: null }; }
262
+ async getDailyStats() { return { success: true, stats: [] }; }
263
+ async getTradeHistory() { return { success: true, trades: [] }; }
290
264
 
291
- // Send all requests
292
- for (const product of products) {
293
- this.tickerConn.send('RequestFrontMonthContract', {
294
- templateId: 113,
295
- userMsg: [product.productCode],
296
- symbol: product.productCode,
297
- exchange: product.exchange,
298
- });
299
- }
265
+ async getMarketStatus() {
266
+ const status = this.checkMarketHours();
267
+ return { success: true, isOpen: status.isOpen, message: status.message };
268
+ }
300
269
 
301
- // Wait for responses (with timeout)
302
- setTimeout(() => {
303
- this.tickerConn.removeListener('message', handler);
304
- resolve(Array.from(results.values()));
305
- }, 5000);
306
- });
270
+ getToken() { return this.loginInfo ? 'connected' : null; }
271
+ getPropfirm() { return this.propfirmKey || 'apex'; }
272
+
273
+ getRithmicCredentials() {
274
+ if (!this.credentials) return null;
275
+ return {
276
+ userId: this.credentials.username,
277
+ password: this.credentials.password,
278
+ systemName: this.propfirm.systemName,
279
+ gateway: this.propfirm.gateway || RITHMIC_ENDPOINTS.CHICAGO,
280
+ };
307
281
  }
308
282
 
283
+ // ==================== CONTRACTS ====================
284
+
309
285
  /**
310
- * Decode ProductCodes response (template 112) - field IDs from Rithmic API
286
+ * Get all available contracts from Rithmic API
287
+ * @returns {Promise<{success: boolean, contracts: Array, source?: string, error?: string}>}
311
288
  */
312
- decodeProductCodesResponse(buffer) {
313
- const result = {};
314
- let offset = 0;
289
+ async getContracts() {
290
+ // Check cache
291
+ if (this._contractsCache && Date.now() - this._contractsCacheTime < CACHE.CONTRACTS_TTL) {
292
+ return { success: true, contracts: this._contractsCache, source: 'cache' };
293
+ }
315
294
 
316
- while (offset < buffer.length) {
317
- try {
318
- const [tag, tagOffset] = readVarint(buffer, offset);
319
- const wireType = tag & 0x7;
320
- const fieldNumber = tag >>> 3;
321
- offset = tagOffset;
295
+ if (!this.credentials) {
296
+ return { success: false, error: 'Not logged in' };
297
+ }
322
298
 
323
- if (wireType === 0) {
324
- const [val, newOff] = readVarint(buffer, offset);
325
- offset = newOff;
326
- if (fieldNumber === 154467) result.templateId = val;
327
- } else if (wireType === 2) {
328
- const [val, newOff] = readLengthDelimited(buffer, offset);
329
- offset = newOff;
330
- if (fieldNumber === 110101) result.exchange = val; // exchange
331
- if (fieldNumber === 100749) result.productCode = val; // product_code (base symbol)
332
- if (fieldNumber === 100003) result.productName = val; // symbol_name
333
- if (fieldNumber === 132760) result.userMsg = val; // user_msg
334
- if (fieldNumber === 132764) result.rpCode = val; // rp_code
335
- } else {
336
- offset = skipField(buffer, offset, wireType);
299
+ try {
300
+ // Connect to TICKER_PLANT if needed
301
+ if (!this.tickerConn) {
302
+ const connected = await this.connectTicker(this.credentials.username, this.credentials.password);
303
+ if (!connected) {
304
+ return { success: false, error: 'Failed to connect to TICKER_PLANT' };
337
305
  }
338
- } catch (e) { break; }
306
+ }
307
+
308
+ this.tickerConn.setMaxListeners(5000);
309
+
310
+ log.debug('Fetching contracts from Rithmic API');
311
+ const contracts = await this._fetchAllFrontMonths();
312
+
313
+ if (!contracts.length) {
314
+ return { success: false, error: 'No tradeable contracts found' };
315
+ }
316
+
317
+ // Cache results
318
+ this._contractsCache = contracts;
319
+ this._contractsCacheTime = Date.now();
320
+
321
+ return { success: true, contracts, source: 'api' };
322
+ } catch (err) {
323
+ log.error('getContracts error', { error: err.message });
324
+ return { success: false, error: err.message };
339
325
  }
326
+ }
340
327
 
341
- return result;
328
+ /**
329
+ * Search contracts
330
+ * @param {string} searchText - Search text
331
+ * @returns {Promise<Array>}
332
+ */
333
+ async searchContracts(searchText) {
334
+ const result = await this.getContracts();
335
+ if (!searchText || !result.success) return result.contracts || [];
336
+
337
+ const search = searchText.toUpperCase();
338
+ return result.contracts.filter(c =>
339
+ c.symbol.toUpperCase().includes(search) ||
340
+ c.name.toUpperCase().includes(search)
341
+ );
342
342
  }
343
343
 
344
344
  /**
345
- * Get all tradeable futures from Rithmic API - 100% REAL DATA
346
- * Sends RequestFrontMonthContract for known futures and gets real trading symbols
345
+ * Fetch all front month contracts from API
346
+ * @private
347
347
  */
348
- async fetchAllFrontMonths() {
348
+ async _fetchAllFrontMonths() {
349
349
  if (!this.tickerConn) {
350
350
  throw new Error('TICKER_PLANT not connected');
351
351
  }
352
352
 
353
- // Request front months for ALL products that might be futures
354
- // The API will return the current trading symbol for each
355
353
  return new Promise((resolve) => {
356
354
  const contracts = new Map();
357
- const productsToCheck = new Map(); // Will collect from ProductCodes
355
+ const productsToCheck = new Map();
358
356
 
359
357
  // Handler for ProductCodes responses
360
358
  const productHandler = (msg) => {
361
- if (msg.templateId === 112) {
362
- const decoded = this.decodeProductCodesResponse(msg.data);
363
- if (!decoded.productCode || !decoded.exchange) return;
364
-
365
- // Only main futures exchanges
366
- const validExchanges = ['CME', 'CBOT', 'NYMEX', 'COMEX', 'NYBOT', 'CFE'];
367
- if (!validExchanges.includes(decoded.exchange)) return;
368
-
369
- // Skip obvious non-futures
370
- const name = (decoded.productName || '').toLowerCase();
371
- if (name.includes('option')) return;
372
- if (name.includes('swap')) return;
373
- if (name.includes('spread')) return;
374
-
375
- const key = `${decoded.productCode}:${decoded.exchange}`;
376
- if (!productsToCheck.has(key)) {
377
- productsToCheck.set(key, {
378
- productCode: decoded.productCode,
379
- productName: decoded.productName || decoded.productCode,
380
- exchange: decoded.exchange,
381
- });
382
- }
359
+ if (msg.templateId !== 112) return;
360
+
361
+ const decoded = this._decodeProductCodes(msg.data);
362
+ if (!decoded.productCode || !decoded.exchange) return;
363
+
364
+ const validExchanges = ['CME', 'CBOT', 'NYMEX', 'COMEX', 'NYBOT', 'CFE'];
365
+ if (!validExchanges.includes(decoded.exchange)) return;
366
+
367
+ const name = (decoded.productName || '').toLowerCase();
368
+ if (name.includes('option') || name.includes('swap') || name.includes('spread')) return;
369
+
370
+ const key = `${decoded.productCode}:${decoded.exchange}`;
371
+ if (!productsToCheck.has(key)) {
372
+ productsToCheck.set(key, {
373
+ productCode: decoded.productCode,
374
+ productName: decoded.productName || decoded.productCode,
375
+ exchange: decoded.exchange,
376
+ });
383
377
  }
384
378
  };
385
379
 
386
380
  // Handler for FrontMonth responses
387
381
  const frontMonthHandler = (msg) => {
388
- if (msg.templateId === 114) {
389
- const decoded = decodeFrontMonthContract(msg.data);
390
- if (decoded.rpCode[0] === '0' && decoded.tradingSymbol) {
391
- contracts.set(decoded.userMsg, {
392
- symbol: decoded.tradingSymbol,
393
- baseSymbol: decoded.userMsg,
394
- exchange: decoded.exchange,
395
- });
396
- }
382
+ if (msg.templateId !== 114) return;
383
+
384
+ const decoded = decodeFrontMonthContract(msg.data);
385
+ if (decoded.rpCode[0] === '0' && decoded.tradingSymbol) {
386
+ contracts.set(decoded.userMsg, {
387
+ symbol: decoded.tradingSymbol,
388
+ baseSymbol: decoded.userMsg,
389
+ exchange: decoded.exchange,
390
+ });
397
391
  }
398
392
  };
399
393
 
400
394
  this.tickerConn.on('message', productHandler);
401
395
  this.tickerConn.on('message', frontMonthHandler);
402
396
 
403
- // Step 1: Get all product codes
397
+ // Request all product codes
404
398
  this.tickerConn.send('RequestProductCodes', {
405
399
  templateId: 111,
406
400
  userMsg: ['get-products'],
407
401
  });
408
402
 
409
- // After 5 seconds, request front months for all collected products
403
+ // After timeout, request front months
410
404
  setTimeout(() => {
411
405
  this.tickerConn.removeListener('message', productHandler);
412
- debug(`Collected ${productsToCheck.size} products, requesting front months...`);
406
+ log.debug('Collected products', { count: productsToCheck.size });
413
407
 
414
- // Send front month requests for all products
415
- for (const [key, product] of productsToCheck) {
408
+ for (const product of productsToCheck.values()) {
416
409
  this.tickerConn.send('RequestFrontMonthContract', {
417
410
  templateId: 113,
418
411
  userMsg: [product.productCode],
@@ -421,20 +414,19 @@ class RithmicService extends EventEmitter {
421
414
  });
422
415
  }
423
416
 
424
- // After another 5 seconds, collect results
417
+ // Collect results after timeout
425
418
  setTimeout(() => {
426
419
  this.tickerConn.removeListener('message', frontMonthHandler);
427
-
428
- // Merge with product names and add category info
420
+
429
421
  const results = [];
430
422
  for (const [baseSymbol, contract] of contracts) {
431
423
  const productKey = `${baseSymbol}:${contract.exchange}`;
432
424
  const product = productsToCheck.get(productKey);
433
425
  const symbolInfo = getSymbolInfo(baseSymbol);
434
-
426
+
435
427
  results.push({
436
428
  symbol: contract.symbol,
437
- baseSymbol: baseSymbol,
429
+ baseSymbol,
438
430
  name: symbolInfo.name || product?.productName || baseSymbol,
439
431
  exchange: contract.exchange,
440
432
  category: symbolInfo.category,
@@ -445,7 +437,7 @@ class RithmicService extends EventEmitter {
445
437
  });
446
438
  }
447
439
 
448
- // Sort by category order, then by symbol name within category
440
+ // Sort by category order
449
441
  results.sort((a, b) => {
450
442
  if (a.categoryOrder !== b.categoryOrder) {
451
443
  return a.categoryOrder - b.categoryOrder;
@@ -453,72 +445,72 @@ class RithmicService extends EventEmitter {
453
445
  return a.baseSymbol.localeCompare(b.baseSymbol);
454
446
  });
455
447
 
456
- debug(`Got ${results.length} tradeable contracts from API`);
448
+ log.debug('Got contracts from API', { count: results.length });
457
449
  resolve(results);
458
- }, 8000);
459
- }, 5000);
450
+ }, TIMEOUTS.RITHMIC_PRODUCTS);
451
+ }, TIMEOUTS.RITHMIC_CONTRACTS);
460
452
  });
461
453
  }
462
454
 
463
455
  /**
464
- * Get all available contracts - 100% REAL DATA from Rithmic API
465
- * Fetches product codes and front months directly from the API
456
+ * Decode ProductCodes response
457
+ * @private
466
458
  */
467
- async getContracts() {
468
- // Return cached if available and fresh (5 min cache)
469
- if (this.cachedContracts && this.cachedContracts.timestamp > Date.now() - 300000) {
470
- return { success: true, contracts: this.cachedContracts.data, source: 'cache' };
471
- }
472
-
473
- // Need credentials to fetch real data
474
- if (!this.credentials) {
475
- return { success: false, error: 'Not logged in - cannot fetch contracts from API' };
476
- }
459
+ _decodeProductCodes(buffer) {
460
+ const result = {};
461
+ let offset = 0;
477
462
 
478
- try {
479
- // Connect to TICKER_PLANT if not connected
480
- if (!this.tickerConn) {
481
- const connected = await this.connectTicker(this.credentials.username, this.credentials.password);
482
- if (!connected) {
483
- return { success: false, error: 'Failed to connect to TICKER_PLANT' };
484
- }
463
+ const readVarint = (buf, off) => {
464
+ let value = 0;
465
+ let shift = 0;
466
+ while (off < buf.length) {
467
+ const byte = buf[off++];
468
+ value |= (byte & 0x7F) << shift;
469
+ if (!(byte & 0x80)) break;
470
+ shift += 7;
485
471
  }
472
+ return [value, off];
473
+ };
486
474
 
487
- // Increase max listeners to avoid warnings
488
- this.tickerConn.setMaxListeners(5000);
475
+ const readString = (buf, off) => {
476
+ const [len, newOff] = readVarint(buf, off);
477
+ return [buf.slice(newOff, newOff + len).toString('utf8'), newOff + len];
478
+ };
489
479
 
490
- debug('Fetching all tradeable contracts from Rithmic API...');
491
- const contracts = await this.fetchAllFrontMonths();
480
+ while (offset < buffer.length) {
481
+ try {
482
+ const [tag, tagOff] = readVarint(buffer, offset);
483
+ const wireType = tag & 0x7;
484
+ const fieldNumber = tag >>> 3;
485
+ offset = tagOff;
492
486
 
493
- if (contracts.length === 0) {
494
- return { success: false, error: 'No tradeable contracts found' };
487
+ if (wireType === 0) {
488
+ const [, newOff] = readVarint(buffer, offset);
489
+ offset = newOff;
490
+ } else if (wireType === 2) {
491
+ const [val, newOff] = readString(buffer, offset);
492
+ offset = newOff;
493
+ if (fieldNumber === 110101) result.exchange = val;
494
+ if (fieldNumber === 100749) result.productCode = val;
495
+ if (fieldNumber === 100003) result.productName = val;
496
+ } else {
497
+ break;
498
+ }
499
+ } catch {
500
+ break;
495
501
  }
496
-
497
- // Cache the results
498
- this.cachedContracts = { data: contracts, timestamp: Date.now() };
499
- return { success: true, contracts, source: 'api' };
500
-
501
- } catch (e) {
502
- debug('getContracts error:', e.message);
503
- return { success: false, error: e.message };
504
502
  }
505
- }
506
503
 
507
- async searchContracts(searchText) {
508
- const result = await this.getContracts();
509
- if (!searchText || !result.success) return result.contracts || [];
510
- const search = searchText.toUpperCase();
511
- return result.contracts.filter(c =>
512
- c.symbol.toUpperCase().includes(search) ||
513
- c.name.toUpperCase().includes(search)
514
- );
504
+ return result;
515
505
  }
516
506
 
507
+ // ==================== MARKET HOURS ====================
508
+
517
509
  checkMarketHours() {
518
510
  const now = new Date();
519
511
  const utcDay = now.getUTCDay();
520
512
  const utcHour = now.getUTCHours();
521
-
513
+
522
514
  const isDST = now.getTimezoneOffset() < Math.max(
523
515
  new Date(now.getFullYear(), 0, 1).getTimezoneOffset(),
524
516
  new Date(now.getFullYear(), 6, 1).getTimezoneOffset()
@@ -531,22 +523,31 @@ class RithmicService extends EventEmitter {
531
523
  if (ctDay === 0 && ctHour < 17) return { isOpen: false, message: 'Market opens Sunday 5:00 PM CT' };
532
524
  if (ctDay === 5 && ctHour >= 16) return { isOpen: false, message: 'Market closed (Friday after 4PM CT)' };
533
525
  if (ctHour === 16 && ctDay >= 1 && ctDay <= 4) return { isOpen: false, message: 'Daily maintenance (4:00-5:00 PM CT)' };
526
+
534
527
  return { isOpen: true, message: 'Market is open' };
535
528
  }
536
529
 
530
+ // ==================== CLEANUP ====================
531
+
532
+ /**
533
+ * Disconnect all connections
534
+ */
537
535
  async disconnect() {
538
- if (this.orderConn) {
539
- await this.orderConn.disconnect();
540
- this.orderConn = null;
541
- }
542
- if (this.pnlConn) {
543
- await this.pnlConn.disconnect();
544
- this.pnlConn = null;
545
- }
546
- if (this.tickerConn) {
547
- await this.tickerConn.disconnect();
548
- this.tickerConn = null;
536
+ const connections = [this.orderConn, this.pnlConn, this.tickerConn];
537
+
538
+ for (const conn of connections) {
539
+ if (conn) {
540
+ try {
541
+ await conn.disconnect();
542
+ } catch (err) {
543
+ log.warn('Disconnect error', { error: err.message });
544
+ }
545
+ }
549
546
  }
547
+
548
+ this.orderConn = null;
549
+ this.pnlConn = null;
550
+ this.tickerConn = null;
550
551
  this.accounts = [];
551
552
  this.accountPnL.clear();
552
553
  this.positions.clear();
@@ -554,7 +555,9 @@ class RithmicService extends EventEmitter {
554
555
  this.loginInfo = null;
555
556
  this.user = null;
556
557
  this.credentials = null;
557
- this.cachedContracts = null;
558
+ this._contractsCache = null;
559
+
560
+ log.info('Disconnected');
558
561
  }
559
562
  }
560
563