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,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
+ };
@@ -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 };