hedgequantx 2.9.48 → 2.9.50

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.50",
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,259 @@
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`;
40
+ async connect(rithmicCredentials) {
41
+ if (this.connected) return;
42
+
43
+ if (!rithmicCredentials || !rithmicCredentials.userId || !rithmicCredentials.password) {
44
+ throw new Error('Rithmic credentials required (userId, password, systemName, gateway)');
75
45
  }
76
-
77
- // Fallback for topstep
78
- return 'https://rtc.topstepx.com/hubs/market';
79
- }
80
46
 
81
- /**
82
- * Connect to market data hub
83
- */
84
- async connect(token, propfirmKey, contractId = null) {
85
- if (this.connected) return;
47
+ this.credentials = rithmicCredentials;
48
+ this.connection = new RithmicConnection();
49
+
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
+ };
86
58
 
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
59
  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');
60
+ await this.connection.connect(this.config);
121
61
 
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;
129
- }
130
- }
62
+ // Setup message handler for market data
63
+ this.connection.on('message', (msg) => this._handleMessage(msg));
131
64
 
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
- });
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);
184
70
 
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
- });
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
+ this.emit('subscribed', `${symbol}:${exchange}`);
117
+ } catch (e) {
118
+ this.emit('error', e);
119
+ throw e;
251
120
  }
252
121
  }
253
122
 
254
123
  /**
255
- * Unsubscribe from contract
124
+ * Unsubscribe from market data for a symbol
256
125
  */
257
- async unsubscribe(contractId) {
258
- if (!this.connection || !this.connected) return;
126
+ async unsubscribe(symbol, exchange = 'CME') {
127
+ if (!this.connected) return;
128
+
129
+ const key = `${symbol}:${exchange}`;
130
+ if (!this.subscriptions.has(key)) return;
259
131
 
260
132
  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
133
+ this.connection.send('RequestMarketDataUpdate', {
134
+ templateId: REQ.MARKET_DATA,
135
+ symbol: symbol,
136
+ exchange: exchange,
137
+ request: 2, // UNSUBSCRIBE
138
+ });
139
+
140
+ this.subscriptions.delete(key);
141
+ } catch (e) {
142
+ // Ignore unsubscribe errors
268
143
  }
269
144
  }
270
145
 
271
146
  /**
272
- * Resubscribe after reconnect
147
+ * Disconnect from TICKER_PLANT
273
148
  */
274
- async _resubscribeAll() {
275
- for (const contractId of this.subscriptions) {
149
+ async disconnect() {
150
+ // Unsubscribe from all
151
+ for (const key of this.subscriptions) {
152
+ const [symbol, exchange] = key.split(':');
153
+ await this.unsubscribe(symbol, exchange);
154
+ }
155
+
156
+ if (this.connection) {
276
157
  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
158
+ await this.connection.disconnect();
159
+ } catch (e) {
160
+ // Ignore
282
161
  }
283
162
  }
163
+
164
+ this.connection = null;
165
+ this.connected = false;
166
+ this.subscriptions.clear();
167
+ this.emit('disconnected');
284
168
  }
285
169
 
286
170
  /**
287
- * Disconnect with timeout to prevent hanging
171
+ * Handle incoming messages from TICKER_PLANT
288
172
  */
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();
173
+ _handleMessage(msg) {
174
+ const { templateId, data } = msg;
175
+
176
+ // Debug: log all message template IDs
177
+ if (!this._seenTemplates) this._seenTemplates = new Set();
178
+ if (!this._seenTemplates.has(templateId)) {
179
+ this._seenTemplates.add(templateId);
180
+ this.emit('debug', `New message type: templateId=${templateId}`);
181
+ }
182
+
183
+ switch (templateId) {
184
+ case RES.MARKET_DATA:
185
+ // Subscription confirmed
186
+ this.emit('debug', 'Market data subscription confirmed');
187
+ break;
188
+
189
+ case STREAM.LAST_TRADE:
190
+ this._handleLastTrade(data);
191
+ break;
192
+
193
+ case STREAM.BBO:
194
+ this._handleBBO(data);
195
+ break;
196
+
197
+ default:
198
+ // Log unknown messages for debugging
199
+ if (templateId && !this._seenTemplates.has(`logged_${templateId}`)) {
200
+ this._seenTemplates.add(`logged_${templateId}`);
201
+ }
202
+ break;
311
203
  }
312
204
  }
313
205
 
314
206
  /**
315
- * Check connection status
207
+ * Handle LastTrade message
316
208
  */
317
- isConnected() {
318
- return this.connected && this.connection?.state === 'Connected';
209
+ _handleLastTrade(data) {
210
+ try {
211
+ const trade = proto.decode('LastTrade', data);
212
+
213
+ const tick = {
214
+ type: 'trade',
215
+ symbol: trade.symbol,
216
+ exchange: trade.exchange,
217
+ price: trade.tradePrice,
218
+ size: trade.tradeSize,
219
+ volume: trade.volume,
220
+ side: trade.aggressor === 1 ? 'buy' : trade.aggressor === 2 ? 'sell' : 'unknown',
221
+ timestamp: Date.now(),
222
+ ssboe: trade.ssboe,
223
+ usecs: trade.usecs,
224
+ };
225
+
226
+ this.emit('tick', tick);
227
+ } catch (e) {
228
+ // Ignore decode errors
229
+ }
319
230
  }
320
231
 
321
232
  /**
322
- * Get active subscriptions
233
+ * Handle BestBidOffer message
323
234
  */
324
- getSubscriptions() {
325
- return Array.from(this.subscriptions);
235
+ _handleBBO(data) {
236
+ try {
237
+ const bbo = proto.decode('BestBidOffer', data);
238
+
239
+ const tick = {
240
+ type: 'quote',
241
+ symbol: bbo.symbol,
242
+ exchange: bbo.exchange,
243
+ bid: bbo.bidPrice,
244
+ bidSize: bbo.bidSize,
245
+ ask: bbo.askPrice,
246
+ askSize: bbo.askSize,
247
+ price: bbo.bidPrice && bbo.askPrice ? (bbo.bidPrice + bbo.askPrice) / 2 : null,
248
+ timestamp: Date.now(),
249
+ ssboe: bbo.ssboe,
250
+ usecs: bbo.usecs,
251
+ };
252
+
253
+ this.emit('tick', tick);
254
+ } catch (e) {
255
+ // Ignore decode errors
256
+ }
326
257
  }
327
258
  }
328
259
 
@@ -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' : ''}`);
@@ -93,7 +93,20 @@ const executeAlgo = async ({ service, account, contract, config, strategy: strat
93
93
 
94
94
  // Handle strategy signals
95
95
  strategy.on('signal', async (signal) => {
96
- if (!running || pendingOrder || currentPosition !== 0) return;
96
+ ui.addLog('info', `SIGNAL DETECTED: ${signal.direction?.toUpperCase()}`);
97
+
98
+ if (!running) {
99
+ ui.addLog('info', 'Signal ignored: not running');
100
+ return;
101
+ }
102
+ if (pendingOrder) {
103
+ ui.addLog('info', 'Signal ignored: order pending');
104
+ return;
105
+ }
106
+ if (currentPosition !== 0) {
107
+ ui.addLog('info', `Signal ignored: position open (${currentPosition})`);
108
+ return;
109
+ }
97
110
 
98
111
  let { direction, entry, stopLoss, takeProfit, confidence } = signal;
99
112
  let orderSize = contracts;
@@ -185,34 +198,96 @@ const executeAlgo = async ({ service, account, contract, config, strategy: strat
185
198
  });
186
199
 
187
200
  // Handle market data ticks
201
+ let lastPrice = null;
202
+ let lastBid = null;
203
+ let lastAsk = null;
204
+ let ticksPerSecond = 0;
205
+ let lastTickSecond = Math.floor(Date.now() / 1000);
206
+
188
207
  marketFeed.on('tick', (tick) => {
189
208
  tickCount++;
190
209
  const latencyStart = Date.now();
210
+ const currentSecond = Math.floor(Date.now() / 1000);
211
+
212
+ // Count ticks per second
213
+ if (currentSecond === lastTickSecond) {
214
+ ticksPerSecond++;
215
+ } else {
216
+ ticksPerSecond = 1;
217
+ lastTickSecond = currentSecond;
218
+ }
191
219
 
192
220
  aiContext.recentTicks.push(tick);
193
221
  if (aiContext.recentTicks.length > aiContext.maxTicks) aiContext.recentTicks.shift();
194
222
 
223
+ // Smart logs for tick flow
224
+ const price = tick.price || tick.tradePrice;
225
+ const bid = tick.bid || tick.bidPrice;
226
+ const ask = tick.ask || tick.askPrice;
227
+
228
+ // Log first tick
229
+ if (tickCount === 1) {
230
+ ui.addLog('info', `First tick received @ ${price?.toFixed(2) || 'N/A'}`);
231
+ ui.addLog('info', `Tick type: ${tick.type || 'unknown'}`);
232
+ }
233
+
234
+ // Log price changes
235
+ if (price && lastPrice && price !== lastPrice) {
236
+ const direction = price > lastPrice ? 'UP' : 'DOWN';
237
+ const change = Math.abs(price - lastPrice).toFixed(2);
238
+ if (tickCount <= 10 || tickCount % 50 === 0) {
239
+ ui.addLog('info', `Price ${direction} ${change} -> ${price.toFixed(2)}`);
240
+ }
241
+ }
242
+
243
+ // Log bid/ask spread
244
+ if (bid && ask && (bid !== lastBid || ask !== lastAsk)) {
245
+ const spread = (ask - bid).toFixed(2);
246
+ if (tickCount <= 5) {
247
+ ui.addLog('info', `Spread: ${spread} (Bid: ${bid.toFixed(2)} / Ask: ${ask.toFixed(2)})`);
248
+ }
249
+ }
250
+
251
+ lastPrice = price;
252
+ lastBid = bid;
253
+ lastAsk = ask;
254
+
195
255
  strategy.processTick({
196
256
  contractId: tick.contractId || contractId,
197
- price: tick.price, bid: tick.bid, ask: tick.ask,
198
- volume: tick.volume || 1, side: tick.lastTradeSide || 'unknown',
257
+ price: price, bid: bid, ask: ask,
258
+ volume: tick.volume || tick.size || 1,
259
+ side: tick.side || tick.lastTradeSide || 'unknown',
199
260
  timestamp: tick.timestamp || Date.now()
200
261
  });
201
262
 
202
263
  stats.latency = Date.now() - latencyStart;
203
- if (tickCount % 100 === 0) ui.addLog('info', `Tick #${tickCount} @ ${tick.price?.toFixed(2) || 'N/A'}`);
264
+
265
+ // Periodic status logs
266
+ if (tickCount === 10) ui.addLog('info', `Receiving ticks... (${ticksPerSecond}/sec)`);
267
+ if (tickCount === 50) ui.addLog('info', `50 ticks processed, strategy analyzing...`);
268
+ if (tickCount % 200 === 0) {
269
+ ui.addLog('info', `Tick #${tickCount} @ ${price?.toFixed(2) || 'N/A'} | ${ticksPerSecond}/sec`);
270
+ }
204
271
  });
205
272
 
206
- marketFeed.on('connected', () => { stats.connected = true; ui.addLog('connected', 'Market data connected!'); });
273
+ marketFeed.on('connected', () => {
274
+ stats.connected = true;
275
+ ui.addLog('connected', 'Market data connected!');
276
+ ui.addLog('info', 'Subscribing to market data...');
277
+ });
278
+ marketFeed.on('subscribed', (symbol) => ui.addLog('info', `Subscribed to ${symbol}`));
279
+ marketFeed.on('debug', (msg) => ui.addLog('info', msg));
207
280
  marketFeed.on('error', (err) => ui.addLog('error', `Market: ${err.message}`));
208
281
  marketFeed.on('disconnected', () => { stats.connected = false; ui.addLog('error', 'Market disconnected'); });
209
282
 
210
- // Connect to market data
283
+ // Connect to market data (Rithmic TICKER_PLANT)
211
284
  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);
285
+ const rithmicCredentials = service.getRithmicCredentials?.();
286
+ if (!rithmicCredentials) {
287
+ throw new Error('Rithmic credentials not available');
288
+ }
289
+ await marketFeed.connect(rithmicCredentials);
290
+ await marketFeed.subscribe(symbolName, contract.exchange || 'CME');
216
291
  } catch (e) {
217
292
  ui.addLog('error', `Failed to connect: ${e.message}`);
218
293
  }
@@ -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
  );