hedgequantx 1.8.31 → 1.8.33

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": "1.8.31",
3
+ "version": "1.8.33",
4
4
  "description": "Prop Futures Algo Trading CLI - Connect to Topstep, Alpha Futures, and other prop firms",
5
5
  "main": "src/app.js",
6
6
  "bin": {
@@ -196,7 +196,7 @@ const PROPFIRMS = {
196
196
  platform: 'Rithmic',
197
197
  rithmicSystem: 'Apex',
198
198
  wsEndpoint: 'wss://ritpa11120.11.rithmic.com:443',
199
- defaultStartingBalance: 300000
199
+
200
200
  },
201
201
  topsteptrader: {
202
202
  id: 'topsteptrader',
@@ -72,7 +72,7 @@ const copyTradingMenu = async () => {
72
72
  // Step 1: Select Lead Account
73
73
  console.log(chalk.cyan(' Step 1: Select LEAD Account'));
74
74
  const leadOptions = allAccounts.map((a, i) => ({
75
- label: `${a.propfirm} - ${a.account.accountName || a.account.accountId} ($${a.account.balance.toLocaleString()})`,
75
+ label: `${a.propfirm} - ${a.account.accountName || a.account.accountId}${a.account.balance !== null ? ` ($${a.account.balance.toLocaleString()})` : ''}`,
76
76
  value: i
77
77
  }));
78
78
  leadOptions.push({ label: '< Cancel', value: -1 });
@@ -88,7 +88,7 @@ const copyTradingMenu = async () => {
88
88
  .map((a, i) => ({ a, i }))
89
89
  .filter(x => x.i !== leadIdx)
90
90
  .map(x => ({
91
- label: `${x.a.propfirm} - ${x.a.account.accountName || x.a.account.accountId} ($${x.a.account.balance.toLocaleString()})`,
91
+ label: `${x.a.propfirm} - ${x.a.account.accountName || x.a.account.accountId}${x.a.account.balance !== null ? ` ($${x.a.account.balance.toLocaleString()})` : ''}`,
92
92
  value: x.i
93
93
  }));
94
94
  followerOptions.push({ label: '< Cancel', value: -1 });
@@ -49,7 +49,7 @@ const oneAccountMenu = async (service) => {
49
49
 
50
50
  // Select account
51
51
  const options = activeAccounts.map(acc => ({
52
- label: `${acc.accountName || acc.accountId} (${acc.propfirm || 'Unknown'}) - $${(acc.balance || 0).toLocaleString()}`,
52
+ label: `${acc.accountName || acc.accountId} (${acc.propfirm || 'Unknown'})${acc.balance !== null ? ` - $${acc.balance.toLocaleString()}` : ''}`,
53
53
  value: acc
54
54
  }));
55
55
  options.push({ label: '< Back', value: 'back' });
@@ -183,26 +183,17 @@ const launchAlgo = async (service, account, contract, config) => {
183
183
  if (!showName && account.accountName) msg = msg.replace(new RegExp(account.accountName, 'gi'), 'HQX *****');
184
184
  ui.addLog(d.type || 'info', msg);
185
185
  });
186
- hqx.on('signal', (d) => {
187
- stats.signals = (stats.signals || 0) + 1;
188
- ui.addLog('signal', `${d.side === 'long' ? 'BUY' : 'SELL'} @ ${d.entry?.toFixed(2) || 'MKT'}`);
189
- });
190
- hqx.on('trade', (d) => {
191
- stats.trades++;
192
- stats.pnl += d.pnl || 0;
193
- if (d.pnl >= 0) { stats.wins++; ui.addLog('trade', `+$${d.pnl.toFixed(2)}`); }
194
- else { stats.losses++; ui.addLog('loss', `-$${Math.abs(d.pnl).toFixed(2)}`); }
195
-
196
- if (stats.pnl >= dailyTarget) {
197
- stopReason = 'target'; running = false;
198
- ui.addLog('success', `TARGET! +$${stats.pnl.toFixed(2)}`);
199
- hqx.stopAlgo();
200
- } else if (stats.pnl <= -maxRisk) {
201
- stopReason = 'risk'; running = false;
202
- ui.addLog('error', `MAX RISK! -$${Math.abs(stats.pnl).toFixed(2)}`);
203
- hqx.stopAlgo();
186
+
187
+ // REAL P&L direct from Rithmic - no calculation
188
+ hqx.on('stats', (d) => {
189
+ if (d.realTimePnL) {
190
+ stats.pnl = d.realTimePnL.totalPnL;
204
191
  }
192
+ stats.trades = d.trades;
193
+ stats.wins = d.wins;
194
+ stats.losses = d.losses;
205
195
  });
196
+
206
197
  hqx.on('error', (d) => { ui.addLog('error', d.message || 'Error'); });
207
198
  hqx.on('disconnected', () => { stats.connected = false; ui.addLog('warning', 'Disconnected'); });
208
199
 
@@ -128,8 +128,9 @@ class AlgoUI {
128
128
  const { W } = this;
129
129
  const isCopyTrading = this.config.mode === 'copy-trading';
130
130
 
131
- const pnlColor = stats.pnl >= 0 ? chalk.green : chalk.red;
132
- const pnlStr = (stats.pnl >= 0 ? '+$' : '-$') + Math.abs(stats.pnl).toFixed(2);
131
+ const pnl = stats.pnl !== null && stats.pnl !== undefined ? stats.pnl : null;
132
+ const pnlColor = pnl === null ? chalk.gray : (pnl >= 0 ? chalk.green : chalk.red);
133
+ const pnlStr = pnl === null ? '--' : ((pnl >= 0 ? '+$' : '-$') + Math.abs(pnl).toFixed(2));
133
134
  const latencyColor = stats.latency < 100 ? chalk.green : (stats.latency < 300 ? chalk.yellow : chalk.red);
134
135
  const serverColor = stats.connected ? chalk.green : chalk.red;
135
136
 
@@ -172,8 +173,10 @@ class AlgoUI {
172
173
  this._line(chalk.cyan(GM));
173
174
 
174
175
  // Row 3: Target | Risk
175
- const r3c1 = buildCell('Target', '$' + (stats.target || 0).toFixed(2), chalk.green, colL);
176
- const r3c2 = buildCell('Risk', '$' + (stats.risk || 0).toFixed(2), chalk.red, colR);
176
+ const targetStr = stats.target !== null && stats.target !== undefined ? '$' + stats.target.toFixed(2) : '--';
177
+ const riskStr = stats.risk !== null && stats.risk !== undefined ? '$' + stats.risk.toFixed(2) : '--';
178
+ const r3c1 = buildCell('Target', targetStr, chalk.green, colL);
179
+ const r3c2 = buildCell('Risk', riskStr, chalk.red, colR);
177
180
  row(r3c1.padded, r3c2.padded);
178
181
 
179
182
  this._line(chalk.cyan(GM));
@@ -65,10 +65,9 @@ const showStats = async (service) => {
65
65
  return;
66
66
  }
67
67
 
68
- // Collect stats
69
- let totalBalance = 0;
70
- let totalStartingBalance = 0;
71
- let totalPnL = 0;
68
+ // Collect REAL stats only - no estimation
69
+ let totalBalance = null;
70
+ let totalPnL = null;
72
71
  let allTrades = [];
73
72
  let totalOpenPositions = 0;
74
73
  let totalOpenOrders = 0;
@@ -78,27 +77,15 @@ const showStats = async (service) => {
78
77
 
79
78
  for (const account of activeAccounts) {
80
79
  const svc = account.service;
81
- const currentBalance = account.balance || 0;
82
- totalBalance += currentBalance;
83
80
 
84
- // Estimate starting balance
85
- let startingBalance = account.startingBalance || account.initialBalance || 0;
86
- if (!startingBalance) {
87
- const name = (account.accountName || '').toUpperCase();
88
- if (name.includes('150K') || name.includes('150')) startingBalance = 150000;
89
- else if (name.includes('100K') || name.includes('100')) startingBalance = 100000;
90
- else if (name.includes('50K') || name.includes('50')) startingBalance = 50000;
91
- else if (currentBalance >= 140000) startingBalance = 150000;
92
- else if (currentBalance >= 90000) startingBalance = 100000;
93
- else if (currentBalance >= 45000) startingBalance = 50000;
94
- else startingBalance = currentBalance;
81
+ // Only add REAL balance from API
82
+ if (account.balance !== null && account.balance !== undefined) {
83
+ totalBalance = (totalBalance || 0) + account.balance;
95
84
  }
96
85
 
97
- totalStartingBalance += startingBalance;
98
- account.startingBalance = startingBalance;
99
-
100
- if (account.profitAndLoss !== undefined) {
101
- totalPnL += account.profitAndLoss;
86
+ // Only add REAL P&L from API
87
+ if (account.profitAndLoss !== null && account.profitAndLoss !== undefined) {
88
+ totalPnL = (totalPnL || 0) + account.profitAndLoss;
102
89
  }
103
90
 
104
91
  // Positions & Orders
@@ -138,9 +125,7 @@ const showStats = async (service) => {
138
125
  }
139
126
  }
140
127
 
141
- if (totalPnL === 0 && totalStartingBalance > 0) {
142
- totalPnL = totalBalance - totalStartingBalance;
143
- }
128
+ // NO estimation - only real data from API
144
129
 
145
130
  // Aggregate stats
146
131
  let stats = {
@@ -34,6 +34,7 @@ const hashAccountId = (str) => {
34
34
  */
35
35
  const fetchAccounts = async (service) => {
36
36
  if (!service.orderConn || !service.loginInfo) {
37
+ debug('fetchAccounts: no connection or loginInfo');
37
38
  return [];
38
39
  }
39
40
 
@@ -41,29 +42,39 @@ const fetchAccounts = async (service) => {
41
42
  const accounts = [];
42
43
 
43
44
  const timeout = setTimeout(() => {
45
+ debug('fetchAccounts: timeout, found', accounts.length, 'accounts');
44
46
  service.accounts = accounts;
45
47
  resolve(accounts);
46
- }, 2000);
48
+ }, 5000);
47
49
 
48
- service.once('accountReceived', (account) => {
50
+ // Listen for ALL accounts (not just once)
51
+ const onAccount = (account) => {
52
+ debug('fetchAccounts: received account', account.accountId);
49
53
  accounts.push(account);
50
- });
54
+ };
55
+ service.on('accountReceived', onAccount);
51
56
 
52
57
  service.once('accountListComplete', () => {
58
+ debug('fetchAccounts: complete, found', accounts.length, 'accounts');
53
59
  clearTimeout(timeout);
60
+ service.removeListener('accountReceived', onAccount);
54
61
  service.accounts = accounts;
55
62
  resolve(accounts);
56
63
  });
57
64
 
58
65
  try {
66
+ debug('fetchAccounts: sending RequestAccountList');
59
67
  service.orderConn.send('RequestAccountList', {
60
68
  templateId: REQ.ACCOUNT_LIST,
61
69
  userMsg: ['HQX'],
62
70
  fcmId: service.loginInfo.fcmId,
63
71
  ibId: service.loginInfo.ibId,
72
+ userType: 3, // USER_TYPE_TRADER - required by Rithmic API
64
73
  });
65
74
  } catch (e) {
75
+ debug('fetchAccounts: error', e.message);
66
76
  clearTimeout(timeout);
77
+ service.removeListener('accountReceived', onAccount);
67
78
  resolve([]);
68
79
  }
69
80
  });
@@ -98,36 +109,19 @@ const getTradingAccounts = async (service) => {
98
109
  debug(`Account ${acc.accountId} pnlData:`, JSON.stringify(pnlData));
99
110
  debug(` accountPnL map size:`, service.accountPnL.size);
100
111
 
101
- // Use API values if available
102
- const accountBalance = parseFloat(pnlData.accountBalance || 0);
103
- const openPnL = parseFloat(pnlData.openPositionPnl || 0);
104
- const closedPnL = parseFloat(pnlData.closedPositionPnl || 0);
105
- const dayPnL = parseFloat(pnlData.dayPnl || 0);
106
-
107
- // Balance: use API value if > 0, otherwise default
108
- // Most prop firms don't report balance via PnL stream, so we use default
109
- const startingBalance = service.propfirm.defaultBalance;
110
- const balance = accountBalance > 0 ? accountBalance : startingBalance;
111
-
112
- // P&L: prefer dayPnl from API, otherwise calculate from open+closed
113
- let profitAndLoss = 0;
114
- if (dayPnL !== 0) {
115
- profitAndLoss = dayPnL;
116
- } else if (openPnL !== 0 || closedPnL !== 0) {
117
- profitAndLoss = openPnL + closedPnL;
118
- }
119
- // Don't calculate P&L from balance difference - that's estimation
120
-
121
- debug(` balance: ${balance}, startingBalance: ${startingBalance}, P&L: ${profitAndLoss}`);
112
+ // REAL DATA FROM RITHMIC ONLY - NO DEFAULTS
113
+ const accountBalance = pnlData.accountBalance ? parseFloat(pnlData.accountBalance) : null;
114
+ const openPnL = pnlData.openPositionPnl ? parseFloat(pnlData.openPositionPnl) : null;
115
+ const closedPnL = pnlData.closedPositionPnl ? parseFloat(pnlData.closedPositionPnl) : null;
116
+ const dayPnL = pnlData.dayPnl ? parseFloat(pnlData.dayPnl) : null;
122
117
 
123
118
  return {
124
119
  accountId: hashAccountId(acc.accountId),
125
120
  rithmicAccountId: acc.accountId,
126
121
  accountName: acc.accountName || acc.accountId,
127
122
  name: acc.accountName || acc.accountId,
128
- balance: balance,
129
- startingBalance: startingBalance,
130
- profitAndLoss: profitAndLoss,
123
+ balance: accountBalance,
124
+ profitAndLoss: dayPnL !== null ? dayPnL : (openPnL !== null || closedPnL !== null ? (openPnL || 0) + (closedPnL || 0) : null),
131
125
  openPnL: openPnL,
132
126
  todayPnL: closedPnL,
133
127
  status: 0,
@@ -136,24 +130,7 @@ const getTradingAccounts = async (service) => {
136
130
  };
137
131
  });
138
132
 
139
- // Fallback if no accounts
140
- if (tradingAccounts.length === 0 && service.user) {
141
- const userName = service.user.userName || 'Unknown';
142
- tradingAccounts = [{
143
- accountId: hashAccountId(userName),
144
- rithmicAccountId: userName,
145
- accountName: userName,
146
- name: userName,
147
- balance: service.propfirm.defaultBalance,
148
- startingBalance: service.propfirm.defaultBalance,
149
- profitAndLoss: 0,
150
- openPnL: 0,
151
- todayPnL: 0,
152
- status: 0,
153
- platform: 'Rithmic',
154
- propfirm: service.propfirm.name,
155
- }];
156
- }
133
+ // No fallback - only real accounts from Rithmic
157
134
 
158
135
  return { success: true, accounts: tradingAccounts };
159
136
  };
@@ -58,6 +58,8 @@ const REQ = {
58
58
  SYSTEM_INFO: 16,
59
59
  HEARTBEAT: 18,
60
60
  MARKET_DATA: 100,
61
+ PRODUCT_CODES: 111,
62
+ FRONT_MONTH_CONTRACT: 113,
61
63
  LOGIN_INFO: 300,
62
64
  ACCOUNT_LIST: 302,
63
65
  ACCOUNT_RMS: 304,
@@ -84,6 +86,8 @@ const RES = {
84
86
  SYSTEM_INFO: 17,
85
87
  HEARTBEAT: 19,
86
88
  MARKET_DATA: 101,
89
+ PRODUCT_CODES: 112,
90
+ FRONT_MONTH_CONTRACT: 114,
87
91
  LOGIN_INFO: 301,
88
92
  ACCOUNT_LIST: 303,
89
93
  ACCOUNT_RMS: 305,
@@ -155,6 +159,10 @@ const PROTO_FILES = [
155
159
  'response_pnl_position_updates.proto',
156
160
  'account_pnl_position_update.proto',
157
161
  'instrument_pnl_position_update.proto',
162
+ 'request_product_codes.proto',
163
+ 'response_product_codes.proto',
164
+ 'request_front_month_contract.proto',
165
+ 'response_front_month_contract.proto',
158
166
  ];
159
167
 
160
168
  module.exports = {
@@ -17,12 +17,16 @@ const debug = (...args) => DEBUG && console.log('[Rithmic:Handler]', ...args);
17
17
  const createOrderHandler = (service) => {
18
18
  return (msg) => {
19
19
  const { templateId, data } = msg;
20
+
21
+ debug('ORDER_PLANT message received, templateId:', templateId);
20
22
 
21
23
  switch (templateId) {
22
24
  case RES.LOGIN_INFO:
25
+ debug('Handling LOGIN_INFO');
23
26
  handleLoginInfo(service, data);
24
27
  break;
25
28
  case RES.ACCOUNT_LIST:
29
+ debug('Handling ACCOUNT_LIST (303)');
26
30
  handleAccountList(service, data);
27
31
  break;
28
32
  case RES.TRADE_ROUTES:
@@ -93,10 +97,13 @@ const handleLoginInfo = (service, data) => {
93
97
  */
94
98
  const handleAccountList = (service, data) => {
95
99
  try {
100
+ debug('Decoding ResponseAccountList...');
96
101
  const res = proto.decode('ResponseAccountList', data);
102
+ debug('Decoded account list response:', JSON.stringify(res));
97
103
 
98
104
  if (res.rpCode?.[0] === '0') {
99
105
  // End of list
106
+ debug('Account list complete signal received');
100
107
  service.emit('accountListComplete');
101
108
  } else if (res.accountId) {
102
109
  const account = {
@@ -106,11 +113,14 @@ const handleAccountList = (service, data) => {
106
113
  accountName: res.accountName,
107
114
  accountCurrency: res.accountCurrency,
108
115
  };
116
+ debug('Account received:', account.accountId);
109
117
  service.accounts.push(account);
110
118
  service.emit('accountReceived', account);
119
+ } else {
120
+ debug('No accountId and no rpCode[0]=0, raw response:', res);
111
121
  }
112
122
  } catch (e) {
113
- // Ignore decode errors
123
+ debug('Error decoding account list:', e.message);
114
124
  }
115
125
  };
116
126
 
@@ -9,29 +9,67 @@ const { RITHMIC_ENDPOINTS, RITHMIC_SYSTEMS } = require('./constants');
9
9
  const { createOrderHandler, createPnLHandler } = require('./handlers');
10
10
  const { fetchAccounts, getTradingAccounts, requestPnLSnapshot, subscribePnLUpdates, getPositions, hashAccountId } = require('./accounts');
11
11
  const { placeOrder, cancelOrder, getOrders, getOrderHistory, closePosition } = require('./orders');
12
+ const { decodeFrontMonthContract } = require('./protobuf');
12
13
 
13
14
  // Debug mode
14
15
  const DEBUG = process.env.HQX_DEBUG === '1';
15
16
  const debug = (...args) => DEBUG && console.log('[Rithmic:Service]', ...args);
16
17
 
17
- // PropFirm configurations
18
+ // Base symbols for futures contracts
19
+ const BASE_SYMBOLS = {
20
+ // Equity Index (Quarterly: H, M, U, Z)
21
+ quarterly: [
22
+ { base: 'ES', name: 'E-mini S&P 500', exchange: 'CME', category: 'Index' },
23
+ { base: 'NQ', name: 'E-mini NASDAQ-100', exchange: 'CME', category: 'Index' },
24
+ { base: 'YM', name: 'E-mini Dow Jones', exchange: 'CBOT', category: 'Index' },
25
+ { base: 'RTY', name: 'E-mini Russell 2000', exchange: 'CME', category: 'Index' },
26
+ { base: 'MES', name: 'Micro E-mini S&P 500', exchange: 'CME', category: 'Micro Index' },
27
+ { base: 'MNQ', name: 'Micro E-mini NASDAQ-100', exchange: 'CME', category: 'Micro Index' },
28
+ { base: 'MYM', name: 'Micro E-mini Dow Jones', exchange: 'CBOT', category: 'Micro Index' },
29
+ { base: 'M2K', name: 'Micro E-mini Russell 2000', exchange: 'CME', category: 'Micro Index' },
30
+ // Currencies (Quarterly)
31
+ { base: '6E', name: 'Euro FX', exchange: 'CME', category: 'Currency' },
32
+ { base: 'M6E', name: 'Micro Euro FX', exchange: 'CME', category: 'Currency' },
33
+ { base: '6B', name: 'British Pound', exchange: 'CME', category: 'Currency' },
34
+ { base: '6J', name: 'Japanese Yen', exchange: 'CME', category: 'Currency' },
35
+ { base: '6A', name: 'Australian Dollar', exchange: 'CME', category: 'Currency' },
36
+ { base: '6C', name: 'Canadian Dollar', exchange: 'CME', category: 'Currency' },
37
+ // Bonds (Quarterly)
38
+ { base: 'ZB', name: '30-Year T-Bond', exchange: 'CBOT', category: 'Bonds' },
39
+ { base: 'ZN', name: '10-Year T-Note', exchange: 'CBOT', category: 'Bonds' },
40
+ { base: 'ZF', name: '5-Year T-Note', exchange: 'CBOT', category: 'Bonds' },
41
+ { base: 'ZT', name: '2-Year T-Note', exchange: 'CBOT', category: 'Bonds' },
42
+ ],
43
+ // Energy & Metals (Monthly)
44
+ monthly: [
45
+ { base: 'CL', name: 'Crude Oil WTI', exchange: 'NYMEX', category: 'Energy' },
46
+ { base: 'MCL', name: 'Micro Crude Oil', exchange: 'NYMEX', category: 'Energy' },
47
+ { base: 'NG', name: 'Natural Gas', exchange: 'NYMEX', category: 'Energy' },
48
+ { base: 'GC', name: 'Gold', exchange: 'COMEX', category: 'Metals' },
49
+ { base: 'MGC', name: 'Micro Gold', exchange: 'COMEX', category: 'Metals' },
50
+ { base: 'SI', name: 'Silver', exchange: 'COMEX', category: 'Metals' },
51
+ { base: 'HG', name: 'Copper', exchange: 'COMEX', category: 'Metals' },
52
+ ],
53
+ };
54
+
55
+ // PropFirm configurations - NO FAKE DATA
18
56
  const PROPFIRM_CONFIGS = {
19
- 'apex': { name: 'Apex Trader Funding', systemName: 'Apex', defaultBalance: 300000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
20
- 'apex_rithmic': { name: 'Apex Trader Funding', systemName: 'Apex', defaultBalance: 300000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
21
- 'topstep_r': { name: 'Topstep (Rithmic)', systemName: RITHMIC_SYSTEMS.TOPSTEP, defaultBalance: 150000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
22
- 'bulenox_r': { name: 'Bulenox (Rithmic)', systemName: RITHMIC_SYSTEMS.BULENOX, defaultBalance: 150000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
23
- 'earn2trade': { name: 'Earn2Trade', systemName: RITHMIC_SYSTEMS.EARN_2_TRADE, defaultBalance: 150000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
24
- 'mescapital': { name: 'MES Capital', systemName: RITHMIC_SYSTEMS.MES_CAPITAL, defaultBalance: 150000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
25
- 'tradefundrr': { name: 'TradeFundrr', systemName: RITHMIC_SYSTEMS.TRADEFUNDRR, defaultBalance: 150000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
26
- 'thetradingpit': { name: 'The Trading Pit', systemName: RITHMIC_SYSTEMS.THE_TRADING_PIT, defaultBalance: 150000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
27
- 'fundedfutures': { name: 'Funded Futures Network', systemName: RITHMIC_SYSTEMS.FUNDED_FUTURES_NETWORK, defaultBalance: 150000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
28
- 'propshop': { name: 'PropShop Trader', systemName: RITHMIC_SYSTEMS.PROPSHOP_TRADER, defaultBalance: 150000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
29
- '4proptrader': { name: '4PropTrader', systemName: RITHMIC_SYSTEMS.FOUR_PROP_TRADER, defaultBalance: 150000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
30
- 'daytraders': { name: 'DayTraders.com', systemName: RITHMIC_SYSTEMS.DAY_TRADERS, defaultBalance: 150000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
31
- '10xfutures': { name: '10X Futures', systemName: RITHMIC_SYSTEMS.TEN_X_FUTURES, defaultBalance: 150000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
32
- 'lucidtrading': { name: 'Lucid Trading', systemName: RITHMIC_SYSTEMS.LUCID_TRADING, defaultBalance: 150000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
33
- 'thrivetrading': { name: 'Thrive Trading', systemName: RITHMIC_SYSTEMS.THRIVE_TRADING, defaultBalance: 150000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
34
- 'legendstrading': { name: 'Legends Trading', systemName: RITHMIC_SYSTEMS.LEGENDS_TRADING, defaultBalance: 150000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
57
+ 'apex': { name: 'Apex Trader Funding', systemName: 'Apex', gateway: RITHMIC_ENDPOINTS.CHICAGO },
58
+ 'apex_rithmic': { name: 'Apex Trader Funding', systemName: 'Apex', gateway: RITHMIC_ENDPOINTS.CHICAGO },
59
+ 'topstep_r': { name: 'Topstep (Rithmic)', systemName: RITHMIC_SYSTEMS.TOPSTEP, gateway: RITHMIC_ENDPOINTS.CHICAGO },
60
+ 'bulenox_r': { name: 'Bulenox (Rithmic)', systemName: RITHMIC_SYSTEMS.BULENOX, gateway: RITHMIC_ENDPOINTS.CHICAGO },
61
+ 'earn2trade': { name: 'Earn2Trade', systemName: RITHMIC_SYSTEMS.EARN_2_TRADE, gateway: RITHMIC_ENDPOINTS.CHICAGO },
62
+ 'mescapital': { name: 'MES Capital', systemName: RITHMIC_SYSTEMS.MES_CAPITAL, gateway: RITHMIC_ENDPOINTS.CHICAGO },
63
+ 'tradefundrr': { name: 'TradeFundrr', systemName: RITHMIC_SYSTEMS.TRADEFUNDRR, gateway: RITHMIC_ENDPOINTS.CHICAGO },
64
+ 'thetradingpit': { name: 'The Trading Pit', systemName: RITHMIC_SYSTEMS.THE_TRADING_PIT, gateway: RITHMIC_ENDPOINTS.CHICAGO },
65
+ 'fundedfutures': { name: 'Funded Futures Network', systemName: RITHMIC_SYSTEMS.FUNDED_FUTURES_NETWORK, gateway: RITHMIC_ENDPOINTS.CHICAGO },
66
+ 'propshop': { name: 'PropShop Trader', systemName: RITHMIC_SYSTEMS.PROPSHOP_TRADER, gateway: RITHMIC_ENDPOINTS.CHICAGO },
67
+ '4proptrader': { name: '4PropTrader', systemName: RITHMIC_SYSTEMS.FOUR_PROP_TRADER, gateway: RITHMIC_ENDPOINTS.CHICAGO },
68
+ 'daytraders': { name: 'DayTraders.com', systemName: RITHMIC_SYSTEMS.DAY_TRADERS, gateway: RITHMIC_ENDPOINTS.CHICAGO },
69
+ '10xfutures': { name: '10X Futures', systemName: RITHMIC_SYSTEMS.TEN_X_FUTURES, gateway: RITHMIC_ENDPOINTS.CHICAGO },
70
+ 'lucidtrading': { name: 'Lucid Trading', systemName: RITHMIC_SYSTEMS.LUCID_TRADING, gateway: RITHMIC_ENDPOINTS.CHICAGO },
71
+ 'thrivetrading': { name: 'Thrive Trading', systemName: RITHMIC_SYSTEMS.THRIVE_TRADING, gateway: RITHMIC_ENDPOINTS.CHICAGO },
72
+ 'legendstrading': { name: 'Legends Trading', systemName: RITHMIC_SYSTEMS.LEGENDS_TRADING, gateway: RITHMIC_ENDPOINTS.CHICAGO },
35
73
  };
36
74
 
37
75
  class RithmicService extends EventEmitter {
@@ -41,11 +79,11 @@ class RithmicService extends EventEmitter {
41
79
  this.propfirm = PROPFIRM_CONFIGS[propfirmKey] || {
42
80
  name: propfirmKey,
43
81
  systemName: 'Rithmic Paper Trading',
44
- defaultBalance: 150000,
45
82
  gateway: RITHMIC_ENDPOINTS.PAPER
46
83
  };
47
84
  this.orderConn = null;
48
85
  this.pnlConn = null;
86
+ this.tickerConn = null; // TICKER_PLANT for symbol lookup
49
87
  this.loginInfo = null;
50
88
  this.accounts = [];
51
89
  this.accountPnL = new Map();
@@ -53,6 +91,7 @@ class RithmicService extends EventEmitter {
53
91
  this.orders = [];
54
92
  this.user = null;
55
93
  this.credentials = null;
94
+ this.cachedContracts = null; // Cache contracts to avoid repeated API calls
56
95
  }
57
96
 
58
97
  /**
@@ -85,17 +124,15 @@ class RithmicService extends EventEmitter {
85
124
  this.loginInfo = data;
86
125
  this.user = { userName: username, fcmId: data.fcmId, ibId: data.ibId };
87
126
 
88
- try { await fetchAccounts(this); } catch (e) {}
89
-
90
- if (this.accounts.length === 0) {
91
- this.accounts = [{
92
- accountId: username,
93
- accountName: username,
94
- fcmId: data.fcmId,
95
- ibId: data.ibId,
96
- }];
127
+ try {
128
+ await fetchAccounts(this);
129
+ debug('Fetched accounts:', this.accounts.map(a => a.accountId));
130
+ } catch (e) {
131
+ debug('fetchAccounts error:', e.message);
97
132
  }
98
133
 
134
+ // NO FAKE ACCOUNTS - only real from Rithmic API
135
+
99
136
  this.credentials = { username, password };
100
137
 
101
138
  debug('Accounts found:', this.accounts.length);
@@ -211,53 +248,165 @@ class RithmicService extends EventEmitter {
211
248
  }
212
249
 
213
250
  /**
214
- * Get contracts from Rithmic
215
- * TODO: Implement TICKER_PLANT connection to fetch real contracts
216
- * For now, returns common futures contracts that are available on Rithmic
251
+ * Connect to TICKER_PLANT for symbol lookup
252
+ */
253
+ async connectTicker(username, password) {
254
+ try {
255
+ this.tickerConn = new RithmicConnection();
256
+ const gateway = this.propfirm.gateway || RITHMIC_ENDPOINTS.CHICAGO;
257
+
258
+ const config = {
259
+ uri: gateway,
260
+ systemName: this.propfirm.systemName,
261
+ userId: username,
262
+ password: password,
263
+ appName: 'HQX-CLI',
264
+ appVersion: '1.0.0',
265
+ };
266
+
267
+ await this.tickerConn.connect(config);
268
+
269
+ return new Promise((resolve) => {
270
+ const timeout = setTimeout(() => {
271
+ debug('TICKER_PLANT login timeout');
272
+ resolve(false);
273
+ }, 10000);
274
+
275
+ this.tickerConn.once('loggedIn', () => {
276
+ clearTimeout(timeout);
277
+ debug('TICKER_PLANT connected');
278
+ resolve(true);
279
+ });
280
+
281
+ this.tickerConn.once('loginFailed', () => {
282
+ clearTimeout(timeout);
283
+ debug('TICKER_PLANT login failed');
284
+ resolve(false);
285
+ });
286
+
287
+ this.tickerConn.login('TICKER_PLANT');
288
+ });
289
+ } catch (e) {
290
+ debug('TICKER_PLANT connection error:', e.message);
291
+ return false;
292
+ }
293
+ }
294
+
295
+ /**
296
+ * Get front month contract from Rithmic API
297
+ */
298
+ async getFrontMonth(baseSymbol, exchange) {
299
+ if (!this.tickerConn) {
300
+ if (!this.credentials) {
301
+ throw new Error('Not logged in - cannot fetch front month');
302
+ }
303
+ const connected = await this.connectTicker(this.credentials.username, this.credentials.password);
304
+ if (!connected) {
305
+ throw new Error('Failed to connect to TICKER_PLANT');
306
+ }
307
+ }
308
+
309
+ return new Promise((resolve, reject) => {
310
+ const timeout = setTimeout(() => {
311
+ reject(new Error(`Timeout getting front month for ${baseSymbol}`));
312
+ }, 10000);
313
+
314
+ const handler = (msg) => {
315
+ if (msg.templateId === 114) { // ResponseFrontMonthContract
316
+ const decoded = decodeFrontMonthContract(msg.data);
317
+ if (decoded.userMsg === baseSymbol) {
318
+ clearTimeout(timeout);
319
+ this.tickerConn.removeListener('message', handler);
320
+
321
+ if (decoded.rpCode[0] === '0') {
322
+ resolve({
323
+ baseSymbol: baseSymbol,
324
+ symbol: decoded.tradingSymbol || decoded.symbol,
325
+ exchange: decoded.exchange || exchange,
326
+ });
327
+ } else {
328
+ reject(new Error(`API error for ${baseSymbol}: ${decoded.rpCode.join(' ')}`));
329
+ }
330
+ }
331
+ }
332
+ };
333
+
334
+ this.tickerConn.on('message', handler);
335
+
336
+ // Send RequestFrontMonthContract (template 113)
337
+ this.tickerConn.send('RequestFrontMonthContract', {
338
+ templateId: 113,
339
+ userMsg: [baseSymbol],
340
+ symbol: baseSymbol,
341
+ exchange: exchange,
342
+ });
343
+ });
344
+ }
345
+
346
+ /**
347
+ * Get all available contracts - REAL DATA from Rithmic API
217
348
  */
218
349
  async getContracts() {
219
- // Calculate current front month dynamically
220
- const now = new Date();
221
- const month = now.getMonth();
222
- const year = now.getFullYear() % 100;
223
-
224
- // Quarterly months for index futures: H=Mar, M=Jun, U=Sep, Z=Dec
225
- const quarterlyMonths = ['H', 'M', 'U', 'Z'];
226
- const monthNames = ['Mar', 'Jun', 'Sep', 'Dec'];
227
- const quarterIndex = Math.floor(month / 3);
228
-
229
- // If past 15th of expiry month, use next quarter
230
- let frontMonth = quarterlyMonths[quarterIndex];
231
- let frontMonthName = monthNames[quarterIndex];
232
- let frontYear = year;
233
-
234
- if (now.getDate() > 15 && [2, 5, 8, 11].includes(month)) {
235
- const nextIdx = (quarterIndex + 1) % 4;
236
- frontMonth = quarterlyMonths[nextIdx];
237
- frontMonthName = monthNames[nextIdx];
238
- if (nextIdx === 0) frontYear++;
350
+ // Return cached if available and fresh (5 min cache)
351
+ if (this.cachedContracts && this.cachedContracts.timestamp > Date.now() - 300000) {
352
+ return { success: true, contracts: this.cachedContracts.data, source: 'cache' };
353
+ }
354
+
355
+ // Need credentials to fetch real data
356
+ if (!this.credentials) {
357
+ return { success: false, error: 'Not logged in - cannot fetch contracts from API' };
358
+ }
359
+
360
+ try {
361
+ // Connect to TICKER_PLANT if not connected
362
+ if (!this.tickerConn) {
363
+ const connected = await this.connectTicker(this.credentials.username, this.credentials.password);
364
+ if (!connected) {
365
+ return { success: false, error: 'Failed to connect to TICKER_PLANT' };
366
+ }
367
+ }
368
+
369
+ const contracts = [];
370
+ const allSymbols = [...BASE_SYMBOLS.quarterly, ...BASE_SYMBOLS.monthly];
371
+
372
+ debug(`Fetching front months for ${allSymbols.length} symbols...`);
373
+
374
+ // Fetch front months in parallel batches
375
+ const batchSize = 10;
376
+ for (let i = 0; i < allSymbols.length; i += batchSize) {
377
+ const batch = allSymbols.slice(i, i + batchSize);
378
+ const promises = batch.map(async (sym) => {
379
+ try {
380
+ const result = await this.getFrontMonth(sym.base, sym.exchange);
381
+ return {
382
+ symbol: result.symbol,
383
+ name: `${sym.name} (${result.symbol})`,
384
+ exchange: result.exchange,
385
+ category: sym.category,
386
+ baseSymbol: sym.base,
387
+ };
388
+ } catch (e) {
389
+ debug(`Failed to get front month for ${sym.base}: ${e.message}`);
390
+ return null;
391
+ }
392
+ });
393
+
394
+ const results = await Promise.all(promises);
395
+ contracts.push(...results.filter(r => r !== null));
396
+ }
397
+
398
+ if (contracts.length === 0) {
399
+ return { success: false, error: 'No contracts returned from API' };
400
+ }
401
+
402
+ // Cache the results
403
+ this.cachedContracts = { data: contracts, timestamp: Date.now() };
404
+ return { success: true, contracts, source: 'api' };
405
+
406
+ } catch (e) {
407
+ debug('getContracts error:', e.message);
408
+ return { success: false, error: e.message };
239
409
  }
240
-
241
- const y = frontYear; // e.g., 26 for 2026
242
- const fy = `${frontYear}`; // full year string
243
-
244
- const contracts = [
245
- { symbol: `ES${frontMonth}${y}`, name: `E-mini S&P 500 (${frontMonthName} ${fy})`, exchange: 'CME' },
246
- { symbol: `NQ${frontMonth}${y}`, name: `E-mini NASDAQ-100 (${frontMonthName} ${fy})`, exchange: 'CME' },
247
- { symbol: `MES${frontMonth}${y}`, name: `Micro E-mini S&P 500 (${frontMonthName} ${fy})`, exchange: 'CME' },
248
- { symbol: `MNQ${frontMonth}${y}`, name: `Micro E-mini NASDAQ-100 (${frontMonthName} ${fy})`, exchange: 'CME' },
249
- { symbol: `RTY${frontMonth}${y}`, name: `E-mini Russell 2000 (${frontMonthName} ${fy})`, exchange: 'CME' },
250
- { symbol: `M2K${frontMonth}${y}`, name: `Micro E-mini Russell 2000 (${frontMonthName} ${fy})`, exchange: 'CME' },
251
- { symbol: `YM${frontMonth}${y}`, name: `E-mini Dow Jones (${frontMonthName} ${fy})`, exchange: 'CBOT' },
252
- { symbol: `MYM${frontMonth}${y}`, name: `Micro E-mini Dow Jones (${frontMonthName} ${fy})`, exchange: 'CBOT' },
253
- { symbol: `CL${frontMonth}${y}`, name: `Crude Oil (${frontMonthName} ${fy})`, exchange: 'NYMEX' },
254
- { symbol: `MCL${frontMonth}${y}`, name: `Micro Crude Oil (${frontMonthName} ${fy})`, exchange: 'NYMEX' },
255
- { symbol: `GC${frontMonth}${y}`, name: `Gold (${frontMonthName} ${fy})`, exchange: 'COMEX' },
256
- { symbol: `MGC${frontMonth}${y}`, name: `Micro Gold (${frontMonthName} ${fy})`, exchange: 'COMEX' },
257
- { symbol: `SI${frontMonth}${y}`, name: `Silver (${frontMonthName} ${fy})`, exchange: 'COMEX' },
258
- { symbol: `NG${frontMonth}${y}`, name: `Natural Gas (${frontMonthName} ${fy})`, exchange: 'NYMEX' },
259
- ];
260
- return { success: true, contracts };
261
410
  }
262
411
 
263
412
  async searchContracts(searchText) {
@@ -299,6 +448,10 @@ class RithmicService extends EventEmitter {
299
448
  await this.pnlConn.disconnect();
300
449
  this.pnlConn = null;
301
450
  }
451
+ if (this.tickerConn) {
452
+ await this.tickerConn.disconnect();
453
+ this.tickerConn = null;
454
+ }
302
455
  this.accounts = [];
303
456
  this.accountPnL.clear();
304
457
  this.positions.clear();
@@ -306,6 +459,7 @@ class RithmicService extends EventEmitter {
306
459
  this.loginInfo = null;
307
460
  this.user = null;
308
461
  this.credentials = null;
462
+ this.cachedContracts = null;
309
463
  }
310
464
  }
311
465
 
@@ -0,0 +1,11 @@
1
+
2
+ package rti;
3
+
4
+ message RequestFrontMonthContract
5
+ {
6
+ required int32 template_id = 154467; // Template ID = 113
7
+ repeated string user_msg = 132760; // User message for tracking
8
+ optional string symbol = 110100; // Base symbol (e.g., "MNQ", "ES")
9
+ optional string exchange = 110101; // Exchange (e.g., "CME")
10
+ optional bool need_updates = 154352; // Request updates
11
+ }
@@ -0,0 +1,9 @@
1
+
2
+ package rti;
3
+
4
+ message RequestProductCodes
5
+ {
6
+ required int32 template_id = 154467; // Template ID = 111
7
+ repeated string user_msg = 132760; // User message for tracking
8
+ optional string exchange = 110101; // Exchange filter (optional)
9
+ }
@@ -0,0 +1,13 @@
1
+
2
+ package rti;
3
+
4
+ message ResponseFrontMonthContract
5
+ {
6
+ required int32 template_id = 154467; // Template ID = 114
7
+ repeated string rp_code = 132766; // Response code
8
+ repeated string user_msg = 132760; // Echo of user message
9
+ optional string symbol = 110100; // Full contract symbol (e.g., "MNQH5")
10
+ optional string exchange = 110101; // Exchange
11
+ optional string trading_symbol = 157095; // Trading symbol
12
+ optional string description = 110114; // Contract description
13
+ }
@@ -0,0 +1,12 @@
1
+
2
+ package rti;
3
+
4
+ message ResponseProductCodes
5
+ {
6
+ required int32 template_id = 154467; // Template ID = 112
7
+ repeated string rp_code = 132766; // Response code
8
+ repeated string user_msg = 132760; // Echo of user message
9
+ optional string exchange = 110101; // Exchange
10
+ optional string product_code = 110102; // Product code (e.g., "MNQ", "ES")
11
+ optional string product_name = 110103; // Product name
12
+ }
@@ -28,6 +28,19 @@ const PNL_FIELDS = {
28
28
  USECS: 150101,
29
29
  };
30
30
 
31
+ // Symbol/Contract field IDs (ResponseProductCodes, ResponseFrontMonthContract)
32
+ const SYMBOL_FIELDS = {
33
+ TEMPLATE_ID: 154467,
34
+ RP_CODE: 132766,
35
+ EXCHANGE: 110101,
36
+ PRODUCT_CODE: 110102, // Base symbol (ES, NQ, MNQ)
37
+ PRODUCT_NAME: 110103, // Product name
38
+ SYMBOL: 110100, // Full contract symbol (ESH26)
39
+ TRADING_SYMBOL: 157095, // Trading symbol
40
+ DESCRIPTION: 110114, // Contract description
41
+ USER_MSG: 132760,
42
+ };
43
+
31
44
  // Instrument PnL Position Update field IDs
32
45
  const INSTRUMENT_PNL_FIELDS = {
33
46
  TEMPLATE_ID: 154467,
@@ -377,6 +390,101 @@ class ProtobufHandler {
377
390
  }
378
391
  }
379
392
 
393
+ /**
394
+ * Decode ResponseProductCodes (template 112) - list of available symbols
395
+ */
396
+ function decodeProductCodes(buffer) {
397
+ const result = { rpCode: [] };
398
+ let offset = 0;
399
+
400
+ while (offset < buffer.length) {
401
+ try {
402
+ const [tag, tagOffset] = readVarint(buffer, offset);
403
+ const wireType = tag & 0x7;
404
+ const fieldNumber = tag >>> 3;
405
+ offset = tagOffset;
406
+
407
+ switch (fieldNumber) {
408
+ case SYMBOL_FIELDS.TEMPLATE_ID:
409
+ [result.templateId, offset] = readVarint(buffer, offset);
410
+ break;
411
+ case SYMBOL_FIELDS.RP_CODE:
412
+ let rpCode;
413
+ [rpCode, offset] = readLengthDelimited(buffer, offset);
414
+ result.rpCode.push(rpCode);
415
+ break;
416
+ case SYMBOL_FIELDS.EXCHANGE:
417
+ [result.exchange, offset] = readLengthDelimited(buffer, offset);
418
+ break;
419
+ case SYMBOL_FIELDS.PRODUCT_CODE:
420
+ [result.productCode, offset] = readLengthDelimited(buffer, offset);
421
+ break;
422
+ case SYMBOL_FIELDS.PRODUCT_NAME:
423
+ [result.productName, offset] = readLengthDelimited(buffer, offset);
424
+ break;
425
+ case SYMBOL_FIELDS.USER_MSG:
426
+ [result.userMsg, offset] = readLengthDelimited(buffer, offset);
427
+ break;
428
+ default:
429
+ offset = skipField(buffer, offset, wireType);
430
+ }
431
+ } catch (error) {
432
+ break;
433
+ }
434
+ }
435
+
436
+ return result;
437
+ }
438
+
439
+ /**
440
+ * Decode ResponseFrontMonthContract (template 114) - current tradeable contract
441
+ */
442
+ function decodeFrontMonthContract(buffer) {
443
+ const result = { rpCode: [] };
444
+ let offset = 0;
445
+
446
+ while (offset < buffer.length) {
447
+ try {
448
+ const [tag, tagOffset] = readVarint(buffer, offset);
449
+ const wireType = tag & 0x7;
450
+ const fieldNumber = tag >>> 3;
451
+ offset = tagOffset;
452
+
453
+ switch (fieldNumber) {
454
+ case SYMBOL_FIELDS.TEMPLATE_ID:
455
+ [result.templateId, offset] = readVarint(buffer, offset);
456
+ break;
457
+ case SYMBOL_FIELDS.RP_CODE:
458
+ let rpCode;
459
+ [rpCode, offset] = readLengthDelimited(buffer, offset);
460
+ result.rpCode.push(rpCode);
461
+ break;
462
+ case SYMBOL_FIELDS.SYMBOL:
463
+ [result.symbol, offset] = readLengthDelimited(buffer, offset);
464
+ break;
465
+ case SYMBOL_FIELDS.EXCHANGE:
466
+ [result.exchange, offset] = readLengthDelimited(buffer, offset);
467
+ break;
468
+ case SYMBOL_FIELDS.TRADING_SYMBOL:
469
+ [result.tradingSymbol, offset] = readLengthDelimited(buffer, offset);
470
+ break;
471
+ case SYMBOL_FIELDS.DESCRIPTION:
472
+ [result.description, offset] = readLengthDelimited(buffer, offset);
473
+ break;
474
+ case SYMBOL_FIELDS.USER_MSG:
475
+ [result.userMsg, offset] = readLengthDelimited(buffer, offset);
476
+ break;
477
+ default:
478
+ offset = skipField(buffer, offset, wireType);
479
+ }
480
+ } catch (error) {
481
+ break;
482
+ }
483
+ }
484
+
485
+ return result;
486
+ }
487
+
380
488
  // Singleton
381
489
  const proto = new ProtobufHandler();
382
490
 
@@ -384,6 +492,8 @@ module.exports = {
384
492
  proto,
385
493
  decodeAccountPnL,
386
494
  decodeInstrumentPnL,
495
+ decodeProductCodes,
496
+ decodeFrontMonthContract,
387
497
  readVarint,
388
498
  readLengthDelimited,
389
499
  skipField,
@@ -34,11 +34,11 @@ class TradovateService extends EventEmitter {
34
34
  */
35
35
  getPropFirmConfig(key) {
36
36
  const propfirms = {
37
- 'apex_tradovate': { name: 'Apex (Tradovate)', isDemo: false, defaultBalance: 300000 },
38
- 'takeprofittrader': { name: 'TakeProfitTrader', isDemo: false, defaultBalance: 150000 },
39
- 'myfundedfutures': { name: 'MyFundedFutures', isDemo: false, defaultBalance: 150000 },
37
+ 'apex_tradovate': { name: 'Apex (Tradovate)', isDemo: false },
38
+ 'takeprofittrader': { name: 'TakeProfitTrader', isDemo: false },
39
+ 'myfundedfutures': { name: 'MyFundedFutures', isDemo: false },
40
40
  };
41
- return propfirms[key] || { name: key, isDemo: false, defaultBalance: 150000 };
41
+ return propfirms[key] || { name: key, isDemo: false };
42
42
  }
43
43
 
44
44
  /**