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.
- package/README.md +247 -0
- package/bin/cli.js +2096 -0
- package/package.json +52 -0
- package/src/api/projectx_gatewayapi.json +1766 -0
- package/src/api/projectx_userapi.json +641 -0
- package/src/config/constants.js +75 -0
- package/src/config/index.js +24 -0
- package/src/config/propfirms.js +56 -0
- package/src/pages/index.js +9 -0
- package/src/pages/stats.js +289 -0
- package/src/services/hqx-server.js +351 -0
- package/src/services/index.js +12 -0
- package/src/services/local-storage.js +309 -0
- package/src/services/projectx.js +369 -0
- package/src/services/session.js +143 -0
- package/src/ui/box.js +105 -0
- package/src/ui/device.js +85 -0
- package/src/ui/index.js +48 -0
- package/src/ui/table.js +81 -0
|
@@ -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,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 };
|