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,369 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProjectX API Service
|
|
3
|
+
* Handles all API communication with PropFirm platforms
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const https = require('https');
|
|
7
|
+
const { PROPFIRMS } = require('../config');
|
|
8
|
+
|
|
9
|
+
class ProjectXService {
|
|
10
|
+
constructor(propfirmKey = 'topstep') {
|
|
11
|
+
this.propfirm = PROPFIRMS[propfirmKey] || PROPFIRMS.topstep;
|
|
12
|
+
this.token = null;
|
|
13
|
+
this.user = null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Make HTTPS request
|
|
18
|
+
*/
|
|
19
|
+
async _request(host, path, method = 'GET', data = null) {
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
const options = {
|
|
22
|
+
hostname: host,
|
|
23
|
+
port: 443,
|
|
24
|
+
path: path,
|
|
25
|
+
method: method,
|
|
26
|
+
headers: {
|
|
27
|
+
'Content-Type': 'application/json',
|
|
28
|
+
'Accept': 'application/json'
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
if (this.token) {
|
|
33
|
+
options.headers['Authorization'] = `Bearer ${this.token}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const req = https.request(options, (res) => {
|
|
37
|
+
let body = '';
|
|
38
|
+
res.on('data', chunk => body += chunk);
|
|
39
|
+
res.on('end', () => {
|
|
40
|
+
try {
|
|
41
|
+
resolve({ statusCode: res.statusCode, data: JSON.parse(body) });
|
|
42
|
+
} catch (e) {
|
|
43
|
+
resolve({ statusCode: res.statusCode, data: body });
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
req.on('error', reject);
|
|
49
|
+
req.setTimeout(10000, () => {
|
|
50
|
+
req.destroy();
|
|
51
|
+
reject(new Error('Request timeout'));
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (data) req.write(JSON.stringify(data));
|
|
55
|
+
req.end();
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ==================== AUTH ====================
|
|
60
|
+
|
|
61
|
+
async login(userName, password) {
|
|
62
|
+
try {
|
|
63
|
+
const response = await this._request(this.propfirm.userApi, '/Login', 'POST', { userName, password });
|
|
64
|
+
if (response.statusCode === 200 && response.data.token) {
|
|
65
|
+
this.token = response.data.token;
|
|
66
|
+
return { success: true, token: this.token };
|
|
67
|
+
}
|
|
68
|
+
return { success: false, error: response.data.errorMessage || 'Invalid credentials' };
|
|
69
|
+
} catch (error) {
|
|
70
|
+
return { success: false, error: error.message };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async loginWithApiKey(userName, apiKey) {
|
|
75
|
+
try {
|
|
76
|
+
const response = await this._request(this.propfirm.userApi, '/Login/key', 'POST', { userName, apiKey });
|
|
77
|
+
if (response.statusCode === 200 && response.data.token) {
|
|
78
|
+
this.token = response.data.token;
|
|
79
|
+
return { success: true, token: this.token };
|
|
80
|
+
}
|
|
81
|
+
return { success: false, error: response.data.errorMessage || 'Invalid API key' };
|
|
82
|
+
} catch (error) {
|
|
83
|
+
return { success: false, error: error.message };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
logout() {
|
|
88
|
+
this.token = null;
|
|
89
|
+
this.user = null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ==================== USER ====================
|
|
93
|
+
|
|
94
|
+
async getUser() {
|
|
95
|
+
try {
|
|
96
|
+
const response = await this._request(this.propfirm.userApi, '/User', 'GET');
|
|
97
|
+
if (response.statusCode === 200) {
|
|
98
|
+
this.user = response.data;
|
|
99
|
+
return { success: true, user: response.data };
|
|
100
|
+
}
|
|
101
|
+
return { success: false, error: 'Failed to get user info' };
|
|
102
|
+
} catch (error) {
|
|
103
|
+
return { success: false, error: error.message };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ==================== ACCOUNTS ====================
|
|
108
|
+
|
|
109
|
+
async getTradingAccounts() {
|
|
110
|
+
try {
|
|
111
|
+
const response = await this._request(this.propfirm.userApi, '/TradingAccount', 'GET');
|
|
112
|
+
if (response.statusCode === 200) {
|
|
113
|
+
const accounts = Array.isArray(response.data) ? response.data : [];
|
|
114
|
+
return { success: true, accounts };
|
|
115
|
+
}
|
|
116
|
+
return { success: false, accounts: [], error: 'Failed to get accounts' };
|
|
117
|
+
} catch (error) {
|
|
118
|
+
return { success: false, accounts: [], error: error.message };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ==================== TRADING (GatewayAPI) ====================
|
|
123
|
+
|
|
124
|
+
async getPositions(accountId) {
|
|
125
|
+
try {
|
|
126
|
+
const response = await this._request(
|
|
127
|
+
this.propfirm.gatewayApi,
|
|
128
|
+
'/api/Position/searchOpen',
|
|
129
|
+
'POST',
|
|
130
|
+
{ accountId: parseInt(accountId) }
|
|
131
|
+
);
|
|
132
|
+
if (response.statusCode === 200) {
|
|
133
|
+
const positions = response.data.positions || response.data || [];
|
|
134
|
+
return { success: true, positions: Array.isArray(positions) ? positions : [] };
|
|
135
|
+
}
|
|
136
|
+
return { success: true, positions: [] };
|
|
137
|
+
} catch (error) {
|
|
138
|
+
return { success: true, positions: [], error: error.message };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async getOrders(accountId) {
|
|
143
|
+
try {
|
|
144
|
+
const response = await this._request(
|
|
145
|
+
this.propfirm.gatewayApi,
|
|
146
|
+
'/api/Order/searchOpen',
|
|
147
|
+
'POST',
|
|
148
|
+
{ accountId: parseInt(accountId) }
|
|
149
|
+
);
|
|
150
|
+
if (response.statusCode === 200) {
|
|
151
|
+
const orders = response.data.orders || response.data || [];
|
|
152
|
+
return { success: true, orders: Array.isArray(orders) ? orders : [] };
|
|
153
|
+
}
|
|
154
|
+
return { success: true, orders: [] };
|
|
155
|
+
} catch (error) {
|
|
156
|
+
return { success: true, orders: [], error: error.message };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async placeOrder(orderData) {
|
|
161
|
+
try {
|
|
162
|
+
const response = await this._request(
|
|
163
|
+
this.propfirm.gatewayApi,
|
|
164
|
+
'/api/Order/place',
|
|
165
|
+
'POST',
|
|
166
|
+
orderData
|
|
167
|
+
);
|
|
168
|
+
if (response.statusCode === 200 && response.data.success) {
|
|
169
|
+
return { success: true, order: response.data };
|
|
170
|
+
}
|
|
171
|
+
return { success: false, error: response.data.errorMessage || 'Order failed' };
|
|
172
|
+
} catch (error) {
|
|
173
|
+
return { success: false, error: error.message };
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async cancelOrder(orderId) {
|
|
178
|
+
try {
|
|
179
|
+
const response = await this._request(
|
|
180
|
+
this.propfirm.gatewayApi,
|
|
181
|
+
'/api/Order/cancel',
|
|
182
|
+
'POST',
|
|
183
|
+
{ orderId: parseInt(orderId) }
|
|
184
|
+
);
|
|
185
|
+
return { success: response.statusCode === 200 && response.data.success };
|
|
186
|
+
} catch (error) {
|
|
187
|
+
return { success: false, error: error.message };
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async closePosition(accountId, contractId) {
|
|
192
|
+
try {
|
|
193
|
+
const response = await this._request(
|
|
194
|
+
this.propfirm.gatewayApi,
|
|
195
|
+
'/api/Position/closeContract',
|
|
196
|
+
'POST',
|
|
197
|
+
{ accountId: parseInt(accountId), contractId }
|
|
198
|
+
);
|
|
199
|
+
return { success: response.statusCode === 200 && response.data.success };
|
|
200
|
+
} catch (error) {
|
|
201
|
+
return { success: false, error: error.message };
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ==================== TRADES & STATS ====================
|
|
206
|
+
|
|
207
|
+
async getTradeHistory(accountId, days = 30) {
|
|
208
|
+
try {
|
|
209
|
+
const endDate = new Date();
|
|
210
|
+
const startDate = new Date();
|
|
211
|
+
startDate.setDate(startDate.getDate() - days);
|
|
212
|
+
|
|
213
|
+
const response = await this._request(
|
|
214
|
+
this.propfirm.gatewayApi,
|
|
215
|
+
'/api/Trade/search',
|
|
216
|
+
'POST',
|
|
217
|
+
{
|
|
218
|
+
accountId: parseInt(accountId),
|
|
219
|
+
startTimestamp: startDate.toISOString(),
|
|
220
|
+
endTimestamp: endDate.toISOString()
|
|
221
|
+
}
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
if (response.statusCode === 200 && response.data) {
|
|
225
|
+
let trades = [];
|
|
226
|
+
if (Array.isArray(response.data)) {
|
|
227
|
+
trades = response.data;
|
|
228
|
+
} else if (response.data.trades) {
|
|
229
|
+
trades = response.data.trades;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
success: true,
|
|
234
|
+
trades: trades.map(t => ({
|
|
235
|
+
...t,
|
|
236
|
+
timestamp: t.creationTimestamp || t.timestamp,
|
|
237
|
+
pnl: t.profitAndLoss || t.pnl || 0
|
|
238
|
+
}))
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
return { success: true, trades: [] };
|
|
242
|
+
} catch (error) {
|
|
243
|
+
return { success: true, trades: [], error: error.message };
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async getDailyStats(accountId) {
|
|
248
|
+
try {
|
|
249
|
+
const now = new Date();
|
|
250
|
+
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
251
|
+
|
|
252
|
+
const response = await this._request(
|
|
253
|
+
this.propfirm.gatewayApi,
|
|
254
|
+
'/api/Trade/search',
|
|
255
|
+
'POST',
|
|
256
|
+
{
|
|
257
|
+
accountId: parseInt(accountId),
|
|
258
|
+
startTimestamp: startOfMonth.toISOString(),
|
|
259
|
+
endTimestamp: now.toISOString()
|
|
260
|
+
}
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
if (response.statusCode === 200 && response.data) {
|
|
264
|
+
let trades = Array.isArray(response.data) ? response.data : (response.data.trades || []);
|
|
265
|
+
|
|
266
|
+
// Group by day
|
|
267
|
+
const dailyPnL = {};
|
|
268
|
+
trades.forEach(t => {
|
|
269
|
+
const ts = t.creationTimestamp || t.timestamp;
|
|
270
|
+
if (ts) {
|
|
271
|
+
const d = new Date(ts);
|
|
272
|
+
const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
273
|
+
dailyPnL[key] = (dailyPnL[key] || 0) + (t.profitAndLoss || t.pnl || 0);
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
success: true,
|
|
279
|
+
stats: Object.entries(dailyPnL).map(([date, pnl]) => ({ date, profitAndLoss: pnl }))
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
return { success: false, stats: [] };
|
|
283
|
+
} catch (error) {
|
|
284
|
+
return { success: false, stats: [], error: error.message };
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async getLifetimeStats(accountId) {
|
|
289
|
+
try {
|
|
290
|
+
const tradesResult = await this.getTradeHistory(accountId, 90);
|
|
291
|
+
|
|
292
|
+
if (!tradesResult.success || tradesResult.trades.length === 0) {
|
|
293
|
+
return { success: true, stats: null };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const trades = tradesResult.trades;
|
|
297
|
+
let stats = {
|
|
298
|
+
totalTrades: trades.length,
|
|
299
|
+
winningTrades: 0,
|
|
300
|
+
losingTrades: 0,
|
|
301
|
+
totalWinAmount: 0,
|
|
302
|
+
totalLossAmount: 0,
|
|
303
|
+
bestTrade: 0,
|
|
304
|
+
worstTrade: 0,
|
|
305
|
+
totalVolume: 0,
|
|
306
|
+
maxConsecutiveWins: 0,
|
|
307
|
+
maxConsecutiveLosses: 0,
|
|
308
|
+
longTrades: 0,
|
|
309
|
+
shortTrades: 0
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
let consecutiveWins = 0, consecutiveLosses = 0;
|
|
313
|
+
|
|
314
|
+
trades.forEach(t => {
|
|
315
|
+
const pnl = t.profitAndLoss || t.pnl || 0;
|
|
316
|
+
const size = t.size || t.quantity || 1;
|
|
317
|
+
|
|
318
|
+
stats.totalVolume += Math.abs(size);
|
|
319
|
+
if (t.side === 0) stats.longTrades++;
|
|
320
|
+
else if (t.side === 1) stats.shortTrades++;
|
|
321
|
+
|
|
322
|
+
if (pnl > 0) {
|
|
323
|
+
stats.winningTrades++;
|
|
324
|
+
stats.totalWinAmount += pnl;
|
|
325
|
+
if (pnl > stats.bestTrade) stats.bestTrade = pnl;
|
|
326
|
+
consecutiveWins++;
|
|
327
|
+
consecutiveLosses = 0;
|
|
328
|
+
if (consecutiveWins > stats.maxConsecutiveWins) stats.maxConsecutiveWins = consecutiveWins;
|
|
329
|
+
} else if (pnl < 0) {
|
|
330
|
+
stats.losingTrades++;
|
|
331
|
+
stats.totalLossAmount += Math.abs(pnl);
|
|
332
|
+
if (pnl < stats.worstTrade) stats.worstTrade = pnl;
|
|
333
|
+
consecutiveLosses++;
|
|
334
|
+
consecutiveWins = 0;
|
|
335
|
+
if (consecutiveLosses > stats.maxConsecutiveLosses) stats.maxConsecutiveLosses = consecutiveLosses;
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
stats.profitFactor = stats.totalLossAmount > 0 ? stats.totalWinAmount / stats.totalLossAmount : 0;
|
|
340
|
+
stats.avgWin = stats.winningTrades > 0 ? stats.totalWinAmount / stats.winningTrades : 0;
|
|
341
|
+
stats.avgLoss = stats.losingTrades > 0 ? stats.totalLossAmount / stats.losingTrades : 0;
|
|
342
|
+
|
|
343
|
+
return { success: true, stats };
|
|
344
|
+
} catch (error) {
|
|
345
|
+
return { success: false, stats: null, error: error.message };
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// ==================== CONTRACTS ====================
|
|
350
|
+
|
|
351
|
+
async searchContracts(searchText) {
|
|
352
|
+
try {
|
|
353
|
+
const response = await this._request(
|
|
354
|
+
this.propfirm.gatewayApi,
|
|
355
|
+
'/api/Contract/search',
|
|
356
|
+
'POST',
|
|
357
|
+
{ searchText, live: false }
|
|
358
|
+
);
|
|
359
|
+
if (response.statusCode === 200) {
|
|
360
|
+
return { success: true, contracts: response.data.contracts || response.data || [] };
|
|
361
|
+
}
|
|
362
|
+
return { success: false, contracts: [] };
|
|
363
|
+
} catch (error) {
|
|
364
|
+
return { success: false, contracts: [], error: error.message };
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
module.exports = { ProjectXService };
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Management
|
|
3
|
+
* Handles multi-connection state and persistence
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
const { ProjectXService } = require('./projectx');
|
|
10
|
+
|
|
11
|
+
const SESSION_FILE = path.join(os.homedir(), '.hedgequantx', 'session.json');
|
|
12
|
+
|
|
13
|
+
// Session Storage
|
|
14
|
+
const storage = {
|
|
15
|
+
save(sessions) {
|
|
16
|
+
try {
|
|
17
|
+
const dir = path.dirname(SESSION_FILE);
|
|
18
|
+
if (!fs.existsSync(dir)) {
|
|
19
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
fs.writeFileSync(SESSION_FILE, JSON.stringify(sessions, null, 2));
|
|
22
|
+
} catch (e) { /* ignore */ }
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
load() {
|
|
26
|
+
try {
|
|
27
|
+
if (fs.existsSync(SESSION_FILE)) {
|
|
28
|
+
return JSON.parse(fs.readFileSync(SESSION_FILE, 'utf8'));
|
|
29
|
+
}
|
|
30
|
+
} catch (e) { /* ignore */ }
|
|
31
|
+
return [];
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
clear() {
|
|
35
|
+
try {
|
|
36
|
+
if (fs.existsSync(SESSION_FILE)) {
|
|
37
|
+
fs.unlinkSync(SESSION_FILE);
|
|
38
|
+
}
|
|
39
|
+
} catch (e) { /* ignore */ }
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Connection Manager
|
|
44
|
+
const connections = {
|
|
45
|
+
services: [],
|
|
46
|
+
|
|
47
|
+
add(type, service, propfirm = null, token = null) {
|
|
48
|
+
this.services.push({
|
|
49
|
+
type,
|
|
50
|
+
service,
|
|
51
|
+
propfirm,
|
|
52
|
+
token,
|
|
53
|
+
connectedAt: new Date()
|
|
54
|
+
});
|
|
55
|
+
this.saveToStorage();
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
saveToStorage() {
|
|
59
|
+
const sessions = this.services.map(conn => ({
|
|
60
|
+
type: conn.type,
|
|
61
|
+
propfirm: conn.propfirm,
|
|
62
|
+
token: conn.service.token || conn.token
|
|
63
|
+
}));
|
|
64
|
+
storage.save(sessions);
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
async restoreFromStorage() {
|
|
68
|
+
const sessions = storage.load();
|
|
69
|
+
for (const session of sessions) {
|
|
70
|
+
try {
|
|
71
|
+
if (session.type === 'projectx' && session.token) {
|
|
72
|
+
const service = new ProjectXService(session.propfirm.toLowerCase().replace(/ /g, '_'));
|
|
73
|
+
service.token = session.token;
|
|
74
|
+
|
|
75
|
+
const userResult = await service.getUser();
|
|
76
|
+
if (userResult.success) {
|
|
77
|
+
this.services.push({
|
|
78
|
+
type: session.type,
|
|
79
|
+
service,
|
|
80
|
+
propfirm: session.propfirm,
|
|
81
|
+
token: session.token,
|
|
82
|
+
connectedAt: new Date()
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
} catch (e) { /* invalid session */ }
|
|
87
|
+
}
|
|
88
|
+
return this.services.length > 0;
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
remove(index) {
|
|
92
|
+
this.services.splice(index, 1);
|
|
93
|
+
this.saveToStorage();
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
getAll() {
|
|
97
|
+
return this.services;
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
getByType(type) {
|
|
101
|
+
return this.services.filter(c => c.type === type);
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
count() {
|
|
105
|
+
return this.services.length;
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
async getAllAccounts() {
|
|
109
|
+
const allAccounts = [];
|
|
110
|
+
for (const conn of this.services) {
|
|
111
|
+
try {
|
|
112
|
+
const result = await conn.service.getTradingAccounts();
|
|
113
|
+
if (result.success && result.accounts) {
|
|
114
|
+
result.accounts.forEach(account => {
|
|
115
|
+
allAccounts.push({
|
|
116
|
+
...account,
|
|
117
|
+
connectionType: conn.type,
|
|
118
|
+
propfirm: conn.propfirm || conn.type,
|
|
119
|
+
service: conn.service
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
} catch (e) { /* ignore */ }
|
|
124
|
+
}
|
|
125
|
+
return allAccounts;
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
isConnected() {
|
|
129
|
+
return this.services.length > 0;
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
disconnectAll() {
|
|
133
|
+
this.services.forEach(conn => {
|
|
134
|
+
if (conn.service && conn.service.logout) {
|
|
135
|
+
conn.service.logout();
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
this.services = [];
|
|
139
|
+
storage.clear();
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
module.exports = { storage, connections };
|
package/src/ui/box.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ASCII Box Drawing Utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const chalk = require('chalk');
|
|
6
|
+
const figlet = require('figlet');
|
|
7
|
+
|
|
8
|
+
// Cache logo width
|
|
9
|
+
let logoWidth = null;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Get logo width for consistent box sizing
|
|
13
|
+
*/
|
|
14
|
+
const getLogoWidth = () => {
|
|
15
|
+
if (!logoWidth) {
|
|
16
|
+
const logoText = figlet.textSync('HEDGEQUANTX', { font: 'ANSI Shadow' });
|
|
17
|
+
const lines = logoText.split('\n').filter(line => line.trim().length > 0);
|
|
18
|
+
logoWidth = Math.max(...lines.map(line => line.length)) + 4;
|
|
19
|
+
}
|
|
20
|
+
return logoWidth;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get visible length of text (excluding ANSI codes)
|
|
25
|
+
*/
|
|
26
|
+
const visibleLength = (text) => {
|
|
27
|
+
return (text || '').replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Center text in a given width
|
|
32
|
+
*/
|
|
33
|
+
const centerText = (text, width) => {
|
|
34
|
+
const len = visibleLength(text);
|
|
35
|
+
if (len >= width) return text;
|
|
36
|
+
const padding = width - len;
|
|
37
|
+
const leftPad = Math.floor(padding / 2);
|
|
38
|
+
const rightPad = padding - leftPad;
|
|
39
|
+
return ' '.repeat(leftPad) + text + ' '.repeat(rightPad);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Pad text to exact width
|
|
44
|
+
*/
|
|
45
|
+
const padText = (text, width) => {
|
|
46
|
+
const len = visibleLength(text);
|
|
47
|
+
if (len >= width) return text;
|
|
48
|
+
return (text || '') + ' '.repeat(width - len);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Draw box header with title
|
|
53
|
+
*/
|
|
54
|
+
const drawBoxHeader = (title, width) => {
|
|
55
|
+
const innerWidth = width - 2;
|
|
56
|
+
console.log(chalk.cyan('\u2554' + '\u2550'.repeat(innerWidth) + '\u2557'));
|
|
57
|
+
console.log(chalk.cyan('\u2551') + chalk.cyan.bold(centerText(title, innerWidth)) + chalk.cyan('\u2551'));
|
|
58
|
+
console.log(chalk.cyan('\u2560' + '\u2550'.repeat(innerWidth) + '\u2563'));
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Draw box footer
|
|
63
|
+
*/
|
|
64
|
+
const drawBoxFooter = (width) => {
|
|
65
|
+
const innerWidth = width - 2;
|
|
66
|
+
console.log(chalk.cyan('\u255A' + '\u2550'.repeat(innerWidth) + '\u255D'));
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Draw a single row inside a box
|
|
71
|
+
*/
|
|
72
|
+
const drawBoxRow = (content, width) => {
|
|
73
|
+
const innerWidth = width - 2;
|
|
74
|
+
console.log(chalk.cyan('\u2551') + padText(content, innerWidth) + chalk.cyan('\u2551'));
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Draw separator line inside a box
|
|
79
|
+
*/
|
|
80
|
+
const drawBoxSeparator = (width) => {
|
|
81
|
+
const innerWidth = width - 2;
|
|
82
|
+
console.log(chalk.cyan('\u2560' + '\u2500'.repeat(innerWidth) + '\u2563'));
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Print centered logo
|
|
87
|
+
*/
|
|
88
|
+
const printLogo = () => {
|
|
89
|
+
const logoText = figlet.textSync('HEDGEQUANTX', { font: 'ANSI Shadow' });
|
|
90
|
+
console.log(chalk.cyan(logoText));
|
|
91
|
+
console.log(chalk.gray.italic(' Prop Futures Algo Trading CLI'));
|
|
92
|
+
console.log();
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
module.exports = {
|
|
96
|
+
getLogoWidth,
|
|
97
|
+
visibleLength,
|
|
98
|
+
centerText,
|
|
99
|
+
padText,
|
|
100
|
+
drawBoxHeader,
|
|
101
|
+
drawBoxFooter,
|
|
102
|
+
drawBoxRow,
|
|
103
|
+
drawBoxSeparator,
|
|
104
|
+
printLogo
|
|
105
|
+
};
|
package/src/ui/device.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device Detection & Terminal Info
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const chalk = require('chalk');
|
|
6
|
+
|
|
7
|
+
let cachedDevice = null;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Detect device type and terminal capabilities
|
|
11
|
+
*/
|
|
12
|
+
const detectDevice = () => {
|
|
13
|
+
const width = process.stdout.columns || 80;
|
|
14
|
+
const height = process.stdout.rows || 24;
|
|
15
|
+
const isTTY = process.stdout.isTTY || false;
|
|
16
|
+
const platform = process.platform;
|
|
17
|
+
const termProgram = process.env.TERM_PROGRAM || '';
|
|
18
|
+
const term = process.env.TERM || '';
|
|
19
|
+
const sshClient = process.env.SSH_CLIENT || process.env.SSH_TTY || '';
|
|
20
|
+
|
|
21
|
+
// Detect mobile terminal apps
|
|
22
|
+
const mobileTerminals = ['termux', 'ish', 'a-shell', 'blink'];
|
|
23
|
+
const isMobileTerminal = mobileTerminals.some(t =>
|
|
24
|
+
termProgram.toLowerCase().includes(t) ||
|
|
25
|
+
term.toLowerCase().includes(t)
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
// Device type based on width
|
|
29
|
+
let deviceType, deviceIcon;
|
|
30
|
+
|
|
31
|
+
if (width < 50 || isMobileTerminal) {
|
|
32
|
+
deviceType = 'mobile';
|
|
33
|
+
deviceIcon = '[M]';
|
|
34
|
+
} else if (width < 80) {
|
|
35
|
+
deviceType = 'tablet';
|
|
36
|
+
deviceIcon = '[T]';
|
|
37
|
+
} else if (width < 120) {
|
|
38
|
+
deviceType = 'desktop';
|
|
39
|
+
deviceIcon = '[D]';
|
|
40
|
+
} else {
|
|
41
|
+
deviceType = 'desktop-large';
|
|
42
|
+
deviceIcon = '[L]';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
width,
|
|
47
|
+
height,
|
|
48
|
+
deviceType,
|
|
49
|
+
deviceIcon,
|
|
50
|
+
isMobile: deviceType === 'mobile',
|
|
51
|
+
isTablet: deviceType === 'tablet',
|
|
52
|
+
isDesktop: deviceType === 'desktop' || deviceType === 'desktop-large',
|
|
53
|
+
isLargeDesktop: deviceType === 'desktop-large',
|
|
54
|
+
platform,
|
|
55
|
+
isTTY,
|
|
56
|
+
isRemote: !!sshClient,
|
|
57
|
+
termProgram,
|
|
58
|
+
supportsColor: chalk.supportsColor ? true : false,
|
|
59
|
+
maxContentWidth: Math.min(width - 4, deviceType === 'mobile' ? 45 : 70),
|
|
60
|
+
menuPageSize: deviceType === 'mobile' ? 6 : (deviceType === 'tablet' ? 10 : 15)
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get cached device info (updates on terminal resize)
|
|
66
|
+
*/
|
|
67
|
+
const getDevice = () => {
|
|
68
|
+
if (!cachedDevice) {
|
|
69
|
+
cachedDevice = detectDevice();
|
|
70
|
+
process.stdout.on('resize', () => {
|
|
71
|
+
cachedDevice = detectDevice();
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return cachedDevice;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get separator line based on device width
|
|
79
|
+
*/
|
|
80
|
+
const getSeparator = (char = '-') => {
|
|
81
|
+
const device = getDevice();
|
|
82
|
+
return char.repeat(Math.min(device.width - 2, 70));
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
module.exports = { detectDevice, getDevice, getSeparator };
|