hedgequantx 1.1.1

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.
@@ -0,0 +1,351 @@
1
+ /**
2
+ * HQX Server Service
3
+ * Secure WebSocket connection to HQX Algo Server
4
+ * All algo logic runs server-side - CLI only receives signals
5
+ */
6
+
7
+ const WebSocket = require('ws');
8
+ const crypto = require('crypto');
9
+ const https = require('https');
10
+
11
+ // HQX Server Configuration - Contabo Dedicated Server
12
+ const HQX_CONFIG = {
13
+ apiUrl: process.env.HQX_API_URL || 'http://173.212.223.75:3500',
14
+ wsUrl: process.env.HQX_WS_URL || 'ws://173.212.223.75:3500/ws',
15
+ version: 'v1'
16
+ };
17
+
18
+ class HQXServerService {
19
+ constructor() {
20
+ this.ws = null;
21
+ this.token = null;
22
+ this.apiKey = null;
23
+ this.sessionId = null;
24
+ this.connected = false;
25
+ this.reconnectAttempts = 0;
26
+ this.maxReconnectAttempts = 5;
27
+ this.listeners = new Map();
28
+ this.heartbeatInterval = null;
29
+ this.messageQueue = [];
30
+ }
31
+
32
+ /**
33
+ * Generate device fingerprint for security
34
+ */
35
+ _generateDeviceId() {
36
+ const os = require('os');
37
+ const data = `${os.hostname()}-${os.platform()}-${os.arch()}-${os.cpus()[0]?.model || 'unknown'}`;
38
+ return crypto.createHash('sha256').update(data).digest('hex').substring(0, 32);
39
+ }
40
+
41
+ /**
42
+ * HTTPS request helper
43
+ */
44
+ _request(endpoint, method = 'GET', data = null) {
45
+ return new Promise((resolve, reject) => {
46
+ const url = new URL(`${HQX_CONFIG.apiUrl}/${HQX_CONFIG.version}${endpoint}`);
47
+
48
+ const options = {
49
+ hostname: url.hostname,
50
+ port: 443,
51
+ path: url.pathname,
52
+ method: method,
53
+ headers: {
54
+ 'Content-Type': 'application/json',
55
+ 'Accept': 'application/json',
56
+ 'X-Device-Id': this._generateDeviceId()
57
+ }
58
+ };
59
+
60
+ if (this.token) {
61
+ options.headers['Authorization'] = `Bearer ${this.token}`;
62
+ }
63
+ if (this.apiKey) {
64
+ options.headers['X-API-Key'] = this.apiKey;
65
+ }
66
+
67
+ const req = https.request(options, (res) => {
68
+ let body = '';
69
+ res.on('data', chunk => body += chunk);
70
+ res.on('end', () => {
71
+ try {
72
+ const json = JSON.parse(body);
73
+ resolve({ statusCode: res.statusCode, data: json });
74
+ } catch (e) {
75
+ resolve({ statusCode: res.statusCode, data: body });
76
+ }
77
+ });
78
+ });
79
+
80
+ req.on('error', reject);
81
+ req.setTimeout(15000, () => {
82
+ req.destroy();
83
+ reject(new Error('Request timeout'));
84
+ });
85
+
86
+ if (data) {
87
+ req.write(JSON.stringify(data));
88
+ }
89
+
90
+ req.end();
91
+ });
92
+ }
93
+
94
+ /**
95
+ * Authenticate with HQX Server
96
+ */
97
+ async authenticate(apiKey) {
98
+ try {
99
+ const response = await this._request('/auth/token', 'POST', {
100
+ apiKey: apiKey,
101
+ deviceId: this._generateDeviceId(),
102
+ timestamp: Date.now()
103
+ });
104
+
105
+ if (response.statusCode === 200 && response.data.success) {
106
+ this.token = response.data.token;
107
+ this.apiKey = apiKey;
108
+ this.sessionId = response.data.sessionId;
109
+ return { success: true, sessionId: this.sessionId };
110
+ } else {
111
+ return {
112
+ success: false,
113
+ error: response.data.error || 'Authentication failed'
114
+ };
115
+ }
116
+ } catch (error) {
117
+ return { success: false, error: error.message };
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Connect to WebSocket server
123
+ */
124
+ async connect() {
125
+ return new Promise((resolve, reject) => {
126
+ if (!this.token) {
127
+ reject(new Error('Not authenticated'));
128
+ return;
129
+ }
130
+
131
+ const wsUrl = `${HQX_CONFIG.wsUrl}?token=${this.token}&session=${this.sessionId}`;
132
+
133
+ this.ws = new WebSocket(wsUrl, {
134
+ headers: {
135
+ 'X-Device-Id': this._generateDeviceId(),
136
+ 'X-API-Key': this.apiKey
137
+ }
138
+ });
139
+
140
+ this.ws.on('open', () => {
141
+ this.connected = true;
142
+ this.reconnectAttempts = 0;
143
+ this._startHeartbeat();
144
+ this._flushMessageQueue();
145
+ this._emit('connected', { sessionId: this.sessionId });
146
+ resolve({ success: true });
147
+ });
148
+
149
+ this.ws.on('message', (data) => {
150
+ try {
151
+ const message = JSON.parse(data.toString());
152
+ this._handleMessage(message);
153
+ } catch (e) {
154
+ // Invalid message format
155
+ }
156
+ });
157
+
158
+ this.ws.on('close', (code, reason) => {
159
+ this.connected = false;
160
+ this._stopHeartbeat();
161
+ this._emit('disconnected', { code, reason: reason.toString() });
162
+ this._attemptReconnect();
163
+ });
164
+
165
+ this.ws.on('error', (error) => {
166
+ this._emit('error', { message: error.message });
167
+ if (!this.connected) {
168
+ reject(error);
169
+ }
170
+ });
171
+
172
+ // Timeout for connection
173
+ setTimeout(() => {
174
+ if (!this.connected) {
175
+ this.ws.terminate();
176
+ reject(new Error('Connection timeout'));
177
+ }
178
+ }, 10000);
179
+ });
180
+ }
181
+
182
+ /**
183
+ * Handle incoming messages
184
+ */
185
+ _handleMessage(message) {
186
+ switch (message.type) {
187
+ case 'signal':
188
+ this._emit('signal', message.data);
189
+ break;
190
+ case 'trade':
191
+ this._emit('trade', message.data);
192
+ break;
193
+ case 'log':
194
+ this._emit('log', message.data);
195
+ break;
196
+ case 'stats':
197
+ this._emit('stats', message.data);
198
+ break;
199
+ case 'error':
200
+ this._emit('error', message.data);
201
+ break;
202
+ case 'pong':
203
+ // Heartbeat response
204
+ break;
205
+ default:
206
+ this._emit('message', message);
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Send message to server
212
+ */
213
+ send(type, data) {
214
+ const message = {
215
+ type,
216
+ data,
217
+ timestamp: Date.now(),
218
+ sessionId: this.sessionId
219
+ };
220
+
221
+ if (this.connected && this.ws.readyState === WebSocket.OPEN) {
222
+ this.ws.send(JSON.stringify(message));
223
+ } else {
224
+ this.messageQueue.push(message);
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Start algo trading session
230
+ */
231
+ startAlgo(config) {
232
+ this.send('start_algo', {
233
+ accountId: config.accountId,
234
+ contractId: config.contractId,
235
+ symbol: config.symbol,
236
+ contracts: config.contracts,
237
+ dailyTarget: config.dailyTarget,
238
+ maxRisk: config.maxRisk,
239
+ propfirm: config.propfirm,
240
+ propfirmToken: config.propfirmToken
241
+ });
242
+ }
243
+
244
+ /**
245
+ * Stop algo trading session
246
+ */
247
+ stopAlgo() {
248
+ this.send('stop_algo', {});
249
+ }
250
+
251
+ /**
252
+ * Event listeners
253
+ */
254
+ on(event, callback) {
255
+ if (!this.listeners.has(event)) {
256
+ this.listeners.set(event, []);
257
+ }
258
+ this.listeners.get(event).push(callback);
259
+ }
260
+
261
+ off(event, callback) {
262
+ if (this.listeners.has(event)) {
263
+ const callbacks = this.listeners.get(event);
264
+ const index = callbacks.indexOf(callback);
265
+ if (index > -1) {
266
+ callbacks.splice(index, 1);
267
+ }
268
+ }
269
+ }
270
+
271
+ _emit(event, data) {
272
+ if (this.listeners.has(event)) {
273
+ this.listeners.get(event).forEach(callback => {
274
+ try {
275
+ callback(data);
276
+ } catch (e) {
277
+ // Callback error
278
+ }
279
+ });
280
+ }
281
+ }
282
+
283
+ /**
284
+ * Heartbeat to keep connection alive
285
+ */
286
+ _startHeartbeat() {
287
+ this.heartbeatInterval = setInterval(() => {
288
+ if (this.connected) {
289
+ this.send('ping', { timestamp: Date.now() });
290
+ }
291
+ }, 30000);
292
+ }
293
+
294
+ _stopHeartbeat() {
295
+ if (this.heartbeatInterval) {
296
+ clearInterval(this.heartbeatInterval);
297
+ this.heartbeatInterval = null;
298
+ }
299
+ }
300
+
301
+ /**
302
+ * Flush queued messages after reconnect
303
+ */
304
+ _flushMessageQueue() {
305
+ while (this.messageQueue.length > 0) {
306
+ const message = this.messageQueue.shift();
307
+ if (this.ws.readyState === WebSocket.OPEN) {
308
+ this.ws.send(JSON.stringify(message));
309
+ }
310
+ }
311
+ }
312
+
313
+ /**
314
+ * Attempt to reconnect
315
+ */
316
+ _attemptReconnect() {
317
+ if (this.reconnectAttempts < this.maxReconnectAttempts) {
318
+ this.reconnectAttempts++;
319
+ const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
320
+
321
+ setTimeout(() => {
322
+ this.connect().catch(() => {
323
+ // Reconnect failed
324
+ });
325
+ }, delay);
326
+ }
327
+ }
328
+
329
+ /**
330
+ * Disconnect from server
331
+ */
332
+ disconnect() {
333
+ this._stopHeartbeat();
334
+ if (this.ws) {
335
+ this.ws.close();
336
+ this.ws = null;
337
+ }
338
+ this.connected = false;
339
+ this.token = null;
340
+ this.sessionId = null;
341
+ }
342
+
343
+ /**
344
+ * Check if connected
345
+ */
346
+ isConnected() {
347
+ return this.connected && this.ws && this.ws.readyState === WebSocket.OPEN;
348
+ }
349
+ }
350
+
351
+ module.exports = { HQXServerService, HQX_CONFIG };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Services Module Exports
3
+ */
4
+
5
+ const { ProjectXService } = require('./projectx');
6
+ const { storage, connections } = require('./session');
7
+
8
+ module.exports = {
9
+ ProjectXService,
10
+ storage,
11
+ connections
12
+ };
@@ -0,0 +1,309 @@
1
+ /**
2
+ * Local Storage Service
3
+ * Stores user data locally on their machine
4
+ * - Saved connections (PropFirm credentials)
5
+ * - Session history
6
+ * - User preferences
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const crypto = require('crypto');
12
+ const os = require('os');
13
+
14
+ // Storage directory in user's home folder
15
+ const STORAGE_DIR = path.join(os.homedir(), '.hedgequantx');
16
+ const CONNECTIONS_FILE = path.join(STORAGE_DIR, 'connections.enc');
17
+ const SETTINGS_FILE = path.join(STORAGE_DIR, 'settings.json');
18
+ const HISTORY_FILE = path.join(STORAGE_DIR, 'history.json');
19
+
20
+ // Encryption key derived from machine ID
21
+ const getEncryptionKey = () => {
22
+ const machineId = `${os.hostname()}-${os.platform()}-${os.userInfo().username}`;
23
+ return crypto.createHash('sha256').update(machineId).digest();
24
+ };
25
+
26
+ class LocalStorageService {
27
+ constructor() {
28
+ this._ensureStorageDir();
29
+ }
30
+
31
+ /**
32
+ * Ensure storage directory exists
33
+ */
34
+ _ensureStorageDir() {
35
+ if (!fs.existsSync(STORAGE_DIR)) {
36
+ fs.mkdirSync(STORAGE_DIR, { recursive: true, mode: 0o700 });
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Encrypt data
42
+ */
43
+ _encrypt(data) {
44
+ const key = getEncryptionKey();
45
+ const iv = crypto.randomBytes(16);
46
+ const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
47
+ let encrypted = cipher.update(JSON.stringify(data), 'utf8', 'hex');
48
+ encrypted += cipher.final('hex');
49
+ return iv.toString('hex') + ':' + encrypted;
50
+ }
51
+
52
+ /**
53
+ * Decrypt data
54
+ */
55
+ _decrypt(encryptedData) {
56
+ try {
57
+ const key = getEncryptionKey();
58
+ const [ivHex, encrypted] = encryptedData.split(':');
59
+ const iv = Buffer.from(ivHex, 'hex');
60
+ const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
61
+ let decrypted = decipher.update(encrypted, 'hex', 'utf8');
62
+ decrypted += decipher.final('utf8');
63
+ return JSON.parse(decrypted);
64
+ } catch (error) {
65
+ return null;
66
+ }
67
+ }
68
+
69
+ // ==================== CONNECTIONS ====================
70
+
71
+ /**
72
+ * Save a PropFirm connection
73
+ */
74
+ saveConnection(connection) {
75
+ const connections = this.getConnections();
76
+
77
+ // Check if connection already exists
78
+ const existingIndex = connections.findIndex(
79
+ c => c.propfirm === connection.propfirm && c.username === connection.username
80
+ );
81
+
82
+ const connectionData = {
83
+ id: connection.id || crypto.randomUUID(),
84
+ propfirm: connection.propfirm,
85
+ propfirmName: connection.propfirmName,
86
+ username: connection.username,
87
+ password: connection.password, // Encrypted in file
88
+ lastUsed: Date.now(),
89
+ createdAt: connection.createdAt || Date.now()
90
+ };
91
+
92
+ if (existingIndex >= 0) {
93
+ connections[existingIndex] = connectionData;
94
+ } else {
95
+ connections.push(connectionData);
96
+ }
97
+
98
+ // Save encrypted
99
+ const encrypted = this._encrypt(connections);
100
+ fs.writeFileSync(CONNECTIONS_FILE, encrypted, { mode: 0o600 });
101
+
102
+ return connectionData;
103
+ }
104
+
105
+ /**
106
+ * Get all saved connections
107
+ */
108
+ getConnections() {
109
+ try {
110
+ if (!fs.existsSync(CONNECTIONS_FILE)) {
111
+ return [];
112
+ }
113
+ const encrypted = fs.readFileSync(CONNECTIONS_FILE, 'utf8');
114
+ return this._decrypt(encrypted) || [];
115
+ } catch (error) {
116
+ return [];
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Get connection by ID
122
+ */
123
+ getConnection(id) {
124
+ const connections = this.getConnections();
125
+ return connections.find(c => c.id === id);
126
+ }
127
+
128
+ /**
129
+ * Delete a connection
130
+ */
131
+ deleteConnection(id) {
132
+ const connections = this.getConnections();
133
+ const filtered = connections.filter(c => c.id !== id);
134
+
135
+ if (filtered.length === connections.length) {
136
+ return false; // Not found
137
+ }
138
+
139
+ const encrypted = this._encrypt(filtered);
140
+ fs.writeFileSync(CONNECTIONS_FILE, encrypted, { mode: 0o600 });
141
+ return true;
142
+ }
143
+
144
+ /**
145
+ * Update last used timestamp
146
+ */
147
+ updateConnectionLastUsed(id) {
148
+ const connections = this.getConnections();
149
+ const connection = connections.find(c => c.id === id);
150
+
151
+ if (connection) {
152
+ connection.lastUsed = Date.now();
153
+ const encrypted = this._encrypt(connections);
154
+ fs.writeFileSync(CONNECTIONS_FILE, encrypted, { mode: 0o600 });
155
+ }
156
+ }
157
+
158
+ // ==================== SETTINGS ====================
159
+
160
+ /**
161
+ * Save user settings
162
+ */
163
+ saveSettings(settings) {
164
+ const currentSettings = this.getSettings();
165
+ const merged = { ...currentSettings, ...settings };
166
+ fs.writeFileSync(SETTINGS_FILE, JSON.stringify(merged, null, 2), { mode: 0o600 });
167
+ return merged;
168
+ }
169
+
170
+ /**
171
+ * Get user settings
172
+ */
173
+ getSettings() {
174
+ try {
175
+ if (!fs.existsSync(SETTINGS_FILE)) {
176
+ return this._getDefaultSettings();
177
+ }
178
+ const data = fs.readFileSync(SETTINGS_FILE, 'utf8');
179
+ return { ...this._getDefaultSettings(), ...JSON.parse(data) };
180
+ } catch (error) {
181
+ return this._getDefaultSettings();
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Default settings
187
+ */
188
+ _getDefaultSettings() {
189
+ return {
190
+ defaultContracts: 1,
191
+ defaultDailyTarget: 500,
192
+ defaultMaxRisk: 250,
193
+ autoConnect: false,
194
+ theme: 'dark',
195
+ notifications: true,
196
+ analyticsEnabled: true // User can opt-out
197
+ };
198
+ }
199
+
200
+ // ==================== HISTORY ====================
201
+
202
+ /**
203
+ * Add to trading history
204
+ */
205
+ addToHistory(entry) {
206
+ const history = this.getHistory();
207
+
208
+ history.push({
209
+ id: crypto.randomUUID(),
210
+ timestamp: Date.now(),
211
+ ...entry
212
+ });
213
+
214
+ // Keep last 1000 entries
215
+ const trimmed = history.slice(-1000);
216
+ fs.writeFileSync(HISTORY_FILE, JSON.stringify(trimmed, null, 2), { mode: 0o600 });
217
+
218
+ return entry;
219
+ }
220
+
221
+ /**
222
+ * Get trading history
223
+ */
224
+ getHistory(limit = 100) {
225
+ try {
226
+ if (!fs.existsSync(HISTORY_FILE)) {
227
+ return [];
228
+ }
229
+ const data = fs.readFileSync(HISTORY_FILE, 'utf8');
230
+ const history = JSON.parse(data);
231
+ return history.slice(-limit);
232
+ } catch (error) {
233
+ return [];
234
+ }
235
+ }
236
+
237
+ /**
238
+ * Get stats from history
239
+ */
240
+ getLocalStats() {
241
+ const history = this.getHistory(1000);
242
+ const trades = history.filter(h => h.type === 'trade');
243
+
244
+ const totalTrades = trades.length;
245
+ const wins = trades.filter(t => t.pnl > 0).length;
246
+ const losses = trades.filter(t => t.pnl < 0).length;
247
+ const totalPnl = trades.reduce((sum, t) => sum + (t.pnl || 0), 0);
248
+
249
+ return {
250
+ totalTrades,
251
+ wins,
252
+ losses,
253
+ winRate: totalTrades > 0 ? ((wins / totalTrades) * 100).toFixed(1) : 0,
254
+ totalPnl: totalPnl.toFixed(2),
255
+ avgPnl: totalTrades > 0 ? (totalPnl / totalTrades).toFixed(2) : 0
256
+ };
257
+ }
258
+
259
+ /**
260
+ * Clear all history
261
+ */
262
+ clearHistory() {
263
+ if (fs.existsSync(HISTORY_FILE)) {
264
+ fs.unlinkSync(HISTORY_FILE);
265
+ }
266
+ }
267
+
268
+ // ==================== UTILITIES ====================
269
+
270
+ /**
271
+ * Get storage path info
272
+ */
273
+ getStoragePath() {
274
+ return STORAGE_DIR;
275
+ }
276
+
277
+ /**
278
+ * Check if storage exists
279
+ */
280
+ hasStoredData() {
281
+ return fs.existsSync(CONNECTIONS_FILE) || fs.existsSync(HISTORY_FILE);
282
+ }
283
+
284
+ /**
285
+ * Export all data (for backup)
286
+ */
287
+ exportData() {
288
+ return {
289
+ connections: this.getConnections(),
290
+ settings: this.getSettings(),
291
+ history: this.getHistory(1000),
292
+ exportedAt: Date.now()
293
+ };
294
+ }
295
+
296
+ /**
297
+ * Clear all data
298
+ */
299
+ clearAll() {
300
+ if (fs.existsSync(CONNECTIONS_FILE)) fs.unlinkSync(CONNECTIONS_FILE);
301
+ if (fs.existsSync(SETTINGS_FILE)) fs.unlinkSync(SETTINGS_FILE);
302
+ if (fs.existsSync(HISTORY_FILE)) fs.unlinkSync(HISTORY_FILE);
303
+ }
304
+ }
305
+
306
+ // Singleton
307
+ const localStorage = new LocalStorageService();
308
+
309
+ module.exports = { LocalStorageService, localStorage };