hedgequantx 1.1.1 → 1.2.31
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/README.md +128 -136
- package/bin/cli.js +28 -2076
- package/package.json +3 -3
- package/src/app.js +550 -0
- package/src/config/index.js +16 -2
- package/src/config/propfirms.js +324 -12
- package/src/pages/accounts.js +115 -0
- package/src/pages/algo.js +538 -0
- package/src/pages/index.js +13 -2
- package/src/pages/orders.js +114 -0
- package/src/pages/positions.js +115 -0
- package/src/pages/stats.js +212 -3
- package/src/pages/user.js +92 -0
- package/src/security/encryption.js +168 -0
- package/src/security/index.js +61 -0
- package/src/security/rateLimit.js +155 -0
- package/src/security/validation.js +253 -0
- package/src/services/hqx-server.js +34 -17
- package/src/services/index.js +2 -1
- package/src/services/projectx.js +383 -35
- package/src/services/session.js +150 -38
- package/src/ui/index.js +4 -1
- package/src/ui/menu.js +154 -0
- package/src/services/local-storage.js +0 -309
package/src/services/session.js
CHANGED
|
@@ -1,60 +1,111 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* @fileoverview Secure session management with encryption
|
|
3
|
+
* @module services/session
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const fs = require('fs');
|
|
7
7
|
const path = require('path');
|
|
8
8
|
const os = require('os');
|
|
9
|
+
const { encrypt, decrypt, maskSensitive } = require('../security');
|
|
9
10
|
const { ProjectXService } = require('./projectx');
|
|
10
11
|
|
|
11
|
-
const
|
|
12
|
+
const SESSION_DIR = path.join(os.homedir(), '.hedgequantx');
|
|
13
|
+
const SESSION_FILE = path.join(SESSION_DIR, 'session.enc');
|
|
12
14
|
|
|
13
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Secure session storage with AES-256 encryption
|
|
17
|
+
*/
|
|
14
18
|
const storage = {
|
|
19
|
+
/**
|
|
20
|
+
* Ensures the session directory exists with proper permissions
|
|
21
|
+
* @private
|
|
22
|
+
*/
|
|
23
|
+
_ensureDir() {
|
|
24
|
+
if (!fs.existsSync(SESSION_DIR)) {
|
|
25
|
+
fs.mkdirSync(SESSION_DIR, { recursive: true, mode: 0o700 });
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Saves sessions with encryption
|
|
31
|
+
* @param {Array} sessions - Sessions to save
|
|
32
|
+
*/
|
|
15
33
|
save(sessions) {
|
|
16
34
|
try {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
|
|
35
|
+
this._ensureDir();
|
|
36
|
+
const data = JSON.stringify(sessions);
|
|
37
|
+
const encrypted = encrypt(data);
|
|
38
|
+
fs.writeFileSync(SESSION_FILE, encrypted, { mode: 0o600 });
|
|
39
|
+
} catch (e) {
|
|
40
|
+
// Silently fail - don't expose errors
|
|
41
|
+
}
|
|
23
42
|
},
|
|
24
|
-
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Loads and decrypts sessions
|
|
46
|
+
* @returns {Array} Decrypted sessions or empty array
|
|
47
|
+
*/
|
|
25
48
|
load() {
|
|
26
49
|
try {
|
|
27
50
|
if (fs.existsSync(SESSION_FILE)) {
|
|
28
|
-
|
|
51
|
+
const encrypted = fs.readFileSync(SESSION_FILE, 'utf8');
|
|
52
|
+
const decrypted = decrypt(encrypted);
|
|
53
|
+
if (decrypted) {
|
|
54
|
+
return JSON.parse(decrypted);
|
|
55
|
+
}
|
|
29
56
|
}
|
|
30
|
-
} catch (e) {
|
|
57
|
+
} catch (e) {
|
|
58
|
+
// Session corrupted or from different machine - clear it
|
|
59
|
+
this.clear();
|
|
60
|
+
}
|
|
31
61
|
return [];
|
|
32
62
|
},
|
|
33
|
-
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Securely clears session data
|
|
66
|
+
*/
|
|
34
67
|
clear() {
|
|
35
68
|
try {
|
|
36
69
|
if (fs.existsSync(SESSION_FILE)) {
|
|
70
|
+
// Overwrite with random data before deleting
|
|
71
|
+
const size = fs.statSync(SESSION_FILE).size;
|
|
72
|
+
fs.writeFileSync(SESSION_FILE, require('crypto').randomBytes(size));
|
|
37
73
|
fs.unlinkSync(SESSION_FILE);
|
|
38
74
|
}
|
|
39
|
-
} catch (e) {
|
|
75
|
+
} catch (e) {
|
|
76
|
+
// Ignore errors
|
|
77
|
+
}
|
|
40
78
|
}
|
|
41
79
|
};
|
|
42
80
|
|
|
43
|
-
|
|
81
|
+
/**
|
|
82
|
+
* Multi-connection manager with secure token handling
|
|
83
|
+
*/
|
|
44
84
|
const connections = {
|
|
85
|
+
/** @type {Array} Active connections */
|
|
45
86
|
services: [],
|
|
46
|
-
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Adds a new connection
|
|
90
|
+
* @param {string} type - Connection type (projectx, rithmic, etc.)
|
|
91
|
+
* @param {Object} service - Service instance
|
|
92
|
+
* @param {string} [propfirm] - PropFirm name
|
|
93
|
+
* @param {string} [token] - Auth token
|
|
94
|
+
*/
|
|
47
95
|
add(type, service, propfirm = null, token = null) {
|
|
48
|
-
this.services.push({
|
|
49
|
-
type,
|
|
50
|
-
service,
|
|
51
|
-
propfirm,
|
|
52
|
-
token,
|
|
53
|
-
connectedAt: new Date()
|
|
96
|
+
this.services.push({
|
|
97
|
+
type,
|
|
98
|
+
service,
|
|
99
|
+
propfirm,
|
|
100
|
+
token: token || service.token,
|
|
101
|
+
connectedAt: new Date()
|
|
54
102
|
});
|
|
55
103
|
this.saveToStorage();
|
|
56
104
|
},
|
|
57
|
-
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Saves all sessions to encrypted storage
|
|
108
|
+
*/
|
|
58
109
|
saveToStorage() {
|
|
59
110
|
const sessions = this.services.map(conn => ({
|
|
60
111
|
type: conn.type,
|
|
@@ -63,15 +114,22 @@ const connections = {
|
|
|
63
114
|
}));
|
|
64
115
|
storage.save(sessions);
|
|
65
116
|
},
|
|
66
|
-
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Restores sessions from encrypted storage
|
|
120
|
+
* @returns {Promise<boolean>} True if sessions were restored
|
|
121
|
+
*/
|
|
67
122
|
async restoreFromStorage() {
|
|
68
123
|
const sessions = storage.load();
|
|
124
|
+
|
|
69
125
|
for (const session of sessions) {
|
|
70
126
|
try {
|
|
71
127
|
if (session.type === 'projectx' && session.token) {
|
|
72
|
-
const
|
|
128
|
+
const propfirmKey = session.propfirm.toLowerCase().replace(/ /g, '_');
|
|
129
|
+
const service = new ProjectXService(propfirmKey);
|
|
73
130
|
service.token = session.token;
|
|
74
|
-
|
|
131
|
+
|
|
132
|
+
// Validate token is still valid
|
|
75
133
|
const userResult = await service.getUser();
|
|
76
134
|
if (userResult.success) {
|
|
77
135
|
this.services.push({
|
|
@@ -83,30 +141,61 @@ const connections = {
|
|
|
83
141
|
});
|
|
84
142
|
}
|
|
85
143
|
}
|
|
86
|
-
} catch (e) {
|
|
144
|
+
} catch (e) {
|
|
145
|
+
// Invalid session - skip
|
|
146
|
+
}
|
|
87
147
|
}
|
|
148
|
+
|
|
88
149
|
return this.services.length > 0;
|
|
89
150
|
},
|
|
90
|
-
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Removes a connection by index
|
|
154
|
+
* @param {number} index - Connection index
|
|
155
|
+
*/
|
|
91
156
|
remove(index) {
|
|
92
|
-
this.services.
|
|
93
|
-
|
|
157
|
+
if (index >= 0 && index < this.services.length) {
|
|
158
|
+
const conn = this.services[index];
|
|
159
|
+
if (conn.service && conn.service.logout) {
|
|
160
|
+
conn.service.logout();
|
|
161
|
+
}
|
|
162
|
+
this.services.splice(index, 1);
|
|
163
|
+
this.saveToStorage();
|
|
164
|
+
}
|
|
94
165
|
},
|
|
95
|
-
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Gets all connections
|
|
169
|
+
* @returns {Array} All connections
|
|
170
|
+
*/
|
|
96
171
|
getAll() {
|
|
97
172
|
return this.services;
|
|
98
173
|
},
|
|
99
|
-
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Gets connections by type
|
|
177
|
+
* @param {string} type - Connection type
|
|
178
|
+
* @returns {Array} Filtered connections
|
|
179
|
+
*/
|
|
100
180
|
getByType(type) {
|
|
101
181
|
return this.services.filter(c => c.type === type);
|
|
102
182
|
},
|
|
103
|
-
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Gets connection count
|
|
186
|
+
* @returns {number} Number of connections
|
|
187
|
+
*/
|
|
104
188
|
count() {
|
|
105
189
|
return this.services.length;
|
|
106
190
|
},
|
|
107
|
-
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Gets all accounts from all connections
|
|
194
|
+
* @returns {Promise<Array>} All accounts
|
|
195
|
+
*/
|
|
108
196
|
async getAllAccounts() {
|
|
109
197
|
const allAccounts = [];
|
|
198
|
+
|
|
110
199
|
for (const conn of this.services) {
|
|
111
200
|
try {
|
|
112
201
|
const result = await conn.service.getTradingAccounts();
|
|
@@ -120,15 +209,25 @@ const connections = {
|
|
|
120
209
|
});
|
|
121
210
|
});
|
|
122
211
|
}
|
|
123
|
-
} catch (e) {
|
|
212
|
+
} catch (e) {
|
|
213
|
+
// Skip failed connections
|
|
214
|
+
}
|
|
124
215
|
}
|
|
216
|
+
|
|
125
217
|
return allAccounts;
|
|
126
218
|
},
|
|
127
|
-
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Checks if any connection is active
|
|
222
|
+
* @returns {boolean} True if connected
|
|
223
|
+
*/
|
|
128
224
|
isConnected() {
|
|
129
225
|
return this.services.length > 0;
|
|
130
226
|
},
|
|
131
|
-
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Disconnects all connections and clears sessions
|
|
230
|
+
*/
|
|
132
231
|
disconnectAll() {
|
|
133
232
|
this.services.forEach(conn => {
|
|
134
233
|
if (conn.service && conn.service.logout) {
|
|
@@ -137,6 +236,19 @@ const connections = {
|
|
|
137
236
|
});
|
|
138
237
|
this.services = [];
|
|
139
238
|
storage.clear();
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Gets masked connection info for logging
|
|
243
|
+
* @returns {Array} Masked connection info
|
|
244
|
+
*/
|
|
245
|
+
getInfo() {
|
|
246
|
+
return this.services.map(conn => ({
|
|
247
|
+
type: conn.type,
|
|
248
|
+
propfirm: conn.propfirm,
|
|
249
|
+
token: maskSensitive(conn.token),
|
|
250
|
+
connectedAt: conn.connectedAt
|
|
251
|
+
}));
|
|
140
252
|
}
|
|
141
253
|
};
|
|
142
254
|
|
package/src/ui/index.js
CHANGED
|
@@ -22,6 +22,7 @@ const {
|
|
|
22
22
|
draw2ColSeparator,
|
|
23
23
|
fmtRow
|
|
24
24
|
} = require('./table');
|
|
25
|
+
const { createBoxMenu } = require('./menu');
|
|
25
26
|
|
|
26
27
|
module.exports = {
|
|
27
28
|
// Device
|
|
@@ -44,5 +45,7 @@ module.exports = {
|
|
|
44
45
|
draw2ColRow,
|
|
45
46
|
draw2ColRowRaw,
|
|
46
47
|
draw2ColSeparator,
|
|
47
|
-
fmtRow
|
|
48
|
+
fmtRow,
|
|
49
|
+
// Menu
|
|
50
|
+
createBoxMenu
|
|
48
51
|
};
|
package/src/ui/menu.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom Box Menu with keyboard navigation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const chalk = require('chalk');
|
|
6
|
+
const readline = require('readline');
|
|
7
|
+
const { getLogoWidth, centerText } = require('./box');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Creates a custom menu inside a box
|
|
11
|
+
* @param {string} title - Menu title
|
|
12
|
+
* @param {Array} items - Menu items [{label, value, color, disabled, separator}]
|
|
13
|
+
* @param {Object} options - Options {headerLines: [], footerText: ''}
|
|
14
|
+
* @returns {Promise<string>} Selected value
|
|
15
|
+
*/
|
|
16
|
+
const createBoxMenu = async (title, items, options = {}) => {
|
|
17
|
+
const boxWidth = getLogoWidth();
|
|
18
|
+
const innerWidth = boxWidth - 2;
|
|
19
|
+
|
|
20
|
+
let selectedIndex = 0;
|
|
21
|
+
|
|
22
|
+
// Find first non-separator, non-disabled item
|
|
23
|
+
while (selectedIndex < items.length && (items[selectedIndex].separator || items[selectedIndex].disabled)) {
|
|
24
|
+
selectedIndex++;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const renderMenu = () => {
|
|
28
|
+
// Clear screen and move cursor to top
|
|
29
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
30
|
+
|
|
31
|
+
const version = require('../../package.json').version;
|
|
32
|
+
|
|
33
|
+
// Full ASCII logo
|
|
34
|
+
const logo = [
|
|
35
|
+
'██╗ ██╗███████╗██████╗ ██████╗ ███████╗ ██████╗ ██╗ ██╗ █████╗ ███╗ ██╗████████╗██╗ ██╗',
|
|
36
|
+
'██║ ██║██╔════╝██╔══██╗██╔════╝ ██╔════╝██╔═══██╗██║ ██║██╔══██╗████╗ ██║╚══██╔══╝╚██╗██╔╝',
|
|
37
|
+
'███████║█████╗ ██║ ██║██║ ███╗█████╗ ██║ ██║██║ ██║███████║██╔██╗ ██║ ██║ ╚███╔╝ ',
|
|
38
|
+
'██╔══██║██╔══╝ ██║ ██║██║ ██║██╔══╝ ██║▄▄ ██║██║ ██║██╔══██║██║╚██╗██║ ██║ ██╔██╗ ',
|
|
39
|
+
'██║ ██║███████╗██████╔╝╚██████╔╝███████╗╚██████╔╝╚██████╔╝██║ ██║██║ ╚████║ ██║ ██╔╝ ██╗',
|
|
40
|
+
'╚═╝ ╚═╝╚══════╝╚═════╝ ╚═════╝ ╚══════╝ ╚══▀▀═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═╝ ╚═╝ ╚═╝'
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
console.log(chalk.cyan('╔' + '═'.repeat(innerWidth) + '╗'));
|
|
44
|
+
|
|
45
|
+
logo.forEach(line => {
|
|
46
|
+
const padded = centerText(line, innerWidth);
|
|
47
|
+
console.log(chalk.cyan('║') + chalk.cyan(padded) + chalk.cyan('║'));
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
console.log(chalk.cyan('╠' + '═'.repeat(innerWidth) + '╣'));
|
|
51
|
+
console.log(chalk.cyan('║') + chalk.white(centerText(`Prop Futures Algo Trading v${version}`, innerWidth)) + chalk.cyan('║'));
|
|
52
|
+
|
|
53
|
+
// Stats bar if provided
|
|
54
|
+
if (options.statsLine) {
|
|
55
|
+
console.log(chalk.cyan('╠' + '═'.repeat(innerWidth) + '╣'));
|
|
56
|
+
const statsLen = options.statsLine.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
57
|
+
const statsPad = innerWidth - statsLen;
|
|
58
|
+
const leftPad = Math.floor(statsPad / 2);
|
|
59
|
+
const rightPad = statsPad - leftPad;
|
|
60
|
+
console.log(chalk.cyan('║') + ' '.repeat(leftPad) + options.statsLine + ' '.repeat(rightPad) + chalk.cyan('║'));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
console.log(chalk.cyan('╠' + '═'.repeat(innerWidth) + '╣'));
|
|
64
|
+
console.log(chalk.cyan('║') + chalk.white.bold(centerText(title, innerWidth)) + chalk.cyan('║'));
|
|
65
|
+
console.log(chalk.cyan('╠' + '═'.repeat(innerWidth) + '╣'));
|
|
66
|
+
|
|
67
|
+
// Header lines (connection info, etc.)
|
|
68
|
+
if (options.headerLines && options.headerLines.length > 0) {
|
|
69
|
+
options.headerLines.forEach(line => {
|
|
70
|
+
const text = ' ' + line;
|
|
71
|
+
console.log(chalk.cyan('║') + text.padEnd(innerWidth) + chalk.cyan('║'));
|
|
72
|
+
});
|
|
73
|
+
console.log(chalk.cyan('╠' + '─'.repeat(innerWidth) + '╣'));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Menu items
|
|
77
|
+
items.forEach((item, index) => {
|
|
78
|
+
if (item.separator) {
|
|
79
|
+
console.log(chalk.cyan('║') + chalk.gray(' ' + '─'.repeat(innerWidth - 4) + ' ') + chalk.cyan('║'));
|
|
80
|
+
} else {
|
|
81
|
+
const isSelected = index === selectedIndex;
|
|
82
|
+
const prefix = isSelected ? chalk.white('▸ ') : ' ';
|
|
83
|
+
const color = item.disabled ? chalk.gray : (item.color || chalk.cyan);
|
|
84
|
+
const label = item.label + (item.disabled ? ' (Coming Soon)' : '');
|
|
85
|
+
const text = prefix + color(label);
|
|
86
|
+
const visLen = text.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
87
|
+
const padding = innerWidth - visLen;
|
|
88
|
+
|
|
89
|
+
if (isSelected && !item.disabled) {
|
|
90
|
+
console.log(chalk.cyan('║') + chalk.bgGray.white(text + ' '.repeat(padding)) + chalk.cyan('║'));
|
|
91
|
+
} else {
|
|
92
|
+
console.log(chalk.cyan('║') + text + ' '.repeat(padding) + chalk.cyan('║'));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Footer
|
|
98
|
+
console.log(chalk.cyan('╠' + '─'.repeat(innerWidth) + '╣'));
|
|
99
|
+
const footerText = options.footerText || 'Use ↑↓ arrows to navigate, Enter to select';
|
|
100
|
+
console.log(chalk.cyan('║') + chalk.gray(centerText(footerText, innerWidth)) + chalk.cyan('║'));
|
|
101
|
+
console.log(chalk.cyan('╚' + '═'.repeat(innerWidth) + '╝'));
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
return new Promise((resolve) => {
|
|
105
|
+
renderMenu();
|
|
106
|
+
|
|
107
|
+
readline.emitKeypressEvents(process.stdin);
|
|
108
|
+
if (process.stdin.isTTY) {
|
|
109
|
+
process.stdin.setRawMode(true);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const onKeyPress = (str, key) => {
|
|
113
|
+
if (key.name === 'up') {
|
|
114
|
+
// Move up, skip separators and disabled
|
|
115
|
+
let newIndex = selectedIndex - 1;
|
|
116
|
+
while (newIndex >= 0 && (items[newIndex].separator || items[newIndex].disabled)) {
|
|
117
|
+
newIndex--;
|
|
118
|
+
}
|
|
119
|
+
if (newIndex >= 0) {
|
|
120
|
+
selectedIndex = newIndex;
|
|
121
|
+
renderMenu();
|
|
122
|
+
}
|
|
123
|
+
} else if (key.name === 'down') {
|
|
124
|
+
// Move down, skip separators and disabled
|
|
125
|
+
let newIndex = selectedIndex + 1;
|
|
126
|
+
while (newIndex < items.length && (items[newIndex].separator || items[newIndex].disabled)) {
|
|
127
|
+
newIndex++;
|
|
128
|
+
}
|
|
129
|
+
if (newIndex < items.length) {
|
|
130
|
+
selectedIndex = newIndex;
|
|
131
|
+
renderMenu();
|
|
132
|
+
}
|
|
133
|
+
} else if (key.name === 'return') {
|
|
134
|
+
// Select current item
|
|
135
|
+
cleanup();
|
|
136
|
+
resolve(items[selectedIndex].value);
|
|
137
|
+
} else if (key.name === 'escape' || (key.ctrl && key.name === 'c')) {
|
|
138
|
+
cleanup();
|
|
139
|
+
process.exit(0);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const cleanup = () => {
|
|
144
|
+
process.stdin.removeListener('keypress', onKeyPress);
|
|
145
|
+
if (process.stdin.isTTY) {
|
|
146
|
+
process.stdin.setRawMode(false);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
process.stdin.on('keypress', onKeyPress);
|
|
151
|
+
});
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
module.exports = { createBoxMenu };
|