hedgequantx 2.9.48 → 2.9.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hedgequantx",
3
- "version": "2.9.48",
3
+ "version": "2.9.49",
4
4
  "description": "HedgeQuantX - Prop Futures Trading CLI",
5
5
  "main": "src/app.js",
6
6
  "bin": {
package/src/app.js CHANGED
@@ -77,7 +77,7 @@ const refreshStats = async () => {
77
77
 
78
78
  try {
79
79
  const allAccounts = await connections.getAllAccounts();
80
- // Filter active accounts: status === 0 (ProjectX) OR status === 'active' (Rithmic) OR no status
80
+ // Filter active accounts: status === 'active' (Rithmic) OR status === 0 OR no status
81
81
  const activeAccounts = allAccounts.filter(acc =>
82
82
  acc.status === 0 || acc.status === 'active' || acc.status === undefined || acc.status === null
83
83
  );
@@ -15,14 +15,7 @@ const PROPFIRMS = {
15
15
  rithmicSystem: 'Apex',
16
16
  wsEndpoint: 'wss://ritpa11120.11.rithmic.com:443',
17
17
  },
18
- topsteptrader: {
19
- id: 'topsteptrader',
20
- name: 'TopstepTrader',
21
- displayName: 'TopstepTrader',
22
- platform: 'Rithmic',
23
- rithmicSystem: 'TopstepTrader',
24
- wsEndpoint: 'wss://ritpa11120.11.rithmic.com:443'
25
- },
18
+
26
19
  mes_capital: {
27
20
  id: 'mes-capital',
28
21
  name: 'MES Capital',
package/src/lib/data.js CHANGED
@@ -1,328 +1,247 @@
1
1
  /**
2
2
  * =============================================================================
3
- * MARKET DATA FEED - SignalR Real-Time Data
3
+ * MARKET DATA FEED - Rithmic TICKER_PLANT Real-Time Data
4
4
  * =============================================================================
5
- * Connects to ProjectX Gateway RTC for real-time market data
5
+ * Connects to Rithmic TICKER_PLANT for real-time market data
6
6
  *
7
7
  * Events emitted:
8
8
  * - tick: Quote/trade updates (price, bid, ask, volume)
9
- * - quote: Quote updates only
10
- * - trade: Trade executions only
11
- * - depth: DOM/Level 2 updates
12
9
  * - connected: Connection established
13
10
  * - disconnected: Connection lost
14
11
  * - error: Connection error
15
- *
16
- * SOURCE: Based on ProjectX Gateway RTC API
17
12
  */
18
13
 
19
14
  'use strict';
20
15
 
21
16
  const EventEmitter = require('events');
22
- const { HubConnectionBuilder, HttpTransportType, LogLevel } = require('@microsoft/signalr');
23
-
24
- // Inline PROPFIRMS config for RTC URLs (standalone module)
25
- // RTC URL pattern: api.xxx.com -> rtc.xxx.com
26
- const PROPFIRMS = {
27
- topstep: { gatewayApi: 'api.topstepx.com' },
28
- alpha_futures: { gatewayApi: 'api.alphafutures.projectx.com' },
29
- tickticktrader: { gatewayApi: 'api.tickticktrader.projectx.com' },
30
- bulenox: { gatewayApi: 'api.bulenox.projectx.com' },
31
- tradeday: { gatewayApi: 'api.tradeday.projectx.com' },
32
- blusky: { gatewayApi: 'api.blusky.projectx.com' },
33
- goat_futures: { gatewayApi: 'api.goatfutures.projectx.com' },
34
- futures_desk: { gatewayApi: 'api.thefuturesdesk.projectx.com' },
35
- daytraders: { gatewayApi: 'api.daytraders.projectx.com' },
36
- e8_futures: { gatewayApi: 'api.e8futures.projectx.com' },
37
- blue_guardian: { gatewayApi: 'api.blueguardianfutures.projectx.com' },
38
- futures_elite: { gatewayApi: 'api.futureselite.projectx.com' },
39
- fxify: { gatewayApi: 'api.fxify.projectx.com' },
40
- hola_prime: { gatewayApi: 'api.holaprime.projectx.com' },
41
- top_one_futures: { gatewayApi: 'api.toponefutures.projectx.com' },
42
- funding_futures: { gatewayApi: 'api.fundingfutures.projectx.com' },
43
- tx3_funding: { gatewayApi: 'api.tx3funding.projectx.com' },
44
- lucid_trading: { gatewayApi: 'api.lucidtrading.projectx.com' },
45
- tradeify: { gatewayApi: 'api.tradeify.projectx.com' }
46
- };
17
+ const { RithmicConnection } = require('../services/rithmic/connection');
18
+ const { proto } = require('../services/rithmic/protobuf');
19
+ const { REQ, RES, STREAM, RITHMIC_ENDPOINTS, RITHMIC_SYSTEMS } = require('../services/rithmic/constants');
47
20
 
48
21
  // =============================================================================
49
- // MARKET DATA FEED CLASS
22
+ // MARKET DATA FEED CLASS (Rithmic TICKER_PLANT)
50
23
  // =============================================================================
51
24
 
52
25
  class MarketDataFeed extends EventEmitter {
53
26
  constructor(options = {}) {
54
27
  super();
55
28
 
56
- this.propfirmKey = (options.propfirm || 'topstep').toLowerCase().replace(/\s+/g, '_');
57
29
  this.connection = null;
58
30
  this.connected = false;
59
31
  this.subscriptions = new Set();
60
- this.reconnectAttempts = 0;
61
- this.maxReconnectAttempts = 5;
32
+ this.credentials = null;
33
+ this.config = null;
62
34
  }
63
35
 
64
36
  /**
65
- * Get market hub URL for propfirm
66
- * Pattern: api.xxx.com -> rtc.xxx.com
37
+ * Connect to Rithmic TICKER_PLANT
38
+ * @param {Object} rithmicCredentials - Credentials from RithmicService.getRithmicCredentials()
67
39
  */
68
- _getMarketHubUrl(propfirmKey) {
69
- const propfirm = PROPFIRMS[propfirmKey] || PROPFIRMS.topstep;
70
-
71
- if (propfirm.gatewayApi) {
72
- // Convert api.xxx.com to rtc.xxx.com
73
- const rtcHost = propfirm.gatewayApi.replace(/^api\./, 'rtc.');
74
- return `https://${rtcHost}/hubs/market`;
75
- }
76
-
77
- // Fallback for topstep
78
- return 'https://rtc.topstepx.com/hubs/market';
79
- }
80
-
81
- /**
82
- * Connect to market data hub
83
- */
84
- async connect(token, propfirmKey, contractId = null) {
40
+ async connect(rithmicCredentials) {
85
41
  if (this.connected) return;
86
42
 
87
- this.propfirmKey = propfirmKey || this.propfirmKey;
88
- this.token = token;
89
- const hubUrl = this._getMarketHubUrl(this.propfirmKey);
90
-
91
- // CRITICAL: Token must be in URL query string (same as HQX-TG and Python SDK)
92
- const urlWithToken = `${hubUrl}?access_token=${encodeURIComponent(this.token)}`;
93
-
94
- try {
95
- // CRITICAL: skipNegotiation=true + WebSockets transport is REQUIRED for ProjectX/TopstepX
96
- // This is how the Python SDK and HQX-TG connect successfully
97
- this.connection = new HubConnectionBuilder()
98
- .withUrl(urlWithToken, {
99
- skipNegotiation: true,
100
- transport: HttpTransportType.WebSockets
101
- })
102
- .withAutomaticReconnect({
103
- nextRetryDelayInMilliseconds: (ctx) => {
104
- if (ctx.previousRetryCount >= this.maxReconnectAttempts) return null;
105
- return Math.min(1000 * Math.pow(2, ctx.previousRetryCount), 30000);
106
- }
107
- })
108
- .configureLogging(LogLevel.Warning)
109
- .build();
110
-
111
- // Set server timeout and keepalive
112
- this.connection.serverTimeoutInMilliseconds = 60000; // 60s
113
- this.connection.keepAliveIntervalInMilliseconds = 15000; // 15s
114
-
115
- this._setupEventHandlers();
116
- await this.connection.start();
117
-
118
- this.connected = true;
119
- this.reconnectAttempts = 0;
120
- this.emit('connected');
121
-
122
- if (contractId) {
123
- await this.subscribe(null, contractId);
124
- }
125
- } catch (error) {
126
- this.connected = false;
127
- this.emit('error', error);
128
- throw error;
43
+ if (!rithmicCredentials || !rithmicCredentials.userId || !rithmicCredentials.password) {
44
+ throw new Error('Rithmic credentials required (userId, password, systemName, gateway)');
129
45
  }
130
- }
131
46
 
132
- /**
133
- * Setup SignalR event handlers
134
- */
135
- _setupEventHandlers() {
136
- if (!this.connection) return;
137
-
138
- // Quote updates - GatewayQuote receives (contractId, data)
139
- this.connection.on('GatewayQuote', (contractId, data) => {
140
- // Handle both (contractId, data) and single object formats
141
- const quote = data || contractId;
142
- const cid = data ? contractId : (quote.symbol || quote.symbolId);
143
-
144
- const tick = {
145
- type: 'quote',
146
- contractId: cid,
147
- symbol: quote.symbolName || quote.symbol || cid,
148
- price: quote.lastPrice,
149
- bid: quote.bestBid,
150
- ask: quote.bestAsk,
151
- change: quote.change,
152
- changePercent: quote.changePercent,
153
- open: quote.open,
154
- high: quote.high,
155
- low: quote.low,
156
- volume: quote.volume,
157
- timestamp: quote.timestamp ? new Date(quote.timestamp).getTime() : Date.now()
158
- };
159
- this.emit('tick', tick);
160
- this.emit('quote', tick);
161
- });
162
-
163
- // Trade executions - GatewayTrade receives (contractId, data[])
164
- this.connection.on('GatewayTrade', (contractId, trades) => {
165
- // Handle both formats
166
- const tradeList = Array.isArray(trades) ? trades : (Array.isArray(contractId) ? contractId : [trades || contractId]);
167
- const cid = typeof contractId === 'string' ? contractId : null;
168
-
169
- for (const trade of tradeList) {
170
- if (!trade) continue;
171
- const tick = {
172
- type: 'trade',
173
- contractId: cid || trade.symbolId,
174
- price: trade.price,
175
- volume: trade.volume,
176
- side: trade.type === 0 ? 'buy' : 'sell',
177
- lastTradeSide: trade.type === 0 ? 'buy' : 'sell',
178
- timestamp: trade.timestamp ? new Date(trade.timestamp).getTime() : Date.now()
179
- };
180
- this.emit('tick', tick);
181
- this.emit('trade', tick);
182
- }
183
- });
47
+ this.credentials = rithmicCredentials;
48
+ this.connection = new RithmicConnection();
184
49
 
185
- // DOM updates - GatewayDepth receives (contractId, data[])
186
- this.connection.on('GatewayDepth', (contractId, depthData) => {
187
- // Handle both formats
188
- const depthList = Array.isArray(depthData) ? depthData : (Array.isArray(contractId) ? contractId : [depthData || contractId]);
189
- const cid = typeof contractId === 'string' ? contractId : null;
190
-
191
- for (const depth of depthList) {
192
- if (!depth) continue;
193
- const domUpdate = {
194
- type: 'depth',
195
- contractId: cid,
196
- price: depth.price,
197
- volume: depth.volume || depth.totalVolume,
198
- currentVolume: depth.currentVolume,
199
- side: depth.type === 0 ? 'bid' : 'ask',
200
- timestamp: depth.timestamp ? new Date(depth.timestamp).getTime() : Date.now()
201
- };
202
- this.emit('depth', domUpdate);
203
- this.emit('dom', domUpdate);
204
- }
205
- });
206
-
207
- // Connection state
208
- this.connection.onreconnecting((error) => {
209
- this.connected = false;
210
- this.emit('reconnecting', error);
211
- });
212
-
213
- this.connection.onreconnected((connectionId) => {
214
- this.connected = true;
215
- this.reconnectAttempts = 0;
216
- this.emit('reconnected', connectionId);
217
- this._resubscribeAll();
218
- });
219
-
220
- this.connection.onclose((error) => {
221
- this.connected = false;
222
- // Emit detailed error for debugging
223
- let errMsg = 'unknown';
224
- if (error) {
225
- errMsg = error.message || error.toString();
226
- if (error.stack) errMsg += ' | ' + error.stack.split('\n')[0];
227
- }
228
- this.emit('disconnected', new Error(errMsg));
229
- });
50
+ this.config = {
51
+ uri: rithmicCredentials.gateway || RITHMIC_ENDPOINTS.CHICAGO,
52
+ systemName: rithmicCredentials.systemName || RITHMIC_SYSTEMS.PAPER,
53
+ userId: rithmicCredentials.userId,
54
+ password: rithmicCredentials.password,
55
+ appName: 'HQX-CLI',
56
+ appVersion: '2.0.0',
57
+ };
58
+
59
+ try {
60
+ await this.connection.connect(this.config);
61
+
62
+ // Setup message handler for market data
63
+ this.connection.on('message', (msg) => this._handleMessage(msg));
64
+
65
+ // Login to TICKER_PLANT
66
+ return new Promise((resolve, reject) => {
67
+ const timeout = setTimeout(() => {
68
+ reject(new Error('TICKER_PLANT login timeout'));
69
+ }, 15000);
70
+
71
+ this.connection.once('loggedIn', () => {
72
+ clearTimeout(timeout);
73
+ this.connected = true;
74
+ this.emit('connected');
75
+ resolve(true);
76
+ });
77
+
78
+ this.connection.once('loginFailed', (data) => {
79
+ clearTimeout(timeout);
80
+ reject(new Error(data.message || 'TICKER_PLANT login failed'));
81
+ });
82
+
83
+ this.connection.login('TICKER_PLANT');
84
+ });
85
+ } catch (e) {
86
+ this.emit('error', e);
87
+ throw e;
88
+ }
230
89
  }
231
90
 
232
91
  /**
233
- * Subscribe to contract market data
92
+ * Subscribe to market data for a symbol
93
+ * @param {string} symbol - Symbol name (e.g., 'ESH6')
94
+ * @param {string} exchange - Exchange (default: 'CME')
234
95
  */
235
- async subscribe(symbol, contractId) {
236
- if (!this.connection || !this.connected) {
237
- throw new Error('Not connected');
96
+ async subscribe(symbol, exchange = 'CME') {
97
+ if (!this.connected) {
98
+ throw new Error('Not connected to TICKER_PLANT');
238
99
  }
239
100
 
240
- const id = contractId || symbol;
241
-
101
+ const key = `${symbol}:${exchange}`;
102
+ if (this.subscriptions.has(key)) return;
103
+
242
104
  try {
243
- await this.connection.invoke('SubscribeContractQuotes', id);
244
- await this.connection.invoke('SubscribeContractTrades', id);
245
- await this.connection.invoke('SubscribeContractMarketDepth', id);
246
- this.subscriptions.add(id);
247
- this.emit('subscribed', { symbol, contractId: id });
248
- } catch (error) {
249
- this.emit('error', new Error(`Subscribe failed: ${error.message}`));
250
- throw error;
105
+ // Subscribe to LAST_TRADE and BBO (Best Bid/Offer)
106
+ // UpdateBits: 1 = LAST_TRADE, 2 = BBO, 3 = BOTH
107
+ this.connection.send('RequestMarketDataUpdate', {
108
+ templateId: REQ.MARKET_DATA,
109
+ symbol: symbol,
110
+ exchange: exchange,
111
+ request: 1, // SUBSCRIBE
112
+ updateBits: 3, // LAST_TRADE + BBO
113
+ });
114
+
115
+ this.subscriptions.add(key);
116
+ } catch (e) {
117
+ this.emit('error', e);
118
+ throw e;
251
119
  }
252
120
  }
253
121
 
254
122
  /**
255
- * Unsubscribe from contract
123
+ * Unsubscribe from market data for a symbol
256
124
  */
257
- async unsubscribe(contractId) {
258
- if (!this.connection || !this.connected) return;
125
+ async unsubscribe(symbol, exchange = 'CME') {
126
+ if (!this.connected) return;
127
+
128
+ const key = `${symbol}:${exchange}`;
129
+ if (!this.subscriptions.has(key)) return;
259
130
 
260
131
  try {
261
- await this.connection.invoke('UnsubscribeContractQuotes', contractId);
262
- await this.connection.invoke('UnsubscribeContractTrades', contractId);
263
- await this.connection.invoke('UnsubscribeContractMarketDepth', contractId);
264
- this.subscriptions.delete(contractId);
265
- this.emit('unsubscribed', { contractId });
266
- } catch (error) {
267
- // Silently handle
132
+ this.connection.send('RequestMarketDataUpdate', {
133
+ templateId: REQ.MARKET_DATA,
134
+ symbol: symbol,
135
+ exchange: exchange,
136
+ request: 2, // UNSUBSCRIBE
137
+ });
138
+
139
+ this.subscriptions.delete(key);
140
+ } catch (e) {
141
+ // Ignore unsubscribe errors
268
142
  }
269
143
  }
270
144
 
271
145
  /**
272
- * Resubscribe after reconnect
146
+ * Disconnect from TICKER_PLANT
273
147
  */
274
- async _resubscribeAll() {
275
- for (const contractId of this.subscriptions) {
148
+ async disconnect() {
149
+ // Unsubscribe from all
150
+ for (const key of this.subscriptions) {
151
+ const [symbol, exchange] = key.split(':');
152
+ await this.unsubscribe(symbol, exchange);
153
+ }
154
+
155
+ if (this.connection) {
276
156
  try {
277
- await this.connection.invoke('SubscribeContractQuotes', contractId);
278
- await this.connection.invoke('SubscribeContractTrades', contractId);
279
- await this.connection.invoke('SubscribeContractMarketDepth', contractId);
280
- } catch (error) {
281
- // Continue
157
+ await this.connection.disconnect();
158
+ } catch (e) {
159
+ // Ignore
282
160
  }
283
161
  }
162
+
163
+ this.connection = null;
164
+ this.connected = false;
165
+ this.subscriptions.clear();
166
+ this.emit('disconnected');
284
167
  }
285
168
 
286
169
  /**
287
- * Disconnect with timeout to prevent hanging
170
+ * Handle incoming messages from TICKER_PLANT
288
171
  */
289
- async disconnect() {
290
- if (this.connection) {
291
- const timeoutPromise = new Promise(r => setTimeout(r, 3000));
292
-
293
- try {
294
- // Race between disconnect and timeout
295
- await Promise.race([
296
- (async () => {
297
- for (const contractId of this.subscriptions) {
298
- try { await this.unsubscribe(contractId); } catch {}
299
- }
300
- await this.connection.stop();
301
- })(),
302
- timeoutPromise
303
- ]);
304
- } catch (error) {
305
- // Ignore disconnect errors
306
- }
307
-
308
- this.connection = null;
309
- this.connected = false;
310
- this.subscriptions.clear();
172
+ _handleMessage(msg) {
173
+ const { templateId, data } = msg;
174
+
175
+ switch (templateId) {
176
+ case RES.MARKET_DATA:
177
+ // Subscription confirmed
178
+ break;
179
+
180
+ case STREAM.LAST_TRADE:
181
+ this._handleLastTrade(data);
182
+ break;
183
+
184
+ case STREAM.BBO:
185
+ this._handleBBO(data);
186
+ break;
187
+
188
+ default:
189
+ // Ignore other messages
190
+ break;
311
191
  }
312
192
  }
313
193
 
314
194
  /**
315
- * Check connection status
195
+ * Handle LastTrade message
316
196
  */
317
- isConnected() {
318
- return this.connected && this.connection?.state === 'Connected';
197
+ _handleLastTrade(data) {
198
+ try {
199
+ const trade = proto.decode('LastTrade', data);
200
+
201
+ const tick = {
202
+ type: 'trade',
203
+ symbol: trade.symbol,
204
+ exchange: trade.exchange,
205
+ price: trade.tradePrice,
206
+ size: trade.tradeSize,
207
+ volume: trade.volume,
208
+ side: trade.aggressor === 1 ? 'buy' : trade.aggressor === 2 ? 'sell' : 'unknown',
209
+ timestamp: Date.now(),
210
+ ssboe: trade.ssboe,
211
+ usecs: trade.usecs,
212
+ };
213
+
214
+ this.emit('tick', tick);
215
+ } catch (e) {
216
+ // Ignore decode errors
217
+ }
319
218
  }
320
219
 
321
220
  /**
322
- * Get active subscriptions
221
+ * Handle BestBidOffer message
323
222
  */
324
- getSubscriptions() {
325
- return Array.from(this.subscriptions);
223
+ _handleBBO(data) {
224
+ try {
225
+ const bbo = proto.decode('BestBidOffer', data);
226
+
227
+ const tick = {
228
+ type: 'quote',
229
+ symbol: bbo.symbol,
230
+ exchange: bbo.exchange,
231
+ bid: bbo.bidPrice,
232
+ bidSize: bbo.bidSize,
233
+ ask: bbo.askPrice,
234
+ askSize: bbo.askSize,
235
+ price: bbo.bidPrice && bbo.askPrice ? (bbo.bidPrice + bbo.askPrice) / 2 : null,
236
+ timestamp: Date.now(),
237
+ ssboe: bbo.ssboe,
238
+ usecs: bbo.usecs,
239
+ };
240
+
241
+ this.emit('tick', tick);
242
+ } catch (e) {
243
+ // Ignore decode errors
244
+ }
326
245
  }
327
246
  }
328
247
 
@@ -13,7 +13,7 @@ const { SupervisionEngine } = require('../../services/ai-supervision');
13
13
  /**
14
14
  * Execute algo strategy with market data
15
15
  * @param {Object} params - Execution parameters
16
- * @param {Object} params.service - Trading service (Rithmic/ProjectX)
16
+ * @param {Object} params.service - Rithmic trading service
17
17
  * @param {Object} params.account - Account object
18
18
  * @param {Object} params.contract - Contract object
19
19
  * @param {Object} params.config - Algo config (contracts, target, risk, showName)
@@ -77,8 +77,8 @@ const executeAlgo = async ({ service, account, contract, config, strategy: strat
77
77
  const strategy = new StrategyClass({ tickSize });
78
78
  strategy.initialize(contractId, tickSize);
79
79
 
80
- // Initialize Market Data Feed
81
- const marketFeed = new MarketDataFeed({ propfirm: account.propfirm });
80
+ // Initialize Market Data Feed (Rithmic TICKER_PLANT)
81
+ const marketFeed = new MarketDataFeed();
82
82
 
83
83
  // Log startup
84
84
  ui.addLog('info', `Strategy: ${strategyName}${supervisionEnabled ? ' + AI' : ''}`);
@@ -207,12 +207,14 @@ const executeAlgo = async ({ service, account, contract, config, strategy: strat
207
207
  marketFeed.on('error', (err) => ui.addLog('error', `Market: ${err.message}`));
208
208
  marketFeed.on('disconnected', () => { stats.connected = false; ui.addLog('error', 'Market disconnected'); });
209
209
 
210
- // Connect to market data
210
+ // Connect to market data (Rithmic TICKER_PLANT)
211
211
  try {
212
- const token = service.token || service.getToken?.();
213
- const propfirmKey = (account.propfirm || 'topstep').toLowerCase().replace(/\s+/g, '_');
214
- await marketFeed.connect(token, propfirmKey, contractId);
215
- await marketFeed.subscribe(symbolName, contractId);
212
+ const rithmicCredentials = service.getRithmicCredentials?.();
213
+ if (!rithmicCredentials) {
214
+ throw new Error('Rithmic credentials not available');
215
+ }
216
+ await marketFeed.connect(rithmicCredentials);
217
+ await marketFeed.subscribe(symbolName, contract.exchange || 'CME');
216
218
  } catch (e) {
217
219
  ui.addLog('error', `Failed to connect: ${e.message}`);
218
220
  }
@@ -77,8 +77,8 @@ const launchCopyTrading = async (config) => {
77
77
  const strategy = new StrategyClass({ tickSize });
78
78
  strategy.initialize(contractId, tickSize);
79
79
 
80
- // Initialize Market Data Feed
81
- const marketFeed = new MarketDataFeed({ propfirm: leadAccount.propfirm });
80
+ // Initialize Market Data Feed (Rithmic TICKER_PLANT)
81
+ const marketFeed = new MarketDataFeed();
82
82
 
83
83
  // Log startup
84
84
  ui.addLog('info', `Lead: ${leadName} | Followers: ${followers.length}`);
@@ -186,12 +186,14 @@ const launchCopyTrading = async (config) => {
186
186
  marketFeed.on('error', (err) => ui.addLog('error', `Market: ${err.message}`));
187
187
  marketFeed.on('disconnected', () => { stats.connected = false; ui.addLog('error', 'Disconnected'); });
188
188
 
189
- // Connect to market data
189
+ // Connect to market data (Rithmic TICKER_PLANT)
190
190
  try {
191
- const token = leadService.token || leadService.getToken?.();
192
- const pk = (leadAccount.propfirm || 'topstep').toLowerCase().replace(/\s+/g, '_');
193
- await marketFeed.connect(token, pk, contractId);
194
- await marketFeed.subscribe(symbolName, contractId);
191
+ const rithmicCredentials = leadService.getRithmicCredentials?.();
192
+ if (!rithmicCredentials) {
193
+ throw new Error('Rithmic credentials not available');
194
+ }
195
+ await marketFeed.connect(rithmicCredentials);
196
+ await marketFeed.subscribe(symbolName, contract.exchange || 'CME');
195
197
  } catch (e) {
196
198
  ui.addLog('error', `Connect failed: ${e.message}`);
197
199
  }
@@ -40,7 +40,7 @@ const copyTradingMenu = async () => {
40
40
  return;
41
41
  }
42
42
 
43
- // Filter active accounts: status === 0 (ProjectX) OR status === 'active' (Rithmic) OR no status
43
+ // Filter active accounts: status === 'active' (Rithmic) OR status === 0 OR no status
44
44
  const activeAccounts = allAccounts.filter(acc =>
45
45
  acc.status === 0 || acc.status === 'active' || acc.status === undefined || acc.status === null
46
46
  );
@@ -46,7 +46,7 @@ const customStrategyMenu = async (service) => {
46
46
 
47
47
  if (!allAccounts?.length) { spinner.fail('No accounts found'); await prompts.waitForEnter(); return; }
48
48
 
49
- // Filter active accounts: status === 0 (ProjectX) OR status === 'active' (Rithmic) OR no status
49
+ // Filter active accounts: status === 'active' (Rithmic) OR status === 0 OR no status
50
50
  const activeAccounts = allAccounts.filter(acc =>
51
51
  acc.status === 0 || acc.status === 'active' || acc.status === undefined || acc.status === null
52
52
  );
@@ -41,7 +41,7 @@ const oneAccountMenu = async (service) => {
41
41
  return;
42
42
  }
43
43
 
44
- // Filter active accounts: status === 0 (ProjectX) OR status === 'active' (Rithmic) OR no status
44
+ // Filter active accounts: status === 'active' (Rithmic) OR status === 0 OR no status
45
45
  const activeAccounts = allAccounts.filter(acc =>
46
46
  acc.status === 0 || acc.status === 'active' || acc.status === undefined || acc.status === null
47
47
  );