hedgequantx 1.8.49 → 2.3.0

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.
Files changed (103) hide show
  1. package/README.md +7 -6
  2. package/bin/cli.js +13 -7
  3. package/dist/algo/copy-engine.js +3 -0
  4. package/dist/algo/copy-engine.jsc +0 -0
  5. package/dist/algo/engine.js +3 -0
  6. package/dist/algo/engine.jsc +0 -0
  7. package/dist/algo/market-data-rithmic.js +3 -0
  8. package/dist/algo/market-data-rithmic.jsc +0 -0
  9. package/dist/algo/market-data.js +3 -0
  10. package/dist/algo/market-data.jsc +0 -0
  11. package/dist/algo/rithmic/connection.js +3 -0
  12. package/dist/algo/rithmic/connection.jsc +0 -0
  13. package/dist/algo/rithmic/constants.js +3 -0
  14. package/dist/algo/rithmic/constants.jsc +0 -0
  15. package/dist/algo/rithmic/index.js +3 -0
  16. package/dist/algo/rithmic/index.jsc +0 -0
  17. package/dist/algo/rithmic/market-data.js +3 -0
  18. package/dist/algo/rithmic/market-data.jsc +0 -0
  19. package/dist/algo/rithmic/pnl.js +3 -0
  20. package/dist/algo/rithmic/pnl.jsc +0 -0
  21. package/dist/algo/rithmic/pool.js +3 -0
  22. package/dist/algo/rithmic/pool.jsc +0 -0
  23. package/dist/algo/rithmic/trading.js +3 -0
  24. package/dist/algo/rithmic/trading.jsc +0 -0
  25. package/dist/algo/rithmic-decoder.js +3 -0
  26. package/dist/algo/rithmic-decoder.jsc +0 -0
  27. package/dist/algo/strategies/ultra-scalping-v2.js +3 -0
  28. package/dist/algo/strategies/ultra-scalping-v2.jsc +0 -0
  29. package/dist/algo/strategies/ultra-scalping.js +3 -0
  30. package/dist/algo/strategies/ultra-scalping.jsc +0 -0
  31. package/dist/algo/trading-api-rithmic.js +3 -0
  32. package/dist/algo/trading-api-rithmic.jsc +0 -0
  33. package/dist/algo/trading-api.js +3 -0
  34. package/dist/algo/trading-api.jsc +0 -0
  35. package/dist/algo/utils/smart-logger.js +3 -0
  36. package/dist/algo/utils/smart-logger.jsc +0 -0
  37. package/dist/algo/utils/smart-logs.js +3 -0
  38. package/dist/algo/utils/smart-logs.jsc +0 -0
  39. package/package.json +33 -10
  40. package/protos/rithmic/account_pnl_position_update.proto +59 -0
  41. package/protos/rithmic/base.proto +7 -0
  42. package/protos/rithmic/best_bid_offer.proto +39 -0
  43. package/protos/rithmic/exchange_order_notification.proto +140 -0
  44. package/protos/rithmic/instrument_pnl_position_update.proto +50 -0
  45. package/protos/rithmic/last_trade.proto +53 -0
  46. package/protos/rithmic/request_account_list.proto +20 -0
  47. package/protos/rithmic/request_cancel_all_orders.proto +15 -0
  48. package/protos/rithmic/request_front_month_contract.proto +10 -0
  49. package/protos/rithmic/request_heartbeat.proto +13 -0
  50. package/protos/rithmic/request_login.proto +28 -0
  51. package/protos/rithmic/request_login_info.proto +10 -0
  52. package/protos/rithmic/request_logout.proto +10 -0
  53. package/protos/rithmic/request_market_data_update.proto +42 -0
  54. package/protos/rithmic/request_new_order.proto +84 -0
  55. package/protos/rithmic/request_pnl_position_snapshot.proto +14 -0
  56. package/protos/rithmic/request_pnl_position_updates.proto +20 -0
  57. package/protos/rithmic/request_product_codes.proto +9 -0
  58. package/protos/rithmic/request_rithmic_system_info.proto +8 -0
  59. package/protos/rithmic/request_show_order_history.proto +16 -0
  60. package/protos/rithmic/request_show_order_history_dates.proto +10 -0
  61. package/protos/rithmic/request_show_order_history_summary.proto +14 -0
  62. package/protos/rithmic/request_show_orders.proto +14 -0
  63. package/protos/rithmic/request_subscribe_for_order_updates.proto +14 -0
  64. package/protos/rithmic/request_tick_bar_replay.proto +48 -0
  65. package/protos/rithmic/request_trade_routes.proto +11 -0
  66. package/protos/rithmic/response_account_list.proto +18 -0
  67. package/protos/rithmic/response_front_month_contract.proto +13 -0
  68. package/protos/rithmic/response_heartbeat.proto +14 -0
  69. package/protos/rithmic/response_login.proto +18 -0
  70. package/protos/rithmic/response_login_info.proto +24 -0
  71. package/protos/rithmic/response_logout.proto +11 -0
  72. package/protos/rithmic/response_market_data_update.proto +9 -0
  73. package/protos/rithmic/response_new_order.proto +18 -0
  74. package/protos/rithmic/response_pnl_position_snapshot.proto +11 -0
  75. package/protos/rithmic/response_pnl_position_updates.proto +11 -0
  76. package/protos/rithmic/response_product_codes.proto +12 -0
  77. package/protos/rithmic/response_rithmic_system_info.proto +12 -0
  78. package/protos/rithmic/response_show_order_history.proto +11 -0
  79. package/protos/rithmic/response_show_order_history_dates.proto +13 -0
  80. package/protos/rithmic/response_show_order_history_summary.proto +11 -0
  81. package/protos/rithmic/response_show_orders.proto +11 -0
  82. package/protos/rithmic/response_subscribe_for_order_updates.proto +11 -0
  83. package/protos/rithmic/response_tick_bar_replay.proto +40 -0
  84. package/protos/rithmic/response_trade_routes.proto +19 -0
  85. package/protos/rithmic/rithmic_order_notification.proto +124 -0
  86. package/src/app.js +136 -89
  87. package/src/config/index.js +27 -8
  88. package/src/config/settings.js +155 -0
  89. package/src/pages/algo/copy-trading.js +293 -200
  90. package/src/pages/algo/one-account.js +1 -1
  91. package/src/security/encryption.js +81 -46
  92. package/src/security/index.js +12 -8
  93. package/src/security/rateLimit.js +68 -65
  94. package/src/security/validation.js +93 -79
  95. package/src/services/hqx-server.js +538 -206
  96. package/src/services/projectx/index.js +327 -204
  97. package/src/services/rithmic/index.js +288 -285
  98. package/src/services/session.js +184 -114
  99. package/src/services/tradovate/index.js +286 -297
  100. package/src/utils/http.js +236 -0
  101. package/src/utils/index.js +11 -2
  102. package/src/utils/logger.js +64 -33
  103. package/src/utils/prompts.js +79 -71
@@ -0,0 +1,12 @@
1
+ syntax = "proto3";
2
+
3
+ package rti;
4
+
5
+ message ResponseProductCodes {
6
+ int32 template_id = 154467; // Template ID = 112
7
+ repeated string rp_code = 132766; // Response code
8
+ string exchange = 110101; // Exchange
9
+ string product_code = 110102; // Product code (e.g., "MNQ", "ES")
10
+ string product_name = 110103; // Product name
11
+ string user_msg = 132760; // Echo of user message
12
+ }
@@ -0,0 +1,12 @@
1
+
2
+ package rti;
3
+
4
+ message ResponseRithmicSystemInfo
5
+ {
6
+ required int32 template_id = 154467;
7
+ repeated string user_msg = 132760;
8
+ repeated string rp_code = 132766;
9
+
10
+ repeated string system_name = 153628;
11
+ repeated bool has_aggregated_quotes = 153649;
12
+ }
@@ -0,0 +1,11 @@
1
+
2
+ package rti;
3
+
4
+ message ResponseShowOrderHistory
5
+ {
6
+ // PB_OFFSET = 100000, is the offset added for each MNM field id
7
+
8
+ required int32 template_id = 154467; // PB_OFFSET + MNM_TEMPLATE_ID
9
+ repeated string user_msg = 132760; // PB_OFFSET + MNM_USER_MSG
10
+ repeated string rp_code = 132766; // PB_OFFSET + MNM_RESPONSE_CODE
11
+ }
@@ -0,0 +1,13 @@
1
+
2
+ package rti;
3
+
4
+ message ResponseShowOrderHistoryDates
5
+ {
6
+ // CORRECT field IDs from rundef/async_rithmic (verified Dec 2025)
7
+ // PB_OFFSET = 100000
8
+ optional int32 template_id = 154467;
9
+ repeated string user_msg = 132760;
10
+ repeated string rq_handler_rp_code = 132764;
11
+ repeated string rp_code = 132766;
12
+ repeated string date = 110615; // Available dates (YYYYMMDD)
13
+ }
@@ -0,0 +1,11 @@
1
+
2
+ package rti;
3
+
4
+ message ResponseShowOrderHistorySummary
5
+ {
6
+ // Based on async_rithmic proto definitions
7
+ optional int32 template_id = 156580;
8
+ repeated string user_msg = 142744;
9
+ repeated string rp_code = 142750;
10
+ optional int32 fills_collected = 156569;
11
+ }
@@ -0,0 +1,11 @@
1
+
2
+ package rti;
3
+
4
+ message ResponseShowOrders
5
+ {
6
+ // PB_OFFSET = 100000, is the offset added for each MNM field id
7
+
8
+ required int32 template_id = 154467; // PB_OFFSET + MNM_TEMPLATE_ID
9
+ repeated string user_msg = 132760; // PB_OFFSET + MNM_USER_MSG
10
+ repeated string rp_code = 132766; // PB_OFFSET + MNM_RESPONSE_CODE
11
+ }
@@ -0,0 +1,11 @@
1
+
2
+ package rti;
3
+
4
+ message ResponseSubscribeForOrderUpdates
5
+ {
6
+ // PB_OFFSET = 100000, is the offset added for each MNM field id
7
+
8
+ required int32 template_id = 154467; // PB_OFFSET + MNM_TEMPLATE_ID
9
+ repeated string user_msg = 132760; // PB_OFFSET + MNM_USER_MSG
10
+ repeated string rp_code = 132766; // PB_OFFSET + MNM_RESPONSE_CODE
11
+ }
@@ -0,0 +1,40 @@
1
+
2
+ package rti;
3
+
4
+ message ResponseTickBarReplay
5
+ {
6
+ enum BarType {
7
+ TICK_BAR = 1;
8
+ RANGE_BAR = 2;
9
+ VOLUME_BAR = 3;
10
+ }
11
+
12
+ enum BarSubType {
13
+ REGULAR = 1;
14
+ CUSTOM = 2;
15
+ }
16
+
17
+ required int32 template_id = 154467;
18
+ optional string request_key = 132758;
19
+ repeated string user_msg = 132760;
20
+ repeated string rq_handler_rp_code = 132764;
21
+ repeated string rp_code = 132766;
22
+
23
+ optional string symbol = 110100;
24
+ optional string exchange = 110101;
25
+
26
+ optional BarType type = 119200;
27
+ optional BarSubType sub_type = 119208;
28
+ optional string type_specifier = 148162;
29
+ optional uint64 num_trades = 119204;
30
+ optional uint64 volume = 119205;
31
+ optional uint64 bid_volume = 119213;
32
+ optional uint64 ask_volume = 119214;
33
+ optional double open_price = 100019;
34
+ optional double close_price = 100021;
35
+ optional double high_price = 100012;
36
+ optional double low_price = 100013;
37
+ optional int32 custom_session_open_ssm = 119209;
38
+ repeated int32 data_bar_ssboe = 119202;
39
+ repeated int32 data_bar_usecs = 119203;
40
+ }
@@ -0,0 +1,19 @@
1
+
2
+ package rti;
3
+
4
+ message ResponseTradeRoutes
5
+ {
6
+ // PB_OFFSET = 100000, is the offset added for each MNM field id
7
+
8
+ required int32 template_id = 154467; // PB_OFFSET + MNM_TEMPLATE_ID
9
+ repeated string user_msg = 132760; // PB_OFFSET + MNM_USER_MSG
10
+ repeated string rq_handler_rp_code = 132764; // PB_OFFSET + MNM_REQUEST_HANDLER_RESPONSE_CODE
11
+ repeated string rp_code = 132766; // PB_OFFSET + MNM_RESPONSE_CODE
12
+
13
+ optional string fcm_id = 154013; // PB_OFFSET + MNM_FCM_ID
14
+ optional string ib_id = 154014; // PB_OFFSET + MNM_IB_ID
15
+ optional string exchange = 110101; // PB_OFFSET + MNM_EXCHANGE
16
+ optional string trade_route = 112016; // PB_OFFSET + MNM_TRADE_ROUTE
17
+ optional string status = 131407; // PB_OFFSET + MNM_SERVICE_STATE
18
+ optional bool is_default = 154689; // PB_OFFSET + MNM_DEFAULT_ROUTE
19
+ }
@@ -0,0 +1,124 @@
1
+
2
+ package rti;
3
+
4
+ message RithmicOrderNotification
5
+ {
6
+ enum NotifyType {
7
+ ORDER_RCVD_FROM_CLNT = 1;
8
+ MODIFY_RCVD_FROM_CLNT = 2;
9
+ CANCEL_RCVD_FROM_CLNT = 3;
10
+ OPEN_PENDING = 4;
11
+ MODIFY_PENDING = 5;
12
+ CANCEL_PENDING = 6;
13
+ ORDER_RCVD_BY_EXCH_GTWY = 7;
14
+ MODIFY_RCVD_BY_EXCH_GTWY = 8;
15
+ CANCEL_RCVD_BY_EXCH_GTWY = 9;
16
+ ORDER_SENT_TO_EXCH = 10;
17
+ MODIFY_SENT_TO_EXCH = 11;
18
+ CANCEL_SENT_TO_EXCH = 12;
19
+ OPEN = 13;
20
+ MODIFIED = 14;
21
+ COMPLETE = 15;
22
+ MODIFICATION_FAILED = 16;
23
+ CANCELLATION_FAILED = 17;
24
+ TRIGGER_PENDING = 18;
25
+ GENERIC = 19;
26
+ LINK_ORDERS_FAILED = 20;
27
+ }
28
+
29
+ enum TransactionType {
30
+ BUY = 1;
31
+ SELL = 2;
32
+ SS = 3;
33
+ }
34
+
35
+ enum Duration {
36
+ DAY = 1;
37
+ GTC = 2;
38
+ IOC = 3;
39
+ FOK = 4;
40
+ }
41
+
42
+ enum PriceType {
43
+ LIMIT = 1;
44
+ MARKET = 2;
45
+ STOP_LIMIT = 3;
46
+ STOP_MARKET = 4;
47
+ }
48
+
49
+ enum BracketType {
50
+ STOP_ONLY = 1;
51
+ TARGET_ONLY = 2;
52
+ TARGET_AND_STOP = 3;
53
+ STOP_ONLY_STATIC = 4;
54
+ TARGET_ONLY_STATIC = 5;
55
+ TARGET_AND_STOP_STATIC = 6;
56
+ }
57
+
58
+ enum OrderPlacement {
59
+ MANUAL = 1;
60
+ AUTO = 2;
61
+ }
62
+
63
+ required int32 template_id = 154467;
64
+ optional string user_tag = 154119;
65
+
66
+ optional NotifyType notify_type = 153625;
67
+ optional bool is_snapshot = 110121;
68
+
69
+ optional string status = 110303;
70
+ optional string basket_id = 110300;
71
+ optional string original_basket_id = 154497;
72
+ optional string linked_basket_ids = 110358;
73
+
74
+ optional string fcm_id = 154013;
75
+ optional string ib_id = 154014;
76
+ optional string user_id = 131003;
77
+ optional string account_id = 154008;
78
+
79
+ optional string symbol = 110100;
80
+ optional string exchange = 110101;
81
+ optional string trade_exchange = 112021;
82
+ optional string trade_route = 112016;
83
+ optional string exchange_order_id = 149238;
84
+ optional string instrument_type = 110116;
85
+ optional string completion_reason = 149273;
86
+
87
+ optional int32 quantity = 112004;
88
+ optional int32 quan_release_pending = 112027;
89
+ optional double price = 110306;
90
+ optional double trigger_price = 149247;
91
+
92
+ optional TransactionType transaction_type = 112003;
93
+ optional Duration duration = 112005;
94
+ optional PriceType price_type = 112008;
95
+ optional PriceType orig_price_type = 154770;
96
+ optional OrderPlacement manual_or_auto = 154710;
97
+ optional BracketType bracket_type = 157087;
98
+
99
+ optional double avg_fill_price = 110322;
100
+
101
+ optional int32 total_fill_size = 154111;
102
+ optional int32 total_unfilled_size = 154112;
103
+
104
+ optional string sequence_number = 112002;
105
+ optional string orig_sequence_number = 149263;
106
+ optional string cor_sequence_number = 149264;
107
+
108
+ optional string currency = 154382;
109
+ optional string country_code = 154172;
110
+
111
+ optional string text = 120008;
112
+ optional string report_text = 120028;
113
+ optional string remarks = 154806;
114
+
115
+ optional string window_name = 154629;
116
+ optional string originator_window_name = 154671;
117
+
118
+ optional int32 cancel_at_ssboe = 157085;
119
+ optional int32 cancel_at_usecs = 157086;
120
+ optional int32 cancel_after_secs = 154488;
121
+
122
+ optional int32 ssboe = 150100;
123
+ optional int32 usecs = 150101;
124
+ }
package/src/app.js CHANGED
@@ -19,13 +19,22 @@ const { showAccounts } = require('./pages/accounts');
19
19
  const { algoTradingMenu } = require('./pages/algo');
20
20
 
21
21
  // Menus
22
- const { projectXMenu, rithmicMenu, tradovateMenu, addPropAccountMenu, dashboardMenu, handleUpdate } = require('./menus');
22
+ const {
23
+ projectXMenu,
24
+ rithmicMenu,
25
+ tradovateMenu,
26
+ addPropAccountMenu,
27
+ dashboardMenu,
28
+ handleUpdate,
29
+ } = require('./menus');
23
30
 
24
- // Current service reference
31
+ /** @type {Object|null} */
25
32
  let currentService = null;
26
33
 
34
+ // ==================== TERMINAL ====================
35
+
27
36
  /**
28
- * Global terminal restoration
37
+ * Restore terminal state
29
38
  */
30
39
  const restoreTerminal = () => {
31
40
  try {
@@ -35,123 +44,153 @@ const restoreTerminal = () => {
35
44
  process.stdin.setRawMode(false);
36
45
  }
37
46
  process.stdin.removeAllListeners('keypress');
38
- } catch (e) {}
47
+ } catch {
48
+ // Ignore terminal errors
49
+ }
39
50
  };
40
51
 
52
+ // Signal handlers
41
53
  process.on('exit', restoreTerminal);
42
54
  process.on('SIGINT', () => { restoreTerminal(); process.exit(0); });
43
55
  process.on('SIGTERM', () => { restoreTerminal(); process.exit(0); });
56
+
44
57
  process.on('uncaughtException', (err) => {
45
58
  restoreTerminal();
59
+ log.error('Uncaught Exception', { error: err.message });
46
60
  console.error(chalk.red('Uncaught Exception:'), err.message);
47
61
  process.exit(1);
48
62
  });
63
+
49
64
  process.on('unhandledRejection', (reason) => {
50
65
  restoreTerminal();
66
+ log.error('Unhandled Rejection', { reason: String(reason) });
51
67
  console.error(chalk.red('Unhandled Rejection:'), reason);
52
68
  process.exit(1);
53
69
  });
54
70
 
71
+ // ==================== STATS ====================
72
+
55
73
  /**
56
- * Refresh cached stats
74
+ * Refresh cached stats from all connections
57
75
  */
58
76
  const refreshStats = async () => {
59
- if (connections.count() > 0) {
60
- try {
61
- const allAccounts = await connections.getAllAccounts();
62
- const activeAccounts = allAccounts.filter(acc => acc.status === 0);
63
-
64
- let totalBalance = null, totalPnl = null;
65
- let hasBalanceData = false, hasPnlData = false;
66
-
67
- activeAccounts.forEach(account => {
68
- if (account.balance !== null && account.balance !== undefined) {
69
- totalBalance = (totalBalance || 0) + account.balance;
70
- hasBalanceData = true;
71
- }
72
- if (account.profitAndLoss !== null && account.profitAndLoss !== undefined) {
73
- totalPnl = (totalPnl || 0) + account.profitAndLoss;
74
- hasPnlData = true;
75
- }
76
- });
77
-
78
- setCachedStats({
79
- connections: connections.count(),
80
- accounts: activeAccounts.length,
81
- balance: hasBalanceData ? totalBalance : null,
82
- pnl: hasPnlData ? totalPnl : null,
83
- pnlPercent: null
84
- });
85
- } catch (e) {}
86
- } else {
77
+ if (connections.count() === 0) {
87
78
  clearCachedStats();
79
+ return;
80
+ }
81
+
82
+ try {
83
+ const allAccounts = await connections.getAllAccounts();
84
+ const activeAccounts = allAccounts.filter(acc => acc.status === 0);
85
+
86
+ let totalBalance = null;
87
+ let totalPnl = null;
88
+ let hasBalanceData = false;
89
+ let hasPnlData = false;
90
+
91
+ for (const account of activeAccounts) {
92
+ if (account.balance != null) {
93
+ totalBalance = (totalBalance || 0) + account.balance;
94
+ hasBalanceData = true;
95
+ }
96
+ if (account.profitAndLoss != null) {
97
+ totalPnl = (totalPnl || 0) + account.profitAndLoss;
98
+ hasPnlData = true;
99
+ }
100
+ }
101
+
102
+ setCachedStats({
103
+ connections: connections.count(),
104
+ accounts: activeAccounts.length,
105
+ balance: hasBalanceData ? totalBalance : null,
106
+ pnl: hasPnlData ? totalPnl : null,
107
+ pnlPercent: null,
108
+ });
109
+ } catch (err) {
110
+ log.warn('Failed to refresh stats', { error: err.message });
88
111
  }
89
112
  };
90
113
 
114
+ // ==================== BANNER ====================
115
+
91
116
  /**
92
- * Display banner
117
+ * Display application banner
93
118
  */
94
119
  const banner = async () => {
95
120
  console.clear();
121
+
96
122
  const termWidth = process.stdout.columns || 100;
97
123
  const isMobile = termWidth < 60;
98
124
  const boxWidth = isMobile ? Math.max(termWidth - 2, 40) : Math.max(getLogoWidth(), 98);
99
125
  const innerWidth = boxWidth - 2;
100
126
  const version = require('../package.json').version;
101
-
127
+
102
128
  console.log(chalk.cyan('╔' + '═'.repeat(innerWidth) + '╗'));
129
+
130
+ // Logo
131
+ const logoLines = isMobile ? getMobileLogo() : getFullLogo();
103
132
 
104
- if (isMobile) {
105
- const logoHQ = ['██╗ ██╗ ██████╗ ','██║ ██║██╔═══██╗','███████║██║ ██║','██╔══██║██║▄▄ ██║','██║ ██║╚██████╔╝','╚═╝ ╚═╝ ╚══▀▀═╝ '];
106
- const logoX = ['██╗ ██╗','╚██╗██╔╝',' ╚███╔╝ ',' ██╔██╗ ','██╔╝ ██╗','╚═╝ ╚═╝'];
107
- logoHQ.forEach((line, i) => {
108
- const fullLine = chalk.cyan(line) + chalk.yellow(logoX[i]);
109
- const totalLen = line.length + logoX[i].length;
110
- const padding = innerWidth - totalLen;
111
- const leftPad = Math.floor(padding / 2);
112
- console.log(chalk.cyan('║') + ' '.repeat(leftPad) + fullLine + ' '.repeat(padding - leftPad) + chalk.cyan('║'));
113
- });
114
- } else {
115
- const logo = [
116
- '██╗ ██╗███████╗██████╗ ██████╗ ███████╗ ██████╗ ██╗ ██╗ █████╗ ███╗ ██╗████████╗',
117
- '██║ ██║██╔════╝██╔══██╗██╔════╝ ██╔════╝██╔═══██╗██║ ██║██╔══██╗████╗ ██║╚══██╔══╝',
118
- '███████║█████╗ ██║ ██║██║ ███╗█████╗ ██║ ██║██║ ██║███████║██╔██╗ ██║ ██║ ',
119
- '██╔══██║██╔══╝ ██║ ██║██║ ██║██╔══╝ ██║▄▄ ██║██║ ██║██╔══██║██║╚██╗██║ ██║ ',
120
- '██║ ██║███████╗██████╔╝╚██████╔╝███████╗╚██████╔╝╚██████╔╝██║ ██║██║ ╚████║ ██║ ',
121
- '╚═╝ ╚═╝╚══════╝╚═════╝ ╚═════╝ ╚══════╝ ╚══▀▀═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═╝ '
122
- ];
123
- const logoX = ['██╗ ██╗','╚██╗██╔╝',' ╚███╔╝ ',' ██╔██╗ ','██╔╝ ██╗','╚═╝ ╚═╝'];
124
- logo.forEach((line, i) => {
125
- const fullLine = chalk.cyan(line) + chalk.yellow(logoX[i]);
126
- const totalLen = line.length + logoX[i].length;
127
- const padding = innerWidth - totalLen;
128
- const leftPad = Math.floor(padding / 2);
129
- console.log(chalk.cyan('║') + ' '.repeat(leftPad) + fullLine + ' '.repeat(padding - leftPad) + chalk.cyan('║'));
130
- });
133
+ for (const [hq, x] of logoLines) {
134
+ const fullLine = chalk.cyan(hq) + chalk.yellow(x);
135
+ const totalLen = hq.length + x.length;
136
+ const padding = innerWidth - totalLen;
137
+ const leftPad = Math.floor(padding / 2);
138
+ console.log(chalk.cyan('║') + ' '.repeat(leftPad) + fullLine + ' '.repeat(padding - leftPad) + chalk.cyan('║'));
131
139
  }
132
-
140
+
133
141
  console.log(chalk.cyan('╠' + '═'.repeat(innerWidth) + '╣'));
142
+
134
143
  const tagline = isMobile ? `HQX v${version}` : `Prop Futures Algo Trading v${version}`;
135
144
  console.log(chalk.cyan('║') + chalk.white(centerText(tagline, innerWidth)) + chalk.cyan('║'));
136
145
  };
137
146
 
138
147
  /**
139
- * Display banner with closing border (for loading states)
148
+ * Get full logo lines
149
+ * @returns {Array<[string, string]>}
150
+ */
151
+ const getFullLogo = () => [
152
+ ['██╗ ██╗███████╗██████╗ ██████╗ ███████╗ ██████╗ ██╗ ██╗ █████╗ ███╗ ██╗████████╗', '██╗ ██╗'],
153
+ ['██║ ██║██╔════╝██╔══██╗██╔════╝ ██╔════╝██╔═══██╗██║ ██║██╔══██╗████╗ ██║╚══██╔══╝', '╚██╗██╔╝'],
154
+ ['███████║█████╗ ██║ ██║██║ ███╗█████╗ ██║ ██║██║ ██║███████║██╔██╗ ██║ ██║ ', ' ╚███╔╝ '],
155
+ ['██╔══██║██╔══╝ ██║ ██║██║ ██║██╔══╝ ██║▄▄ ██║██║ ██║██╔══██║██║╚██╗██║ ██║ ', ' ██╔██╗ '],
156
+ ['██║ ██║███████╗██████╔╝╚██████╔╝███████╗╚██████╔╝╚██████╔╝██║ ██║██║ ╚████║ ██║ ', '██╔╝ ██╗'],
157
+ ['╚═╝ ╚═╝╚══════╝╚═════╝ ╚═════╝ ╚══════╝ ╚══▀▀═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═╝ ', '╚═╝ ╚═╝'],
158
+ ];
159
+
160
+ /**
161
+ * Get mobile logo lines
162
+ * @returns {Array<[string, string]>}
163
+ */
164
+ const getMobileLogo = () => [
165
+ ['██╗ ██╗ ██████╗ ', '██╗ ██╗'],
166
+ ['██║ ██║██╔═══██╗', '╚██╗██╔╝'],
167
+ ['███████║██║ ██║', ' ╚███╔╝ '],
168
+ ['██╔══██║██║▄▄ ██║', ' ██╔██╗ '],
169
+ ['██║ ██║╚██████╔╝', '██╔╝ ██╗'],
170
+ ['╚═╝ ╚═╝ ╚══▀▀═╝ ', '╚═╝ ╚═╝'],
171
+ ];
172
+
173
+ /**
174
+ * Display banner with closing border
140
175
  */
141
176
  const bannerClosed = async () => {
142
177
  await banner();
143
- const boxWidth = process.stdout.columns < 60 ? Math.max(process.stdout.columns - 2, 40) : Math.max(getLogoWidth(), 98);
178
+ const termWidth = process.stdout.columns || 100;
179
+ const boxWidth = termWidth < 60 ? Math.max(termWidth - 2, 40) : Math.max(getLogoWidth(), 98);
144
180
  console.log(chalk.cyan('╚' + '═'.repeat(boxWidth - 2) + '╝'));
145
181
  };
146
182
 
183
+ // ==================== MENUS ====================
184
+
147
185
  /**
148
- * Main menu
186
+ * Main menu (platform selection)
187
+ * @returns {Promise<string>}
149
188
  */
150
189
  const mainMenu = async () => {
151
190
  const boxWidth = getLogoWidth();
152
191
  const innerWidth = boxWidth - 2;
153
192
  const col1Width = Math.floor(innerWidth / 2);
154
-
193
+
155
194
  const menuRow = (left, right) => {
156
195
  const leftPlain = left.replace(/\x1b\[[0-9;]*m/g, '');
157
196
  const rightPlain = right ? right.replace(/\x1b\[[0-9;]*m/g, '') : '';
@@ -159,29 +198,24 @@ const mainMenu = async () => {
159
198
  const rightPadded = (right || '') + ' '.repeat(Math.max(0, innerWidth - col1Width - rightPlain.length));
160
199
  console.log(chalk.cyan('║') + leftPadded + rightPadded + chalk.cyan('║'));
161
200
  };
162
-
163
- // Continue from banner (use ╠ not ╔)
201
+
164
202
  console.log(chalk.cyan('╠' + '═'.repeat(innerWidth) + '╣'));
165
203
  console.log(chalk.cyan('║') + chalk.white.bold(centerText('SELECT PLATFORM', innerWidth)) + chalk.cyan('║'));
166
204
  console.log(chalk.cyan('╠' + '═'.repeat(innerWidth) + '╣'));
167
-
205
+
168
206
  menuRow(chalk.cyan('[1] ProjectX'), chalk.cyan('[2] Rithmic'));
169
207
  menuRow(chalk.cyan('[3] Tradovate'), chalk.red('[X] Exit'));
170
-
208
+
171
209
  console.log(chalk.cyan('╚' + '═'.repeat(innerWidth) + '╝'));
172
210
 
173
211
  const input = await prompts.textInput(chalk.cyan('Select (1/2/3/X)'));
174
-
175
- const actionMap = {
176
- '1': 'projectx',
177
- '2': 'rithmic',
178
- '3': 'tradovate',
179
- 'x': 'exit'
180
- };
181
212
 
182
- return actionMap[(input || '').toLowerCase()] || 'exit';
213
+ const actions = { '1': 'projectx', '2': 'rithmic', '3': 'tradovate', 'x': 'exit' };
214
+ return actions[(input || '').toLowerCase()] || 'exit';
183
215
  };
184
216
 
217
+ // ==================== MAIN LOOP ====================
218
+
185
219
  /**
186
220
  * Main application loop
187
221
  */
@@ -189,10 +223,11 @@ const run = async () => {
189
223
  try {
190
224
  log.info('Starting HQX CLI');
191
225
  await bannerClosed();
192
-
226
+
227
+ // Restore session
193
228
  const spinner = ora({ text: 'Restoring session...', color: 'yellow' }).start();
194
229
  const restored = await connections.restoreFromStorage();
195
-
230
+
196
231
  if (restored) {
197
232
  spinner.succeed('Session restored');
198
233
  currentService = connections.getAll()[0].service;
@@ -201,39 +236,45 @@ const run = async () => {
201
236
  spinner.info('No active session');
202
237
  }
203
238
 
239
+ // Main loop
204
240
  while (true) {
205
241
  try {
206
242
  prepareStdin();
207
243
  await banner();
208
-
244
+
209
245
  if (!connections.isConnected()) {
210
246
  const choice = await mainMenu();
211
-
247
+
212
248
  if (choice === 'exit') {
213
249
  console.log(chalk.gray('Goodbye!'));
214
250
  process.exit(0);
215
251
  }
216
-
252
+
217
253
  let service = null;
218
254
  if (choice === 'projectx') service = await projectXMenu();
219
255
  else if (choice === 'rithmic') service = await rithmicMenu();
220
256
  else if (choice === 'tradovate') service = await tradovateMenu();
221
-
257
+
222
258
  if (service) {
223
259
  currentService = service;
224
260
  await refreshStats();
225
261
  }
226
262
  } else {
227
- const action = await dashboardMenu(currentService);
263
+ // Refresh stats before showing dashboard
264
+ await refreshStats();
228
265
 
266
+ const action = await dashboardMenu(currentService);
267
+
229
268
  switch (action) {
230
269
  case 'accounts':
231
270
  await showAccounts(currentService);
232
271
  break;
272
+
233
273
  case 'stats':
234
274
  await showStats(currentService);
235
275
  break;
236
- case 'add_prop_account':
276
+
277
+ case 'add_prop_account': {
237
278
  const platformChoice = await addPropAccountMenu();
238
279
  let newService = null;
239
280
  if (platformChoice === 'projectx') newService = await projectXMenu();
@@ -244,17 +285,21 @@ const run = async () => {
244
285
  await refreshStats();
245
286
  }
246
287
  break;
288
+ }
289
+
247
290
  case 'algotrading':
248
291
  try {
249
292
  await algoTradingMenu(currentService);
250
- } catch (algoErr) {
251
- console.log(chalk.red(` Algo error: ${algoErr.message}`));
293
+ } catch (err) {
294
+ console.log(chalk.red(` Algo error: ${err.message}`));
252
295
  prepareStdin();
253
296
  }
254
297
  break;
298
+
255
299
  case 'update':
256
300
  await handleUpdate();
257
301
  break;
302
+
258
303
  case 'disconnect':
259
304
  connections.disconnectAll();
260
305
  currentService = null;
@@ -264,10 +309,12 @@ const run = async () => {
264
309
  }
265
310
  }
266
311
  } catch (loopError) {
312
+ log.error('Loop error', { error: loopError.message });
267
313
  console.error(chalk.red('Error:'), loopError.message);
268
314
  }
269
315
  }
270
316
  } catch (error) {
317
+ log.error('Fatal error', { error: error.message });
271
318
  console.error(chalk.red('Fatal error:'), error.message);
272
319
  process.exit(1);
273
320
  }