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.
- package/README.md +7 -6
- package/bin/cli.js +13 -7
- package/dist/algo/copy-engine.js +3 -0
- package/dist/algo/copy-engine.jsc +0 -0
- package/dist/algo/engine.js +3 -0
- package/dist/algo/engine.jsc +0 -0
- package/dist/algo/market-data-rithmic.js +3 -0
- package/dist/algo/market-data-rithmic.jsc +0 -0
- package/dist/algo/market-data.js +3 -0
- package/dist/algo/market-data.jsc +0 -0
- package/dist/algo/rithmic/connection.js +3 -0
- package/dist/algo/rithmic/connection.jsc +0 -0
- package/dist/algo/rithmic/constants.js +3 -0
- package/dist/algo/rithmic/constants.jsc +0 -0
- package/dist/algo/rithmic/index.js +3 -0
- package/dist/algo/rithmic/index.jsc +0 -0
- package/dist/algo/rithmic/market-data.js +3 -0
- package/dist/algo/rithmic/market-data.jsc +0 -0
- package/dist/algo/rithmic/pnl.js +3 -0
- package/dist/algo/rithmic/pnl.jsc +0 -0
- package/dist/algo/rithmic/pool.js +3 -0
- package/dist/algo/rithmic/pool.jsc +0 -0
- package/dist/algo/rithmic/trading.js +3 -0
- package/dist/algo/rithmic/trading.jsc +0 -0
- package/dist/algo/rithmic-decoder.js +3 -0
- package/dist/algo/rithmic-decoder.jsc +0 -0
- package/dist/algo/strategies/ultra-scalping-v2.js +3 -0
- package/dist/algo/strategies/ultra-scalping-v2.jsc +0 -0
- package/dist/algo/strategies/ultra-scalping.js +3 -0
- package/dist/algo/strategies/ultra-scalping.jsc +0 -0
- package/dist/algo/trading-api-rithmic.js +3 -0
- package/dist/algo/trading-api-rithmic.jsc +0 -0
- package/dist/algo/trading-api.js +3 -0
- package/dist/algo/trading-api.jsc +0 -0
- package/dist/algo/utils/smart-logger.js +3 -0
- package/dist/algo/utils/smart-logger.jsc +0 -0
- package/dist/algo/utils/smart-logs.js +3 -0
- package/dist/algo/utils/smart-logs.jsc +0 -0
- package/package.json +33 -10
- package/protos/rithmic/account_pnl_position_update.proto +59 -0
- package/protos/rithmic/base.proto +7 -0
- package/protos/rithmic/best_bid_offer.proto +39 -0
- package/protos/rithmic/exchange_order_notification.proto +140 -0
- package/protos/rithmic/instrument_pnl_position_update.proto +50 -0
- package/protos/rithmic/last_trade.proto +53 -0
- package/protos/rithmic/request_account_list.proto +20 -0
- package/protos/rithmic/request_cancel_all_orders.proto +15 -0
- package/protos/rithmic/request_front_month_contract.proto +10 -0
- package/protos/rithmic/request_heartbeat.proto +13 -0
- package/protos/rithmic/request_login.proto +28 -0
- package/protos/rithmic/request_login_info.proto +10 -0
- package/protos/rithmic/request_logout.proto +10 -0
- package/protos/rithmic/request_market_data_update.proto +42 -0
- package/protos/rithmic/request_new_order.proto +84 -0
- package/protos/rithmic/request_pnl_position_snapshot.proto +14 -0
- package/protos/rithmic/request_pnl_position_updates.proto +20 -0
- package/protos/rithmic/request_product_codes.proto +9 -0
- package/protos/rithmic/request_rithmic_system_info.proto +8 -0
- package/protos/rithmic/request_show_order_history.proto +16 -0
- package/protos/rithmic/request_show_order_history_dates.proto +10 -0
- package/protos/rithmic/request_show_order_history_summary.proto +14 -0
- package/protos/rithmic/request_show_orders.proto +14 -0
- package/protos/rithmic/request_subscribe_for_order_updates.proto +14 -0
- package/protos/rithmic/request_tick_bar_replay.proto +48 -0
- package/protos/rithmic/request_trade_routes.proto +11 -0
- package/protos/rithmic/response_account_list.proto +18 -0
- package/protos/rithmic/response_front_month_contract.proto +13 -0
- package/protos/rithmic/response_heartbeat.proto +14 -0
- package/protos/rithmic/response_login.proto +18 -0
- package/protos/rithmic/response_login_info.proto +24 -0
- package/protos/rithmic/response_logout.proto +11 -0
- package/protos/rithmic/response_market_data_update.proto +9 -0
- package/protos/rithmic/response_new_order.proto +18 -0
- package/protos/rithmic/response_pnl_position_snapshot.proto +11 -0
- package/protos/rithmic/response_pnl_position_updates.proto +11 -0
- package/protos/rithmic/response_product_codes.proto +12 -0
- package/protos/rithmic/response_rithmic_system_info.proto +12 -0
- package/protos/rithmic/response_show_order_history.proto +11 -0
- package/protos/rithmic/response_show_order_history_dates.proto +13 -0
- package/protos/rithmic/response_show_order_history_summary.proto +11 -0
- package/protos/rithmic/response_show_orders.proto +11 -0
- package/protos/rithmic/response_subscribe_for_order_updates.proto +11 -0
- package/protos/rithmic/response_tick_bar_replay.proto +40 -0
- package/protos/rithmic/response_trade_routes.proto +19 -0
- package/protos/rithmic/rithmic_order_notification.proto +124 -0
- package/src/app.js +136 -89
- package/src/config/index.js +27 -8
- package/src/config/settings.js +155 -0
- package/src/pages/algo/copy-trading.js +293 -200
- package/src/pages/algo/one-account.js +1 -1
- package/src/security/encryption.js +81 -46
- package/src/security/index.js +12 -8
- package/src/security/rateLimit.js +68 -65
- package/src/security/validation.js +93 -79
- package/src/services/hqx-server.js +538 -206
- package/src/services/projectx/index.js +327 -204
- package/src/services/rithmic/index.js +288 -285
- package/src/services/session.js +184 -114
- package/src/services/tradovate/index.js +286 -297
- package/src/utils/http.js +236 -0
- package/src/utils/index.js +11 -2
- package/src/utils/logger.js +64 -33
- package/src/utils/prompts.js +79 -71
|
@@ -1,94 +1,97 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ProjectX API Service
|
|
3
|
-
*
|
|
2
|
+
* @fileoverview ProjectX API Service
|
|
3
|
+
* @module services/projectx
|
|
4
4
|
*
|
|
5
5
|
* STRICT RULE: Display ONLY values returned by API. No estimation, no simulation.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
const
|
|
8
|
+
const { request } = require('../../utils/http');
|
|
9
9
|
const { PROPFIRMS } = require('../../config');
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
validateUsername,
|
|
16
|
-
validatePassword,
|
|
10
|
+
const { TIMEOUTS, DEBUG } = require('../../config/settings');
|
|
11
|
+
const { getLimiter } = require('../../security/rateLimit');
|
|
12
|
+
const {
|
|
13
|
+
validateUsername,
|
|
14
|
+
validatePassword,
|
|
17
15
|
validateApiKey,
|
|
18
16
|
validateAccountId,
|
|
19
17
|
sanitizeString,
|
|
20
|
-
maskSensitive
|
|
18
|
+
maskSensitive,
|
|
21
19
|
} = require('../../security');
|
|
22
|
-
const { getLimiter } = require('../../security/rateLimit');
|
|
23
20
|
const { getMarketHolidays, checkHoliday, checkMarketHours } = require('./market');
|
|
24
21
|
const { calculateLifetimeStats, calculateDailyPnL, formatTrades } = require('./stats');
|
|
22
|
+
const { logger } = require('../../utils/logger');
|
|
25
23
|
|
|
24
|
+
const log = logger.scope('ProjectX');
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* ProjectX API Service for prop firm connections
|
|
28
|
+
*/
|
|
26
29
|
class ProjectXService {
|
|
30
|
+
/**
|
|
31
|
+
* @param {string} [propfirmKey='topstep'] - PropFirm identifier
|
|
32
|
+
*/
|
|
27
33
|
constructor(propfirmKey = 'topstep') {
|
|
28
34
|
this.propfirm = PROPFIRMS[propfirmKey] || PROPFIRMS.topstep;
|
|
29
35
|
this.propfirmKey = propfirmKey;
|
|
30
36
|
this.token = null;
|
|
31
37
|
this.user = null;
|
|
32
|
-
this.
|
|
33
|
-
|
|
34
|
-
|
|
38
|
+
this._limiters = {
|
|
39
|
+
api: getLimiter('api'),
|
|
40
|
+
login: getLimiter('login'),
|
|
41
|
+
orders: getLimiter('orders'),
|
|
42
|
+
};
|
|
35
43
|
}
|
|
36
44
|
|
|
45
|
+
// ==================== GETTERS ====================
|
|
46
|
+
|
|
37
47
|
getToken() { return this.token; }
|
|
38
48
|
getPropfirm() { return this.propfirmKey; }
|
|
39
49
|
|
|
40
50
|
// ==================== HTTP ====================
|
|
41
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Make an API request with rate limiting
|
|
54
|
+
* @private
|
|
55
|
+
*/
|
|
42
56
|
async _request(host, path, method = 'GET', data = null, limiterType = 'api') {
|
|
43
|
-
const limiter = limiterType
|
|
44
|
-
limiterType === 'orders' ? this.orderLimiter : this.rateLimiter;
|
|
57
|
+
const limiter = this._limiters[limiterType] || this._limiters.api;
|
|
45
58
|
return limiter.execute(() => this._doRequest(host, path, method, data));
|
|
46
59
|
}
|
|
47
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Execute the actual HTTP request
|
|
63
|
+
* @private
|
|
64
|
+
*/
|
|
48
65
|
async _doRequest(host, path, method, data) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
'Accept': 'application/json',
|
|
58
|
-
'User-Agent': 'HedgeQuantX-CLI/1.3.0'
|
|
59
|
-
},
|
|
60
|
-
timeout: 15000
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
if (this.token) {
|
|
64
|
-
options.headers['Authorization'] = `Bearer ${this.token}`;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const req = https.request(options, (res) => {
|
|
68
|
-
let body = '';
|
|
69
|
-
res.on('data', chunk => body += chunk);
|
|
70
|
-
res.on('end', () => {
|
|
71
|
-
try {
|
|
72
|
-
resolve({ statusCode: res.statusCode, data: JSON.parse(body) });
|
|
73
|
-
} catch (e) {
|
|
74
|
-
resolve({ statusCode: res.statusCode, data: body });
|
|
75
|
-
}
|
|
76
|
-
});
|
|
66
|
+
const url = `https://${host}${path}`;
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const response = await request(url, {
|
|
70
|
+
method,
|
|
71
|
+
body: data,
|
|
72
|
+
token: this.token,
|
|
73
|
+
timeout: TIMEOUTS.API_REQUEST,
|
|
77
74
|
});
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
75
|
+
|
|
76
|
+
return response;
|
|
77
|
+
} catch (err) {
|
|
78
|
+
log.error('Request failed', { path, error: err.message });
|
|
79
|
+
throw err;
|
|
80
|
+
}
|
|
84
81
|
}
|
|
85
82
|
|
|
86
83
|
// ==================== AUTH ====================
|
|
87
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Login with username and password
|
|
87
|
+
* @param {string} userName - Username
|
|
88
|
+
* @param {string} password - Password
|
|
89
|
+
* @returns {Promise<{success: boolean, token?: string, error?: string}>}
|
|
90
|
+
*/
|
|
88
91
|
async login(userName, password) {
|
|
89
92
|
try {
|
|
90
93
|
validateUsername(userName);
|
|
91
|
-
validatePassword(password);
|
|
94
|
+
validatePassword(password, { requireUppercase: false, requireNumber: false });
|
|
92
95
|
|
|
93
96
|
const response = await this._request(
|
|
94
97
|
this.propfirm.userApi, '/Login', 'POST',
|
|
@@ -98,15 +101,25 @@ class ProjectXService {
|
|
|
98
101
|
|
|
99
102
|
if (response.statusCode === 200 && response.data.token) {
|
|
100
103
|
this.token = response.data.token;
|
|
104
|
+
log.info('Login successful', { user: sanitizeString(userName) });
|
|
101
105
|
return { success: true, token: maskSensitive(this.token) };
|
|
102
106
|
}
|
|
103
107
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
return { success: false, error
|
|
108
|
+
const error = response.data.errorMessage || 'Invalid credentials';
|
|
109
|
+
log.warn('Login failed', { error });
|
|
110
|
+
return { success: false, error };
|
|
111
|
+
} catch (err) {
|
|
112
|
+
log.error('Login error', { error: err.message });
|
|
113
|
+
return { success: false, error: err.message };
|
|
107
114
|
}
|
|
108
115
|
}
|
|
109
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Login with API key
|
|
119
|
+
* @param {string} userName - Username
|
|
120
|
+
* @param {string} apiKey - API key
|
|
121
|
+
* @returns {Promise<{success: boolean, token?: string, error?: string}>}
|
|
122
|
+
*/
|
|
110
123
|
async loginWithApiKey(userName, apiKey) {
|
|
111
124
|
try {
|
|
112
125
|
validateUsername(userName);
|
|
@@ -120,32 +133,44 @@ class ProjectXService {
|
|
|
120
133
|
|
|
121
134
|
if (response.statusCode === 200 && response.data.token) {
|
|
122
135
|
this.token = response.data.token;
|
|
136
|
+
log.info('API key login successful');
|
|
123
137
|
return { success: true, token: maskSensitive(this.token) };
|
|
124
138
|
}
|
|
125
139
|
|
|
126
140
|
return { success: false, error: response.data.errorMessage || 'Invalid API key' };
|
|
127
|
-
} catch (
|
|
128
|
-
|
|
141
|
+
} catch (err) {
|
|
142
|
+
log.error('API key login error', { error: err.message });
|
|
143
|
+
return { success: false, error: err.message };
|
|
129
144
|
}
|
|
130
145
|
}
|
|
131
146
|
|
|
147
|
+
/**
|
|
148
|
+
* Logout and clear token
|
|
149
|
+
*/
|
|
132
150
|
logout() {
|
|
133
151
|
this.token = null;
|
|
134
152
|
this.user = null;
|
|
153
|
+
log.debug('Logged out');
|
|
135
154
|
}
|
|
136
155
|
|
|
137
156
|
// ==================== USER ====================
|
|
138
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Get user information
|
|
160
|
+
* @returns {Promise<{success: boolean, user?: Object, error?: string}>}
|
|
161
|
+
*/
|
|
139
162
|
async getUser() {
|
|
140
163
|
try {
|
|
141
164
|
const response = await this._request(this.propfirm.userApi, '/User', 'GET');
|
|
165
|
+
|
|
142
166
|
if (response.statusCode === 200) {
|
|
143
167
|
this.user = response.data;
|
|
144
168
|
return { success: true, user: response.data };
|
|
145
169
|
}
|
|
170
|
+
|
|
146
171
|
return { success: false, error: 'Failed to get user info' };
|
|
147
|
-
} catch (
|
|
148
|
-
return { success: false, error:
|
|
172
|
+
} catch (err) {
|
|
173
|
+
return { success: false, error: err.message };
|
|
149
174
|
}
|
|
150
175
|
}
|
|
151
176
|
|
|
@@ -154,18 +179,16 @@ class ProjectXService {
|
|
|
154
179
|
/**
|
|
155
180
|
* Get trading accounts with REAL P&L from API
|
|
156
181
|
*
|
|
157
|
-
* Data sources
|
|
182
|
+
* Data sources:
|
|
158
183
|
* - /TradingAccount: accountId, accountName, balance, status, type
|
|
159
184
|
* - /AccountTemplate/userTemplates: startingBalance
|
|
160
|
-
* - /Position?accountId=X: profitAndLoss (unrealized P&L
|
|
185
|
+
* - /Position?accountId=X: profitAndLoss (unrealized P&L)
|
|
161
186
|
*
|
|
162
|
-
*
|
|
187
|
+
* @returns {Promise<{success: boolean, accounts: Array, error?: string}>}
|
|
163
188
|
*/
|
|
164
189
|
async getTradingAccounts() {
|
|
165
190
|
try {
|
|
166
|
-
// 1. Get accounts
|
|
167
191
|
const response = await this._request(this.propfirm.userApi, '/TradingAccount', 'GET');
|
|
168
|
-
debug('getTradingAccounts response:', JSON.stringify(response.data, null, 2));
|
|
169
192
|
|
|
170
193
|
if (response.statusCode !== 200) {
|
|
171
194
|
return { success: false, accounts: [], error: 'Failed to get accounts' };
|
|
@@ -173,188 +196,233 @@ class ProjectXService {
|
|
|
173
196
|
|
|
174
197
|
const accounts = Array.isArray(response.data) ? response.data : [];
|
|
175
198
|
|
|
176
|
-
//
|
|
199
|
+
// Get account templates for startingBalance
|
|
177
200
|
let templates = [];
|
|
178
201
|
try {
|
|
179
202
|
const templateRes = await this._request(this.propfirm.userApi, '/AccountTemplate/userTemplates', 'GET');
|
|
180
203
|
if (templateRes.statusCode === 200 && Array.isArray(templateRes.data)) {
|
|
181
204
|
templates = templateRes.data;
|
|
182
|
-
debug('Templates:', JSON.stringify(templates, null, 2));
|
|
183
205
|
}
|
|
184
|
-
} catch
|
|
185
|
-
debug('Failed to get templates
|
|
206
|
+
} catch {
|
|
207
|
+
log.debug('Failed to get templates');
|
|
186
208
|
}
|
|
187
209
|
|
|
188
|
-
const enrichedAccounts =
|
|
210
|
+
const enrichedAccounts = await Promise.all(
|
|
211
|
+
accounts.map(account => this._enrichAccount(account, templates))
|
|
212
|
+
);
|
|
189
213
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
)
|
|
197
|
-
);
|
|
198
|
-
|
|
199
|
-
const enriched = {
|
|
200
|
-
accountId: account.accountId,
|
|
201
|
-
accountName: account.accountName,
|
|
202
|
-
balance: account.balance, // From /TradingAccount
|
|
203
|
-
status: account.status, // From /TradingAccount
|
|
204
|
-
type: account.type, // From /TradingAccount
|
|
205
|
-
startingBalance: template?.startingBalance || null, // From /AccountTemplate
|
|
206
|
-
platform: 'ProjectX',
|
|
207
|
-
propfirm: this.propfirm.name,
|
|
208
|
-
openPnL: null,
|
|
209
|
-
profitAndLoss: null,
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
// Get P&L for active accounts only
|
|
213
|
-
if (account.status === 0) {
|
|
214
|
-
let openPnL = 0;
|
|
215
|
-
let todayPnL = 0;
|
|
216
|
-
|
|
217
|
-
// 1. Get unrealized P&L from open positions
|
|
218
|
-
try {
|
|
219
|
-
const posRes = await this._request(
|
|
220
|
-
this.propfirm.userApi,
|
|
221
|
-
`/Position?accountId=${account.accountId}`,
|
|
222
|
-
'GET'
|
|
223
|
-
);
|
|
224
|
-
debug(`Positions for ${account.accountId}:`, JSON.stringify(posRes.data, null, 2));
|
|
225
|
-
|
|
226
|
-
if (posRes.statusCode === 200 && Array.isArray(posRes.data)) {
|
|
227
|
-
for (const pos of posRes.data) {
|
|
228
|
-
if (pos.profitAndLoss !== undefined && pos.profitAndLoss !== null) {
|
|
229
|
-
openPnL += pos.profitAndLoss;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
} catch (e) {
|
|
234
|
-
debug('Failed to get positions:', e.message);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// 2. Get realized P&L from today's closed trades
|
|
238
|
-
try {
|
|
239
|
-
const today = new Date();
|
|
240
|
-
today.setHours(0, 0, 0, 0);
|
|
241
|
-
const now = new Date();
|
|
242
|
-
|
|
243
|
-
const tradesRes = await this._request(
|
|
244
|
-
this.propfirm.gatewayApi,
|
|
245
|
-
'/api/Trade/search',
|
|
246
|
-
'POST',
|
|
247
|
-
{
|
|
248
|
-
accountId: account.accountId,
|
|
249
|
-
startTimestamp: today.toISOString(),
|
|
250
|
-
endTimestamp: now.toISOString()
|
|
251
|
-
}
|
|
252
|
-
);
|
|
253
|
-
debug(`Today trades for ${account.accountId}:`, JSON.stringify(tradesRes.data, null, 2));
|
|
254
|
-
|
|
255
|
-
if (tradesRes.statusCode === 200) {
|
|
256
|
-
const trades = Array.isArray(tradesRes.data) ? tradesRes.data : (tradesRes.data.trades || []);
|
|
257
|
-
for (const trade of trades) {
|
|
258
|
-
if (trade.profitAndLoss !== undefined && trade.profitAndLoss !== null) {
|
|
259
|
-
todayPnL += trade.profitAndLoss;
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
} catch (e) {
|
|
264
|
-
debug('Failed to get today trades:', e.message);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
enriched.openPnL = openPnL;
|
|
268
|
-
enriched.todayPnL = todayPnL;
|
|
269
|
-
enriched.profitAndLoss = openPnL + todayPnL; // Total day P&L = unrealized + realized
|
|
270
|
-
}
|
|
214
|
+
return { success: true, accounts: enrichedAccounts };
|
|
215
|
+
} catch (err) {
|
|
216
|
+
log.error('Failed to get accounts', { error: err.message });
|
|
217
|
+
return { success: false, accounts: [], error: err.message };
|
|
218
|
+
}
|
|
219
|
+
}
|
|
271
220
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
221
|
+
/**
|
|
222
|
+
* Enrich account with P&L data
|
|
223
|
+
* @private
|
|
224
|
+
*/
|
|
225
|
+
async _enrichAccount(account, templates) {
|
|
226
|
+
const template = templates.find(t =>
|
|
227
|
+
account.accountName && (
|
|
228
|
+
account.accountName.includes(t.title) ||
|
|
229
|
+
t.title.includes(account.accountName)
|
|
230
|
+
)
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
const enriched = {
|
|
234
|
+
accountId: account.accountId,
|
|
235
|
+
accountName: account.accountName,
|
|
236
|
+
balance: account.balance,
|
|
237
|
+
status: account.status,
|
|
238
|
+
type: account.type,
|
|
239
|
+
startingBalance: template?.startingBalance || null,
|
|
240
|
+
platform: 'ProjectX',
|
|
241
|
+
propfirm: this.propfirm.name,
|
|
242
|
+
openPnL: null,
|
|
243
|
+
todayPnL: null,
|
|
244
|
+
profitAndLoss: null,
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
// Only fetch P&L for active accounts
|
|
248
|
+
if (account.status !== 0) {
|
|
249
|
+
return enriched;
|
|
250
|
+
}
|
|
278
251
|
|
|
279
|
-
|
|
252
|
+
// Get unrealized P&L from open positions
|
|
253
|
+
let openPnL = 0;
|
|
254
|
+
try {
|
|
255
|
+
const posRes = await this._request(
|
|
256
|
+
this.propfirm.userApi,
|
|
257
|
+
`/Position?accountId=${account.accountId}`,
|
|
258
|
+
'GET'
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
if (posRes.statusCode === 200 && Array.isArray(posRes.data)) {
|
|
262
|
+
for (const pos of posRes.data) {
|
|
263
|
+
if (pos.profitAndLoss != null) {
|
|
264
|
+
openPnL += pos.profitAndLoss;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
280
267
|
}
|
|
268
|
+
} catch {
|
|
269
|
+
log.debug('Failed to get positions', { accountId: account.accountId });
|
|
270
|
+
}
|
|
281
271
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
272
|
+
// Get realized P&L from today's trades
|
|
273
|
+
let todayPnL = 0;
|
|
274
|
+
try {
|
|
275
|
+
const today = new Date();
|
|
276
|
+
today.setHours(0, 0, 0, 0);
|
|
277
|
+
|
|
278
|
+
const tradesRes = await this._request(
|
|
279
|
+
this.propfirm.gatewayApi,
|
|
280
|
+
'/api/Trade/search',
|
|
281
|
+
'POST',
|
|
282
|
+
{
|
|
283
|
+
accountId: account.accountId,
|
|
284
|
+
startTimestamp: today.toISOString(),
|
|
285
|
+
endTimestamp: new Date().toISOString(),
|
|
286
|
+
}
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
if (tradesRes.statusCode === 200) {
|
|
290
|
+
const trades = Array.isArray(tradesRes.data) ? tradesRes.data : (tradesRes.data.trades || []);
|
|
291
|
+
for (const trade of trades) {
|
|
292
|
+
if (trade.profitAndLoss != null) {
|
|
293
|
+
todayPnL += trade.profitAndLoss;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
} catch {
|
|
298
|
+
log.debug('Failed to get today trades', { accountId: account.accountId });
|
|
285
299
|
}
|
|
300
|
+
|
|
301
|
+
enriched.openPnL = openPnL;
|
|
302
|
+
enriched.todayPnL = todayPnL;
|
|
303
|
+
enriched.profitAndLoss = openPnL + todayPnL;
|
|
304
|
+
|
|
305
|
+
return enriched;
|
|
286
306
|
}
|
|
287
307
|
|
|
288
308
|
// ==================== TRADING ====================
|
|
289
309
|
|
|
310
|
+
/**
|
|
311
|
+
* Get open positions
|
|
312
|
+
* @param {number|string} accountId - Account ID
|
|
313
|
+
* @returns {Promise<{success: boolean, positions: Array, error?: string}>}
|
|
314
|
+
*/
|
|
290
315
|
async getPositions(accountId) {
|
|
291
316
|
try {
|
|
292
317
|
const id = validateAccountId(accountId);
|
|
293
318
|
const response = await this._request(
|
|
294
|
-
this.propfirm.gatewayApi,
|
|
319
|
+
this.propfirm.gatewayApi,
|
|
320
|
+
'/api/Position/searchOpen',
|
|
321
|
+
'POST',
|
|
322
|
+
{ accountId: id }
|
|
295
323
|
);
|
|
324
|
+
|
|
296
325
|
if (response.statusCode === 200) {
|
|
297
326
|
const positions = response.data.positions || response.data || [];
|
|
298
327
|
return { success: true, positions: Array.isArray(positions) ? positions : [] };
|
|
299
328
|
}
|
|
329
|
+
|
|
300
330
|
return { success: true, positions: [] };
|
|
301
|
-
} catch (
|
|
302
|
-
return { success: true, positions: [], error:
|
|
331
|
+
} catch (err) {
|
|
332
|
+
return { success: true, positions: [], error: err.message };
|
|
303
333
|
}
|
|
304
334
|
}
|
|
305
335
|
|
|
336
|
+
/**
|
|
337
|
+
* Get open orders
|
|
338
|
+
* @param {number|string} accountId - Account ID
|
|
339
|
+
* @returns {Promise<{success: boolean, orders: Array, error?: string}>}
|
|
340
|
+
*/
|
|
306
341
|
async getOrders(accountId) {
|
|
307
342
|
try {
|
|
308
343
|
const id = validateAccountId(accountId);
|
|
309
344
|
const response = await this._request(
|
|
310
|
-
this.propfirm.gatewayApi,
|
|
345
|
+
this.propfirm.gatewayApi,
|
|
346
|
+
'/api/Order/searchOpen',
|
|
347
|
+
'POST',
|
|
348
|
+
{ accountId: id }
|
|
311
349
|
);
|
|
350
|
+
|
|
312
351
|
if (response.statusCode === 200) {
|
|
313
352
|
const orders = response.data.orders || response.data || [];
|
|
314
353
|
return { success: true, orders: Array.isArray(orders) ? orders : [] };
|
|
315
354
|
}
|
|
355
|
+
|
|
316
356
|
return { success: true, orders: [] };
|
|
317
|
-
} catch (
|
|
318
|
-
return { success: true, orders: [], error:
|
|
357
|
+
} catch (err) {
|
|
358
|
+
return { success: true, orders: [], error: err.message };
|
|
319
359
|
}
|
|
320
360
|
}
|
|
321
361
|
|
|
362
|
+
/**
|
|
363
|
+
* Place an order
|
|
364
|
+
* @param {Object} orderData - Order details
|
|
365
|
+
* @returns {Promise<{success: boolean, order?: Object, error?: string}>}
|
|
366
|
+
*/
|
|
322
367
|
async placeOrder(orderData) {
|
|
323
368
|
try {
|
|
324
369
|
const response = await this._request(
|
|
325
|
-
this.propfirm.gatewayApi,
|
|
370
|
+
this.propfirm.gatewayApi,
|
|
371
|
+
'/api/Order/place',
|
|
372
|
+
'POST',
|
|
373
|
+
orderData,
|
|
374
|
+
'orders'
|
|
326
375
|
);
|
|
376
|
+
|
|
327
377
|
if (response.statusCode === 200 && response.data.success) {
|
|
378
|
+
log.info('Order placed', { orderId: response.data.orderId });
|
|
328
379
|
return { success: true, order: response.data };
|
|
329
380
|
}
|
|
381
|
+
|
|
330
382
|
return { success: false, error: response.data.errorMessage || 'Order failed' };
|
|
331
|
-
} catch (
|
|
332
|
-
return { success: false, error:
|
|
383
|
+
} catch (err) {
|
|
384
|
+
return { success: false, error: err.message };
|
|
333
385
|
}
|
|
334
386
|
}
|
|
335
387
|
|
|
388
|
+
/**
|
|
389
|
+
* Cancel an order
|
|
390
|
+
* @param {number|string} orderId - Order ID
|
|
391
|
+
* @returns {Promise<{success: boolean, error?: string}>}
|
|
392
|
+
*/
|
|
336
393
|
async cancelOrder(orderId) {
|
|
337
394
|
try {
|
|
338
395
|
const response = await this._request(
|
|
339
|
-
this.propfirm.gatewayApi,
|
|
340
|
-
|
|
396
|
+
this.propfirm.gatewayApi,
|
|
397
|
+
'/api/Order/cancel',
|
|
398
|
+
'POST',
|
|
399
|
+
{ orderId: parseInt(orderId, 10) },
|
|
400
|
+
'orders'
|
|
341
401
|
);
|
|
402
|
+
|
|
342
403
|
return { success: response.statusCode === 200 && response.data.success };
|
|
343
|
-
} catch (
|
|
344
|
-
return { success: false, error:
|
|
404
|
+
} catch (err) {
|
|
405
|
+
return { success: false, error: err.message };
|
|
345
406
|
}
|
|
346
407
|
}
|
|
347
408
|
|
|
409
|
+
/**
|
|
410
|
+
* Cancel all open orders for an account
|
|
411
|
+
* @param {number|string} accountId - Account ID
|
|
412
|
+
* @returns {Promise<{success: boolean, cancelled: number, error?: string}>}
|
|
413
|
+
*/
|
|
348
414
|
async cancelAllOrders(accountId) {
|
|
349
415
|
try {
|
|
350
416
|
const id = validateAccountId(accountId);
|
|
351
417
|
const ordersResult = await this.getOrders(id);
|
|
352
|
-
|
|
418
|
+
|
|
419
|
+
if (!ordersResult.success || !ordersResult.orders?.length) {
|
|
353
420
|
return { success: true, cancelled: 0 };
|
|
354
421
|
}
|
|
355
422
|
|
|
356
|
-
const pendingOrders = ordersResult.orders.filter(o =>
|
|
357
|
-
o.status === 'Working' || o.status === 'Pending' ||
|
|
423
|
+
const pendingOrders = ordersResult.orders.filter(o =>
|
|
424
|
+
o.status === 'Working' || o.status === 'Pending' ||
|
|
425
|
+
o.status === 0 || o.status === 1
|
|
358
426
|
);
|
|
359
427
|
|
|
360
428
|
let cancelled = 0;
|
|
@@ -364,26 +432,42 @@ class ProjectXService {
|
|
|
364
432
|
}
|
|
365
433
|
|
|
366
434
|
return { success: true, cancelled };
|
|
367
|
-
} catch (
|
|
368
|
-
return { success: false, error:
|
|
435
|
+
} catch (err) {
|
|
436
|
+
return { success: false, cancelled: 0, error: err.message };
|
|
369
437
|
}
|
|
370
438
|
}
|
|
371
439
|
|
|
440
|
+
/**
|
|
441
|
+
* Close a position
|
|
442
|
+
* @param {number|string} accountId - Account ID
|
|
443
|
+
* @param {number|string} contractId - Contract ID
|
|
444
|
+
* @returns {Promise<{success: boolean, error?: string}>}
|
|
445
|
+
*/
|
|
372
446
|
async closePosition(accountId, contractId) {
|
|
373
447
|
try {
|
|
374
448
|
const id = validateAccountId(accountId);
|
|
375
449
|
const response = await this._request(
|
|
376
|
-
this.propfirm.gatewayApi,
|
|
377
|
-
|
|
450
|
+
this.propfirm.gatewayApi,
|
|
451
|
+
'/api/Position/closeContract',
|
|
452
|
+
'POST',
|
|
453
|
+
{ accountId: id, contractId },
|
|
454
|
+
'orders'
|
|
378
455
|
);
|
|
456
|
+
|
|
379
457
|
return { success: response.statusCode === 200 && response.data.success };
|
|
380
|
-
} catch (
|
|
381
|
-
return { success: false, error:
|
|
458
|
+
} catch (err) {
|
|
459
|
+
return { success: false, error: err.message };
|
|
382
460
|
}
|
|
383
461
|
}
|
|
384
462
|
|
|
385
463
|
// ==================== TRADES & STATS ====================
|
|
386
464
|
|
|
465
|
+
/**
|
|
466
|
+
* Get trade history
|
|
467
|
+
* @param {number|string} accountId - Account ID
|
|
468
|
+
* @param {number} [days=30] - Days of history
|
|
469
|
+
* @returns {Promise<{success: boolean, trades: Array, error?: string}>}
|
|
470
|
+
*/
|
|
387
471
|
async getTradeHistory(accountId, days = 30) {
|
|
388
472
|
try {
|
|
389
473
|
const id = validateAccountId(accountId);
|
|
@@ -392,21 +476,32 @@ class ProjectXService {
|
|
|
392
476
|
startDate.setDate(startDate.getDate() - days);
|
|
393
477
|
|
|
394
478
|
const response = await this._request(
|
|
395
|
-
this.propfirm.gatewayApi,
|
|
396
|
-
|
|
479
|
+
this.propfirm.gatewayApi,
|
|
480
|
+
'/api/Trade/search',
|
|
481
|
+
'POST',
|
|
482
|
+
{
|
|
483
|
+
accountId: id,
|
|
484
|
+
startTimestamp: startDate.toISOString(),
|
|
485
|
+
endTimestamp: endDate.toISOString(),
|
|
486
|
+
}
|
|
397
487
|
);
|
|
398
488
|
|
|
399
489
|
if (response.statusCode === 200 && response.data) {
|
|
400
|
-
|
|
490
|
+
const trades = Array.isArray(response.data) ? response.data : (response.data.trades || []);
|
|
401
491
|
return { success: true, trades: formatTrades(trades) };
|
|
402
492
|
}
|
|
403
493
|
|
|
404
494
|
return { success: true, trades: [] };
|
|
405
|
-
} catch (
|
|
406
|
-
return { success: true, trades: [], error:
|
|
495
|
+
} catch (err) {
|
|
496
|
+
return { success: true, trades: [], error: err.message };
|
|
407
497
|
}
|
|
408
498
|
}
|
|
409
499
|
|
|
500
|
+
/**
|
|
501
|
+
* Get daily statistics
|
|
502
|
+
* @param {number|string} accountId - Account ID
|
|
503
|
+
* @returns {Promise<{success: boolean, stats: Array, error?: string}>}
|
|
504
|
+
*/
|
|
410
505
|
async getDailyStats(accountId) {
|
|
411
506
|
try {
|
|
412
507
|
const id = validateAccountId(accountId);
|
|
@@ -414,48 +509,63 @@ class ProjectXService {
|
|
|
414
509
|
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
415
510
|
|
|
416
511
|
const response = await this._request(
|
|
417
|
-
this.propfirm.gatewayApi,
|
|
418
|
-
|
|
512
|
+
this.propfirm.gatewayApi,
|
|
513
|
+
'/api/Trade/search',
|
|
514
|
+
'POST',
|
|
515
|
+
{
|
|
516
|
+
accountId: id,
|
|
517
|
+
startTimestamp: startOfMonth.toISOString(),
|
|
518
|
+
endTimestamp: now.toISOString(),
|
|
519
|
+
}
|
|
419
520
|
);
|
|
420
521
|
|
|
421
522
|
if (response.statusCode === 200 && response.data) {
|
|
422
|
-
|
|
523
|
+
const trades = Array.isArray(response.data) ? response.data : (response.data.trades || []);
|
|
423
524
|
return { success: true, stats: calculateDailyPnL(trades) };
|
|
424
525
|
}
|
|
425
526
|
|
|
426
527
|
return { success: false, stats: [] };
|
|
427
|
-
} catch (
|
|
428
|
-
return { success: false, stats: [], error:
|
|
528
|
+
} catch (err) {
|
|
529
|
+
return { success: false, stats: [], error: err.message };
|
|
429
530
|
}
|
|
430
531
|
}
|
|
431
532
|
|
|
533
|
+
/**
|
|
534
|
+
* Get lifetime statistics
|
|
535
|
+
* @param {number|string} accountId - Account ID
|
|
536
|
+
* @returns {Promise<{success: boolean, stats: Object|null, error?: string}>}
|
|
537
|
+
*/
|
|
432
538
|
async getLifetimeStats(accountId) {
|
|
433
539
|
try {
|
|
434
540
|
const tradesResult = await this.getTradeHistory(accountId, 90);
|
|
435
|
-
|
|
541
|
+
|
|
542
|
+
if (!tradesResult.success || !tradesResult.trades.length) {
|
|
436
543
|
return { success: true, stats: null };
|
|
437
544
|
}
|
|
545
|
+
|
|
438
546
|
return { success: true, stats: calculateLifetimeStats(tradesResult.trades) };
|
|
439
|
-
} catch (
|
|
440
|
-
return { success: false, stats: null, error:
|
|
547
|
+
} catch (err) {
|
|
548
|
+
return { success: false, stats: null, error: err.message };
|
|
441
549
|
}
|
|
442
550
|
}
|
|
443
551
|
|
|
444
552
|
// ==================== CONTRACTS ====================
|
|
445
553
|
|
|
446
554
|
/**
|
|
447
|
-
* Get
|
|
555
|
+
* Get available contracts
|
|
556
|
+
* @returns {Promise<{success: boolean, contracts: Array, error?: string}>}
|
|
448
557
|
*/
|
|
449
558
|
async getContracts() {
|
|
450
559
|
try {
|
|
451
560
|
const response = await this._request(
|
|
452
|
-
this.propfirm.gatewayApi,
|
|
561
|
+
this.propfirm.gatewayApi,
|
|
562
|
+
'/api/Contract/available',
|
|
563
|
+
'POST',
|
|
453
564
|
{ live: false }
|
|
454
565
|
);
|
|
455
566
|
|
|
456
567
|
if (response.statusCode === 200) {
|
|
457
568
|
const contracts = response.data.contracts || response.data || [];
|
|
458
|
-
// Filter only active contracts and sort by description
|
|
459
569
|
const activeContracts = contracts
|
|
460
570
|
.filter(c => c.activeContract === true)
|
|
461
571
|
.sort((a, b) => (a.description || '').localeCompare(b.description || ''));
|
|
@@ -464,23 +574,32 @@ class ProjectXService {
|
|
|
464
574
|
}
|
|
465
575
|
|
|
466
576
|
return { success: false, contracts: [], error: 'Failed to fetch contracts' };
|
|
467
|
-
} catch (
|
|
468
|
-
return { success: false, contracts: [], error:
|
|
577
|
+
} catch (err) {
|
|
578
|
+
return { success: false, contracts: [], error: err.message };
|
|
469
579
|
}
|
|
470
580
|
}
|
|
471
581
|
|
|
582
|
+
/**
|
|
583
|
+
* Search contracts
|
|
584
|
+
* @param {string} searchText - Search text
|
|
585
|
+
* @returns {Promise<{success: boolean, contracts: Array, error?: string}>}
|
|
586
|
+
*/
|
|
472
587
|
async searchContracts(searchText) {
|
|
473
588
|
try {
|
|
474
589
|
const response = await this._request(
|
|
475
|
-
this.propfirm.gatewayApi,
|
|
590
|
+
this.propfirm.gatewayApi,
|
|
591
|
+
'/api/Contract/search',
|
|
592
|
+
'POST',
|
|
476
593
|
{ searchText: sanitizeString(searchText), live: false }
|
|
477
594
|
);
|
|
595
|
+
|
|
478
596
|
if (response.statusCode === 200) {
|
|
479
597
|
return { success: true, contracts: response.data.contracts || response.data || [] };
|
|
480
598
|
}
|
|
599
|
+
|
|
481
600
|
return { success: false, contracts: [] };
|
|
482
|
-
} catch (
|
|
483
|
-
return { success: false, contracts: [], error:
|
|
601
|
+
} catch (err) {
|
|
602
|
+
return { success: false, contracts: [], error: err.message };
|
|
484
603
|
}
|
|
485
604
|
}
|
|
486
605
|
|
|
@@ -490,7 +609,11 @@ class ProjectXService {
|
|
|
490
609
|
checkHoliday() { return checkHoliday(); }
|
|
491
610
|
checkMarketHours() { return checkMarketHours(); }
|
|
492
611
|
|
|
493
|
-
|
|
612
|
+
/**
|
|
613
|
+
* Get market status
|
|
614
|
+
* @returns {Promise<{success: boolean, isOpen: boolean, message: string}>}
|
|
615
|
+
*/
|
|
616
|
+
async getMarketStatus() {
|
|
494
617
|
const hours = checkMarketHours();
|
|
495
618
|
return { success: true, isOpen: hours.isOpen, message: hours.message };
|
|
496
619
|
}
|