hedgequantx 2.9.142 → 2.9.144

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.142",
3
+ "version": "2.9.144",
4
4
  "description": "HedgeQuantX - Prop Futures Trading CLI",
5
5
  "main": "src/app.js",
6
6
  "bin": {
@@ -214,15 +214,25 @@ const SYMBOLS = {
214
214
  // S&P
215
215
  ES: { name: 'ES', floor: 'Spooz', asset: 'index', slang: 'the Spooz' },
216
216
  MES: { name: 'MES', floor: 'Micro S&P', asset: 'index', slang: 'micro spooz' },
217
+ // Dow
218
+ YM: { name: 'YM', floor: 'Dow', asset: 'index', slang: 'the Dow' },
219
+ MYM: { name: 'MYM', floor: 'Micro Dow', asset: 'index', slang: 'mini Dow' },
217
220
  // Crude
218
221
  CL: { name: 'CL', floor: 'Crude', asset: 'energy', slang: 'oil' },
219
222
  MCL: { name: 'MCL', floor: 'Micro Crude', asset: 'energy', slang: 'micro crude' },
220
223
  // Gold
221
224
  GC: { name: 'GC', floor: 'Gold', asset: 'metals', slang: 'yellow metal' },
222
225
  MGC: { name: 'MGC', floor: 'Micro Gold', asset: 'metals', slang: 'micro gold' },
226
+ '1OZ': { name: '1OZ', floor: 'Micro Gold', asset: 'metals', slang: '1oz gold' },
227
+ // Silver
228
+ SI: { name: 'SI', floor: 'Silver', asset: 'metals', slang: 'silver' },
229
+ SIL: { name: 'SIL', floor: 'Micro Silver', asset: 'metals', slang: 'micro silver' },
223
230
  // Bonds
224
231
  ZB: { name: 'ZB', floor: 'Bonds', asset: 'rates', slang: 'long bond' },
225
232
  ZN: { name: 'ZN', floor: '10Y', asset: 'rates', slang: 'tens' },
233
+ // Russell
234
+ RTY: { name: 'RTY', floor: 'Russell', asset: 'index', slang: 'small caps' },
235
+ M2K: { name: 'M2K', floor: 'Micro Russell', asset: 'index', slang: 'micro russell' },
226
236
  // Default
227
237
  DEFAULT: { name: 'Contract', floor: 'Futures', asset: 'futures', slang: 'contract' }
228
238
  };
@@ -6,7 +6,7 @@ const chalk = require('chalk');
6
6
  const ora = require('ora');
7
7
 
8
8
  const { connections } = require('../services');
9
- const { RithmicService } = require('../services/rithmic');
9
+ const { RithmicBrokerClient, manager: brokerManager } = require('../services/rithmic-broker');
10
10
  const { PROPFIRM_CHOICES } = require('../config');
11
11
  const { getLogoWidth, centerText, prepareStdin, displayBanner , clearScreen } = require('../ui');
12
12
  const { validateUsername, validatePassword } = require('../security');
@@ -87,19 +87,29 @@ const rithmicMenu = async () => {
87
87
  const credentials = await loginPrompt(selectedPropfirm.name);
88
88
  if (!credentials) return null;
89
89
 
90
- const spinner = ora({ text: 'CONNECTING TO RITHMIC...', color: 'yellow' }).start();
90
+ const spinner = ora({ text: 'STARTING BROKER DAEMON...', color: 'yellow' }).start();
91
91
 
92
92
  try {
93
- const service = new RithmicService(selectedPropfirm.key);
94
- const result = await service.login(credentials.username, credentials.password);
93
+ // Ensure broker daemon is running
94
+ const daemonResult = await brokerManager.ensureRunning();
95
+ if (!daemonResult.success) {
96
+ spinner.fail('FAILED TO START BROKER DAEMON');
97
+ console.log(chalk.yellow(` → ${daemonResult.error}`));
98
+ await new Promise(r => setTimeout(r, 3000));
99
+ return null;
100
+ }
101
+
102
+ spinner.text = 'CONNECTING TO RITHMIC...';
103
+ const client = new RithmicBrokerClient(selectedPropfirm.key);
104
+ const result = await client.login(credentials.username, credentials.password);
95
105
 
96
106
  if (result.success) {
97
107
  spinner.text = 'FETCHING ACCOUNTS...';
98
- const accResult = await service.getTradingAccounts();
99
- connections.add('rithmic', service, service.propfirm.name);
100
- spinner.succeed(`CONNECTED TO ${service.propfirm.name.toUpperCase()} (${accResult.accounts?.length || 0} ACCOUNTS)`);
108
+ const accResult = await client.getTradingAccounts();
109
+ connections.add('rithmic', client, client.propfirm.name || selectedPropfirm.name);
110
+ spinner.succeed(`CONNECTED TO ${selectedPropfirm.name.toUpperCase()} (${accResult.accounts?.length || 0} ACCOUNTS)`);
101
111
  await new Promise(r => setTimeout(r, 1500));
102
- return service;
112
+ return client;
103
113
  } else {
104
114
  // Detailed error messages for common Rithmic issues
105
115
  const err = (result.error || '').toLowerCase();
@@ -358,32 +358,36 @@ const executeAlgo = async ({ service, account, contract, config, strategy: strat
358
358
  ui.addLog('error', `Failed to connect: ${e.message}`);
359
359
  }
360
360
 
361
+ // P&L polling - uses CACHED data (NO API CALLS to avoid Rithmic rate limits)
361
362
  const pollPnL = async () => {
362
363
  try {
363
- const accountResult = await service.getTradingAccounts();
364
- if (accountResult.success && accountResult.accounts) {
365
- const acc = accountResult.accounts.find(a => a.accountId === account.accountId);
366
- if (acc && acc.profitAndLoss !== undefined) {
367
- if (startingPnL === null) startingPnL = acc.profitAndLoss;
368
- stats.pnl = acc.profitAndLoss - startingPnL;
369
- if (stats.pnl !== 0) strategy.recordTradeResult(stats.pnl);
370
- }
364
+ // Get P&L from cache (no API call)
365
+ const accId = account.rithmicAccountId || account.accountId;
366
+ const pnlData = service.getAccountPnL ? service.getAccountPnL(accId) : null;
367
+
368
+ if (pnlData && pnlData.pnl !== null) {
369
+ if (startingPnL === null) startingPnL = pnlData.pnl;
370
+ stats.pnl = pnlData.pnl - startingPnL;
371
+ if (stats.pnl !== 0) strategy.recordTradeResult(stats.pnl);
371
372
  }
372
373
 
373
- const posResult = await service.getPositions(account.accountId);
374
- if (posResult.success && posResult.positions) {
375
- const pos = posResult.positions.find(p => {
376
- const sym = p.contractId || p.symbol || '';
377
- return sym.includes(contract.name) || sym.includes(contractId);
378
- });
379
-
380
- if (pos && pos.quantity !== 0) {
381
- currentPosition = pos.quantity;
382
- const pnl = pos.profitAndLoss || 0;
383
- if (pnl > 0) stats.wins = Math.max(stats.wins, 1);
384
- else if (pnl < 0) stats.losses = Math.max(stats.losses, 1);
385
- } else {
386
- currentPosition = 0;
374
+ // Check positions (less frequent - every 10s to reduce API calls)
375
+ if (Date.now() % 10000 < 2000) {
376
+ const posResult = await service.getPositions(accId);
377
+ if (posResult.success && posResult.positions) {
378
+ const pos = posResult.positions.find(p => {
379
+ const sym = p.contractId || p.symbol || '';
380
+ return sym.includes(contract.name) || sym.includes(contractId);
381
+ });
382
+
383
+ if (pos && pos.quantity !== 0) {
384
+ currentPosition = pos.quantity;
385
+ const pnl = pos.profitAndLoss || 0;
386
+ if (pnl > 0) stats.wins = Math.max(stats.wins, 1);
387
+ else if (pnl < 0) stats.losses = Math.max(stats.losses, 1);
388
+ } else {
389
+ currentPosition = 0;
390
+ }
387
391
  }
388
392
  }
389
393
 
@@ -282,24 +282,27 @@ const executeMultiSymbol = async ({ service, account, contracts, config, strateg
282
282
  ui.addLog('error', `Failed to connect: ${e.message}`);
283
283
  }
284
284
 
285
- // P&L polling
285
+ // P&L polling - uses CACHED data (NO API CALLS)
286
+ let startingPnL = null;
286
287
  const pollPnL = async () => {
287
288
  try {
288
- const accountResult = await service.getTradingAccounts();
289
- if (accountResult.success && accountResult.accounts) {
290
- const acc = accountResult.accounts.find(a => a.accountId === account.accountId);
291
- if (acc && acc.profitAndLoss !== undefined) {
292
- // For multi-symbol, we track total P&L
293
- globalStats.pnl = acc.profitAndLoss;
294
- }
289
+ // Get P&L from cache (no API call)
290
+ const accId = account.rithmicAccountId || account.accountId;
291
+ const pnlData = service.getAccountPnL ? service.getAccountPnL(accId) : null;
292
+
293
+ if (pnlData && pnlData.pnl !== null) {
294
+ if (startingPnL === null) startingPnL = pnlData.pnl;
295
+ globalStats.pnl = pnlData.pnl - startingPnL;
295
296
  }
296
297
 
297
- // Check positions per symbol
298
- const posResult = await service.getPositions(account.accountId);
299
- if (posResult.success && posResult.positions) {
300
- for (const [sym, data] of symbolData) {
301
- const pos = posResult.positions.find(p => (p.contractId || p.symbol || '').includes(sym));
302
- data.stats.position = pos?.quantity || 0;
298
+ // Check positions (less frequent - every 10s instead of 2s)
299
+ if (Date.now() % 10000 < 2000) {
300
+ const posResult = await service.getPositions(accId);
301
+ if (posResult.success && posResult.positions) {
302
+ for (const [sym, data] of symbolData) {
303
+ const pos = posResult.positions.find(p => (p.contractId || p.symbol || '').includes(sym));
304
+ data.stats.position = pos?.quantity || 0;
305
+ }
303
306
  }
304
307
  }
305
308
 
@@ -269,6 +269,23 @@ class RithmicService extends EventEmitter {
269
269
 
270
270
  async getTradingAccounts() { return getTradingAccounts(this); }
271
271
  async getPositions() { return getPositions(this); }
272
+
273
+ /**
274
+ * Get cached P&L for an account (NO API CALL - from PNL_PLANT stream cache)
275
+ * @param {string} accountId - Account ID (rithmicAccountId)
276
+ * @returns {Object} { pnl, openPnl, closedPnl, balance }
277
+ */
278
+ getAccountPnL(accountId) {
279
+ const pnlData = this.accountPnL.get(accountId);
280
+ if (!pnlData) return { pnl: null, openPnl: null, closedPnl: null, balance: null };
281
+ return {
282
+ pnl: pnlData.dayPnl !== undefined ? pnlData.dayPnl :
283
+ ((pnlData.openPositionPnl || 0) + (pnlData.closedPositionPnl || 0)),
284
+ openPnl: pnlData.openPositionPnl || 0,
285
+ closedPnl: pnlData.closedPositionPnl || 0,
286
+ balance: pnlData.accountBalance || null,
287
+ };
288
+ }
272
289
  async getOrders() { return getOrders(this); }
273
290
  async getOrderHistory(date) { return getOrderHistory(this, date); }
274
291
  async getOrderHistoryDates() { return getOrderHistoryDates(this); }
@@ -381,44 +398,26 @@ class RithmicService extends EventEmitter {
381
398
  return { success: true, stats };
382
399
  }
383
400
 
384
- async getMarketStatus() {
385
- const status = this.checkMarketHours();
386
- return { success: true, isOpen: status.isOpen, message: status.message };
387
- }
388
-
401
+ async getMarketStatus() { return { success: true, ...this.checkMarketHours() }; }
389
402
  getToken() { return this.loginInfo ? 'connected' : null; }
390
403
  getPropfirm() { return this.propfirmKey || 'apex'; }
391
-
392
404
  getRithmicCredentials() {
393
405
  if (!this.credentials) return null;
394
- return {
395
- userId: this.credentials.username,
396
- password: this.credentials.password,
397
- systemName: this.propfirm.systemName,
398
- gateway: this.propfirm.gateway || RITHMIC_ENDPOINTS.CHICAGO,
399
- };
406
+ return { userId: this.credentials.username, password: this.credentials.password,
407
+ systemName: this.propfirm.systemName, gateway: this.propfirm.gateway || RITHMIC_ENDPOINTS.CHICAGO };
400
408
  }
401
409
 
402
- // ==================== MARKET HOURS ====================
403
-
404
410
  checkMarketHours() {
405
- const now = new Date();
406
- const utcDay = now.getUTCDay();
407
- const utcHour = now.getUTCHours();
408
-
411
+ const now = new Date(), utcDay = now.getUTCDay(), utcHour = now.getUTCHours();
409
412
  const isDST = now.getTimezoneOffset() < Math.max(
410
413
  new Date(now.getFullYear(), 0, 1).getTimezoneOffset(),
411
- new Date(now.getFullYear(), 6, 1).getTimezoneOffset()
412
- );
413
- const ctOffset = isDST ? 5 : 6;
414
- const ctHour = (utcHour - ctOffset + 24) % 24;
414
+ new Date(now.getFullYear(), 6, 1).getTimezoneOffset());
415
+ const ctOffset = isDST ? 5 : 6, ctHour = (utcHour - ctOffset + 24) % 24;
415
416
  const ctDay = utcHour < ctOffset ? (utcDay + 6) % 7 : utcDay;
416
-
417
417
  if (ctDay === 6) return { isOpen: false, message: 'Market closed (Saturday)' };
418
- if (ctDay === 0 && ctHour < 17) return { isOpen: false, message: 'Market opens Sunday 5:00 PM CT' };
419
- if (ctDay === 5 && ctHour >= 16) return { isOpen: false, message: 'Market closed (Friday after 4PM CT)' };
420
- if (ctHour === 16 && ctDay >= 1 && ctDay <= 4) return { isOpen: false, message: 'Daily maintenance (4:00-5:00 PM CT)' };
421
-
418
+ if (ctDay === 0 && ctHour < 17) return { isOpen: false, message: 'Market opens Sunday 5PM CT' };
419
+ if (ctDay === 5 && ctHour >= 16) return { isOpen: false, message: 'Market closed (Friday 4PM CT)' };
420
+ if (ctHour === 16 && ctDay >= 1 && ctDay <= 4) return { isOpen: false, message: 'Daily maintenance' };
422
421
  return { isOpen: true, message: 'Market is open' };
423
422
  }
424
423
 
@@ -0,0 +1,300 @@
1
+ /**
2
+ * RithmicBroker Client
3
+ *
4
+ * Client for CLI to communicate with the RithmicBroker daemon.
5
+ * Provides same interface as RithmicService for seamless integration.
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ const WebSocket = require('ws');
11
+ const EventEmitter = require('events');
12
+ const { BROKER_PORT } = require('./daemon');
13
+ const manager = require('./manager');
14
+
15
+ /**
16
+ * RithmicBroker Client - connects to daemon via WebSocket
17
+ */
18
+ class RithmicBrokerClient extends EventEmitter {
19
+ constructor(propfirmKey) {
20
+ super();
21
+ this.propfirmKey = propfirmKey;
22
+ this.ws = null;
23
+ this.connected = false;
24
+ this.requestId = 0;
25
+ this.pendingRequests = new Map();
26
+ this.credentials = null;
27
+ this.accounts = [];
28
+ this.propfirm = { name: propfirmKey };
29
+ }
30
+
31
+ /**
32
+ * Connect to daemon
33
+ */
34
+ async connect() {
35
+ if (this.connected) return { success: true };
36
+
37
+ // Ensure daemon is running
38
+ const daemonStatus = await manager.ensureRunning();
39
+ if (!daemonStatus.success) {
40
+ return { success: false, error: daemonStatus.error || 'Failed to start daemon' };
41
+ }
42
+
43
+ return new Promise((resolve) => {
44
+ this.ws = new WebSocket(`ws://127.0.0.1:${BROKER_PORT}`);
45
+
46
+ const timeout = setTimeout(() => {
47
+ this.ws?.terminate();
48
+ resolve({ success: false, error: 'Connection timeout' });
49
+ }, 5000);
50
+
51
+ this.ws.on('open', () => {
52
+ clearTimeout(timeout);
53
+ this.connected = true;
54
+ resolve({ success: true });
55
+ });
56
+
57
+ this.ws.on('message', (data) => this._handleMessage(data));
58
+
59
+ this.ws.on('close', () => {
60
+ this.connected = false;
61
+ this.emit('disconnected');
62
+ });
63
+
64
+ this.ws.on('error', (err) => {
65
+ clearTimeout(timeout);
66
+ this.connected = false;
67
+ resolve({ success: false, error: err.message });
68
+ });
69
+ });
70
+ }
71
+
72
+ /**
73
+ * Handle incoming message from daemon
74
+ */
75
+ _handleMessage(data) {
76
+ try {
77
+ const msg = JSON.parse(data.toString());
78
+
79
+ // Handle response to pending request
80
+ if (msg.requestId && this.pendingRequests.has(msg.requestId)) {
81
+ const { resolve } = this.pendingRequests.get(msg.requestId);
82
+ this.pendingRequests.delete(msg.requestId);
83
+ resolve(msg);
84
+ return;
85
+ }
86
+
87
+ // Handle broadcast events
88
+ if (msg.type === 'pnlUpdate') this.emit('pnlUpdate', msg.payload);
89
+ if (msg.type === 'positionUpdate') this.emit('positionUpdate', msg.payload);
90
+ if (msg.type === 'trade') this.emit('trade', msg.payload);
91
+ } catch (e) { /* ignore parse errors */ }
92
+ }
93
+
94
+ /**
95
+ * Send request to daemon and wait for response
96
+ */
97
+ async _request(type, payload = {}, timeout = 30000) {
98
+ if (!this.connected) {
99
+ const conn = await this.connect();
100
+ if (!conn.success) return { error: conn.error };
101
+ }
102
+
103
+ const requestId = String(++this.requestId);
104
+
105
+ return new Promise((resolve) => {
106
+ const timer = setTimeout(() => {
107
+ this.pendingRequests.delete(requestId);
108
+ resolve({ error: 'Request timeout' });
109
+ }, timeout);
110
+
111
+ this.pendingRequests.set(requestId, {
112
+ resolve: (msg) => {
113
+ clearTimeout(timer);
114
+ resolve(msg);
115
+ }
116
+ });
117
+
118
+ try {
119
+ this.ws.send(JSON.stringify({ type, payload, requestId }));
120
+ } catch (e) {
121
+ clearTimeout(timer);
122
+ this.pendingRequests.delete(requestId);
123
+ resolve({ error: e.message });
124
+ }
125
+ });
126
+ }
127
+
128
+ // ==================== RithmicService-compatible API ====================
129
+
130
+ /**
131
+ * Login to Rithmic via daemon
132
+ */
133
+ async login(username, password) {
134
+ const result = await this._request('login', {
135
+ propfirmKey: this.propfirmKey,
136
+ username,
137
+ password,
138
+ }, 60000);
139
+
140
+ if (result.error) {
141
+ return { success: false, error: result.error };
142
+ }
143
+
144
+ if (result.payload?.success) {
145
+ this.credentials = { username, password };
146
+ this.accounts = result.payload.accounts || [];
147
+ return { success: true, accounts: this.accounts, user: { userName: username } };
148
+ }
149
+
150
+ return { success: false, error: result.payload?.error || 'Login failed' };
151
+ }
152
+
153
+ /**
154
+ * Get trading accounts
155
+ */
156
+ async getTradingAccounts() {
157
+ const result = await this._request('getAccounts');
158
+ if (result.error) return { success: false, accounts: [] };
159
+
160
+ const accounts = (result.payload?.accounts || [])
161
+ .filter(a => a.propfirmKey === this.propfirmKey);
162
+
163
+ return { success: true, accounts };
164
+ }
165
+
166
+ /**
167
+ * Get cached P&L (NO API CALL - from daemon cache)
168
+ */
169
+ async getAccountPnL(accountId) {
170
+ const result = await this._request('getPnL', { accountId });
171
+ return result.payload || { pnl: null };
172
+ }
173
+
174
+ /**
175
+ * Get positions
176
+ */
177
+ async getPositions() {
178
+ const result = await this._request('getPositions', { propfirmKey: this.propfirmKey });
179
+ if (result.error) return { success: false, positions: [] };
180
+ return result.payload || { success: true, positions: [] };
181
+ }
182
+
183
+ /**
184
+ * Place order
185
+ */
186
+ async placeOrder(orderData) {
187
+ const result = await this._request('placeOrder', {
188
+ propfirmKey: this.propfirmKey,
189
+ orderData,
190
+ }, 15000);
191
+
192
+ if (result.error) return { success: false, error: result.error };
193
+ return result.payload || { success: false, error: 'No response' };
194
+ }
195
+
196
+ /**
197
+ * Cancel order
198
+ */
199
+ async cancelOrder(orderId) {
200
+ const result = await this._request('cancelOrder', {
201
+ propfirmKey: this.propfirmKey,
202
+ orderId,
203
+ });
204
+
205
+ if (result.error) return { success: false, error: result.error };
206
+ return result.payload || { success: false, error: 'No response' };
207
+ }
208
+
209
+ /**
210
+ * Get contracts
211
+ */
212
+ async getContracts() {
213
+ const result = await this._request('getContracts', { propfirmKey: this.propfirmKey });
214
+ if (result.error) return { success: false, contracts: [] };
215
+ return result.payload || { success: true, contracts: [] };
216
+ }
217
+
218
+ /**
219
+ * Search contracts
220
+ */
221
+ async searchContracts(searchText) {
222
+ const result = await this._request('searchContracts', {
223
+ propfirmKey: this.propfirmKey,
224
+ searchText,
225
+ });
226
+ if (result.error) return { success: false, contracts: [] };
227
+ return result.payload || { success: true, contracts: [] };
228
+ }
229
+
230
+ /**
231
+ * Get Rithmic credentials for MarketDataFeed
232
+ */
233
+ getRithmicCredentials() {
234
+ // Sync call - return cached credentials
235
+ // For async, use _request('getRithmicCredentials')
236
+ return this.credentials ? {
237
+ userId: this.credentials.username,
238
+ password: this.credentials.password,
239
+ systemName: this.propfirm?.systemName || 'Apex',
240
+ gateway: 'wss://rituz.rithmic.com:443',
241
+ } : null;
242
+ }
243
+
244
+ /**
245
+ * Get async Rithmic credentials from daemon
246
+ */
247
+ async getRithmicCredentialsAsync() {
248
+ const result = await this._request('getRithmicCredentials', { propfirmKey: this.propfirmKey });
249
+ return result.payload || null;
250
+ }
251
+
252
+ /**
253
+ * Disconnect from daemon (does NOT stop daemon)
254
+ */
255
+ disconnect() {
256
+ if (this.ws) {
257
+ this.ws.close();
258
+ this.ws = null;
259
+ }
260
+ this.connected = false;
261
+ this.credentials = null;
262
+ this.accounts = [];
263
+ }
264
+
265
+ /**
266
+ * Logout from Rithmic (stops daemon connection for this propfirm)
267
+ */
268
+ async logout() {
269
+ const result = await this._request('logout', { propfirmKey: this.propfirmKey });
270
+ this.disconnect();
271
+ return result.payload || { success: true };
272
+ }
273
+
274
+ // ==================== Compatibility methods ====================
275
+
276
+ getToken() { return this.connected ? 'broker-connected' : null; }
277
+ getPropfirm() { return this.propfirmKey; }
278
+ async getUser() { return { userName: this.credentials?.username }; }
279
+
280
+ checkMarketHours() {
281
+ const now = new Date();
282
+ const utcDay = now.getUTCDay();
283
+ const utcHour = now.getUTCHours();
284
+ const isDST = now.getTimezoneOffset() < Math.max(
285
+ new Date(now.getFullYear(), 0, 1).getTimezoneOffset(),
286
+ new Date(now.getFullYear(), 6, 1).getTimezoneOffset()
287
+ );
288
+ const ctOffset = isDST ? 5 : 6;
289
+ const ctHour = (utcHour - ctOffset + 24) % 24;
290
+ const ctDay = utcHour < ctOffset ? (utcDay + 6) % 7 : utcDay;
291
+
292
+ if (ctDay === 6) return { isOpen: false, message: 'Market closed (Saturday)' };
293
+ if (ctDay === 0 && ctHour < 17) return { isOpen: false, message: 'Market opens Sunday 5:00 PM CT' };
294
+ if (ctDay === 5 && ctHour >= 16) return { isOpen: false, message: 'Market closed (Friday after 4PM CT)' };
295
+ if (ctHour === 16 && ctDay >= 1 && ctDay <= 4) return { isOpen: false, message: 'Daily maintenance' };
296
+ return { isOpen: true, message: 'Market is open' };
297
+ }
298
+ }
299
+
300
+ module.exports = { RithmicBrokerClient };
@@ -0,0 +1,305 @@
1
+ /**
2
+ * RithmicBroker Daemon
3
+ *
4
+ * Background process that maintains persistent Rithmic connections.
5
+ * Survives CLI restarts/updates. Only stops on explicit logout or reboot.
6
+ *
7
+ * Communication: WebSocket server on port 18765
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ const WebSocket = require('ws');
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+ const os = require('os');
16
+
17
+ // Paths
18
+ const BROKER_DIR = path.join(os.homedir(), '.hqx', 'rithmic-broker');
19
+ const PID_FILE = path.join(BROKER_DIR, 'broker.pid');
20
+ const LOG_FILE = path.join(BROKER_DIR, 'broker.log');
21
+ const STATE_FILE = path.join(BROKER_DIR, 'state.json');
22
+ const BROKER_PORT = 18765;
23
+
24
+ // Lazy load RithmicService
25
+ let RithmicService = null;
26
+ const loadRithmicService = () => {
27
+ if (!RithmicService) {
28
+ ({ RithmicService } = require('../rithmic'));
29
+ }
30
+ return RithmicService;
31
+ };
32
+
33
+ // Logger
34
+ const log = (level, msg, data = {}) => {
35
+ const ts = new Date().toISOString();
36
+ const line = `[${ts}] [${level}] ${msg} ${JSON.stringify(data)}\n`;
37
+ try { fs.appendFileSync(LOG_FILE, line); } catch (e) { /* ignore */ }
38
+ if (process.env.HQX_DEBUG === '1') console.log(`[Broker] [${level}] ${msg}`, data);
39
+ };
40
+
41
+ /**
42
+ * RithmicBroker Daemon Class
43
+ */
44
+ class RithmicBrokerDaemon {
45
+ constructor() {
46
+ this.wss = null;
47
+ this.clients = new Set();
48
+ this.connections = new Map(); // propfirmKey -> { service, credentials, connectedAt, accounts }
49
+ this.pnlCache = new Map(); // accountId -> { pnl, openPnl, closedPnl, balance, updatedAt }
50
+ this.running = false;
51
+ }
52
+
53
+ async start() {
54
+ if (this.running) return;
55
+
56
+ if (!fs.existsSync(BROKER_DIR)) fs.mkdirSync(BROKER_DIR, { recursive: true });
57
+ fs.writeFileSync(PID_FILE, String(process.pid));
58
+
59
+ await this._restoreState();
60
+
61
+ this.wss = new WebSocket.Server({ port: BROKER_PORT, host: '127.0.0.1' });
62
+ this.wss.on('connection', (ws) => this._handleClient(ws));
63
+ this.wss.on('error', (err) => log('ERROR', 'WSS error', { error: err.message }));
64
+
65
+ this.running = true;
66
+ log('INFO', 'Daemon started', { pid: process.pid, port: BROKER_PORT });
67
+
68
+ process.on('SIGTERM', () => this.stop());
69
+ process.on('SIGINT', () => this.stop());
70
+ setInterval(() => this._saveState(), 30000);
71
+ }
72
+
73
+ async stop() {
74
+ log('INFO', 'Daemon stopping...');
75
+ this.running = false;
76
+
77
+ for (const [key, conn] of this.connections) {
78
+ try { if (conn.service?.disconnect) await conn.service.disconnect(); }
79
+ catch (e) { log('WARN', 'Disconnect error', { propfirm: key, error: e.message }); }
80
+ }
81
+ this.connections.clear();
82
+
83
+ if (this.wss) {
84
+ for (const client of this.clients) client.close(1000, 'Daemon shutting down');
85
+ this.wss.close();
86
+ }
87
+
88
+ if (fs.existsSync(PID_FILE)) fs.unlinkSync(PID_FILE);
89
+ if (fs.existsSync(STATE_FILE)) fs.unlinkSync(STATE_FILE);
90
+
91
+ log('INFO', 'Daemon stopped');
92
+ process.exit(0);
93
+ }
94
+
95
+ _handleClient(ws) {
96
+ this.clients.add(ws);
97
+ log('DEBUG', 'Client connected', { total: this.clients.size });
98
+
99
+ ws.on('message', async (data) => {
100
+ try {
101
+ const msg = JSON.parse(data.toString());
102
+ const response = await this._handleMessage(msg);
103
+ ws.send(JSON.stringify(response));
104
+ } catch (e) {
105
+ ws.send(JSON.stringify({ error: e.message, requestId: null }));
106
+ }
107
+ });
108
+
109
+ ws.on('close', () => { this.clients.delete(ws); });
110
+ ws.on('error', () => { this.clients.delete(ws); });
111
+ }
112
+
113
+ async _handleMessage(msg) {
114
+ const { type, payload = {}, requestId } = msg;
115
+
116
+ const handlers = {
117
+ ping: () => ({ type: 'pong', requestId }),
118
+ status: () => ({ type: 'status', payload: this._getStatus(), requestId }),
119
+ login: () => this._handleLogin(payload, requestId),
120
+ logout: () => this._handleLogout(payload, requestId),
121
+ getAccounts: () => this._handleGetAccounts(requestId),
122
+ getPnL: () => this._handleGetPnL(payload, requestId),
123
+ getPositions: () => this._handleGetPositions(payload, requestId),
124
+ placeOrder: () => this._handlePlaceOrder(payload, requestId),
125
+ cancelOrder: () => this._handleCancelOrder(payload, requestId),
126
+ getContracts: () => this._handleGetContracts(payload, requestId),
127
+ searchContracts: () => this._handleSearchContracts(payload, requestId),
128
+ getRithmicCredentials: () => this._handleGetCredentials(payload, requestId),
129
+ };
130
+
131
+ if (handlers[type]) {
132
+ try { return await handlers[type](); }
133
+ catch (e) { return { error: e.message, requestId }; }
134
+ }
135
+ return { error: `Unknown type: ${type}`, requestId };
136
+ }
137
+
138
+ _getStatus() {
139
+ const conns = [];
140
+ for (const [key, conn] of this.connections) {
141
+ conns.push({
142
+ propfirmKey: key,
143
+ propfirm: conn.service?.propfirm?.name || key,
144
+ connectedAt: conn.connectedAt,
145
+ accountCount: conn.accounts?.length || 0,
146
+ });
147
+ }
148
+ return { running: this.running, pid: process.pid, uptime: process.uptime(), connections: conns };
149
+ }
150
+
151
+ async _handleLogin(payload, requestId) {
152
+ const { propfirmKey, username, password } = payload;
153
+ if (!propfirmKey || !username || !password) {
154
+ return { error: 'Missing credentials', requestId };
155
+ }
156
+
157
+ // Already connected?
158
+ if (this.connections.has(propfirmKey)) {
159
+ const conn = this.connections.get(propfirmKey);
160
+ if (conn.service?.loginInfo) {
161
+ return { type: 'loginResult', payload: { success: true, accounts: conn.accounts, alreadyConnected: true }, requestId };
162
+ }
163
+ }
164
+
165
+ const Service = loadRithmicService();
166
+ const service = new Service(propfirmKey);
167
+
168
+ log('INFO', 'Logging in...', { propfirm: propfirmKey });
169
+ const result = await service.login(username, password);
170
+
171
+ if (result.success) {
172
+ this.connections.set(propfirmKey, {
173
+ service,
174
+ credentials: { username, password },
175
+ connectedAt: new Date().toISOString(),
176
+ accounts: result.accounts || [],
177
+ });
178
+ this._setupPnLUpdates(propfirmKey, service);
179
+ this._saveState();
180
+ log('INFO', 'Login successful', { propfirm: propfirmKey, accounts: result.accounts?.length });
181
+ return { type: 'loginResult', payload: { success: true, accounts: result.accounts }, requestId };
182
+ }
183
+
184
+ log('WARN', 'Login failed', { propfirm: propfirmKey, error: result.error });
185
+ return { type: 'loginResult', payload: { success: false, error: result.error }, requestId };
186
+ }
187
+
188
+ _setupPnLUpdates(propfirmKey, service) {
189
+ service.on('pnlUpdate', (pnl) => {
190
+ if (pnl.accountId) {
191
+ this.pnlCache.set(pnl.accountId, {
192
+ pnl: pnl.dayPnl || ((pnl.openPositionPnl || 0) + (pnl.closedPositionPnl || 0)),
193
+ openPnl: pnl.openPositionPnl || 0,
194
+ closedPnl: pnl.closedPositionPnl || 0,
195
+ balance: pnl.accountBalance || 0,
196
+ updatedAt: Date.now(),
197
+ });
198
+ }
199
+ this._broadcast({ type: 'pnlUpdate', payload: pnl });
200
+ });
201
+ service.on('positionUpdate', (pos) => this._broadcast({ type: 'positionUpdate', payload: pos }));
202
+ service.on('trade', (trade) => this._broadcast({ type: 'trade', payload: trade }));
203
+ }
204
+
205
+ _broadcast(msg) {
206
+ const data = JSON.stringify(msg);
207
+ for (const client of this.clients) {
208
+ if (client.readyState === WebSocket.OPEN) {
209
+ try { client.send(data); } catch (e) { /* ignore */ }
210
+ }
211
+ }
212
+ }
213
+
214
+ async _handleLogout(payload, requestId) {
215
+ const { propfirmKey } = payload;
216
+ if (propfirmKey) {
217
+ const conn = this.connections.get(propfirmKey);
218
+ if (conn?.service) { await conn.service.disconnect(); this.connections.delete(propfirmKey); }
219
+ } else {
220
+ await this.stop();
221
+ }
222
+ this._saveState();
223
+ return { type: 'logoutResult', payload: { success: true }, requestId };
224
+ }
225
+
226
+ async _handleGetAccounts(requestId) {
227
+ const allAccounts = [];
228
+ for (const [propfirmKey, conn] of this.connections) {
229
+ for (const acc of conn.accounts || []) {
230
+ allAccounts.push({ ...acc, propfirmKey, propfirm: conn.service.propfirm?.name || propfirmKey });
231
+ }
232
+ }
233
+ return { type: 'accounts', payload: { accounts: allAccounts }, requestId };
234
+ }
235
+
236
+ _handleGetPnL(payload, requestId) {
237
+ const cached = this.pnlCache.get(payload.accountId);
238
+ return { type: 'pnl', payload: cached || { pnl: null }, requestId };
239
+ }
240
+
241
+ async _handleGetPositions(payload, requestId) {
242
+ const conn = this.connections.get(payload.propfirmKey);
243
+ if (!conn?.service) return { error: 'Not connected', requestId };
244
+ return { type: 'positions', payload: await conn.service.getPositions(), requestId };
245
+ }
246
+
247
+ async _handlePlaceOrder(payload, requestId) {
248
+ const conn = this.connections.get(payload.propfirmKey);
249
+ if (!conn?.service) return { error: 'Not connected', requestId };
250
+ return { type: 'orderResult', payload: await conn.service.placeOrder(payload.orderData), requestId };
251
+ }
252
+
253
+ async _handleCancelOrder(payload, requestId) {
254
+ const conn = this.connections.get(payload.propfirmKey);
255
+ if (!conn?.service) return { error: 'Not connected', requestId };
256
+ return { type: 'cancelResult', payload: await conn.service.cancelOrder(payload.orderId), requestId };
257
+ }
258
+
259
+ async _handleGetContracts(payload, requestId) {
260
+ const conn = this.connections.get(payload.propfirmKey);
261
+ if (!conn?.service) return { error: 'Not connected', requestId };
262
+ return { type: 'contracts', payload: await conn.service.getContracts(), requestId };
263
+ }
264
+
265
+ async _handleSearchContracts(payload, requestId) {
266
+ const conn = this.connections.get(payload.propfirmKey);
267
+ if (!conn?.service) return { error: 'Not connected', requestId };
268
+ return { type: 'searchResults', payload: await conn.service.searchContracts(payload.searchText), requestId };
269
+ }
270
+
271
+ _handleGetCredentials(payload, requestId) {
272
+ const conn = this.connections.get(payload.propfirmKey);
273
+ if (!conn?.service) return { error: 'Not connected', requestId };
274
+ return { type: 'credentials', payload: conn.service.getRithmicCredentials?.() || null, requestId };
275
+ }
276
+
277
+ _saveState() {
278
+ const state = { connections: [] };
279
+ for (const [key, conn] of this.connections) {
280
+ if (conn.credentials) state.connections.push({ propfirmKey: key, credentials: conn.credentials });
281
+ }
282
+ try { fs.writeFileSync(STATE_FILE, JSON.stringify(state)); } catch (e) { /* ignore */ }
283
+ }
284
+
285
+ async _restoreState() {
286
+ if (!fs.existsSync(STATE_FILE)) return;
287
+ try {
288
+ const data = JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'));
289
+ for (const conn of data.connections || []) {
290
+ if (conn.credentials && conn.propfirmKey) {
291
+ log('INFO', 'Restoring connection...', { propfirm: conn.propfirmKey });
292
+ await this._handleLogin({ ...conn.credentials, propfirmKey: conn.propfirmKey }, null);
293
+ }
294
+ }
295
+ } catch (e) { log('WARN', 'Restore failed', { error: e.message }); }
296
+ }
297
+ }
298
+
299
+ // Main entry point
300
+ if (require.main === module) {
301
+ const daemon = new RithmicBrokerDaemon();
302
+ daemon.start().catch((e) => { console.error('Daemon failed:', e.message); process.exit(1); });
303
+ }
304
+
305
+ module.exports = { RithmicBrokerDaemon, BROKER_PORT, BROKER_DIR, PID_FILE, LOG_FILE, STATE_FILE };
@@ -0,0 +1,46 @@
1
+ /**
2
+ * RithmicBroker Service
3
+ *
4
+ * Persistent Rithmic connection manager.
5
+ * Daemon runs in background, survives CLI restarts.
6
+ *
7
+ * Usage:
8
+ * const { RithmicBrokerClient, manager } = require('./rithmic-broker');
9
+ *
10
+ * // Start daemon if not running
11
+ * await manager.ensureRunning();
12
+ *
13
+ * // Create client (same API as RithmicService)
14
+ * const client = new RithmicBrokerClient('apex');
15
+ * await client.login(username, password);
16
+ *
17
+ * // Use like RithmicService
18
+ * const accounts = await client.getTradingAccounts();
19
+ */
20
+
21
+ 'use strict';
22
+
23
+ const { RithmicBrokerClient } = require('./client');
24
+ const manager = require('./manager');
25
+ const { BROKER_PORT, BROKER_DIR, PID_FILE, LOG_FILE, STATE_FILE } = require('./daemon');
26
+
27
+ module.exports = {
28
+ // Client class (use instead of RithmicService)
29
+ RithmicBrokerClient,
30
+
31
+ // Manager functions
32
+ manager,
33
+ isRunning: manager.isRunning,
34
+ start: manager.start,
35
+ stop: manager.stop,
36
+ getStatus: manager.getStatus,
37
+ ensureRunning: manager.ensureRunning,
38
+ restart: manager.restart,
39
+
40
+ // Constants
41
+ BROKER_PORT,
42
+ BROKER_DIR,
43
+ PID_FILE,
44
+ LOG_FILE,
45
+ STATE_FILE,
46
+ };
@@ -0,0 +1,256 @@
1
+ /**
2
+ * RithmicBroker Manager
3
+ *
4
+ * Start/stop/status functions for the RithmicBroker daemon.
5
+ * Similar pattern to cliproxy/manager.js
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ const { spawn } = require('child_process');
11
+ const path = require('path');
12
+ const fs = require('fs');
13
+ const http = require('http');
14
+ const WebSocket = require('ws');
15
+
16
+ const { BROKER_PORT, BROKER_DIR, PID_FILE, LOG_FILE } = require('./daemon');
17
+
18
+ // Path to daemon script
19
+ const DAEMON_SCRIPT = path.join(__dirname, 'daemon.js');
20
+
21
+ /**
22
+ * Check if daemon is running
23
+ * @returns {Promise<{running: boolean, pid: number|null}>}
24
+ */
25
+ const isRunning = async () => {
26
+ // Check PID file first
27
+ if (fs.existsSync(PID_FILE)) {
28
+ try {
29
+ const pid = parseInt(fs.readFileSync(PID_FILE, 'utf8').trim(), 10);
30
+ process.kill(pid, 0); // Test if process exists
31
+ return { running: true, pid };
32
+ } catch (e) {
33
+ // Process doesn't exist, clean up stale PID file
34
+ try { fs.unlinkSync(PID_FILE); } catch (e2) { /* ignore */ }
35
+ }
36
+ }
37
+
38
+ // Try connecting to WebSocket
39
+ return new Promise((resolve) => {
40
+ const ws = new WebSocket(`ws://127.0.0.1:${BROKER_PORT}`);
41
+ const timeout = setTimeout(() => {
42
+ ws.terminate();
43
+ resolve({ running: false, pid: null });
44
+ }, 2000);
45
+
46
+ ws.on('open', () => {
47
+ clearTimeout(timeout);
48
+ ws.close();
49
+ resolve({ running: true, pid: null });
50
+ });
51
+
52
+ ws.on('error', () => {
53
+ clearTimeout(timeout);
54
+ resolve({ running: false, pid: null });
55
+ });
56
+ });
57
+ };
58
+
59
+ /**
60
+ * Start the daemon
61
+ * @returns {Promise<{success: boolean, error: string|null, pid: number|null}>}
62
+ */
63
+ const start = async () => {
64
+ const status = await isRunning();
65
+ if (status.running) {
66
+ return { success: true, error: null, pid: status.pid, alreadyRunning: true };
67
+ }
68
+
69
+ // Ensure directory exists
70
+ if (!fs.existsSync(BROKER_DIR)) {
71
+ fs.mkdirSync(BROKER_DIR, { recursive: true });
72
+ }
73
+
74
+ try {
75
+ // Open log file for daemon output
76
+ const logFd = fs.openSync(LOG_FILE, 'a');
77
+
78
+ // Spawn detached daemon process
79
+ const child = spawn(process.execPath, [DAEMON_SCRIPT], {
80
+ detached: true,
81
+ stdio: ['ignore', logFd, logFd],
82
+ cwd: BROKER_DIR,
83
+ env: { ...process.env, HQX_BROKER_DAEMON: '1' },
84
+ });
85
+
86
+ child.unref();
87
+ fs.closeSync(logFd);
88
+
89
+ // Wait for daemon to start
90
+ await new Promise(r => setTimeout(r, 2000));
91
+
92
+ const runStatus = await isRunning();
93
+ if (runStatus.running) {
94
+ return { success: true, error: null, pid: runStatus.pid || child.pid };
95
+ } else {
96
+ // Read log for error details
97
+ let errorDetail = 'Failed to start RithmicBroker daemon';
98
+ if (fs.existsSync(LOG_FILE)) {
99
+ const log = fs.readFileSync(LOG_FILE, 'utf8').slice(-500);
100
+ if (log) errorDetail += `: ${log.split('\n').filter(l => l).pop()}`;
101
+ }
102
+ return { success: false, error: errorDetail, pid: null };
103
+ }
104
+ } catch (error) {
105
+ return { success: false, error: error.message, pid: null };
106
+ }
107
+ };
108
+
109
+ /**
110
+ * Stop the daemon
111
+ * @returns {Promise<{success: boolean, error: string|null}>}
112
+ */
113
+ const stop = async () => {
114
+ const status = await isRunning();
115
+ if (!status.running) {
116
+ return { success: true, error: null };
117
+ }
118
+
119
+ try {
120
+ // Try graceful shutdown via WebSocket
121
+ const ws = new WebSocket(`ws://127.0.0.1:${BROKER_PORT}`);
122
+
123
+ await new Promise((resolve, reject) => {
124
+ const timeout = setTimeout(() => {
125
+ ws.terminate();
126
+ reject(new Error('Shutdown timeout'));
127
+ }, 5000);
128
+
129
+ ws.on('open', () => {
130
+ ws.send(JSON.stringify({ type: 'logout', payload: {}, requestId: 'shutdown' }));
131
+ setTimeout(() => {
132
+ clearTimeout(timeout);
133
+ ws.close();
134
+ resolve();
135
+ }, 1000);
136
+ });
137
+
138
+ ws.on('error', () => {
139
+ clearTimeout(timeout);
140
+ reject(new Error('Connection failed'));
141
+ });
142
+ });
143
+
144
+ // Wait for process to exit
145
+ await new Promise(r => setTimeout(r, 1000));
146
+
147
+ // Verify stopped
148
+ const newStatus = await isRunning();
149
+ if (!newStatus.running) {
150
+ return { success: true, error: null };
151
+ }
152
+
153
+ // Force kill if still running
154
+ if (status.pid) {
155
+ try {
156
+ process.kill(status.pid, 'SIGKILL');
157
+ } catch (e) { /* ignore */ }
158
+ }
159
+
160
+ // Clean up PID file
161
+ if (fs.existsSync(PID_FILE)) {
162
+ try { fs.unlinkSync(PID_FILE); } catch (e) { /* ignore */ }
163
+ }
164
+
165
+ return { success: true, error: null };
166
+ } catch (error) {
167
+ // Force kill via PID
168
+ if (status.pid) {
169
+ try {
170
+ process.kill(status.pid, 'SIGKILL');
171
+ if (fs.existsSync(PID_FILE)) fs.unlinkSync(PID_FILE);
172
+ return { success: true, error: null };
173
+ } catch (e) { /* ignore */ }
174
+ }
175
+ return { success: false, error: error.message };
176
+ }
177
+ };
178
+
179
+ /**
180
+ * Get daemon status
181
+ * @returns {Promise<Object>}
182
+ */
183
+ const getStatus = async () => {
184
+ const status = await isRunning();
185
+
186
+ if (!status.running) {
187
+ return { running: false, pid: null, connections: [], uptime: 0 };
188
+ }
189
+
190
+ // Get detailed status from daemon
191
+ return new Promise((resolve) => {
192
+ const ws = new WebSocket(`ws://127.0.0.1:${BROKER_PORT}`);
193
+ const timeout = setTimeout(() => {
194
+ ws.terminate();
195
+ resolve({ running: true, pid: status.pid, connections: [], uptime: 0 });
196
+ }, 3000);
197
+
198
+ ws.on('open', () => {
199
+ ws.send(JSON.stringify({ type: 'status', requestId: 'status' }));
200
+ });
201
+
202
+ ws.on('message', (data) => {
203
+ clearTimeout(timeout);
204
+ try {
205
+ const msg = JSON.parse(data.toString());
206
+ if (msg.type === 'status') {
207
+ ws.close();
208
+ resolve(msg.payload);
209
+ }
210
+ } catch (e) {
211
+ ws.close();
212
+ resolve({ running: true, pid: status.pid, connections: [], uptime: 0 });
213
+ }
214
+ });
215
+
216
+ ws.on('error', () => {
217
+ clearTimeout(timeout);
218
+ resolve({ running: true, pid: status.pid, connections: [], uptime: 0 });
219
+ });
220
+ });
221
+ };
222
+
223
+ /**
224
+ * Ensure daemon is running (start if not)
225
+ * @returns {Promise<{success: boolean, error: string|null}>}
226
+ */
227
+ const ensureRunning = async () => {
228
+ const status = await isRunning();
229
+ if (status.running) {
230
+ return { success: true, error: null };
231
+ }
232
+ return start();
233
+ };
234
+
235
+ /**
236
+ * Restart the daemon
237
+ * @returns {Promise<{success: boolean, error: string|null}>}
238
+ */
239
+ const restart = async () => {
240
+ await stop();
241
+ await new Promise(r => setTimeout(r, 1000));
242
+ return start();
243
+ };
244
+
245
+ module.exports = {
246
+ isRunning,
247
+ start,
248
+ stop,
249
+ getStatus,
250
+ ensureRunning,
251
+ restart,
252
+ BROKER_PORT,
253
+ BROKER_DIR,
254
+ PID_FILE,
255
+ LOG_FILE,
256
+ };
@@ -84,11 +84,11 @@ const storage = {
84
84
  },
85
85
  };
86
86
 
87
- // Lazy load RithmicService to avoid circular dependencies
88
- let RithmicService;
87
+ // Lazy load services to avoid circular dependencies
88
+ let RithmicBrokerClient, brokerManager;
89
89
  const loadServices = () => {
90
- if (!RithmicService) {
91
- ({ RithmicService } = require('./rithmic'));
90
+ if (!RithmicBrokerClient) {
91
+ ({ RithmicBrokerClient, manager: brokerManager } = require('./rithmic-broker'));
92
92
  }
93
93
  };
94
94
 
@@ -130,16 +130,44 @@ const connections = {
130
130
 
131
131
  async restoreFromStorage() {
132
132
  loadServices();
133
- const sessions = storage.load();
134
133
 
135
- // Filter only Rithmic sessions (AI sessions are managed by ai-agents.js)
134
+ // Check if daemon is already running with active connections
135
+ const daemonStatus = await brokerManager.getStatus();
136
+
137
+ if (daemonStatus.running && daemonStatus.connections?.length > 0) {
138
+ // Daemon has active connections - just create clients (NO API calls)
139
+ log.info('Daemon active, restoring from broker', { connections: daemonStatus.connections.length });
140
+
141
+ for (const conn of daemonStatus.connections) {
142
+ const client = new RithmicBrokerClient(conn.propfirmKey);
143
+ await client.connect();
144
+
145
+ // Get accounts from daemon cache
146
+ const accountsResult = await client.getTradingAccounts();
147
+ client.accounts = accountsResult.accounts || [];
148
+
149
+ this.services.push({
150
+ type: 'rithmic',
151
+ service: client,
152
+ propfirm: conn.propfirm,
153
+ propfirmKey: conn.propfirmKey,
154
+ connectedAt: new Date(conn.connectedAt),
155
+ });
156
+ log.debug('Restored from broker', { propfirm: conn.propfirmKey });
157
+ }
158
+
159
+ return this.services.length > 0;
160
+ }
161
+
162
+ // Daemon not running or no connections - check local storage
163
+ const sessions = storage.load();
136
164
  const rithmicSessions = sessions.filter(s => s.type === 'rithmic');
137
165
 
138
166
  if (!rithmicSessions.length) {
139
167
  return false;
140
168
  }
141
169
 
142
- log.info('Restoring sessions', { count: rithmicSessions.length });
170
+ log.info('Restoring sessions via broker', { count: rithmicSessions.length });
143
171
 
144
172
  for (const session of rithmicSessions) {
145
173
  try {
@@ -155,20 +183,20 @@ const connections = {
155
183
  async _restoreSession(session) {
156
184
  const { type, propfirm, propfirmKey } = session;
157
185
 
158
- // Only restore Rithmic sessions
186
+ // Use broker client (daemon handles persistence)
159
187
  if (type === 'rithmic' && session.credentials) {
160
- const service = new RithmicService(propfirmKey || 'apex_rithmic');
161
- const result = await service.login(session.credentials.username, session.credentials.password);
188
+ const client = new RithmicBrokerClient(propfirmKey || 'apex_rithmic');
189
+ const result = await client.login(session.credentials.username, session.credentials.password);
162
190
 
163
191
  if (result.success) {
164
192
  this.services.push({
165
193
  type,
166
- service,
194
+ service: client,
167
195
  propfirm,
168
196
  propfirmKey,
169
197
  connectedAt: new Date(),
170
198
  });
171
- log.debug('Rithmic session restored');
199
+ log.debug('Rithmic session restored via broker');
172
200
  }
173
201
  }
174
202
  },
@@ -251,15 +279,23 @@ const connections = {
251
279
  return this.services.length > 0;
252
280
  },
253
281
 
254
- disconnectAll() {
282
+ async disconnectAll() {
283
+ loadServices();
284
+
285
+ // Stop the broker daemon (closes all Rithmic connections)
286
+ try {
287
+ await brokerManager.stop();
288
+ log.info('Broker daemon stopped');
289
+ } catch (err) {
290
+ log.warn('Broker stop failed', { error: err.message });
291
+ }
292
+
293
+ // Disconnect local clients
255
294
  for (const conn of this.services) {
256
295
  try {
257
296
  if (conn.service?.disconnect) {
258
297
  conn.service.disconnect();
259
298
  }
260
- if (conn.service?.credentials) {
261
- conn.service.credentials = null;
262
- }
263
299
  } catch (err) {
264
300
  log.warn('Disconnect failed', { type: conn.type, error: err.message });
265
301
  }