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 +1 -1
- package/src/app.js +45 -21
- package/src/services/projectx/index.js +118 -3
- package/src/services/rithmic/accounts.js +57 -16
- package/src/services/tradovate/index.js +34 -128
- package/src/services/tradovate/market.js +47 -0
- package/src/services/tradovate/websocket.js +97 -0
- package/src/ui/index.js +4 -2
package/package.json
CHANGED
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
|
-
|
|
79
|
-
|
|
80
|
-
let
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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:
|
|
96
|
-
|
|
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
|
-
|
|
180
|
-
|
|
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} ${
|
|
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: ') +
|
|
192
|
-
chalk.white('P&L: ') + pnlColor(
|
|
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
|
|
152
|
-
return { success:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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:
|
|
103
|
-
startingBalance:
|
|
104
|
-
|
|
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
|
-
|
|
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
|
-
|
|
169
|
-
|
|
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 };
|
|
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
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
const
|
|
134
|
-
|
|
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
|
-
|
|
143
|
-
profitAndLoss: profitAndLoss,
|
|
151
|
+
todayPnL: realizedPnL,
|
|
144
152
|
openPnL: openPnL,
|
|
145
|
-
|
|
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,
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
41
|
-
process.stdin.
|
|
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
|
}
|