hedgequantx 2.9.181 → 2.9.183
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 +1 -1
- package/src/app.js +8 -21
- package/src/menus/connect.js +10 -22
- package/src/services/session.js +79 -105
- package/src/services/rithmic-broker/client.js +0 -321
- package/src/services/rithmic-broker/daemon-reconnect.js +0 -353
- package/src/services/rithmic-broker/daemon.js +0 -469
- package/src/services/rithmic-broker/index.js +0 -46
- package/src/services/rithmic-broker/manager.js +0 -264
package/package.json
CHANGED
package/src/app.js
CHANGED
|
@@ -271,31 +271,18 @@ const run = async () => {
|
|
|
271
271
|
const credentials = await loginPrompt(selectedPropfirm.name);
|
|
272
272
|
|
|
273
273
|
if (credentials) {
|
|
274
|
-
const spinner = ora({ text: '
|
|
274
|
+
const spinner = ora({ text: 'CONNECTING TO RITHMIC...', color: 'yellow' }).start();
|
|
275
275
|
try {
|
|
276
|
-
//
|
|
277
|
-
const {
|
|
276
|
+
// Direct connection to Rithmic (no daemon)
|
|
277
|
+
const { RithmicService } = require('./services/rithmic');
|
|
278
278
|
|
|
279
|
-
|
|
280
|
-
const
|
|
281
|
-
if (!daemonResult.success) {
|
|
282
|
-
spinner.fail('FAILED TO START BROKER DAEMON');
|
|
283
|
-
console.log(chalk.yellow(` → ${daemonResult.error}`));
|
|
284
|
-
await new Promise(r => setTimeout(r, 3000));
|
|
285
|
-
continue;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
spinner.text = 'CONNECTING TO RITHMIC...';
|
|
289
|
-
const client = new RithmicBrokerClient(selectedPropfirm.key);
|
|
290
|
-
const result = await client.login(credentials.username, credentials.password);
|
|
279
|
+
const service = new RithmicService(selectedPropfirm.key);
|
|
280
|
+
const result = await service.login(credentials.username, credentials.password);
|
|
291
281
|
|
|
292
282
|
if (result.success) {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
connections.add('rithmic', client, selectedPropfirm.name);
|
|
297
|
-
spinner.succeed(`CONNECTED TO ${selectedPropfirm.name.toUpperCase()} (${accResult.accounts?.length || 0} ACCOUNTS)`);
|
|
298
|
-
currentService = client;
|
|
283
|
+
connections.add('rithmic', service, selectedPropfirm.name);
|
|
284
|
+
spinner.succeed(`CONNECTED TO ${selectedPropfirm.name.toUpperCase()} (${result.accounts?.length || 0} ACCOUNTS)`);
|
|
285
|
+
currentService = service;
|
|
299
286
|
await refreshStats();
|
|
300
287
|
await new Promise(r => setTimeout(r, 1500));
|
|
301
288
|
} else {
|
package/src/menus/connect.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Connection Menus - Rithmic
|
|
2
|
+
* Connection Menus - Direct Rithmic Connection
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
const chalk = require('chalk');
|
|
6
6
|
const ora = require('ora');
|
|
7
7
|
|
|
8
8
|
const { connections } = require('../services');
|
|
9
|
-
const {
|
|
9
|
+
const { RithmicService } = require('../services/rithmic');
|
|
10
10
|
const { PROPFIRM_CHOICES } = require('../config');
|
|
11
|
-
const { getLogoWidth, centerText, prepareStdin, displayBanner
|
|
11
|
+
const { getLogoWidth, centerText, prepareStdin, displayBanner, clearScreen } = require('../ui');
|
|
12
12
|
const { validateUsername, validatePassword } = require('../security');
|
|
13
13
|
const { prompts } = require('../utils');
|
|
14
14
|
|
|
@@ -38,7 +38,6 @@ const loginPrompt = async (propfirmName) => {
|
|
|
38
38
|
* Rithmic menu - Main connection menu
|
|
39
39
|
*/
|
|
40
40
|
const rithmicMenu = async () => {
|
|
41
|
-
// Clear screen and show banner
|
|
42
41
|
clearScreen();
|
|
43
42
|
displayBanner();
|
|
44
43
|
|
|
@@ -87,29 +86,18 @@ const rithmicMenu = async () => {
|
|
|
87
86
|
const credentials = await loginPrompt(selectedPropfirm.name);
|
|
88
87
|
if (!credentials) return null;
|
|
89
88
|
|
|
90
|
-
const spinner = ora({ text: '
|
|
89
|
+
const spinner = ora({ text: 'CONNECTING TO RITHMIC...', color: 'yellow' }).start();
|
|
91
90
|
|
|
92
91
|
try {
|
|
93
|
-
//
|
|
94
|
-
const
|
|
95
|
-
|
|
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);
|
|
92
|
+
// Direct connection to Rithmic (no daemon)
|
|
93
|
+
const service = new RithmicService(selectedPropfirm.key);
|
|
94
|
+
const result = await service.login(credentials.username, credentials.password);
|
|
105
95
|
|
|
106
96
|
if (result.success) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
connections.add('rithmic', client, client.propfirm.name || selectedPropfirm.name);
|
|
110
|
-
spinner.succeed(`CONNECTED TO ${selectedPropfirm.name.toUpperCase()} (${accResult.accounts?.length || 0} ACCOUNTS)`);
|
|
97
|
+
connections.add('rithmic', service, selectedPropfirm.name);
|
|
98
|
+
spinner.succeed(`CONNECTED TO ${selectedPropfirm.name.toUpperCase()} (${result.accounts?.length || 0} ACCOUNTS)`);
|
|
111
99
|
await new Promise(r => setTimeout(r, 1500));
|
|
112
|
-
return
|
|
100
|
+
return service;
|
|
113
101
|
} else {
|
|
114
102
|
// Detailed error messages for common Rithmic issues
|
|
115
103
|
const err = (result.error || '').toLowerCase();
|
package/src/services/session.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview Secure session management - Rithmic
|
|
2
|
+
* @fileoverview Secure session management - Direct Rithmic Connection
|
|
3
3
|
* @module services/session
|
|
4
|
+
*
|
|
5
|
+
* NO DAEMON - Direct connection to Rithmic API
|
|
4
6
|
*/
|
|
5
7
|
|
|
6
8
|
const fs = require('fs');
|
|
7
9
|
const path = require('path');
|
|
8
10
|
const os = require('os');
|
|
9
11
|
const crypto = require('crypto');
|
|
10
|
-
const { encrypt, decrypt
|
|
12
|
+
const { encrypt, decrypt } = require('../security/encryption');
|
|
11
13
|
const { SECURITY } = require('../config/settings');
|
|
12
14
|
const { logger } = require('../utils/logger');
|
|
13
15
|
|
|
@@ -23,7 +25,6 @@ const storage = {
|
|
|
23
25
|
_ensureDir() {
|
|
24
26
|
if (!fs.existsSync(SESSION_DIR)) {
|
|
25
27
|
fs.mkdirSync(SESSION_DIR, { recursive: true, mode: SECURITY.DIR_PERMISSIONS });
|
|
26
|
-
log.debug('Created session directory');
|
|
27
28
|
}
|
|
28
29
|
},
|
|
29
30
|
|
|
@@ -33,7 +34,6 @@ const storage = {
|
|
|
33
34
|
const data = JSON.stringify(sessions);
|
|
34
35
|
const encrypted = encrypt(data);
|
|
35
36
|
fs.writeFileSync(SESSION_FILE, encrypted, { mode: SECURITY.FILE_PERMISSIONS });
|
|
36
|
-
log.debug('Session saved', { count: sessions.length });
|
|
37
37
|
return true;
|
|
38
38
|
} catch (err) {
|
|
39
39
|
log.error('Failed to save session', { error: err.message });
|
|
@@ -51,14 +51,11 @@ const storage = {
|
|
|
51
51
|
const decrypted = decrypt(encrypted);
|
|
52
52
|
|
|
53
53
|
if (!decrypted) {
|
|
54
|
-
log.warn('Session decryption failed - clearing');
|
|
55
54
|
this.clear();
|
|
56
55
|
return [];
|
|
57
56
|
}
|
|
58
57
|
|
|
59
|
-
|
|
60
|
-
log.debug('Session loaded', { count: sessions.length });
|
|
61
|
-
return sessions;
|
|
58
|
+
return JSON.parse(decrypted);
|
|
62
59
|
} catch (err) {
|
|
63
60
|
log.error('Failed to load session', { error: err.message });
|
|
64
61
|
this.clear();
|
|
@@ -74,7 +71,6 @@ const storage = {
|
|
|
74
71
|
fs.writeFileSync(SESSION_FILE, crypto.randomBytes(size));
|
|
75
72
|
}
|
|
76
73
|
fs.unlinkSync(SESSION_FILE);
|
|
77
|
-
log.debug('Session cleared securely');
|
|
78
74
|
}
|
|
79
75
|
return true;
|
|
80
76
|
} catch (err) {
|
|
@@ -84,21 +80,25 @@ const storage = {
|
|
|
84
80
|
},
|
|
85
81
|
};
|
|
86
82
|
|
|
87
|
-
// Lazy load
|
|
88
|
-
let
|
|
89
|
-
const
|
|
90
|
-
if (!
|
|
91
|
-
({
|
|
83
|
+
// Lazy load RithmicService
|
|
84
|
+
let RithmicService;
|
|
85
|
+
const loadRithmicService = () => {
|
|
86
|
+
if (!RithmicService) {
|
|
87
|
+
({ RithmicService } = require('./rithmic'));
|
|
92
88
|
}
|
|
89
|
+
return RithmicService;
|
|
93
90
|
};
|
|
94
91
|
|
|
95
92
|
/**
|
|
96
|
-
* Multi-connection manager
|
|
93
|
+
* Multi-connection manager - Direct Rithmic connections
|
|
97
94
|
*/
|
|
98
95
|
const connections = {
|
|
99
96
|
/** @type {Array<{type: string, service: Object, propfirm: string, propfirmKey: string, connectedAt: Date}>} */
|
|
100
97
|
services: [],
|
|
101
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Add a new connection
|
|
101
|
+
*/
|
|
102
102
|
add(type, service, propfirm = null) {
|
|
103
103
|
this.services.push({
|
|
104
104
|
type,
|
|
@@ -112,7 +112,7 @@ const connections = {
|
|
|
112
112
|
},
|
|
113
113
|
|
|
114
114
|
/**
|
|
115
|
-
* Sanitize account data
|
|
115
|
+
* Sanitize account data
|
|
116
116
|
*/
|
|
117
117
|
_sanitizeAccount(acc) {
|
|
118
118
|
if (!acc || typeof acc !== 'object' || !acc.accountId) return null;
|
|
@@ -124,14 +124,14 @@ const connections = {
|
|
|
124
124
|
};
|
|
125
125
|
},
|
|
126
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Save sessions to encrypted storage
|
|
129
|
+
*/
|
|
127
130
|
saveToStorage() {
|
|
128
|
-
// Load existing sessions to preserve AI agents
|
|
129
131
|
const existingSessions = storage.load();
|
|
130
132
|
const aiSessions = existingSessions.filter(s => s.type === 'ai');
|
|
131
133
|
|
|
132
|
-
// Build Rithmic sessions - INCLUDE accounts to avoid Rithmic API limit on restore
|
|
133
134
|
const rithmicSessions = this.services.map(conn => {
|
|
134
|
-
// Sanitize accounts to prevent corrupted data
|
|
135
135
|
const rawAccounts = conn.service.accounts || [];
|
|
136
136
|
const accounts = rawAccounts.map(a => this._sanitizeAccount(a)).filter(Boolean);
|
|
137
137
|
|
|
@@ -140,58 +140,17 @@ const connections = {
|
|
|
140
140
|
propfirm: conn.propfirm,
|
|
141
141
|
propfirmKey: conn.service.propfirmKey || conn.propfirmKey,
|
|
142
142
|
credentials: conn.service.credentials,
|
|
143
|
-
accounts,
|
|
143
|
+
accounts,
|
|
144
144
|
};
|
|
145
145
|
});
|
|
146
146
|
|
|
147
|
-
// Merge: AI sessions + Rithmic sessions
|
|
148
147
|
storage.save([...aiSessions, ...rithmicSessions]);
|
|
149
|
-
log.debug('Session saved', { rithmicCount: rithmicSessions.length, hasAccounts: rithmicSessions.some(s => s.accounts?.length > 0) });
|
|
150
148
|
},
|
|
151
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Restore sessions from storage - Direct connection to Rithmic
|
|
152
|
+
*/
|
|
152
153
|
async restoreFromStorage() {
|
|
153
|
-
loadServices();
|
|
154
|
-
|
|
155
|
-
// Check if daemon is already running with active connections
|
|
156
|
-
const daemonStatus = await brokerManager.getStatus();
|
|
157
|
-
|
|
158
|
-
if (daemonStatus.running && daemonStatus.connections?.length > 0) {
|
|
159
|
-
// Daemon has active connections - just create clients (NO API calls)
|
|
160
|
-
log.info('Daemon active, restoring from broker', { connections: daemonStatus.connections.length });
|
|
161
|
-
|
|
162
|
-
for (const conn of daemonStatus.connections) {
|
|
163
|
-
const client = new RithmicBrokerClient(conn.propfirmKey);
|
|
164
|
-
await client.connect();
|
|
165
|
-
|
|
166
|
-
// Get accounts from daemon cache
|
|
167
|
-
const accountsResult = await client.getTradingAccounts();
|
|
168
|
-
client.accounts = accountsResult.accounts || [];
|
|
169
|
-
|
|
170
|
-
// Cache credentials locally for sync access (fetch from daemon)
|
|
171
|
-
try {
|
|
172
|
-
const creds = await client.getRithmicCredentialsAsync();
|
|
173
|
-
if (creds && creds.userId && creds.password) {
|
|
174
|
-
client.credentials = { username: creds.userId, password: creds.password };
|
|
175
|
-
client.propfirm = { name: conn.propfirmKey, systemName: creds.systemName, gateway: creds.gateway };
|
|
176
|
-
}
|
|
177
|
-
} catch (e) {
|
|
178
|
-
log.warn('Failed to cache credentials', { propfirm: conn.propfirmKey, error: e.message });
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
this.services.push({
|
|
182
|
-
type: 'rithmic',
|
|
183
|
-
service: client,
|
|
184
|
-
propfirm: conn.propfirm,
|
|
185
|
-
propfirmKey: conn.propfirmKey,
|
|
186
|
-
connectedAt: new Date(conn.connectedAt),
|
|
187
|
-
});
|
|
188
|
-
log.debug('Restored from broker', { propfirm: conn.propfirmKey, hasCreds: !!client.credentials });
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
return this.services.length > 0;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Daemon not running or no connections - check local storage
|
|
195
154
|
const sessions = storage.load();
|
|
196
155
|
const rithmicSessions = sessions.filter(s => s.type === 'rithmic');
|
|
197
156
|
|
|
@@ -199,52 +158,69 @@ const connections = {
|
|
|
199
158
|
return false;
|
|
200
159
|
}
|
|
201
160
|
|
|
202
|
-
log.info('Restoring sessions
|
|
161
|
+
log.info('Restoring sessions', { count: rithmicSessions.length });
|
|
203
162
|
|
|
204
163
|
for (const session of rithmicSessions) {
|
|
205
164
|
try {
|
|
206
165
|
await this._restoreSession(session);
|
|
207
166
|
} catch (err) {
|
|
208
|
-
log.warn('Failed to restore session', {
|
|
167
|
+
log.warn('Failed to restore session', { error: err.message });
|
|
209
168
|
}
|
|
210
169
|
}
|
|
211
170
|
|
|
212
171
|
return this.services.length > 0;
|
|
213
172
|
},
|
|
214
173
|
|
|
174
|
+
/**
|
|
175
|
+
* Restore a single session using direct RithmicService
|
|
176
|
+
*/
|
|
215
177
|
async _restoreSession(session) {
|
|
216
178
|
const { type, propfirm, propfirmKey } = session;
|
|
217
179
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
180
|
+
if (type !== 'rithmic' || !session.credentials) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const Service = loadRithmicService();
|
|
185
|
+
const service = new Service(propfirmKey || 'apex_rithmic');
|
|
186
|
+
|
|
187
|
+
// Validate cached accounts
|
|
188
|
+
let validAccounts = null;
|
|
189
|
+
if (session.accounts && Array.isArray(session.accounts)) {
|
|
190
|
+
validAccounts = session.accounts
|
|
191
|
+
.map(a => this._sanitizeAccount(a))
|
|
192
|
+
.filter(Boolean);
|
|
193
|
+
if (validAccounts.length === 0) validAccounts = null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Login with cached accounts to avoid Rithmic API limit
|
|
197
|
+
const loginOptions = validAccounts
|
|
198
|
+
? { skipFetchAccounts: true, cachedAccounts: validAccounts }
|
|
199
|
+
: {};
|
|
200
|
+
|
|
201
|
+
const result = await service.login(
|
|
202
|
+
session.credentials.username,
|
|
203
|
+
session.credentials.password,
|
|
204
|
+
loginOptions
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
if (result.success) {
|
|
208
|
+
this.services.push({
|
|
209
|
+
type,
|
|
210
|
+
service,
|
|
211
|
+
propfirm,
|
|
212
|
+
propfirmKey,
|
|
213
|
+
connectedAt: new Date(),
|
|
214
|
+
});
|
|
215
|
+
log.info('Session restored', { propfirm, accounts: service.accounts?.length || 0 });
|
|
216
|
+
} else {
|
|
217
|
+
log.warn('Session restore failed', { propfirm, error: result.error });
|
|
245
218
|
}
|
|
246
219
|
},
|
|
247
220
|
|
|
221
|
+
/**
|
|
222
|
+
* Remove a connection by index
|
|
223
|
+
*/
|
|
248
224
|
remove(index) {
|
|
249
225
|
if (index < 0 || index >= this.services.length) return;
|
|
250
226
|
|
|
@@ -279,6 +255,9 @@ const connections = {
|
|
|
279
255
|
return this.services.length;
|
|
280
256
|
},
|
|
281
257
|
|
|
258
|
+
/**
|
|
259
|
+
* Get all accounts from all connections
|
|
260
|
+
*/
|
|
282
261
|
async getAllAccounts() {
|
|
283
262
|
const allAccounts = [];
|
|
284
263
|
|
|
@@ -304,6 +283,9 @@ const connections = {
|
|
|
304
283
|
return allAccounts;
|
|
305
284
|
},
|
|
306
285
|
|
|
286
|
+
/**
|
|
287
|
+
* Get service for a specific account
|
|
288
|
+
*/
|
|
307
289
|
getServiceForAccount(accountId) {
|
|
308
290
|
for (const conn of this.services) {
|
|
309
291
|
if (!conn.service?.accounts) continue;
|
|
@@ -323,22 +305,14 @@ const connections = {
|
|
|
323
305
|
return this.services.length > 0;
|
|
324
306
|
},
|
|
325
307
|
|
|
308
|
+
/**
|
|
309
|
+
* Disconnect all connections
|
|
310
|
+
*/
|
|
326
311
|
async disconnectAll() {
|
|
327
|
-
loadServices();
|
|
328
|
-
|
|
329
|
-
// Stop the broker daemon (closes all Rithmic connections)
|
|
330
|
-
try {
|
|
331
|
-
await brokerManager.stop();
|
|
332
|
-
log.info('Broker daemon stopped');
|
|
333
|
-
} catch (err) {
|
|
334
|
-
log.warn('Broker stop failed', { error: err.message });
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// Disconnect local clients
|
|
338
312
|
for (const conn of this.services) {
|
|
339
313
|
try {
|
|
340
314
|
if (conn.service?.disconnect) {
|
|
341
|
-
conn.service.disconnect();
|
|
315
|
+
await conn.service.disconnect();
|
|
342
316
|
}
|
|
343
317
|
} catch (err) {
|
|
344
318
|
log.warn('Disconnect failed', { type: conn.type, error: err.message });
|