hedgequantx 2.6.163 → 2.7.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 +15 -88
- package/bin/cli.js +0 -11
- package/dist/lib/api.jsc +0 -0
- package/dist/lib/api2.jsc +0 -0
- package/dist/lib/core.jsc +0 -0
- package/dist/lib/core2.jsc +0 -0
- package/dist/lib/data.js +1 -1
- package/dist/lib/data.jsc +0 -0
- package/dist/lib/data2.jsc +0 -0
- package/dist/lib/decoder.jsc +0 -0
- package/dist/lib/m/mod1.jsc +0 -0
- package/dist/lib/m/mod2.jsc +0 -0
- package/dist/lib/n/r1.jsc +0 -0
- package/dist/lib/n/r2.jsc +0 -0
- package/dist/lib/n/r3.jsc +0 -0
- package/dist/lib/n/r4.jsc +0 -0
- package/dist/lib/n/r5.jsc +0 -0
- package/dist/lib/n/r6.jsc +0 -0
- package/dist/lib/n/r7.jsc +0 -0
- package/dist/lib/o/util1.jsc +0 -0
- package/dist/lib/o/util2.jsc +0 -0
- package/package.json +6 -3
- package/src/app.js +40 -162
- package/src/config/constants.js +31 -33
- package/src/config/propfirms.js +13 -217
- package/src/config/settings.js +0 -43
- package/src/lib/api.js +198 -0
- package/src/lib/api2.js +353 -0
- package/src/lib/core.js +539 -0
- package/src/lib/core2.js +341 -0
- package/src/lib/data.js +555 -0
- package/src/lib/data2.js +492 -0
- package/src/lib/decoder.js +599 -0
- package/src/lib/m/s1.js +804 -0
- package/src/lib/m/s2.js +34 -0
- package/src/lib/n/r1.js +454 -0
- package/src/lib/n/r2.js +514 -0
- package/src/lib/n/r3.js +631 -0
- package/src/lib/n/r4.js +401 -0
- package/src/lib/n/r5.js +335 -0
- package/src/lib/n/r6.js +425 -0
- package/src/lib/n/r7.js +530 -0
- package/src/lib/o/l1.js +44 -0
- package/src/lib/o/l2.js +427 -0
- package/src/lib/python-bridge.js +206 -0
- package/src/menus/connect.js +14 -176
- package/src/menus/dashboard.js +65 -110
- package/src/pages/accounts.js +18 -18
- package/src/pages/algo/copy-trading.js +210 -240
- package/src/pages/algo/index.js +41 -104
- package/src/pages/algo/one-account.js +386 -33
- package/src/pages/algo/ui.js +312 -151
- package/src/pages/orders.js +3 -3
- package/src/pages/positions.js +3 -3
- package/src/pages/stats/chart.js +74 -0
- package/src/pages/stats/display.js +228 -0
- package/src/pages/stats/index.js +236 -0
- package/src/pages/stats/metrics.js +213 -0
- package/src/pages/user.js +6 -6
- package/src/services/hqx-server/constants.js +55 -0
- package/src/services/hqx-server/index.js +401 -0
- package/src/services/hqx-server/latency.js +81 -0
- package/src/services/index.js +12 -3
- package/src/services/rithmic/accounts.js +7 -32
- package/src/services/rithmic/connection.js +1 -204
- package/src/services/rithmic/contracts.js +116 -99
- package/src/services/rithmic/handlers.js +21 -196
- package/src/services/rithmic/index.js +63 -120
- package/src/services/rithmic/market.js +31 -0
- package/src/services/rithmic/orders.js +5 -111
- package/src/services/rithmic/protobuf.js +384 -138
- package/src/services/session.js +22 -173
- package/src/ui/box.js +10 -18
- package/src/ui/index.js +1 -3
- package/src/ui/menu.js +1 -1
- package/src/utils/prompts.js +2 -2
- package/dist/lib/m/s1.js +0 -1
- package/src/menus/ai-agent-connect.js +0 -181
- package/src/menus/ai-agent-models.js +0 -219
- package/src/menus/ai-agent-oauth.js +0 -292
- package/src/menus/ai-agent-ui.js +0 -141
- package/src/menus/ai-agent.js +0 -484
- package/src/pages/algo/algo-config.js +0 -195
- package/src/pages/algo/algo-multi.js +0 -801
- package/src/pages/algo/algo-utils.js +0 -58
- package/src/pages/algo/copy-engine.js +0 -449
- package/src/pages/algo/custom-strategy.js +0 -459
- package/src/pages/algo/logger.js +0 -245
- package/src/pages/algo/smart-logs-data.js +0 -218
- package/src/pages/algo/smart-logs.js +0 -387
- package/src/pages/algo/ui-constants.js +0 -144
- package/src/pages/algo/ui-summary.js +0 -184
- package/src/pages/stats-calculations.js +0 -191
- package/src/pages/stats-ui.js +0 -381
- package/src/pages/stats.js +0 -339
- package/src/services/ai/client-analysis.js +0 -194
- package/src/services/ai/client-models.js +0 -333
- package/src/services/ai/client.js +0 -343
- package/src/services/ai/index.js +0 -384
- package/src/services/ai/oauth-anthropic.js +0 -265
- package/src/services/ai/oauth-gemini.js +0 -223
- package/src/services/ai/oauth-iflow.js +0 -269
- package/src/services/ai/oauth-openai.js +0 -233
- package/src/services/ai/oauth-qwen.js +0 -279
- package/src/services/ai/providers/direct-providers.js +0 -323
- package/src/services/ai/providers/index.js +0 -62
- package/src/services/ai/providers/other-providers.js +0 -104
- package/src/services/ai/proxy-install.js +0 -249
- package/src/services/ai/proxy-manager.js +0 -494
- package/src/services/ai/proxy-remote.js +0 -161
- package/src/services/ai/strategy-supervisor.js +0 -1312
- package/src/services/ai/supervisor-data.js +0 -195
- package/src/services/ai/supervisor-optimize.js +0 -215
- package/src/services/ai/supervisor-sync.js +0 -178
- package/src/services/ai/supervisor-utils.js +0 -158
- package/src/services/ai/supervisor.js +0 -484
- package/src/services/ai/validation.js +0 -250
- package/src/services/hqx-server-events.js +0 -110
- package/src/services/hqx-server-handlers.js +0 -217
- package/src/services/hqx-server-latency.js +0 -136
- package/src/services/hqx-server.js +0 -403
- package/src/services/position-constants.js +0 -28
- package/src/services/position-exit-logic.js +0 -174
- package/src/services/position-manager.js +0 -438
- package/src/services/position-momentum.js +0 -206
- package/src/services/projectx/accounts.js +0 -142
- package/src/services/projectx/index.js +0 -443
- package/src/services/projectx/market.js +0 -172
- package/src/services/projectx/stats.js +0 -110
- package/src/services/projectx/trading.js +0 -180
- package/src/services/rithmic/latency-tracker.js +0 -182
- package/src/services/rithmic/market-data-decoders.js +0 -229
- package/src/services/rithmic/market-data.js +0 -272
- package/src/services/rithmic/orders-fast.js +0 -246
- package/src/services/rithmic/proto-decoders.js +0 -403
- package/src/services/rithmic/specs.js +0 -146
- package/src/services/rithmic/trade-history.js +0 -254
- package/src/services/session-history.js +0 -475
- package/src/services/strategy/hft-signal-calc.js +0 -147
- package/src/services/strategy/hft-tick.js +0 -407
- package/src/services/strategy/recovery-math.js +0 -402
- package/src/services/tradovate/constants.js +0 -109
- package/src/services/tradovate/index.js +0 -392
- package/src/services/tradovate/market.js +0 -47
- package/src/services/tradovate/orders.js +0 -145
- package/src/services/tradovate/websocket.js +0 -97
|
@@ -1,392 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Tradovate Service - Main service for Tradovate prop firm connections
|
|
3
|
-
* @module services/tradovate
|
|
4
|
-
*
|
|
5
|
-
* STRICT RULE: Display ONLY values returned by API. No estimation, no simulation.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
const crypto = require('crypto');
|
|
9
|
-
const os = require('os');
|
|
10
|
-
const EventEmitter = require('events');
|
|
11
|
-
const { request } = require('../../utils/http');
|
|
12
|
-
const { TIMEOUTS } = require('../../config/settings');
|
|
13
|
-
const { TRADOVATE_URLS, API_PATHS, getBaseUrl, getTradingWebSocketUrl } = require('./constants');
|
|
14
|
-
const { checkMarketHours, isDST } = require('./market');
|
|
15
|
-
const { connectWebSocket, wsSend, disconnectWebSocket } = require('./websocket');
|
|
16
|
-
const { getOrders, placeOrder, cancelOrder, closePosition, getOrderHistory } = require('./orders');
|
|
17
|
-
const { logger } = require('../../utils/logger');
|
|
18
|
-
|
|
19
|
-
const log = logger.scope('Tradovate');
|
|
20
|
-
|
|
21
|
-
/** PropFirm configurations */
|
|
22
|
-
const PROPFIRM_CONFIGS = {
|
|
23
|
-
apex_tradovate: { name: 'Apex (Tradovate)', isDemo: false },
|
|
24
|
-
takeprofittrader: { name: 'TakeProfitTrader', isDemo: false },
|
|
25
|
-
myfundedfutures: { name: 'MyFundedFutures', isDemo: false },
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Tradovate Service for prop firm trading
|
|
30
|
-
*/
|
|
31
|
-
class TradovateService extends EventEmitter {
|
|
32
|
-
/**
|
|
33
|
-
* @param {string} propfirmKey - PropFirm identifier
|
|
34
|
-
*/
|
|
35
|
-
constructor(propfirmKey) {
|
|
36
|
-
super();
|
|
37
|
-
this.propfirmKey = propfirmKey;
|
|
38
|
-
this.propfirm = PROPFIRM_CONFIGS[propfirmKey] || { name: propfirmKey, isDemo: false };
|
|
39
|
-
|
|
40
|
-
// Auth
|
|
41
|
-
this.accessToken = null;
|
|
42
|
-
this.mdAccessToken = null;
|
|
43
|
-
this.userId = null;
|
|
44
|
-
this.tokenExpiration = null;
|
|
45
|
-
|
|
46
|
-
// State
|
|
47
|
-
this.accounts = [];
|
|
48
|
-
this.user = null;
|
|
49
|
-
this.isDemo = true;
|
|
50
|
-
this.credentials = null;
|
|
51
|
-
|
|
52
|
-
// WebSocket
|
|
53
|
-
this.ws = null;
|
|
54
|
-
this.wsRequestId = 1;
|
|
55
|
-
this.renewalTimer = null;
|
|
56
|
-
|
|
57
|
-
// Device ID cache
|
|
58
|
-
this._deviceId = null;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// ==================== AUTH ====================
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Login to Tradovate
|
|
65
|
-
* @param {string} username - Username
|
|
66
|
-
* @param {string} password - Password
|
|
67
|
-
* @param {Object} [options] - Additional options (cid, sec)
|
|
68
|
-
* @returns {Promise<{success: boolean, error?: string}>}
|
|
69
|
-
*/
|
|
70
|
-
async login(username, password, options = {}) {
|
|
71
|
-
try {
|
|
72
|
-
const authData = {
|
|
73
|
-
name: username,
|
|
74
|
-
password,
|
|
75
|
-
appId: 'HQX-CLI',
|
|
76
|
-
appVersion: '2.0.0',
|
|
77
|
-
deviceId: this._getDeviceId(),
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
if (options.cid) authData.cid = options.cid;
|
|
81
|
-
if (options.sec) authData.sec = options.sec;
|
|
82
|
-
|
|
83
|
-
const result = await this._request(API_PATHS.AUTH_TOKEN_REQUEST, 'POST', authData);
|
|
84
|
-
|
|
85
|
-
if (result.data.errorText) {
|
|
86
|
-
log.warn('Login failed', { error: result.data.errorText });
|
|
87
|
-
return { success: false, error: result.data.errorText };
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
if (!result.data.accessToken) {
|
|
91
|
-
return { success: false, error: 'No access token received' };
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
this.accessToken = result.data.accessToken;
|
|
95
|
-
this.mdAccessToken = result.data.mdAccessToken;
|
|
96
|
-
this.userId = result.data.userId;
|
|
97
|
-
this.tokenExpiration = new Date(result.data.expirationTime);
|
|
98
|
-
this.user = { userName: result.data.name, userId: result.data.userId };
|
|
99
|
-
this.credentials = { username, password };
|
|
100
|
-
|
|
101
|
-
this._setupTokenRenewal();
|
|
102
|
-
await this._fetchAccounts();
|
|
103
|
-
|
|
104
|
-
log.info('Login successful', { accounts: this.accounts.length });
|
|
105
|
-
return { success: true };
|
|
106
|
-
} catch (err) {
|
|
107
|
-
log.error('Login error', { error: err.message });
|
|
108
|
-
return { success: false, error: err.message };
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Generate device ID (cached)
|
|
114
|
-
* @private
|
|
115
|
-
*/
|
|
116
|
-
_getDeviceId() {
|
|
117
|
-
if (this._deviceId) return this._deviceId;
|
|
118
|
-
|
|
119
|
-
const data = `${os.hostname()}-${os.platform()}-${os.arch()}-hqx-cli`;
|
|
120
|
-
this._deviceId = crypto.createHash('md5').update(data).digest('hex');
|
|
121
|
-
return this._deviceId;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Setup automatic token renewal
|
|
126
|
-
* @private
|
|
127
|
-
*/
|
|
128
|
-
_setupTokenRenewal() {
|
|
129
|
-
if (this.renewalTimer) {
|
|
130
|
-
clearTimeout(this.renewalTimer);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Renew 15 minutes before expiration
|
|
134
|
-
const renewInMs = (90 - 15) * 60 * 1000;
|
|
135
|
-
|
|
136
|
-
this.renewalTimer = setTimeout(async () => {
|
|
137
|
-
try {
|
|
138
|
-
await this._renewToken();
|
|
139
|
-
} catch (err) {
|
|
140
|
-
log.warn('Token renewal failed', { error: err.message });
|
|
141
|
-
}
|
|
142
|
-
}, renewInMs);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Renew access token
|
|
147
|
-
* @private
|
|
148
|
-
*/
|
|
149
|
-
async _renewToken() {
|
|
150
|
-
if (!this.accessToken) return;
|
|
151
|
-
|
|
152
|
-
const result = await this._request(API_PATHS.AUTH_RENEW_TOKEN, 'GET');
|
|
153
|
-
|
|
154
|
-
if (result.data.accessToken) {
|
|
155
|
-
this.accessToken = result.data.accessToken;
|
|
156
|
-
this.mdAccessToken = result.data.mdAccessToken;
|
|
157
|
-
this.tokenExpiration = new Date(result.data.expirationTime);
|
|
158
|
-
this._setupTokenRenewal();
|
|
159
|
-
log.debug('Token renewed');
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// ==================== ACCOUNTS ====================
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Fetch accounts from API
|
|
167
|
-
* @private
|
|
168
|
-
*/
|
|
169
|
-
async _fetchAccounts() {
|
|
170
|
-
const result = await this._request(API_PATHS.ACCOUNT_LIST, 'GET');
|
|
171
|
-
|
|
172
|
-
if (!Array.isArray(result.data)) {
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
this.accounts = result.data;
|
|
177
|
-
|
|
178
|
-
// Fetch cash balance for each account
|
|
179
|
-
for (const acc of this.accounts) {
|
|
180
|
-
try {
|
|
181
|
-
const balanceResult = await this._request(
|
|
182
|
-
API_PATHS.CASH_BALANCE_SNAPSHOT,
|
|
183
|
-
'POST',
|
|
184
|
-
{ accountId: acc.id }
|
|
185
|
-
);
|
|
186
|
-
acc.cashBalance = balanceResult.data;
|
|
187
|
-
} catch {
|
|
188
|
-
acc.cashBalance = null;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Get trading accounts with REAL P&L from API
|
|
195
|
-
* @returns {Promise<{success: boolean, accounts: Array}>}
|
|
196
|
-
*/
|
|
197
|
-
async getTradingAccounts() {
|
|
198
|
-
if (!this.accounts.length) {
|
|
199
|
-
await this._fetchAccounts();
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const tradingAccounts = this.accounts.map((acc) => {
|
|
203
|
-
const cb = acc.cashBalance || {};
|
|
204
|
-
|
|
205
|
-
// ONLY use values from API - null if not available
|
|
206
|
-
const balance = cb.totalCashValue ?? cb.netLiquidatingValue ?? null;
|
|
207
|
-
const realizedPnL = cb.realizedPnL ?? null;
|
|
208
|
-
const openPnL = cb.openPnL ?? null;
|
|
209
|
-
|
|
210
|
-
// Total P&L
|
|
211
|
-
let profitAndLoss = null;
|
|
212
|
-
if (cb.totalPnL !== undefined) {
|
|
213
|
-
profitAndLoss = cb.totalPnL;
|
|
214
|
-
} else if (realizedPnL !== null || openPnL !== null) {
|
|
215
|
-
profitAndLoss = (realizedPnL || 0) + (openPnL || 0);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
return {
|
|
219
|
-
accountId: acc.id,
|
|
220
|
-
tradovateAccountId: acc.id,
|
|
221
|
-
accountName: acc.name,
|
|
222
|
-
name: acc.name,
|
|
223
|
-
balance,
|
|
224
|
-
todayPnL: realizedPnL,
|
|
225
|
-
openPnL,
|
|
226
|
-
profitAndLoss,
|
|
227
|
-
startingBalance: null,
|
|
228
|
-
status: acc.active ? 0 : 3,
|
|
229
|
-
platform: 'Tradovate',
|
|
230
|
-
propfirm: this.propfirm.name,
|
|
231
|
-
accountType: acc.accountType,
|
|
232
|
-
};
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
return { success: true, accounts: tradingAccounts };
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// ==================== POSITIONS ====================
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* Get positions for an account
|
|
242
|
-
* @param {number} accountId - Account ID
|
|
243
|
-
* @returns {Promise<Array>}
|
|
244
|
-
*/
|
|
245
|
-
async getPositions(accountId) {
|
|
246
|
-
try {
|
|
247
|
-
const result = await this._request(API_PATHS.POSITION_DEPS, 'GET', null, { masterid: accountId });
|
|
248
|
-
return result.data.filter(p => p.netPos !== 0);
|
|
249
|
-
} catch {
|
|
250
|
-
return [];
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// ==================== ORDERS ====================
|
|
255
|
-
|
|
256
|
-
async getOrders(accountId) { return getOrders(this, accountId); }
|
|
257
|
-
async placeOrder(orderData) { return placeOrder(this, orderData); }
|
|
258
|
-
async cancelOrder(orderId) { return cancelOrder(this, orderId); }
|
|
259
|
-
async closePosition(accountId, contractId) { return closePosition(this, accountId, contractId); }
|
|
260
|
-
|
|
261
|
-
// ==================== TRADES ====================
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* Get fills/trades
|
|
265
|
-
* @returns {Promise<Array>}
|
|
266
|
-
*/
|
|
267
|
-
async getFills() {
|
|
268
|
-
try {
|
|
269
|
-
const result = await this._request(API_PATHS.FILL_LIST, 'GET');
|
|
270
|
-
return result.data || [];
|
|
271
|
-
} catch {
|
|
272
|
-
return [];
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
/**
|
|
277
|
-
* Get trade history
|
|
278
|
-
* @param {number} [accountId] - Account filter
|
|
279
|
-
* @param {number} [days=30] - Days of history
|
|
280
|
-
* @returns {Promise<{success: boolean, trades: Array, error?: string}>}
|
|
281
|
-
*/
|
|
282
|
-
async getTradeHistory(accountId, days = 30) {
|
|
283
|
-
try {
|
|
284
|
-
const fills = await this.getFills();
|
|
285
|
-
const filtered = accountId
|
|
286
|
-
? fills.filter(f => f.accountId === accountId)
|
|
287
|
-
: fills;
|
|
288
|
-
|
|
289
|
-
return {
|
|
290
|
-
success: true,
|
|
291
|
-
trades: filtered.map(f => ({
|
|
292
|
-
tradeId: f.id,
|
|
293
|
-
accountId: f.accountId,
|
|
294
|
-
symbol: f.contractId,
|
|
295
|
-
side: f.action === 'Buy' ? 0 : 1,
|
|
296
|
-
quantity: f.qty,
|
|
297
|
-
price: f.price,
|
|
298
|
-
timestamp: f.timestamp,
|
|
299
|
-
})),
|
|
300
|
-
};
|
|
301
|
-
} catch (err) {
|
|
302
|
-
return { success: false, error: err.message, trades: [] };
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
async getOrderHistory(days = 30) { return getOrderHistory(this, days); }
|
|
307
|
-
|
|
308
|
-
// ==================== CONTRACTS ====================
|
|
309
|
-
|
|
310
|
-
/**
|
|
311
|
-
* Search contracts
|
|
312
|
-
* @param {string} text - Search text
|
|
313
|
-
* @param {number} [limit=10] - Result limit
|
|
314
|
-
* @returns {Promise<Array>}
|
|
315
|
-
*/
|
|
316
|
-
async searchContracts(text, limit = 10) {
|
|
317
|
-
try {
|
|
318
|
-
const result = await this._request(API_PATHS.CONTRACT_SUGGEST, 'GET', null, { t: text, l: limit });
|
|
319
|
-
return result.data || [];
|
|
320
|
-
} catch {
|
|
321
|
-
return [];
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// ==================== STUBS ====================
|
|
326
|
-
|
|
327
|
-
async getUser() { return this.user; }
|
|
328
|
-
async getLifetimeStats() { return { success: true, stats: null }; }
|
|
329
|
-
async getDailyStats() { return { success: true, stats: [] }; }
|
|
330
|
-
|
|
331
|
-
getToken() { return this.accessToken; }
|
|
332
|
-
|
|
333
|
-
async getMarketStatus() {
|
|
334
|
-
const hours = checkMarketHours();
|
|
335
|
-
return { success: true, isOpen: hours.isOpen, message: hours.message };
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// ==================== WEBSOCKET ====================
|
|
339
|
-
|
|
340
|
-
async connectWebSocket() { return connectWebSocket(this); }
|
|
341
|
-
|
|
342
|
-
wsSend(url, query = '', body = null) {
|
|
343
|
-
return wsSend(this, url, query, body);
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// ==================== HTTP ====================
|
|
347
|
-
|
|
348
|
-
/**
|
|
349
|
-
* Make an HTTP request
|
|
350
|
-
* @private
|
|
351
|
-
*/
|
|
352
|
-
async _request(path, method = 'GET', body = null, queryParams = null) {
|
|
353
|
-
const baseUrl = getBaseUrl(this.isDemo);
|
|
354
|
-
let url = `${baseUrl}${path}`;
|
|
355
|
-
|
|
356
|
-
if (queryParams) {
|
|
357
|
-
const params = new URLSearchParams(queryParams).toString();
|
|
358
|
-
url += `?${params}`;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
return request(url, {
|
|
362
|
-
method,
|
|
363
|
-
body,
|
|
364
|
-
token: this.accessToken,
|
|
365
|
-
timeout: TIMEOUTS.API_REQUEST,
|
|
366
|
-
});
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
// ==================== CLEANUP ====================
|
|
370
|
-
|
|
371
|
-
/**
|
|
372
|
-
* Disconnect and cleanup
|
|
373
|
-
*/
|
|
374
|
-
async disconnect() {
|
|
375
|
-
if (this.renewalTimer) {
|
|
376
|
-
clearTimeout(this.renewalTimer);
|
|
377
|
-
this.renewalTimer = null;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
disconnectWebSocket(this);
|
|
381
|
-
|
|
382
|
-
this.accessToken = null;
|
|
383
|
-
this.mdAccessToken = null;
|
|
384
|
-
this.accounts = [];
|
|
385
|
-
this.user = null;
|
|
386
|
-
this.credentials = null;
|
|
387
|
-
|
|
388
|
-
log.info('Disconnected');
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
module.exports = { TradovateService };
|
|
@@ -1,47 +0,0 @@
|
|
|
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 };
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tradovate Orders Module
|
|
3
|
-
* @module services/tradovate/orders
|
|
4
|
-
*
|
|
5
|
-
* Order placement, cancellation, and queries
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
const { API_PATHS } = require('./constants');
|
|
9
|
-
const { logger } = require('../../utils/logger');
|
|
10
|
-
|
|
11
|
-
const log = logger.scope('TradovateOrders');
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Get orders
|
|
15
|
-
* @param {TradovateService} service
|
|
16
|
-
* @param {number} [accountId]
|
|
17
|
-
* @returns {Promise<{success: boolean, orders: Array, error?: string}>}
|
|
18
|
-
*/
|
|
19
|
-
async function getOrders(service, accountId) {
|
|
20
|
-
try {
|
|
21
|
-
const result = await service._request(API_PATHS.ORDER_LIST, 'GET');
|
|
22
|
-
const orders = Array.isArray(result.data) ? result.data : [];
|
|
23
|
-
|
|
24
|
-
const filtered = accountId
|
|
25
|
-
? orders.filter(o => o.accountId === accountId)
|
|
26
|
-
: orders;
|
|
27
|
-
|
|
28
|
-
return {
|
|
29
|
-
success: true,
|
|
30
|
-
orders: filtered.map(o => ({
|
|
31
|
-
orderId: o.id,
|
|
32
|
-
accountId: o.accountId,
|
|
33
|
-
symbol: o.contractId,
|
|
34
|
-
side: o.action === 'Buy' ? 0 : 1,
|
|
35
|
-
quantity: o.orderQty,
|
|
36
|
-
filledQuantity: o.filledQty || 0,
|
|
37
|
-
price: o.price,
|
|
38
|
-
status: o.ordStatus === 'Working' ? 1 : (o.ordStatus === 'Filled' ? 2 : 0),
|
|
39
|
-
orderType: o.orderType,
|
|
40
|
-
})),
|
|
41
|
-
};
|
|
42
|
-
} catch (err) {
|
|
43
|
-
return { success: false, error: err.message, orders: [] };
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Place an order
|
|
49
|
-
* @param {TradovateService} service
|
|
50
|
-
* @param {Object} orderData
|
|
51
|
-
* @returns {Promise<{success: boolean, orderId?: number, error?: string}>}
|
|
52
|
-
*/
|
|
53
|
-
async function placeOrder(service, orderData) {
|
|
54
|
-
try {
|
|
55
|
-
const result = await service._request(API_PATHS.ORDER_PLACE, 'POST', {
|
|
56
|
-
accountId: orderData.accountId,
|
|
57
|
-
action: orderData.side === 0 ? 'Buy' : 'Sell',
|
|
58
|
-
symbol: orderData.symbol,
|
|
59
|
-
orderQty: orderData.size,
|
|
60
|
-
orderType: orderData.type === 2 ? 'Market' : 'Limit',
|
|
61
|
-
price: orderData.price,
|
|
62
|
-
isAutomated: true,
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
if (result.data.errorText || result.data.failureReason) {
|
|
66
|
-
return { success: false, error: result.data.errorText || result.data.failureText };
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
log.info('Order placed', { orderId: result.data.orderId });
|
|
70
|
-
return { success: true, orderId: result.data.orderId };
|
|
71
|
-
} catch (err) {
|
|
72
|
-
return { success: false, error: err.message };
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Cancel an order
|
|
78
|
-
* @param {TradovateService} service
|
|
79
|
-
* @param {number} orderId
|
|
80
|
-
* @returns {Promise<{success: boolean, error?: string}>}
|
|
81
|
-
*/
|
|
82
|
-
async function cancelOrder(service, orderId) {
|
|
83
|
-
try {
|
|
84
|
-
const result = await service._request(API_PATHS.ORDER_CANCEL, 'POST', {
|
|
85
|
-
orderId,
|
|
86
|
-
isAutomated: true,
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
if (result.data.errorText) {
|
|
90
|
-
return { success: false, error: result.data.errorText };
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return { success: true };
|
|
94
|
-
} catch (err) {
|
|
95
|
-
return { success: false, error: err.message };
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Close a position
|
|
101
|
-
* @param {TradovateService} service
|
|
102
|
-
* @param {number} accountId
|
|
103
|
-
* @param {number} contractId
|
|
104
|
-
* @returns {Promise<{success: boolean, error?: string}>}
|
|
105
|
-
*/
|
|
106
|
-
async function closePosition(service, accountId, contractId) {
|
|
107
|
-
try {
|
|
108
|
-
const result = await service._request(API_PATHS.ORDER_LIQUIDATE_POSITION, 'POST', {
|
|
109
|
-
accountId,
|
|
110
|
-
contractId,
|
|
111
|
-
isAutomated: true,
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
if (result.data.errorText) {
|
|
115
|
-
return { success: false, error: result.data.errorText };
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
return { success: true };
|
|
119
|
-
} catch (err) {
|
|
120
|
-
return { success: false, error: err.message };
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Get order history
|
|
126
|
-
* @param {TradovateService} service
|
|
127
|
-
* @param {number} [days=30]
|
|
128
|
-
* @returns {Promise<{success: boolean, orders: Array, error?: string}>}
|
|
129
|
-
*/
|
|
130
|
-
async function getOrderHistory(service, days = 30) {
|
|
131
|
-
try {
|
|
132
|
-
const result = await service._request(API_PATHS.ORDER_LIST, 'GET');
|
|
133
|
-
return { success: true, orders: result.data || [] };
|
|
134
|
-
} catch (err) {
|
|
135
|
-
return { success: false, error: err.message, orders: [] };
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
module.exports = {
|
|
140
|
-
getOrders,
|
|
141
|
-
placeOrder,
|
|
142
|
-
cancelOrder,
|
|
143
|
-
closePosition,
|
|
144
|
-
getOrderHistory,
|
|
145
|
-
};
|
|
@@ -1,97 +0,0 @@
|
|
|
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
|
-
};
|