hedgequantx 1.8.48 → 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/accounts.js +2 -3
- 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/ui/index.js +53 -1
- 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,57 +1,79 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tradovate Service
|
|
3
|
-
*
|
|
2
|
+
* @fileoverview Tradovate Service - Main service for Tradovate prop firm connections
|
|
3
|
+
* @module services/tradovate
|
|
4
4
|
*
|
|
5
5
|
* STRICT RULE: Display ONLY values returned by API. No estimation, no simulation.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
const
|
|
8
|
+
const crypto = require('crypto');
|
|
9
|
+
const os = require('os');
|
|
9
10
|
const EventEmitter = require('events');
|
|
10
|
-
const {
|
|
11
|
+
const { request } = require('../../utils/http');
|
|
12
|
+
const { TIMEOUTS } = require('../../config/settings');
|
|
13
|
+
const { TRADOVATE_URLS, API_PATHS, getBaseUrl, getTradingWebSocketUrl } = require('./constants');
|
|
11
14
|
const { checkMarketHours, isDST } = require('./market');
|
|
12
15
|
const { connectWebSocket, wsSend, disconnectWebSocket } = require('./websocket');
|
|
16
|
+
const { logger } = require('../../utils/logger');
|
|
13
17
|
|
|
18
|
+
const log = logger.scope('Tradovate');
|
|
19
|
+
|
|
20
|
+
/** PropFirm configurations */
|
|
21
|
+
const PROPFIRM_CONFIGS = {
|
|
22
|
+
apex_tradovate: { name: 'Apex (Tradovate)', isDemo: false },
|
|
23
|
+
takeprofittrader: { name: 'TakeProfitTrader', isDemo: false },
|
|
24
|
+
myfundedfutures: { name: 'MyFundedFutures', isDemo: false },
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Tradovate Service for prop firm trading
|
|
29
|
+
*/
|
|
14
30
|
class TradovateService extends EventEmitter {
|
|
31
|
+
/**
|
|
32
|
+
* @param {string} propfirmKey - PropFirm identifier
|
|
33
|
+
*/
|
|
15
34
|
constructor(propfirmKey) {
|
|
16
35
|
super();
|
|
17
36
|
this.propfirmKey = propfirmKey;
|
|
18
|
-
this.propfirm =
|
|
37
|
+
this.propfirm = PROPFIRM_CONFIGS[propfirmKey] || { name: propfirmKey, isDemo: false };
|
|
38
|
+
|
|
39
|
+
// Auth
|
|
19
40
|
this.accessToken = null;
|
|
20
41
|
this.mdAccessToken = null;
|
|
21
42
|
this.userId = null;
|
|
22
43
|
this.tokenExpiration = null;
|
|
44
|
+
|
|
45
|
+
// State
|
|
23
46
|
this.accounts = [];
|
|
24
47
|
this.user = null;
|
|
25
|
-
this.isDemo = true;
|
|
48
|
+
this.isDemo = true;
|
|
49
|
+
this.credentials = null;
|
|
50
|
+
|
|
51
|
+
// WebSocket
|
|
26
52
|
this.ws = null;
|
|
27
53
|
this.wsRequestId = 1;
|
|
28
54
|
this.renewalTimer = null;
|
|
29
|
-
|
|
55
|
+
|
|
56
|
+
// Device ID cache
|
|
57
|
+
this._deviceId = null;
|
|
30
58
|
}
|
|
31
59
|
|
|
32
|
-
|
|
33
|
-
* Get PropFirm configuration
|
|
34
|
-
*/
|
|
35
|
-
getPropFirmConfig(key) {
|
|
36
|
-
const propfirms = {
|
|
37
|
-
'apex_tradovate': { name: 'Apex (Tradovate)', isDemo: false },
|
|
38
|
-
'takeprofittrader': { name: 'TakeProfitTrader', isDemo: false },
|
|
39
|
-
'myfundedfutures': { name: 'MyFundedFutures', isDemo: false },
|
|
40
|
-
};
|
|
41
|
-
return propfirms[key] || { name: key, isDemo: false };
|
|
42
|
-
}
|
|
60
|
+
// ==================== AUTH ====================
|
|
43
61
|
|
|
44
62
|
/**
|
|
45
63
|
* Login to Tradovate
|
|
64
|
+
* @param {string} username - Username
|
|
65
|
+
* @param {string} password - Password
|
|
66
|
+
* @param {Object} [options] - Additional options (cid, sec)
|
|
67
|
+
* @returns {Promise<{success: boolean, error?: string}>}
|
|
46
68
|
*/
|
|
47
69
|
async login(username, password, options = {}) {
|
|
48
70
|
try {
|
|
49
71
|
const authData = {
|
|
50
72
|
name: username,
|
|
51
|
-
password
|
|
73
|
+
password,
|
|
52
74
|
appId: 'HQX-CLI',
|
|
53
|
-
appVersion: '
|
|
54
|
-
deviceId: this.
|
|
75
|
+
appVersion: '2.0.0',
|
|
76
|
+
deviceId: this._getDeviceId(),
|
|
55
77
|
};
|
|
56
78
|
|
|
57
79
|
if (options.cid) authData.cid = options.cid;
|
|
@@ -59,81 +81,132 @@ class TradovateService extends EventEmitter {
|
|
|
59
81
|
|
|
60
82
|
const result = await this._request(API_PATHS.AUTH_TOKEN_REQUEST, 'POST', authData);
|
|
61
83
|
|
|
62
|
-
if (result.errorText) {
|
|
63
|
-
|
|
84
|
+
if (result.data.errorText) {
|
|
85
|
+
log.warn('Login failed', { error: result.data.errorText });
|
|
86
|
+
return { success: false, error: result.data.errorText };
|
|
64
87
|
}
|
|
65
88
|
|
|
66
|
-
if (!result.accessToken) {
|
|
89
|
+
if (!result.data.accessToken) {
|
|
67
90
|
return { success: false, error: 'No access token received' };
|
|
68
91
|
}
|
|
69
92
|
|
|
70
|
-
this.accessToken = result.accessToken;
|
|
71
|
-
this.mdAccessToken = result.mdAccessToken;
|
|
72
|
-
this.userId = result.userId;
|
|
73
|
-
this.tokenExpiration = new Date(result.expirationTime);
|
|
74
|
-
this.user = { userName: result.name, userId: result.userId };
|
|
93
|
+
this.accessToken = result.data.accessToken;
|
|
94
|
+
this.mdAccessToken = result.data.mdAccessToken;
|
|
95
|
+
this.userId = result.data.userId;
|
|
96
|
+
this.tokenExpiration = new Date(result.data.expirationTime);
|
|
97
|
+
this.user = { userName: result.data.name, userId: result.data.userId };
|
|
75
98
|
this.credentials = { username, password };
|
|
76
99
|
|
|
77
|
-
this.
|
|
78
|
-
await this.
|
|
100
|
+
this._setupTokenRenewal();
|
|
101
|
+
await this._fetchAccounts();
|
|
79
102
|
|
|
103
|
+
log.info('Login successful', { accounts: this.accounts.length });
|
|
80
104
|
return { success: true };
|
|
81
|
-
} catch (
|
|
82
|
-
|
|
105
|
+
} catch (err) {
|
|
106
|
+
log.error('Login error', { error: err.message });
|
|
107
|
+
return { success: false, error: err.message };
|
|
83
108
|
}
|
|
84
109
|
}
|
|
85
110
|
|
|
86
111
|
/**
|
|
87
|
-
*
|
|
112
|
+
* Generate device ID (cached)
|
|
113
|
+
* @private
|
|
88
114
|
*/
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
115
|
+
_getDeviceId() {
|
|
116
|
+
if (this._deviceId) return this._deviceId;
|
|
117
|
+
|
|
118
|
+
const data = `${os.hostname()}-${os.platform()}-${os.arch()}-hqx-cli`;
|
|
119
|
+
this._deviceId = crypto.createHash('md5').update(data).digest('hex');
|
|
120
|
+
return this._deviceId;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Setup automatic token renewal
|
|
125
|
+
* @private
|
|
126
|
+
*/
|
|
127
|
+
_setupTokenRenewal() {
|
|
128
|
+
if (this.renewalTimer) {
|
|
129
|
+
clearTimeout(this.renewalTimer);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Renew 15 minutes before expiration
|
|
133
|
+
const renewInMs = (90 - 15) * 60 * 1000;
|
|
134
|
+
|
|
135
|
+
this.renewalTimer = setTimeout(async () => {
|
|
136
|
+
try {
|
|
137
|
+
await this._renewToken();
|
|
138
|
+
} catch (err) {
|
|
139
|
+
log.warn('Token renewal failed', { error: err.message });
|
|
108
140
|
}
|
|
141
|
+
}, renewInMs);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Renew access token
|
|
146
|
+
* @private
|
|
147
|
+
*/
|
|
148
|
+
async _renewToken() {
|
|
149
|
+
if (!this.accessToken) return;
|
|
150
|
+
|
|
151
|
+
const result = await this._request(API_PATHS.AUTH_RENEW_TOKEN, 'GET');
|
|
152
|
+
|
|
153
|
+
if (result.data.accessToken) {
|
|
154
|
+
this.accessToken = result.data.accessToken;
|
|
155
|
+
this.mdAccessToken = result.data.mdAccessToken;
|
|
156
|
+
this.tokenExpiration = new Date(result.data.expirationTime);
|
|
157
|
+
this._setupTokenRenewal();
|
|
158
|
+
log.debug('Token renewed');
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ==================== ACCOUNTS ====================
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Fetch accounts from API
|
|
166
|
+
* @private
|
|
167
|
+
*/
|
|
168
|
+
async _fetchAccounts() {
|
|
169
|
+
const result = await this._request(API_PATHS.ACCOUNT_LIST, 'GET');
|
|
170
|
+
|
|
171
|
+
if (!Array.isArray(result.data)) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
this.accounts = result.data;
|
|
109
176
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
177
|
+
// Fetch cash balance for each account
|
|
178
|
+
for (const acc of this.accounts) {
|
|
179
|
+
try {
|
|
180
|
+
const balanceResult = await this._request(
|
|
181
|
+
API_PATHS.CASH_BALANCE_SNAPSHOT,
|
|
182
|
+
'POST',
|
|
183
|
+
{ accountId: acc.id }
|
|
184
|
+
);
|
|
185
|
+
acc.cashBalance = balanceResult.data;
|
|
186
|
+
} catch {
|
|
187
|
+
acc.cashBalance = null;
|
|
188
|
+
}
|
|
113
189
|
}
|
|
114
190
|
}
|
|
115
191
|
|
|
116
192
|
/**
|
|
117
|
-
* Get trading accounts
|
|
118
|
-
*
|
|
193
|
+
* Get trading accounts with REAL P&L from API
|
|
194
|
+
* @returns {Promise<{success: boolean, accounts: Array}>}
|
|
119
195
|
*/
|
|
120
196
|
async getTradingAccounts() {
|
|
121
|
-
if (this.accounts.length
|
|
122
|
-
await this.
|
|
197
|
+
if (!this.accounts.length) {
|
|
198
|
+
await this._fetchAccounts();
|
|
123
199
|
}
|
|
124
200
|
|
|
125
201
|
const tradingAccounts = this.accounts.map((acc) => {
|
|
126
202
|
const cb = acc.cashBalance || {};
|
|
127
|
-
|
|
203
|
+
|
|
128
204
|
// ONLY use values from API - null if not available
|
|
129
|
-
const balance = cb.totalCashValue
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const openPnL = cb.openPnL !== undefined ? cb.openPnL : null;
|
|
135
|
-
|
|
136
|
-
// Total P&L from API
|
|
205
|
+
const balance = cb.totalCashValue ?? cb.netLiquidatingValue ?? null;
|
|
206
|
+
const realizedPnL = cb.realizedPnL ?? null;
|
|
207
|
+
const openPnL = cb.openPnL ?? null;
|
|
208
|
+
|
|
209
|
+
// Total P&L
|
|
137
210
|
let profitAndLoss = null;
|
|
138
211
|
if (cb.totalPnL !== undefined) {
|
|
139
212
|
profitAndLoss = cb.totalPnL;
|
|
@@ -146,12 +219,11 @@ class TradovateService extends EventEmitter {
|
|
|
146
219
|
tradovateAccountId: acc.id,
|
|
147
220
|
accountName: acc.name,
|
|
148
221
|
name: acc.name,
|
|
149
|
-
|
|
150
|
-
balance: balance,
|
|
222
|
+
balance,
|
|
151
223
|
todayPnL: realizedPnL,
|
|
152
|
-
openPnL
|
|
153
|
-
profitAndLoss
|
|
154
|
-
startingBalance: null,
|
|
224
|
+
openPnL,
|
|
225
|
+
profitAndLoss,
|
|
226
|
+
startingBalance: null,
|
|
155
227
|
status: acc.active ? 0 : 3,
|
|
156
228
|
platform: 'Tradovate',
|
|
157
229
|
propfirm: this.propfirm.name,
|
|
@@ -162,31 +234,61 @@ class TradovateService extends EventEmitter {
|
|
|
162
234
|
return { success: true, accounts: tradingAccounts };
|
|
163
235
|
}
|
|
164
236
|
|
|
237
|
+
// ==================== POSITIONS ====================
|
|
238
|
+
|
|
165
239
|
/**
|
|
166
240
|
* Get positions for an account
|
|
241
|
+
* @param {number} accountId - Account ID
|
|
242
|
+
* @returns {Promise<Array>}
|
|
167
243
|
*/
|
|
168
244
|
async getPositions(accountId) {
|
|
169
245
|
try {
|
|
170
|
-
const
|
|
171
|
-
return
|
|
172
|
-
} catch
|
|
246
|
+
const result = await this._request(API_PATHS.POSITION_DEPS, 'GET', null, { masterid: accountId });
|
|
247
|
+
return result.data.filter(p => p.netPos !== 0);
|
|
248
|
+
} catch {
|
|
173
249
|
return [];
|
|
174
250
|
}
|
|
175
251
|
}
|
|
176
252
|
|
|
253
|
+
// ==================== ORDERS ====================
|
|
254
|
+
|
|
177
255
|
/**
|
|
178
|
-
* Get
|
|
256
|
+
* Get orders
|
|
257
|
+
* @param {number} [accountId] - Optional account filter
|
|
258
|
+
* @returns {Promise<{success: boolean, orders: Array, error?: string}>}
|
|
179
259
|
*/
|
|
180
|
-
async
|
|
260
|
+
async getOrders(accountId) {
|
|
181
261
|
try {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
262
|
+
const result = await this._request(API_PATHS.ORDER_LIST, 'GET');
|
|
263
|
+
const orders = Array.isArray(result.data) ? result.data : [];
|
|
264
|
+
|
|
265
|
+
const filtered = accountId
|
|
266
|
+
? orders.filter(o => o.accountId === accountId)
|
|
267
|
+
: orders;
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
success: true,
|
|
271
|
+
orders: filtered.map(o => ({
|
|
272
|
+
orderId: o.id,
|
|
273
|
+
accountId: o.accountId,
|
|
274
|
+
symbol: o.contractId,
|
|
275
|
+
side: o.action === 'Buy' ? 0 : 1,
|
|
276
|
+
quantity: o.orderQty,
|
|
277
|
+
filledQuantity: o.filledQty || 0,
|
|
278
|
+
price: o.price,
|
|
279
|
+
status: o.ordStatus === 'Working' ? 1 : (o.ordStatus === 'Filled' ? 2 : 0),
|
|
280
|
+
orderType: o.orderType,
|
|
281
|
+
})),
|
|
282
|
+
};
|
|
283
|
+
} catch (err) {
|
|
284
|
+
return { success: false, error: err.message, orders: [] };
|
|
185
285
|
}
|
|
186
286
|
}
|
|
187
287
|
|
|
188
288
|
/**
|
|
189
289
|
* Place an order
|
|
290
|
+
* @param {Object} orderData - Order details
|
|
291
|
+
* @returns {Promise<{success: boolean, orderId?: number, error?: string}>}
|
|
190
292
|
*/
|
|
191
293
|
async placeOrder(orderData) {
|
|
192
294
|
try {
|
|
@@ -200,159 +302,93 @@ class TradovateService extends EventEmitter {
|
|
|
200
302
|
isAutomated: true,
|
|
201
303
|
});
|
|
202
304
|
|
|
203
|
-
if (result.errorText || result.failureReason) {
|
|
204
|
-
return { success: false, error: result.errorText || result.failureText };
|
|
305
|
+
if (result.data.errorText || result.data.failureReason) {
|
|
306
|
+
return { success: false, error: result.data.errorText || result.data.failureText };
|
|
205
307
|
}
|
|
206
308
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
309
|
+
log.info('Order placed', { orderId: result.data.orderId });
|
|
310
|
+
return { success: true, orderId: result.data.orderId };
|
|
311
|
+
} catch (err) {
|
|
312
|
+
return { success: false, error: err.message };
|
|
210
313
|
}
|
|
211
314
|
}
|
|
212
315
|
|
|
213
316
|
/**
|
|
214
317
|
* Cancel an order
|
|
318
|
+
* @param {number} orderId - Order ID
|
|
319
|
+
* @returns {Promise<{success: boolean, error?: string}>}
|
|
215
320
|
*/
|
|
216
321
|
async cancelOrder(orderId) {
|
|
217
322
|
try {
|
|
218
323
|
const result = await this._request(API_PATHS.ORDER_CANCEL, 'POST', {
|
|
219
|
-
orderId
|
|
324
|
+
orderId,
|
|
220
325
|
isAutomated: true,
|
|
221
326
|
});
|
|
222
327
|
|
|
223
|
-
if (result.errorText) {
|
|
224
|
-
return { success: false, error: result.errorText };
|
|
328
|
+
if (result.data.errorText) {
|
|
329
|
+
return { success: false, error: result.data.errorText };
|
|
225
330
|
}
|
|
226
331
|
|
|
227
332
|
return { success: true };
|
|
228
|
-
} catch (
|
|
229
|
-
return { success: false, error:
|
|
333
|
+
} catch (err) {
|
|
334
|
+
return { success: false, error: err.message };
|
|
230
335
|
}
|
|
231
336
|
}
|
|
232
337
|
|
|
233
338
|
/**
|
|
234
339
|
* Close a position
|
|
340
|
+
* @param {number} accountId - Account ID
|
|
341
|
+
* @param {number} contractId - Contract ID
|
|
342
|
+
* @returns {Promise<{success: boolean, error?: string}>}
|
|
235
343
|
*/
|
|
236
344
|
async closePosition(accountId, contractId) {
|
|
237
345
|
try {
|
|
238
346
|
const result = await this._request(API_PATHS.ORDER_LIQUIDATE_POSITION, 'POST', {
|
|
239
|
-
accountId
|
|
240
|
-
contractId
|
|
347
|
+
accountId,
|
|
348
|
+
contractId,
|
|
241
349
|
isAutomated: true,
|
|
242
350
|
});
|
|
243
351
|
|
|
244
|
-
if (result.errorText) {
|
|
245
|
-
return { success: false, error: result.errorText };
|
|
352
|
+
if (result.data.errorText) {
|
|
353
|
+
return { success: false, error: result.data.errorText };
|
|
246
354
|
}
|
|
247
355
|
|
|
248
356
|
return { success: true };
|
|
249
|
-
} catch (
|
|
250
|
-
return { success: false, error:
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* Search contracts
|
|
256
|
-
*/
|
|
257
|
-
async searchContracts(text, limit = 10) {
|
|
258
|
-
try {
|
|
259
|
-
return await this._request(API_PATHS.CONTRACT_SUGGEST, 'GET', null, { t: text, l: limit });
|
|
260
|
-
} catch (error) {
|
|
261
|
-
return [];
|
|
357
|
+
} catch (err) {
|
|
358
|
+
return { success: false, error: err.message };
|
|
262
359
|
}
|
|
263
360
|
}
|
|
264
361
|
|
|
265
|
-
|
|
266
|
-
* Get user info
|
|
267
|
-
*/
|
|
268
|
-
async getUser() {
|
|
269
|
-
return this.user;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/**
|
|
273
|
-
* Get market status
|
|
274
|
-
*/
|
|
275
|
-
async getMarketStatus(accountId) {
|
|
276
|
-
const marketHours = checkMarketHours();
|
|
277
|
-
return {
|
|
278
|
-
success: true,
|
|
279
|
-
isOpen: marketHours.isOpen,
|
|
280
|
-
message: marketHours.message,
|
|
281
|
-
};
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
/**
|
|
285
|
-
* Get token
|
|
286
|
-
*/
|
|
287
|
-
getToken() {
|
|
288
|
-
return this.accessToken;
|
|
289
|
-
}
|
|
362
|
+
// ==================== TRADES ====================
|
|
290
363
|
|
|
291
364
|
/**
|
|
292
|
-
* Get
|
|
293
|
-
|
|
294
|
-
async getOrders(accountId) {
|
|
295
|
-
try {
|
|
296
|
-
const orders = await this._request(API_PATHS.ORDER_LIST, 'GET');
|
|
297
|
-
const filtered = accountId
|
|
298
|
-
? orders.filter(o => o.accountId === accountId)
|
|
299
|
-
: orders;
|
|
300
|
-
return {
|
|
301
|
-
success: true,
|
|
302
|
-
orders: filtered.map(o => ({
|
|
303
|
-
orderId: o.id,
|
|
304
|
-
accountId: o.accountId,
|
|
305
|
-
symbol: o.contractId,
|
|
306
|
-
side: o.action === 'Buy' ? 0 : 1,
|
|
307
|
-
quantity: o.orderQty,
|
|
308
|
-
filledQuantity: o.filledQty || 0,
|
|
309
|
-
price: o.price,
|
|
310
|
-
status: o.ordStatus === 'Working' ? 1 : (o.ordStatus === 'Filled' ? 2 : 0),
|
|
311
|
-
orderType: o.orderType,
|
|
312
|
-
}))
|
|
313
|
-
};
|
|
314
|
-
} catch (error) {
|
|
315
|
-
return { success: false, error: error.message, orders: [] };
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
/**
|
|
320
|
-
* Get order history
|
|
365
|
+
* Get fills/trades
|
|
366
|
+
* @returns {Promise<Array>}
|
|
321
367
|
*/
|
|
322
|
-
async
|
|
368
|
+
async getFills() {
|
|
323
369
|
try {
|
|
324
|
-
const
|
|
325
|
-
return
|
|
326
|
-
} catch
|
|
327
|
-
return
|
|
370
|
+
const result = await this._request(API_PATHS.FILL_LIST, 'GET');
|
|
371
|
+
return result.data || [];
|
|
372
|
+
} catch {
|
|
373
|
+
return [];
|
|
328
374
|
}
|
|
329
375
|
}
|
|
330
376
|
|
|
331
|
-
/**
|
|
332
|
-
* Get lifetime stats (stub - Tradovate doesn't provide this directly)
|
|
333
|
-
*/
|
|
334
|
-
async getLifetimeStats(accountId) {
|
|
335
|
-
return { success: true, stats: null };
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
/**
|
|
339
|
-
* Get daily stats (stub - Tradovate doesn't provide this directly)
|
|
340
|
-
*/
|
|
341
|
-
async getDailyStats(accountId) {
|
|
342
|
-
return { success: true, stats: [] };
|
|
343
|
-
}
|
|
344
|
-
|
|
345
377
|
/**
|
|
346
378
|
* Get trade history
|
|
379
|
+
* @param {number} [accountId] - Account filter
|
|
380
|
+
* @param {number} [days=30] - Days of history
|
|
381
|
+
* @returns {Promise<{success: boolean, trades: Array, error?: string}>}
|
|
347
382
|
*/
|
|
348
383
|
async getTradeHistory(accountId, days = 30) {
|
|
349
384
|
try {
|
|
350
385
|
const fills = await this.getFills();
|
|
351
|
-
const filtered = accountId
|
|
386
|
+
const filtered = accountId
|
|
352
387
|
? fills.filter(f => f.accountId === accountId)
|
|
353
388
|
: fills;
|
|
354
|
-
|
|
355
|
-
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
success: true,
|
|
356
392
|
trades: filtered.map(f => ({
|
|
357
393
|
tradeId: f.id,
|
|
358
394
|
accountId: f.accountId,
|
|
@@ -361,69 +397,92 @@ class TradovateService extends EventEmitter {
|
|
|
361
397
|
quantity: f.qty,
|
|
362
398
|
price: f.price,
|
|
363
399
|
timestamp: f.timestamp,
|
|
364
|
-
}))
|
|
400
|
+
})),
|
|
365
401
|
};
|
|
366
|
-
} catch (
|
|
367
|
-
return { success: false, error:
|
|
402
|
+
} catch (err) {
|
|
403
|
+
return { success: false, error: err.message, trades: [] };
|
|
368
404
|
}
|
|
369
405
|
}
|
|
370
406
|
|
|
371
407
|
/**
|
|
372
|
-
*
|
|
408
|
+
* Get order history
|
|
409
|
+
* @param {number} [days=30] - Days of history
|
|
410
|
+
* @returns {Promise<{success: boolean, orders: Array, error?: string}>}
|
|
373
411
|
*/
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
412
|
+
async getOrderHistory(days = 30) {
|
|
413
|
+
try {
|
|
414
|
+
const result = await this._request(API_PATHS.ORDER_LIST, 'GET');
|
|
415
|
+
return { success: true, orders: result.data || [] };
|
|
416
|
+
} catch (err) {
|
|
417
|
+
return { success: false, error: err.message, orders: [] };
|
|
377
418
|
}
|
|
378
|
-
|
|
379
|
-
// Renew 15 minutes before expiration
|
|
380
|
-
const renewInMs = (90 - 15) * 60 * 1000;
|
|
381
|
-
|
|
382
|
-
this.renewalTimer = setTimeout(async () => {
|
|
383
|
-
try {
|
|
384
|
-
await this.renewToken();
|
|
385
|
-
} catch (error) {
|
|
386
|
-
// Silent fail
|
|
387
|
-
}
|
|
388
|
-
}, renewInMs);
|
|
389
419
|
}
|
|
390
420
|
|
|
421
|
+
// ==================== CONTRACTS ====================
|
|
422
|
+
|
|
391
423
|
/**
|
|
392
|
-
*
|
|
424
|
+
* Search contracts
|
|
425
|
+
* @param {string} text - Search text
|
|
426
|
+
* @param {number} [limit=10] - Result limit
|
|
427
|
+
* @returns {Promise<Array>}
|
|
393
428
|
*/
|
|
394
|
-
async
|
|
395
|
-
if (!this.accessToken) return;
|
|
396
|
-
|
|
429
|
+
async searchContracts(text, limit = 10) {
|
|
397
430
|
try {
|
|
398
|
-
const result = await this._request(API_PATHS.
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
this.mdAccessToken = result.mdAccessToken;
|
|
403
|
-
this.tokenExpiration = new Date(result.expirationTime);
|
|
404
|
-
this.setupTokenRenewal();
|
|
405
|
-
}
|
|
406
|
-
} catch (error) {
|
|
407
|
-
// Silent fail - will need to re-login
|
|
431
|
+
const result = await this._request(API_PATHS.CONTRACT_SUGGEST, 'GET', null, { t: text, l: limit });
|
|
432
|
+
return result.data || [];
|
|
433
|
+
} catch {
|
|
434
|
+
return [];
|
|
408
435
|
}
|
|
409
436
|
}
|
|
410
437
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
async
|
|
415
|
-
|
|
438
|
+
// ==================== STUBS ====================
|
|
439
|
+
|
|
440
|
+
async getUser() { return this.user; }
|
|
441
|
+
async getLifetimeStats() { return { success: true, stats: null }; }
|
|
442
|
+
async getDailyStats() { return { success: true, stats: [] }; }
|
|
443
|
+
|
|
444
|
+
getToken() { return this.accessToken; }
|
|
445
|
+
|
|
446
|
+
async getMarketStatus() {
|
|
447
|
+
const hours = checkMarketHours();
|
|
448
|
+
return { success: true, isOpen: hours.isOpen, message: hours.message };
|
|
416
449
|
}
|
|
417
450
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
451
|
+
// ==================== WEBSOCKET ====================
|
|
452
|
+
|
|
453
|
+
async connectWebSocket() { return connectWebSocket(this); }
|
|
454
|
+
|
|
421
455
|
wsSend(url, query = '', body = null) {
|
|
422
456
|
return wsSend(this, url, query, body);
|
|
423
457
|
}
|
|
424
458
|
|
|
459
|
+
// ==================== HTTP ====================
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Make an HTTP request
|
|
463
|
+
* @private
|
|
464
|
+
*/
|
|
465
|
+
async _request(path, method = 'GET', body = null, queryParams = null) {
|
|
466
|
+
const baseUrl = getBaseUrl(this.isDemo);
|
|
467
|
+
let url = `${baseUrl}${path}`;
|
|
468
|
+
|
|
469
|
+
if (queryParams) {
|
|
470
|
+
const params = new URLSearchParams(queryParams).toString();
|
|
471
|
+
url += `?${params}`;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return request(url, {
|
|
475
|
+
method,
|
|
476
|
+
body,
|
|
477
|
+
token: this.accessToken,
|
|
478
|
+
timeout: TIMEOUTS.API_REQUEST,
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// ==================== CLEANUP ====================
|
|
483
|
+
|
|
425
484
|
/**
|
|
426
|
-
* Disconnect
|
|
485
|
+
* Disconnect and cleanup
|
|
427
486
|
*/
|
|
428
487
|
async disconnect() {
|
|
429
488
|
if (this.renewalTimer) {
|
|
@@ -438,78 +497,8 @@ class TradovateService extends EventEmitter {
|
|
|
438
497
|
this.accounts = [];
|
|
439
498
|
this.user = null;
|
|
440
499
|
this.credentials = null;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
/**
|
|
444
|
-
* Generate device ID
|
|
445
|
-
*/
|
|
446
|
-
generateDeviceId() {
|
|
447
|
-
const crypto = require('crypto');
|
|
448
|
-
const os = require('os');
|
|
449
|
-
const data = `${os.hostname()}-${os.platform()}-${os.arch()}-hqx-cli`;
|
|
450
|
-
return crypto.createHash('md5').update(data).digest('hex');
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
/**
|
|
454
|
-
* HTTP request helper
|
|
455
|
-
*/
|
|
456
|
-
_request(path, method = 'GET', body = null, queryParams = null) {
|
|
457
|
-
return new Promise((resolve, reject) => {
|
|
458
|
-
const baseUrl = getBaseUrl(this.isDemo);
|
|
459
|
-
const url = new URL(baseUrl + path);
|
|
460
|
-
|
|
461
|
-
if (queryParams) {
|
|
462
|
-
Object.entries(queryParams).forEach(([key, value]) => {
|
|
463
|
-
url.searchParams.append(key, value);
|
|
464
|
-
});
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
const postData = body ? JSON.stringify(body) : null;
|
|
468
|
-
|
|
469
|
-
const options = {
|
|
470
|
-
hostname: url.hostname,
|
|
471
|
-
port: 443,
|
|
472
|
-
path: url.pathname + url.search,
|
|
473
|
-
method: method,
|
|
474
|
-
headers: {
|
|
475
|
-
'Content-Type': 'application/json',
|
|
476
|
-
'Accept': 'application/json',
|
|
477
|
-
},
|
|
478
|
-
};
|
|
479
|
-
|
|
480
|
-
if (postData) {
|
|
481
|
-
options.headers['Content-Length'] = Buffer.byteLength(postData);
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
if (this.accessToken) {
|
|
485
|
-
options.headers['Authorization'] = `Bearer ${this.accessToken}`;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
const req = https.request(options, (res) => {
|
|
489
|
-
let data = '';
|
|
490
|
-
res.on('data', chunk => data += chunk);
|
|
491
|
-
res.on('end', () => {
|
|
492
|
-
try {
|
|
493
|
-
const json = JSON.parse(data);
|
|
494
|
-
resolve(json);
|
|
495
|
-
} catch (e) {
|
|
496
|
-
resolve(data);
|
|
497
|
-
}
|
|
498
|
-
});
|
|
499
|
-
});
|
|
500
|
-
|
|
501
|
-
req.on('error', reject);
|
|
502
|
-
req.setTimeout(30000, () => {
|
|
503
|
-
req.destroy();
|
|
504
|
-
reject(new Error('Request timeout'));
|
|
505
|
-
});
|
|
506
500
|
|
|
507
|
-
|
|
508
|
-
req.write(postData);
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
req.end();
|
|
512
|
-
});
|
|
501
|
+
log.info('Disconnected');
|
|
513
502
|
}
|
|
514
503
|
}
|
|
515
504
|
|