hedgequantx 1.3.1 → 1.3.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hedgequantx",
3
- "version": "1.3.1",
3
+ "version": "1.3.3",
4
4
  "description": "Prop Futures Algo Trading CLI - Connect to Topstep, Alpha Futures, and other prop firms",
5
5
  "main": "src/app.js",
6
6
  "bin": {
package/src/app.js CHANGED
@@ -70,30 +70,40 @@ const banner = async () => {
70
70
  const version = require('../package.json').version;
71
71
 
72
72
  // Get stats if connected (only active accounts: status === 0)
73
+ // STRICT: Only display values from API, no estimation
73
74
  let statsInfo = null;
74
75
  if (connections.count() > 0) {
75
76
  try {
76
77
  const allAccounts = await connections.getAllAccounts();
77
78
  const activeAccounts = allAccounts.filter(acc => acc.status === 0);
78
- let totalBalance = 0;
79
- let totalStartingBalance = 0;
80
- let totalPnl = 0;
79
+
80
+ // Sum only non-null values from API
81
+ let totalBalance = null;
82
+ let totalPnl = null;
83
+ let hasBalanceData = false;
84
+ let hasPnlData = false;
81
85
 
82
86
  activeAccounts.forEach(account => {
83
- totalBalance += account.balance || 0;
84
- totalStartingBalance += account.startingBalance || 0;
85
- totalPnl += account.profitAndLoss || 0;
87
+ // Balance: only sum if API returned a value
88
+ if (account.balance !== null && account.balance !== undefined) {
89
+ totalBalance = (totalBalance || 0) + account.balance;
90
+ hasBalanceData = true;
91
+ }
92
+
93
+ // P&L: only sum if API returned a value
94
+ if (account.profitAndLoss !== null && account.profitAndLoss !== undefined) {
95
+ totalPnl = (totalPnl || 0) + account.profitAndLoss;
96
+ hasPnlData = true;
97
+ }
86
98
  });
87
99
 
88
- const pnl = totalPnl !== 0 ? totalPnl : (totalBalance - totalStartingBalance);
89
- const pnlPercent = totalStartingBalance > 0 ? ((pnl / totalStartingBalance) * 100).toFixed(1) : '0.0';
90
-
91
100
  statsInfo = {
92
101
  connections: connections.count(),
93
102
  accounts: activeAccounts.length,
94
- balance: totalBalance,
95
- pnl: pnl,
96
- pnlPercent: pnlPercent
103
+ balance: hasBalanceData ? totalBalance : null,
104
+ pnl: hasPnlData ? totalPnl : null,
105
+ // No percentage calculation if no verified data
106
+ pnlPercent: null
97
107
  };
98
108
  } catch (e) {
99
109
  // Ignore errors
@@ -168,19 +178,33 @@ const banner = async () => {
168
178
  console.log(chalk.cyan('║') + chalk.white(centerText(tagline, innerWidth)) + chalk.cyan('║'));
169
179
 
170
180
  // Stats bar if connected
181
+ // STRICT: Only display verified values from API, show '--' for unavailable data
171
182
  if (statsInfo) {
172
183
  console.log(chalk.cyan('╠' + '═'.repeat(innerWidth) + '╣'));
173
184
 
174
- const pnlColor = statsInfo.pnl >= 0 ? chalk.green : chalk.red;
175
- const pnlSign = statsInfo.pnl >= 0 ? '+' : '';
176
-
177
185
  const connStr = `Connections: ${statsInfo.connections}`;
178
186
  const accStr = `Accounts: ${statsInfo.accounts}`;
179
- const balStr = `Balance: $${statsInfo.balance.toLocaleString()}`;
180
- const pnlStr = `P&L: $${statsInfo.pnl.toLocaleString()} (${pnlSign}${statsInfo.pnlPercent}%)`;
187
+
188
+ // Balance: show '--' if not available from API
189
+ const balStr = statsInfo.balance !== null
190
+ ? `Balance: $${statsInfo.balance.toLocaleString()}`
191
+ : `Balance: --`;
192
+ const balColor = statsInfo.balance !== null ? chalk.green : chalk.gray;
193
+
194
+ // P&L: show '--' if not available from API
195
+ let pnlDisplay;
196
+ let pnlColor;
197
+ if (statsInfo.pnl !== null) {
198
+ const pnlSign = statsInfo.pnl >= 0 ? '+' : '';
199
+ pnlColor = statsInfo.pnl >= 0 ? chalk.green : chalk.red;
200
+ pnlDisplay = `$${statsInfo.pnl.toLocaleString()} (${pnlSign}${statsInfo.pnl.toFixed(1)})`;
201
+ } else {
202
+ pnlColor = chalk.gray;
203
+ pnlDisplay = '--';
204
+ }
181
205
 
182
206
  // Build full stats text and calculate padding
183
- const statsText = `${connStr} ${accStr} ${balStr} ${pnlStr}`;
207
+ const statsText = `${connStr} ${accStr} ${balStr} P&L: ${pnlDisplay}`;
184
208
  const statsLen = statsText.length;
185
209
  const statsLeftPad = Math.floor((innerWidth - statsLen) / 2);
186
210
  const statsRightPad = innerWidth - statsLen - statsLeftPad;
@@ -188,9 +212,9 @@ const banner = async () => {
188
212
  console.log(chalk.cyan('║') + ' '.repeat(statsLeftPad) +
189
213
  chalk.white(connStr) + ' ' +
190
214
  chalk.white(accStr) + ' ' +
191
- chalk.white('Balance: ') + chalk.green(`$${statsInfo.balance.toLocaleString()}`) + ' ' +
192
- chalk.white('P&L: ') + pnlColor(`$${statsInfo.pnl.toLocaleString()} (${pnlSign}${statsInfo.pnlPercent}%)`) +
193
- ' '.repeat(statsRightPad) + chalk.cyan('║')
215
+ chalk.white('Balance: ') + balColor(statsInfo.balance !== null ? `$${statsInfo.balance.toLocaleString()}` : '--') + ' ' +
216
+ chalk.white('P&L: ') + pnlColor(pnlDisplay) +
217
+ ' '.repeat(Math.max(0, statsRightPad)) + chalk.cyan('║')
194
218
  );
195
219
  }
196
220
 
@@ -1,6 +1,8 @@
1
1
  /**
2
2
  * ProjectX API Service
3
3
  * Main service for ProjectX prop firm connections
4
+ *
5
+ * STRICT RULE: Display ONLY values returned by API. No estimation, no simulation.
4
6
  */
5
7
 
6
8
  const https = require('https');
@@ -145,18 +147,131 @@ class ProjectXService {
145
147
 
146
148
  // ==================== ACCOUNTS ====================
147
149
 
150
+ /**
151
+ * Get trading accounts - ONLY returns values from API
152
+ * P&L comes from: today's trades + open positions (both from API)
153
+ */
148
154
  async getTradingAccounts() {
149
155
  try {
150
156
  const response = await this._request(this.propfirm.userApi, '/TradingAccount', 'GET');
151
- if (response.statusCode === 200) {
152
- return { success: true, accounts: Array.isArray(response.data) ? response.data : [] };
157
+ if (response.statusCode !== 200) {
158
+ return { success: false, accounts: [], error: 'Failed to get accounts' };
159
+ }
160
+
161
+ const accounts = Array.isArray(response.data) ? response.data : [];
162
+ const enrichedAccounts = [];
163
+
164
+ for (const account of accounts) {
165
+ // Start with RAW API data only
166
+ const enriched = {
167
+ accountId: account.accountId,
168
+ accountName: account.accountName,
169
+ balance: account.balance, // From API
170
+ status: account.status, // From API
171
+ type: account.type, // From API
172
+ platform: 'ProjectX',
173
+ propfirm: this.propfirm.name,
174
+ // P&L fields - will be populated from API calls
175
+ todayPnL: null,
176
+ openPnL: null,
177
+ profitAndLoss: null,
178
+ startingBalance: null,
179
+ };
180
+
181
+ // Only fetch P&L for active accounts
182
+ if (account.status === 0) {
183
+ // Get today's realized P&L from trades API
184
+ const todayPnL = await this._getTodayRealizedPnL(account.accountId);
185
+ enriched.todayPnL = todayPnL;
186
+
187
+ // Get unrealized P&L from open positions API
188
+ const openPnL = await this._getOpenPositionsPnL(account.accountId);
189
+ enriched.openPnL = openPnL;
190
+
191
+ // Total P&L = realized + unrealized (both from API)
192
+ if (todayPnL !== null || openPnL !== null) {
193
+ enriched.profitAndLoss = (todayPnL || 0) + (openPnL || 0);
194
+ }
195
+ }
196
+
197
+ enrichedAccounts.push(enriched);
153
198
  }
154
- return { success: false, accounts: [], error: 'Failed to get accounts' };
199
+
200
+ return { success: true, accounts: enrichedAccounts };
155
201
  } catch (error) {
156
202
  return { success: false, accounts: [], error: error.message };
157
203
  }
158
204
  }
159
205
 
206
+ /**
207
+ * Get today's realized P&L from Trade API
208
+ * Returns null if API fails (not 0)
209
+ * @private
210
+ */
211
+ async _getTodayRealizedPnL(accountId) {
212
+ try {
213
+ const now = new Date();
214
+ const startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate());
215
+
216
+ const response = await this._request(
217
+ this.propfirm.gatewayApi, '/api/Trade/search', 'POST',
218
+ {
219
+ accountId: accountId,
220
+ startTimestamp: startOfDay.toISOString(),
221
+ endTimestamp: now.toISOString()
222
+ }
223
+ );
224
+
225
+ if (response.statusCode === 200 && response.data) {
226
+ const trades = Array.isArray(response.data)
227
+ ? response.data
228
+ : (response.data.trades || []);
229
+
230
+ // Sum P&L from API response only
231
+ let totalPnL = 0;
232
+ for (const trade of trades) {
233
+ if (trade.profitAndLoss !== undefined && trade.profitAndLoss !== null) {
234
+ totalPnL += trade.profitAndLoss;
235
+ }
236
+ }
237
+ return totalPnL;
238
+ }
239
+ return null; // API failed - return null, not 0
240
+ } catch (e) {
241
+ return null;
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Get unrealized P&L from open positions API
247
+ * Returns null if API fails (not 0)
248
+ * @private
249
+ */
250
+ async _getOpenPositionsPnL(accountId) {
251
+ try {
252
+ const response = await this._request(
253
+ this.propfirm.gatewayApi, '/api/Position/searchOpen', 'POST',
254
+ { accountId: accountId }
255
+ );
256
+
257
+ if (response.statusCode === 200 && response.data) {
258
+ const positions = response.data.positions || response.data || [];
259
+ if (Array.isArray(positions)) {
260
+ let totalPnL = 0;
261
+ for (const pos of positions) {
262
+ if (pos.profitAndLoss !== undefined && pos.profitAndLoss !== null) {
263
+ totalPnL += pos.profitAndLoss;
264
+ }
265
+ }
266
+ return totalPnL;
267
+ }
268
+ }
269
+ return null;
270
+ } catch (e) {
271
+ return null;
272
+ }
273
+ }
274
+
160
275
  // ==================== TRADING ====================
161
276
 
162
277
  async getPositions(accountId) {
@@ -1,6 +1,8 @@
1
1
  /**
2
2
  * Rithmic Accounts Module
3
3
  * Account fetching, PnL, and positions
4
+ *
5
+ * STRICT RULE: Display ONLY values returned by API. No estimation, no simulation.
4
6
  */
5
7
 
6
8
  const { REQ } = require('./constants');
@@ -60,10 +62,13 @@ const fetchAccounts = async (service) => {
60
62
  };
61
63
 
62
64
  /**
63
- * Get trading accounts formatted like ProjectX
65
+ * Get trading accounts - ONLY returns values from API
66
+ * No estimation, no simulation
67
+ *
64
68
  * @param {RithmicService} service - The Rithmic service instance
65
69
  */
66
70
  const getTradingAccounts = async (service) => {
71
+ // Fetch accounts if not already loaded
67
72
  if (service.accounts.length === 0 && service.orderConn && service.loginInfo) {
68
73
  try {
69
74
  await fetchAccounts(service);
@@ -72,40 +77,72 @@ const getTradingAccounts = async (service) => {
72
77
  }
73
78
  }
74
79
 
75
- let tradingAccounts = service.accounts.map((acc) => {
76
- const pnl = service.accountPnL.get(acc.accountId) || {};
77
- const balance = parseFloat(pnl.accountBalance || pnl.marginBalance || pnl.cashOnHand || 0) || service.propfirm.defaultBalance;
78
- const startingBalance = service.propfirm.defaultBalance;
79
- const profitAndLoss = balance - startingBalance;
80
+ // Request fresh P&L data from API
81
+ if (service.pnlConn && service.accounts.length > 0) {
82
+ await requestPnLSnapshot(service);
83
+ }
84
+
85
+ const tradingAccounts = service.accounts.map((acc) => {
86
+ // Get P&L data from API (stored in accountPnL map from handlers.js)
87
+ const pnlData = service.accountPnL.get(acc.accountId);
88
+
89
+ // ONLY use values that came from API - null if not available
90
+ let balance = null;
91
+ let todayPnL = null;
92
+ let openPnL = null;
93
+ let closedPnL = null;
94
+
95
+ if (pnlData) {
96
+ // These values come directly from Rithmic API via handleAccountPnLUpdate
97
+ balance = pnlData.accountBalance !== undefined ? pnlData.accountBalance : null;
98
+ openPnL = pnlData.openPositionPnl !== undefined ? pnlData.openPositionPnl : null;
99
+ closedPnL = pnlData.closedPositionPnl !== undefined ? pnlData.closedPositionPnl : null;
100
+ todayPnL = pnlData.dayPnl !== undefined ? pnlData.dayPnl : null;
101
+ }
102
+
103
+ // Total P&L from API only
104
+ let profitAndLoss = null;
105
+ if (todayPnL !== null) {
106
+ profitAndLoss = todayPnL;
107
+ } else if (openPnL !== null || closedPnL !== null) {
108
+ profitAndLoss = (openPnL || 0) + (closedPnL || 0);
109
+ }
80
110
 
81
111
  return {
82
112
  accountId: hashAccountId(acc.accountId),
83
113
  rithmicAccountId: acc.accountId,
84
114
  accountName: acc.accountName || acc.accountId,
85
115
  name: acc.accountName || acc.accountId,
116
+ // From API only - null if not available
86
117
  balance: balance,
87
- startingBalance: startingBalance,
118
+ todayPnL: closedPnL, // Realized P&L from API
119
+ openPnL: openPnL, // Unrealized P&L from API
88
120
  profitAndLoss: profitAndLoss,
121
+ // No estimation - these are null
122
+ startingBalance: null,
89
123
  status: 0,
90
124
  platform: 'Rithmic',
91
125
  propfirm: service.propfirm.name,
92
126
  };
93
127
  });
94
128
 
129
+ // Fallback if no accounts found
95
130
  if (tradingAccounts.length === 0 && service.user) {
96
131
  const userName = service.user.userName || 'Unknown';
97
- tradingAccounts = [{
132
+ tradingAccounts.push({
98
133
  accountId: hashAccountId(userName),
99
134
  rithmicAccountId: userName,
100
135
  accountName: userName,
101
136
  name: userName,
102
- balance: service.propfirm.defaultBalance,
103
- startingBalance: service.propfirm.defaultBalance,
104
- profitAndLoss: 0,
137
+ balance: null,
138
+ startingBalance: null,
139
+ todayPnL: null,
140
+ openPnL: null,
141
+ profitAndLoss: null,
105
142
  status: 0,
106
143
  platform: 'Rithmic',
107
144
  propfirm: service.propfirm.name,
108
- }];
145
+ });
109
146
  }
110
147
 
111
148
  return { success: true, accounts: tradingAccounts };
@@ -128,7 +165,8 @@ const requestPnLSnapshot = async (service) => {
128
165
  });
129
166
  }
130
167
 
131
- await new Promise(resolve => setTimeout(resolve, 2000));
168
+ // Wait for P&L data to arrive
169
+ await new Promise(resolve => setTimeout(resolve, 1500));
132
170
  };
133
171
 
134
172
  /**
@@ -151,10 +189,11 @@ const subscribePnLUpdates = (service) => {
151
189
  };
152
190
 
153
191
  /**
154
- * Get positions
192
+ * Get positions - ONLY returns values from API
155
193
  * @param {RithmicService} service - The Rithmic service instance
156
194
  */
157
195
  const getPositions = async (service) => {
196
+ // Ensure PnL connection is active
158
197
  if (!service.pnlConn && service.credentials) {
159
198
  await service.connectPnL(service.credentials.username, service.credentials.password);
160
199
  await requestPnLSnapshot(service);
@@ -165,8 +204,10 @@ const getPositions = async (service) => {
165
204
  exchange: pos.exchange,
166
205
  quantity: pos.quantity,
167
206
  averagePrice: pos.averagePrice,
168
- unrealizedPnl: pos.openPnl,
169
- realizedPnl: pos.closedPnl,
207
+ // From API only
208
+ unrealizedPnl: pos.openPnl !== undefined ? pos.openPnl : null,
209
+ realizedPnl: pos.closedPnl !== undefined ? pos.closedPnl : null,
210
+ dayPnl: pos.dayPnl !== undefined ? pos.dayPnl : null,
170
211
  side: pos.quantity > 0 ? 'LONG' : 'SHORT',
171
212
  }));
172
213
 
@@ -1,12 +1,15 @@
1
1
  /**
2
2
  * Tradovate Service
3
3
  * Main service for Tradovate prop firm connections (Apex, TakeProfitTrader)
4
+ *
5
+ * STRICT RULE: Display ONLY values returned by API. No estimation, no simulation.
4
6
  */
5
7
 
6
8
  const https = require('https');
7
- const WebSocket = require('ws');
8
9
  const EventEmitter = require('events');
9
10
  const { TRADOVATE_URLS, API_PATHS, WS_EVENTS, getBaseUrl, getTradingWebSocketUrl } = require('./constants');
11
+ const { checkMarketHours, isDST } = require('./market');
12
+ const { connectWebSocket, wsSend, disconnectWebSocket } = require('./websocket');
10
13
 
11
14
  class TradovateService extends EventEmitter {
12
15
  constructor(propfirmKey) {
@@ -21,6 +24,7 @@ class TradovateService extends EventEmitter {
21
24
  this.user = null;
22
25
  this.isDemo = true; // Default to demo
23
26
  this.ws = null;
27
+ this.wsRequestId = 1;
24
28
  this.renewalTimer = null;
25
29
  this.credentials = null; // Store for session restore
26
30
  }
@@ -39,9 +43,6 @@ class TradovateService extends EventEmitter {
39
43
 
40
44
  /**
41
45
  * Login to Tradovate
42
- * @param {string} username - Tradovate username
43
- * @param {string} password - Tradovate password
44
- * @param {object} options - Optional { cid, sec } for API key auth
45
46
  */
46
47
  async login(username, password, options = {}) {
47
48
  try {
@@ -53,7 +54,6 @@ class TradovateService extends EventEmitter {
53
54
  deviceId: this.generateDeviceId(),
54
55
  };
55
56
 
56
- // Add API key if provided
57
57
  if (options.cid) authData.cid = options.cid;
58
58
  if (options.sec) authData.sec = options.sec;
59
59
 
@@ -72,16 +72,12 @@ class TradovateService extends EventEmitter {
72
72
  this.userId = result.userId;
73
73
  this.tokenExpiration = new Date(result.expirationTime);
74
74
  this.user = { userName: result.name, userId: result.userId };
75
- this.credentials = { username, password }; // Store for session restore
75
+ this.credentials = { username, password };
76
76
 
77
- // Setup token renewal
78
77
  this.setupTokenRenewal();
79
-
80
- // Fetch accounts
81
78
  await this.fetchAccounts();
82
79
 
83
80
  return { success: true };
84
-
85
81
  } catch (error) {
86
82
  return { success: false, error: error.message };
87
83
  }
@@ -97,7 +93,6 @@ class TradovateService extends EventEmitter {
97
93
  if (Array.isArray(accounts)) {
98
94
  this.accounts = accounts;
99
95
 
100
- // Fetch cash balance for each account
101
96
  for (const acc of this.accounts) {
102
97
  try {
103
98
  const cashBalance = await this._request(
@@ -119,7 +114,8 @@ class TradovateService extends EventEmitter {
119
114
  }
120
115
 
121
116
  /**
122
- * Get trading accounts (formatted for HQX)
117
+ * Get trading accounts - ONLY returns values from API
118
+ * No estimation, no simulation
123
119
  */
124
120
  async getTradingAccounts() {
125
121
  if (this.accounts.length === 0) {
@@ -128,24 +124,38 @@ class TradovateService extends EventEmitter {
128
124
 
129
125
  const tradingAccounts = this.accounts.map((acc) => {
130
126
  const cb = acc.cashBalance || {};
131
- const balance = cb.totalCashValue || cb.netLiquidatingValue || this.propfirm.defaultBalance;
132
- const startingBalance = this.propfirm.defaultBalance;
133
- const profitAndLoss = cb.totalPnL || (balance - startingBalance);
134
- const openPnL = cb.openPnL || 0;
127
+
128
+ // ONLY use values from API - null if not available
129
+ const balance = cb.totalCashValue !== undefined ? cb.totalCashValue :
130
+ (cb.netLiquidatingValue !== undefined ? cb.netLiquidatingValue : null);
131
+
132
+ // P&L from API only - null if not available
133
+ const realizedPnL = cb.realizedPnL !== undefined ? cb.realizedPnL : null;
134
+ const openPnL = cb.openPnL !== undefined ? cb.openPnL : null;
135
+
136
+ // Total P&L from API
137
+ let profitAndLoss = null;
138
+ if (cb.totalPnL !== undefined) {
139
+ profitAndLoss = cb.totalPnL;
140
+ } else if (realizedPnL !== null || openPnL !== null) {
141
+ profitAndLoss = (realizedPnL || 0) + (openPnL || 0);
142
+ }
135
143
 
136
144
  return {
137
145
  accountId: acc.id,
138
146
  tradovateAccountId: acc.id,
139
147
  accountName: acc.name,
140
148
  name: acc.name,
149
+ // From API only - null if not available
141
150
  balance: balance,
142
- startingBalance: startingBalance,
143
- profitAndLoss: profitAndLoss,
151
+ todayPnL: realizedPnL,
144
152
  openPnL: openPnL,
145
- status: acc.active ? 0 : 3, // 0=Active, 3=Inactive
153
+ profitAndLoss: profitAndLoss,
154
+ startingBalance: null, // API doesn't provide this
155
+ status: acc.active ? 0 : 3,
146
156
  platform: 'Tradovate',
147
157
  propfirm: this.propfirm.name,
148
- accountType: acc.accountType, // 'Customer' or 'Demo'
158
+ accountType: acc.accountType,
149
159
  };
150
160
  });
151
161
 
@@ -263,7 +273,7 @@ class TradovateService extends EventEmitter {
263
273
  * Get market status
264
274
  */
265
275
  async getMarketStatus(accountId) {
266
- const marketHours = this.checkMarketHours();
276
+ const marketHours = checkMarketHours();
267
277
  return {
268
278
  success: true,
269
279
  isOpen: marketHours.isOpen,
@@ -358,44 +368,6 @@ class TradovateService extends EventEmitter {
358
368
  }
359
369
  }
360
370
 
361
- /**
362
- * Check market hours (same logic as ProjectX)
363
- */
364
- checkMarketHours() {
365
- const now = new Date();
366
- const utcDay = now.getUTCDay();
367
- const utcHour = now.getUTCHours();
368
-
369
- const ctOffset = this.isDST(now) ? 5 : 6;
370
- const ctHour = (utcHour - ctOffset + 24) % 24;
371
- const ctDay = utcHour < ctOffset ? (utcDay + 6) % 7 : utcDay;
372
-
373
- if (ctDay === 6) {
374
- return { isOpen: false, message: 'Market closed (Saturday)' };
375
- }
376
-
377
- if (ctDay === 0 && ctHour < 17) {
378
- return { isOpen: false, message: 'Market opens Sunday 5:00 PM CT' };
379
- }
380
-
381
- if (ctDay === 5 && ctHour >= 16) {
382
- return { isOpen: false, message: 'Market closed (Friday after 4PM CT)' };
383
- }
384
-
385
- if (ctHour === 16 && ctDay >= 1 && ctDay <= 4) {
386
- return { isOpen: false, message: 'Daily maintenance (4:00-5:00 PM CT)' };
387
- }
388
-
389
- return { isOpen: true, message: 'Market is open' };
390
- }
391
-
392
- isDST(date) {
393
- const jan = new Date(date.getFullYear(), 0, 1);
394
- const jul = new Date(date.getFullYear(), 6, 1);
395
- const stdOffset = Math.max(jan.getTimezoneOffset(), jul.getTimezoneOffset());
396
- return date.getTimezoneOffset() < stdOffset;
397
- }
398
-
399
371
  /**
400
372
  * Setup automatic token renewal
401
373
  */
@@ -440,77 +412,14 @@ class TradovateService extends EventEmitter {
440
412
  * Connect to WebSocket for real-time updates
441
413
  */
442
414
  async connectWebSocket() {
443
- return new Promise((resolve, reject) => {
444
- const wsUrl = getTradingWebSocketUrl(this.isDemo);
445
- this.ws = new WebSocket(wsUrl);
446
-
447
- this.ws.on('open', () => {
448
- // Authorize
449
- this.wsSend('authorize', '', { token: this.accessToken });
450
- resolve(true);
451
- });
452
-
453
- this.ws.on('message', (data) => {
454
- this.handleWsMessage(data);
455
- });
456
-
457
- this.ws.on('error', (err) => {
458
- this.emit('error', err);
459
- reject(err);
460
- });
461
-
462
- this.ws.on('close', () => {
463
- this.emit('disconnected');
464
- });
465
-
466
- setTimeout(() => reject(new Error('WebSocket timeout')), 10000);
467
- });
415
+ return connectWebSocket(this);
468
416
  }
469
417
 
470
418
  /**
471
419
  * Send WebSocket message
472
420
  */
473
421
  wsSend(url, query = '', body = null) {
474
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
475
-
476
- const msg = body
477
- ? `${url}\n${this.wsRequestId++}\n${query}\n${JSON.stringify(body)}`
478
- : `${url}\n${this.wsRequestId++}\n${query}\n`;
479
-
480
- this.ws.send(msg);
481
- }
482
-
483
- wsRequestId = 1;
484
-
485
- /**
486
- * Handle WebSocket message
487
- */
488
- handleWsMessage(data) {
489
- try {
490
- const str = data.toString();
491
-
492
- // Tradovate WS format: frame\nid\ndata
493
- if (str.startsWith('a')) {
494
- const json = JSON.parse(str.slice(1));
495
- if (Array.isArray(json)) {
496
- json.forEach(msg => this.processWsEvent(msg));
497
- }
498
- }
499
- } catch (e) {
500
- // Ignore parse errors
501
- }
502
- }
503
-
504
- /**
505
- * Process WebSocket event
506
- */
507
- processWsEvent(msg) {
508
- if (msg.e === 'props') {
509
- // User data sync
510
- if (msg.d?.orders) this.emit(WS_EVENTS.ORDER, msg.d.orders);
511
- if (msg.d?.positions) this.emit(WS_EVENTS.POSITION, msg.d.positions);
512
- if (msg.d?.cashBalances) this.emit(WS_EVENTS.CASH_BALANCE, msg.d.cashBalances);
513
- }
422
+ return wsSend(this, url, query, body);
514
423
  }
515
424
 
516
425
  /**
@@ -522,10 +431,7 @@ class TradovateService extends EventEmitter {
522
431
  this.renewalTimer = null;
523
432
  }
524
433
 
525
- if (this.ws) {
526
- this.ws.close();
527
- this.ws = null;
528
- }
434
+ disconnectWebSocket(this);
529
435
 
530
436
  this.accessToken = null;
531
437
  this.mdAccessToken = null;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Tradovate Market Hours
3
+ * CME Futures trading hours
4
+ */
5
+
6
+ /**
7
+ * Check if currently in DST
8
+ */
9
+ const isDST = (date) => {
10
+ const jan = new Date(date.getFullYear(), 0, 1);
11
+ const jul = new Date(date.getFullYear(), 6, 1);
12
+ const stdOffset = Math.max(jan.getTimezoneOffset(), jul.getTimezoneOffset());
13
+ return date.getTimezoneOffset() < stdOffset;
14
+ };
15
+
16
+ /**
17
+ * Check market hours (CME Futures)
18
+ */
19
+ const checkMarketHours = () => {
20
+ const now = new Date();
21
+ const utcDay = now.getUTCDay();
22
+ const utcHour = now.getUTCHours();
23
+
24
+ const ctOffset = isDST(now) ? 5 : 6;
25
+ const ctHour = (utcHour - ctOffset + 24) % 24;
26
+ const ctDay = utcHour < ctOffset ? (utcDay + 6) % 7 : utcDay;
27
+
28
+ if (ctDay === 6) {
29
+ return { isOpen: false, message: 'Market closed (Saturday)' };
30
+ }
31
+
32
+ if (ctDay === 0 && ctHour < 17) {
33
+ return { isOpen: false, message: 'Market opens Sunday 5:00 PM CT' };
34
+ }
35
+
36
+ if (ctDay === 5 && ctHour >= 16) {
37
+ return { isOpen: false, message: 'Market closed (Friday after 4PM CT)' };
38
+ }
39
+
40
+ if (ctHour === 16 && ctDay >= 1 && ctDay <= 4) {
41
+ return { isOpen: false, message: 'Daily maintenance (4:00-5:00 PM CT)' };
42
+ }
43
+
44
+ return { isOpen: true, message: 'Market is open' };
45
+ };
46
+
47
+ module.exports = { checkMarketHours, isDST };
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Tradovate WebSocket Module
3
+ * Real-time updates via WebSocket
4
+ */
5
+
6
+ const WebSocket = require('ws');
7
+ const { getTradingWebSocketUrl, WS_EVENTS } = require('./constants');
8
+
9
+ /**
10
+ * Create WebSocket connection
11
+ * @param {TradovateService} service - The Tradovate service instance
12
+ */
13
+ const connectWebSocket = async (service) => {
14
+ return new Promise((resolve, reject) => {
15
+ const wsUrl = getTradingWebSocketUrl(service.isDemo);
16
+ service.ws = new WebSocket(wsUrl);
17
+ service.wsRequestId = 1;
18
+
19
+ service.ws.on('open', () => {
20
+ wsSend(service, 'authorize', '', { token: service.accessToken });
21
+ resolve(true);
22
+ });
23
+
24
+ service.ws.on('message', (data) => {
25
+ handleWsMessage(service, data);
26
+ });
27
+
28
+ service.ws.on('error', (err) => {
29
+ service.emit('error', err);
30
+ reject(err);
31
+ });
32
+
33
+ service.ws.on('close', () => {
34
+ service.emit('disconnected');
35
+ });
36
+
37
+ setTimeout(() => reject(new Error('WebSocket timeout')), 10000);
38
+ });
39
+ };
40
+
41
+ /**
42
+ * Send WebSocket message
43
+ */
44
+ const wsSend = (service, url, query = '', body = null) => {
45
+ if (!service.ws || service.ws.readyState !== WebSocket.OPEN) return;
46
+
47
+ const msg = body
48
+ ? `${url}\n${service.wsRequestId++}\n${query}\n${JSON.stringify(body)}`
49
+ : `${url}\n${service.wsRequestId++}\n${query}\n`;
50
+
51
+ service.ws.send(msg);
52
+ };
53
+
54
+ /**
55
+ * Handle WebSocket message
56
+ */
57
+ const handleWsMessage = (service, data) => {
58
+ try {
59
+ const str = data.toString();
60
+
61
+ if (str.startsWith('a')) {
62
+ const json = JSON.parse(str.slice(1));
63
+ if (Array.isArray(json)) {
64
+ json.forEach(msg => processWsEvent(service, msg));
65
+ }
66
+ }
67
+ } catch (e) {
68
+ // Ignore parse errors
69
+ }
70
+ };
71
+
72
+ /**
73
+ * Process WebSocket event
74
+ */
75
+ const processWsEvent = (service, msg) => {
76
+ if (msg.e === 'props') {
77
+ if (msg.d?.orders) service.emit(WS_EVENTS.ORDER, msg.d.orders);
78
+ if (msg.d?.positions) service.emit(WS_EVENTS.POSITION, msg.d.positions);
79
+ if (msg.d?.cashBalances) service.emit(WS_EVENTS.CASH_BALANCE, msg.d.cashBalances);
80
+ }
81
+ };
82
+
83
+ /**
84
+ * Disconnect WebSocket
85
+ */
86
+ const disconnectWebSocket = (service) => {
87
+ if (service.ws) {
88
+ service.ws.close();
89
+ service.ws = null;
90
+ }
91
+ };
92
+
93
+ module.exports = {
94
+ connectWebSocket,
95
+ wsSend,
96
+ disconnectWebSocket
97
+ };
package/src/ui/index.js CHANGED
@@ -37,8 +37,10 @@ const prepareStdin = () => {
37
37
  // Remove any lingering keypress listeners
38
38
  process.stdin.removeAllListeners('keypress');
39
39
  process.stdin.removeAllListeners('data');
40
- // Pause stdin so inquirer can take control
41
- process.stdin.pause();
40
+ // Resume stdin so inquirer can take control
41
+ if (process.stdin.isPaused()) {
42
+ process.stdin.resume();
43
+ }
42
44
  } catch (e) {
43
45
  // Ignore errors
44
46
  }