hedgequantx 2.6.160 → 2.6.162
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/menus/ai-agent-connect.js +181 -0
- package/src/menus/ai-agent-models.js +219 -0
- package/src/menus/ai-agent-oauth.js +292 -0
- package/src/menus/ai-agent-ui.js +141 -0
- package/src/menus/ai-agent.js +88 -1489
- package/src/pages/algo/copy-engine.js +449 -0
- package/src/pages/algo/copy-trading.js +11 -543
- package/src/pages/algo/smart-logs-data.js +218 -0
- package/src/pages/algo/smart-logs.js +9 -214
- package/src/pages/algo/ui-constants.js +144 -0
- package/src/pages/algo/ui-summary.js +184 -0
- package/src/pages/algo/ui.js +42 -526
- package/src/pages/stats-calculations.js +191 -0
- package/src/pages/stats-ui.js +381 -0
- package/src/pages/stats.js +14 -507
- package/src/services/ai/client-analysis.js +194 -0
- package/src/services/ai/client-models.js +333 -0
- package/src/services/ai/client.js +6 -489
- package/src/services/ai/index.js +2 -257
- package/src/services/ai/proxy-install.js +249 -0
- package/src/services/ai/proxy-manager.js +29 -411
- package/src/services/ai/proxy-remote.js +161 -0
- package/src/services/ai/strategy-supervisor.js +10 -765
- package/src/services/ai/supervisor-data.js +195 -0
- package/src/services/ai/supervisor-optimize.js +215 -0
- package/src/services/ai/supervisor-sync.js +178 -0
- package/src/services/ai/supervisor-utils.js +158 -0
- package/src/services/ai/supervisor.js +50 -515
- package/src/services/ai/validation.js +250 -0
- package/src/services/hqx-server-events.js +110 -0
- package/src/services/hqx-server-handlers.js +217 -0
- package/src/services/hqx-server-latency.js +136 -0
- package/src/services/hqx-server.js +51 -403
- package/src/services/position-constants.js +28 -0
- package/src/services/position-manager.js +105 -554
- package/src/services/position-momentum.js +206 -0
- package/src/services/projectx/accounts.js +142 -0
- package/src/services/projectx/index.js +40 -289
- package/src/services/projectx/trading.js +180 -0
- package/src/services/rithmic/handlers.js +2 -208
- package/src/services/rithmic/index.js +32 -542
- package/src/services/rithmic/latency-tracker.js +182 -0
- package/src/services/rithmic/specs.js +146 -0
- package/src/services/rithmic/trade-history.js +254 -0
|
@@ -9,109 +9,22 @@ const EventEmitter = require('events');
|
|
|
9
9
|
const { RithmicConnection } = require('./connection');
|
|
10
10
|
const { RITHMIC_ENDPOINTS, RITHMIC_SYSTEMS, REQ } = require('./constants');
|
|
11
11
|
const { createOrderHandler, createPnLHandler, LatencyTracker } = require('./handlers');
|
|
12
|
-
const {
|
|
13
|
-
fetchAccounts,
|
|
14
|
-
getTradingAccounts,
|
|
15
|
-
requestPnLSnapshot,
|
|
16
|
-
subscribePnLUpdates,
|
|
17
|
-
getPositions,
|
|
18
|
-
hashAccountId,
|
|
19
|
-
} = require('./accounts');
|
|
12
|
+
const { fetchAccounts, getTradingAccounts, requestPnLSnapshot, subscribePnLUpdates, getPositions } = require('./accounts');
|
|
20
13
|
const { placeOrder, cancelOrder, cancelAllOrders, getOrders, getOrderHistory, closePosition, flattenAll, emergencyStop, fastEntry, fastExit } = require('./orders');
|
|
21
14
|
const { decodeFrontMonthContract } = require('./protobuf');
|
|
22
15
|
const { TIMEOUTS, CACHE } = require('../../config/settings');
|
|
23
16
|
const { logger } = require('../../utils/logger');
|
|
24
17
|
|
|
25
|
-
|
|
18
|
+
// Extracted modules
|
|
19
|
+
const { CME_CONTRACT_SPECS, PROPFIRM_CONFIGS, checkMarketHours } = require('./specs');
|
|
20
|
+
const { getTradeHistory, setupOrderFillListener } = require('./trade-history');
|
|
26
21
|
|
|
27
|
-
|
|
28
|
-
* CME Contract Specifications - Official exchange tick sizes, values, and names
|
|
29
|
-
* These are technical constants defined by the exchange, not market data.
|
|
30
|
-
* Source: CME Group contract specifications
|
|
31
|
-
*/
|
|
32
|
-
const CME_CONTRACT_SPECS = {
|
|
33
|
-
// E-mini Index Futures (CME)
|
|
34
|
-
ES: { tickSize: 0.25, tickValue: 12.50, name: 'E-mini S&P 500' },
|
|
35
|
-
NQ: { tickSize: 0.25, tickValue: 5.00, name: 'E-mini NASDAQ-100' },
|
|
36
|
-
RTY: { tickSize: 0.10, tickValue: 5.00, name: 'E-mini Russell 2000' },
|
|
37
|
-
YM: { tickSize: 1.00, tickValue: 5.00, name: 'E-mini Dow' },
|
|
38
|
-
|
|
39
|
-
// Micro Index Futures (CME)
|
|
40
|
-
MES: { tickSize: 0.25, tickValue: 1.25, name: 'Micro E-mini S&P 500' },
|
|
41
|
-
MNQ: { tickSize: 0.25, tickValue: 0.50, name: 'Micro E-mini NASDAQ-100' },
|
|
42
|
-
M2K: { tickSize: 0.10, tickValue: 0.50, name: 'Micro E-mini Russell 2000' },
|
|
43
|
-
MYM: { tickSize: 1.00, tickValue: 0.50, name: 'Micro E-mini Dow' },
|
|
44
|
-
|
|
45
|
-
// Energy Futures (NYMEX)
|
|
46
|
-
CL: { tickSize: 0.01, tickValue: 10.00, name: 'Crude Oil' },
|
|
47
|
-
QM: { tickSize: 0.025, tickValue: 12.50, name: 'E-mini Crude Oil' },
|
|
48
|
-
MCL: { tickSize: 0.01, tickValue: 1.00, name: 'Micro Crude Oil' },
|
|
49
|
-
NG: { tickSize: 0.001, tickValue: 10.00, name: 'Natural Gas' },
|
|
50
|
-
QG: { tickSize: 0.005, tickValue: 12.50, name: 'E-mini Natural Gas' },
|
|
51
|
-
|
|
52
|
-
// Metal Futures (COMEX)
|
|
53
|
-
GC: { tickSize: 0.10, tickValue: 10.00, name: 'Gold' },
|
|
54
|
-
MGC: { tickSize: 0.10, tickValue: 1.00, name: 'Micro Gold' },
|
|
55
|
-
SI: { tickSize: 0.005, tickValue: 25.00, name: 'Silver' },
|
|
56
|
-
SIL: { tickSize: 0.005, tickValue: 2.50, name: '1000oz Silver' },
|
|
57
|
-
HG: { tickSize: 0.0005, tickValue: 12.50, name: 'Copper' },
|
|
58
|
-
MHG: { tickSize: 0.0005, tickValue: 1.25, name: 'Micro Copper' },
|
|
59
|
-
|
|
60
|
-
// Treasury Futures (CBOT)
|
|
61
|
-
ZB: { tickSize: 0.03125, tickValue: 31.25, name: '30-Year T-Bond' },
|
|
62
|
-
ZN: { tickSize: 0.015625, tickValue: 15.625, name: '10-Year T-Note' },
|
|
63
|
-
ZF: { tickSize: 0.0078125, tickValue: 7.8125, name: '5-Year T-Note' },
|
|
64
|
-
ZT: { tickSize: 0.0078125, tickValue: 15.625, name: '2-Year T-Note' },
|
|
65
|
-
|
|
66
|
-
// Agricultural Futures (CBOT)
|
|
67
|
-
ZC: { tickSize: 0.25, tickValue: 12.50, name: 'Corn' },
|
|
68
|
-
ZS: { tickSize: 0.25, tickValue: 12.50, name: 'Soybeans' },
|
|
69
|
-
ZW: { tickSize: 0.25, tickValue: 12.50, name: 'Wheat' },
|
|
70
|
-
ZL: { tickSize: 0.01, tickValue: 6.00, name: 'Soybean Oil' },
|
|
71
|
-
ZM: { tickSize: 0.10, tickValue: 10.00, name: 'Soybean Meal' },
|
|
72
|
-
|
|
73
|
-
// Currency Futures (CME)
|
|
74
|
-
'6E': { tickSize: 0.00005, tickValue: 6.25, name: 'Euro FX' },
|
|
75
|
-
'6J': { tickSize: 0.0000005, tickValue: 6.25, name: 'Japanese Yen' },
|
|
76
|
-
'6B': { tickSize: 0.0001, tickValue: 6.25, name: 'British Pound' },
|
|
77
|
-
'6A': { tickSize: 0.0001, tickValue: 10.00, name: 'Australian Dollar' },
|
|
78
|
-
'6C': { tickSize: 0.00005, tickValue: 5.00, name: 'Canadian Dollar' },
|
|
79
|
-
'6M': { tickSize: 0.0001, tickValue: 5.00, name: 'Mexican Peso' },
|
|
80
|
-
|
|
81
|
-
// Nikkei (CME)
|
|
82
|
-
NKD: { tickSize: 5.0, tickValue: 25.00, name: 'Nikkei 225' },
|
|
83
|
-
|
|
84
|
-
// VIX Futures (CFE)
|
|
85
|
-
VX: { tickSize: 0.05, tickValue: 50.00, name: 'VIX Futures' },
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
/** PropFirm configurations */
|
|
89
|
-
const PROPFIRM_CONFIGS = {
|
|
90
|
-
apex: { name: 'Apex Trader Funding', systemName: 'Apex', gateway: RITHMIC_ENDPOINTS.CHICAGO },
|
|
91
|
-
apex_rithmic: { name: 'Apex Trader Funding', systemName: 'Apex', gateway: RITHMIC_ENDPOINTS.CHICAGO },
|
|
92
|
-
topstep_r: { name: 'Topstep (Rithmic)', systemName: RITHMIC_SYSTEMS.TOPSTEP, gateway: RITHMIC_ENDPOINTS.CHICAGO },
|
|
93
|
-
bulenox_r: { name: 'Bulenox (Rithmic)', systemName: RITHMIC_SYSTEMS.BULENOX, gateway: RITHMIC_ENDPOINTS.CHICAGO },
|
|
94
|
-
earn2trade: { name: 'Earn2Trade', systemName: RITHMIC_SYSTEMS.EARN_2_TRADE, gateway: RITHMIC_ENDPOINTS.CHICAGO },
|
|
95
|
-
mescapital: { name: 'MES Capital', systemName: RITHMIC_SYSTEMS.MES_CAPITAL, gateway: RITHMIC_ENDPOINTS.CHICAGO },
|
|
96
|
-
tradefundrr: { name: 'TradeFundrr', systemName: RITHMIC_SYSTEMS.TRADEFUNDRR, gateway: RITHMIC_ENDPOINTS.CHICAGO },
|
|
97
|
-
thetradingpit: { name: 'The Trading Pit', systemName: RITHMIC_SYSTEMS.THE_TRADING_PIT, gateway: RITHMIC_ENDPOINTS.CHICAGO },
|
|
98
|
-
fundedfutures: { name: 'Funded Futures Network', systemName: RITHMIC_SYSTEMS.FUNDED_FUTURES_NETWORK, gateway: RITHMIC_ENDPOINTS.CHICAGO },
|
|
99
|
-
propshop: { name: 'PropShop Trader', systemName: RITHMIC_SYSTEMS.PROPSHOP_TRADER, gateway: RITHMIC_ENDPOINTS.CHICAGO },
|
|
100
|
-
'4proptrader': { name: '4PropTrader', systemName: RITHMIC_SYSTEMS.FOUR_PROP_TRADER, gateway: RITHMIC_ENDPOINTS.CHICAGO },
|
|
101
|
-
daytraders: { name: 'DayTraders.com', systemName: RITHMIC_SYSTEMS.DAY_TRADERS, gateway: RITHMIC_ENDPOINTS.CHICAGO },
|
|
102
|
-
'10xfutures': { name: '10X Futures', systemName: RITHMIC_SYSTEMS.TEN_X_FUTURES, gateway: RITHMIC_ENDPOINTS.CHICAGO },
|
|
103
|
-
lucidtrading: { name: 'Lucid Trading', systemName: RITHMIC_SYSTEMS.LUCID_TRADING, gateway: RITHMIC_ENDPOINTS.CHICAGO },
|
|
104
|
-
thrivetrading: { name: 'Thrive Trading', systemName: RITHMIC_SYSTEMS.THRIVE_TRADING, gateway: RITHMIC_ENDPOINTS.CHICAGO },
|
|
105
|
-
legendstrading: { name: 'Legends Trading', systemName: RITHMIC_SYSTEMS.LEGENDS_TRADING, gateway: RITHMIC_ENDPOINTS.CHICAGO },
|
|
106
|
-
};
|
|
22
|
+
const log = logger.scope('Rithmic');
|
|
107
23
|
|
|
108
24
|
/**
|
|
109
25
|
* Rithmic Service for prop firm trading
|
|
110
26
|
*/
|
|
111
27
|
class RithmicService extends EventEmitter {
|
|
112
|
-
/**
|
|
113
|
-
* @param {string} propfirmKey - PropFirm identifier
|
|
114
|
-
*/
|
|
115
28
|
constructor(propfirmKey) {
|
|
116
29
|
super();
|
|
117
30
|
this.propfirmKey = propfirmKey;
|
|
@@ -132,10 +45,10 @@ class RithmicService extends EventEmitter {
|
|
|
132
45
|
this.accountPnL = new Map();
|
|
133
46
|
this.positions = new Map();
|
|
134
47
|
this.orders = [];
|
|
135
|
-
this.completedTrades = [];
|
|
48
|
+
this.completedTrades = [];
|
|
136
49
|
this.user = null;
|
|
137
50
|
this.credentials = null;
|
|
138
|
-
this.tradeRoutes = new Map();
|
|
51
|
+
this.tradeRoutes = new Map();
|
|
139
52
|
|
|
140
53
|
// Cache
|
|
141
54
|
this._contractsCache = null;
|
|
@@ -144,12 +57,6 @@ class RithmicService extends EventEmitter {
|
|
|
144
57
|
|
|
145
58
|
// ==================== AUTH ====================
|
|
146
59
|
|
|
147
|
-
/**
|
|
148
|
-
* Login to Rithmic
|
|
149
|
-
* @param {string} username - Username
|
|
150
|
-
* @param {string} password - Password
|
|
151
|
-
* @returns {Promise<{success: boolean, user?: Object, accounts?: Array, error?: string}>}
|
|
152
|
-
*/
|
|
153
60
|
async login(username, password) {
|
|
154
61
|
try {
|
|
155
62
|
this.orderConn = new RithmicConnection();
|
|
@@ -176,7 +83,7 @@ class RithmicService extends EventEmitter {
|
|
|
176
83
|
this.loginInfo = data;
|
|
177
84
|
this.user = { userName: username, fcmId: data.fcmId, ibId: data.ibId };
|
|
178
85
|
|
|
179
|
-
// Fetch trade routes
|
|
86
|
+
// Fetch trade routes
|
|
180
87
|
try {
|
|
181
88
|
await this._fetchTradeRoutes();
|
|
182
89
|
log.debug('Fetched trade routes', { count: this.tradeRoutes.size });
|
|
@@ -192,7 +99,7 @@ class RithmicService extends EventEmitter {
|
|
|
192
99
|
log.warn('Failed to fetch accounts', { error: err.message });
|
|
193
100
|
}
|
|
194
101
|
|
|
195
|
-
// Subscribe to order updates
|
|
102
|
+
// Subscribe to order updates
|
|
196
103
|
try {
|
|
197
104
|
for (const acc of this.accounts) {
|
|
198
105
|
this.orderConn.send('RequestSubscribeForOrderUpdates', {
|
|
@@ -202,85 +109,13 @@ class RithmicService extends EventEmitter {
|
|
|
202
109
|
ibId: acc.ibId || data.ibId,
|
|
203
110
|
accountId: acc.accountId,
|
|
204
111
|
});
|
|
205
|
-
log.debug('Subscribed to order updates for account', { accountId: acc.accountId });
|
|
206
112
|
}
|
|
207
113
|
} catch (err) {
|
|
208
114
|
log.warn('Failed to subscribe to order updates', { error: err.message });
|
|
209
115
|
}
|
|
210
116
|
|
|
211
|
-
//
|
|
212
|
-
this
|
|
213
|
-
|
|
214
|
-
// Listen for filled orders to build trade history
|
|
215
|
-
this.on('orderFilled', (fillInfo) => {
|
|
216
|
-
const key = `${fillInfo.accountId}:${fillInfo.symbol}`;
|
|
217
|
-
const side = fillInfo.transactionType === 1 ? 0 : 1; // 0=BUY, 1=SELL (entry side)
|
|
218
|
-
const qty = fillInfo.fillQuantity || fillInfo.totalFillQuantity || 0;
|
|
219
|
-
const price = fillInfo.avgFillPrice || fillInfo.lastFillPrice || 0;
|
|
220
|
-
|
|
221
|
-
// Check if this is closing an existing position
|
|
222
|
-
const openEntry = this._openEntries.get(key);
|
|
223
|
-
let pnl = null;
|
|
224
|
-
let isClosingTrade = false;
|
|
225
|
-
|
|
226
|
-
if (openEntry && openEntry.side !== side) {
|
|
227
|
-
// This is a closing trade (opposite side of open position)
|
|
228
|
-
isClosingTrade = true;
|
|
229
|
-
const closeQty = Math.min(qty, openEntry.qty);
|
|
230
|
-
|
|
231
|
-
// Calculate P&L based on direction
|
|
232
|
-
// If we bought (side=0), we sell to close - profit if close price > entry price
|
|
233
|
-
// If we sold (side=1), we buy to close - profit if entry price > close price
|
|
234
|
-
if (openEntry.side === 0) {
|
|
235
|
-
// Was long, selling to close
|
|
236
|
-
pnl = (price - openEntry.price) * closeQty;
|
|
237
|
-
} else {
|
|
238
|
-
// Was short, buying to close
|
|
239
|
-
pnl = (openEntry.price - price) * closeQty;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// Update or remove open entry
|
|
243
|
-
const remainingQty = openEntry.qty - closeQty;
|
|
244
|
-
if (remainingQty <= 0) {
|
|
245
|
-
this._openEntries.delete(key);
|
|
246
|
-
} else {
|
|
247
|
-
openEntry.qty = remainingQty;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// Store the closing trade with P&L
|
|
251
|
-
this.completedTrades.push({
|
|
252
|
-
id: fillInfo.orderId || fillInfo.orderTag,
|
|
253
|
-
orderTag: fillInfo.orderTag,
|
|
254
|
-
accountId: fillInfo.accountId,
|
|
255
|
-
symbol: fillInfo.symbol,
|
|
256
|
-
exchange: fillInfo.exchange,
|
|
257
|
-
side: openEntry.side, // Original entry side (for stats: LONG or SHORT)
|
|
258
|
-
size: closeQty,
|
|
259
|
-
entryPrice: openEntry.price,
|
|
260
|
-
exitPrice: price,
|
|
261
|
-
price: price,
|
|
262
|
-
timestamp: fillInfo.localTimestamp || Date.now(),
|
|
263
|
-
creationTimestamp: new Date().toISOString(),
|
|
264
|
-
status: 'CLOSED',
|
|
265
|
-
profitAndLoss: pnl,
|
|
266
|
-
pnl: pnl,
|
|
267
|
-
fees: 0,
|
|
268
|
-
});
|
|
269
|
-
log.debug('Trade closed', { symbol: fillInfo.symbol, pnl, trades: this.completedTrades.length });
|
|
270
|
-
} else {
|
|
271
|
-
// This is opening a new position or adding to existing
|
|
272
|
-
if (openEntry && openEntry.side === side) {
|
|
273
|
-
// Adding to position - update average price
|
|
274
|
-
const totalQty = openEntry.qty + qty;
|
|
275
|
-
openEntry.price = ((openEntry.price * openEntry.qty) + (price * qty)) / totalQty;
|
|
276
|
-
openEntry.qty = totalQty;
|
|
277
|
-
} else {
|
|
278
|
-
// New position
|
|
279
|
-
this._openEntries.set(key, { side, qty, price, timestamp: Date.now() });
|
|
280
|
-
}
|
|
281
|
-
log.debug('Position opened/added', { symbol: fillInfo.symbol, side, qty, price });
|
|
282
|
-
}
|
|
283
|
-
});
|
|
117
|
+
// Setup order fill listener for P&L tracking
|
|
118
|
+
setupOrderFillListener(this);
|
|
284
119
|
|
|
285
120
|
// Store credentials for reconnection
|
|
286
121
|
this.credentials = { username, password };
|
|
@@ -316,12 +151,6 @@ class RithmicService extends EventEmitter {
|
|
|
316
151
|
}
|
|
317
152
|
}
|
|
318
153
|
|
|
319
|
-
/**
|
|
320
|
-
* Connect to PNL_PLANT for balance data
|
|
321
|
-
* @param {string} username - Username
|
|
322
|
-
* @param {string} password - Password
|
|
323
|
-
* @returns {Promise<boolean>}
|
|
324
|
-
*/
|
|
325
154
|
async connectPnL(username, password) {
|
|
326
155
|
try {
|
|
327
156
|
this.pnlConn = new RithmicConnection();
|
|
@@ -360,12 +189,6 @@ class RithmicService extends EventEmitter {
|
|
|
360
189
|
}
|
|
361
190
|
}
|
|
362
191
|
|
|
363
|
-
/**
|
|
364
|
-
* Connect to TICKER_PLANT for symbol lookup
|
|
365
|
-
* @param {string} username - Username
|
|
366
|
-
* @param {string} password - Password
|
|
367
|
-
* @returns {Promise<boolean>}
|
|
368
|
-
*/
|
|
369
192
|
async connectTicker(username, password) {
|
|
370
193
|
try {
|
|
371
194
|
this.tickerConn = new RithmicConnection();
|
|
@@ -408,31 +231,17 @@ class RithmicService extends EventEmitter {
|
|
|
408
231
|
|
|
409
232
|
// ==================== TRADE ROUTES ====================
|
|
410
233
|
|
|
411
|
-
/**
|
|
412
|
-
* Fetch trade routes from Rithmic API
|
|
413
|
-
* Trade routes are required for order submission
|
|
414
|
-
* @private
|
|
415
|
-
* @returns {Promise<void>}
|
|
416
|
-
*/
|
|
417
234
|
async _fetchTradeRoutes() {
|
|
418
|
-
if (!this.orderConn || !this.loginInfo)
|
|
419
|
-
return;
|
|
420
|
-
}
|
|
235
|
+
if (!this.orderConn || !this.loginInfo) return;
|
|
421
236
|
|
|
422
237
|
return new Promise((resolve) => {
|
|
423
|
-
const timeout = setTimeout(() =>
|
|
424
|
-
log.debug('Trade routes fetch timeout');
|
|
425
|
-
resolve();
|
|
426
|
-
}, 5000);
|
|
238
|
+
const timeout = setTimeout(() => resolve(), 5000);
|
|
427
239
|
|
|
428
240
|
const onTradeRoute = (res) => {
|
|
429
241
|
if (res.tradeRoute && res.exchange) {
|
|
430
|
-
// Store trade route by exchange (from API)
|
|
431
242
|
this.tradeRoutes.set(res.exchange, res.tradeRoute);
|
|
432
|
-
log.debug('Trade route received', { exchange: res.exchange, route: res.tradeRoute });
|
|
433
243
|
}
|
|
434
244
|
if (res.rpCode?.[0] === '0' && !res.tradeRoute) {
|
|
435
|
-
// End of trade routes
|
|
436
245
|
clearTimeout(timeout);
|
|
437
246
|
this.removeListener('tradeRoutes', onTradeRoute);
|
|
438
247
|
resolve();
|
|
@@ -445,7 +254,7 @@ class RithmicService extends EventEmitter {
|
|
|
445
254
|
this.orderConn.send('RequestTradeRoutes', {
|
|
446
255
|
templateId: REQ.TRADE_ROUTES,
|
|
447
256
|
userMsg: ['HQX'],
|
|
448
|
-
subscribeForUpdates: true,
|
|
257
|
+
subscribeForUpdates: true,
|
|
449
258
|
});
|
|
450
259
|
} catch (e) {
|
|
451
260
|
clearTimeout(timeout);
|
|
@@ -455,11 +264,6 @@ class RithmicService extends EventEmitter {
|
|
|
455
264
|
});
|
|
456
265
|
}
|
|
457
266
|
|
|
458
|
-
/**
|
|
459
|
-
* Get trade route for an exchange
|
|
460
|
-
* @param {string} exchange - Exchange name (e.g., 'CME', 'NYMEX')
|
|
461
|
-
* @returns {string|null} Trade route from API, or null if not available
|
|
462
|
-
*/
|
|
463
267
|
getTradeRoute(exchange) {
|
|
464
268
|
return this.tradeRoutes.get(exchange) || null;
|
|
465
269
|
}
|
|
@@ -477,67 +281,22 @@ class RithmicService extends EventEmitter {
|
|
|
477
281
|
async flattenAll(accountId) { return flattenAll(this, accountId); }
|
|
478
282
|
async emergencyStop(accountId) { return emergencyStop(this, accountId); }
|
|
479
283
|
|
|
480
|
-
// ==================== FAST SCALPING
|
|
284
|
+
// ==================== FAST SCALPING ====================
|
|
481
285
|
|
|
482
|
-
/**
|
|
483
|
-
* Ultra-fast market order entry - fire-and-forget
|
|
484
|
-
* Target latency: < 5ms local processing (network latency separate)
|
|
485
|
-
* @param {Object} orderData - { accountId, symbol, exchange, size, side }
|
|
486
|
-
* @returns {{ success: boolean, orderTag: string, entryTime: number, latencyMs: number }}
|
|
487
|
-
*/
|
|
488
286
|
fastEntry(orderData) { return fastEntry(this, orderData); }
|
|
489
|
-
|
|
490
|
-
/**
|
|
491
|
-
* Ultra-fast market exit - fire-and-forget
|
|
492
|
-
* @param {Object} orderData - { accountId, symbol, exchange, size, side }
|
|
493
|
-
* @returns {{ success: boolean, orderTag: string, exitTime: number, latencyMs: number }}
|
|
494
|
-
*/
|
|
495
287
|
fastExit(orderData) { return fastExit(this, orderData); }
|
|
496
288
|
|
|
497
|
-
/**
|
|
498
|
-
* Warmup connections for minimum latency
|
|
499
|
-
* Call after login but before trading starts
|
|
500
|
-
* @returns {Promise<boolean>}
|
|
501
|
-
*/
|
|
502
289
|
async warmup() {
|
|
503
290
|
const results = [];
|
|
504
|
-
|
|
505
|
-
if (this.
|
|
506
|
-
|
|
507
|
-
}
|
|
508
|
-
if (this.pnlConn) {
|
|
509
|
-
results.push(await this.pnlConn.warmup());
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
log.debug('Connection warmup complete', {
|
|
513
|
-
success: results.filter(Boolean).length,
|
|
514
|
-
total: results.length,
|
|
515
|
-
});
|
|
516
|
-
|
|
291
|
+
if (this.orderConn) results.push(await this.orderConn.warmup());
|
|
292
|
+
if (this.pnlConn) results.push(await this.pnlConn.warmup());
|
|
293
|
+
log.debug('Connection warmup complete', { success: results.filter(Boolean).length, total: results.length });
|
|
517
294
|
return results.every(Boolean);
|
|
518
295
|
}
|
|
519
296
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
* @returns {Object} Latency stats: min, max, avg, p50, p99, samples
|
|
523
|
-
*/
|
|
524
|
-
getLatencyStats() {
|
|
525
|
-
return LatencyTracker.getStats();
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
/**
|
|
529
|
-
* Get recent latency samples
|
|
530
|
-
* @param {number} n - Number of samples to return
|
|
531
|
-
* @returns {number[]}
|
|
532
|
-
*/
|
|
533
|
-
getRecentLatencies(n = 10) {
|
|
534
|
-
return LatencyTracker.getRecent(n);
|
|
535
|
-
}
|
|
297
|
+
getLatencyStats() { return LatencyTracker.getStats(); }
|
|
298
|
+
getRecentLatencies(n = 10) { return LatencyTracker.getRecent(n); }
|
|
536
299
|
|
|
537
|
-
/**
|
|
538
|
-
* Get connection diagnostics
|
|
539
|
-
* @returns {Object}
|
|
540
|
-
*/
|
|
541
300
|
getDiagnostics() {
|
|
542
301
|
return {
|
|
543
302
|
orderConn: this.orderConn?.getDiagnostics() || null,
|
|
@@ -554,210 +313,14 @@ class RithmicService extends EventEmitter {
|
|
|
554
313
|
async getUser() { return this.user; }
|
|
555
314
|
async getLifetimeStats() { return { success: true, stats: null }; }
|
|
556
315
|
async getDailyStats() { return { success: true, stats: [] }; }
|
|
557
|
-
|
|
558
|
-
/**
|
|
559
|
-
* Get trade history from Rithmic API
|
|
560
|
-
* Uses RequestShowOrderHistory to fetch historical fills
|
|
561
|
-
* @param {string} accountId - Optional account filter
|
|
562
|
-
* @param {number} days - Number of days to fetch (default: 30)
|
|
563
|
-
* @returns {Promise<{success: boolean, trades: Array}>}
|
|
564
|
-
*/
|
|
565
|
-
async getTradeHistory(accountId, days = 30) {
|
|
566
|
-
if (!this.orderConn || !this.loginInfo) {
|
|
567
|
-
return { success: true, trades: this.completedTrades || [] };
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
return new Promise((resolve) => {
|
|
571
|
-
const trades = [];
|
|
572
|
-
const historyOrders = [];
|
|
573
|
-
let resolved = false;
|
|
574
|
-
|
|
575
|
-
// Timeout after 5 seconds
|
|
576
|
-
const timeout = setTimeout(() => {
|
|
577
|
-
if (!resolved) {
|
|
578
|
-
resolved = true;
|
|
579
|
-
cleanup();
|
|
580
|
-
// Combine API history with session trades
|
|
581
|
-
const allTrades = [...this._processHistoryToTrades(historyOrders), ...this.completedTrades];
|
|
582
|
-
resolve({ success: true, trades: allTrades });
|
|
583
|
-
}
|
|
584
|
-
}, 5000);
|
|
585
|
-
|
|
586
|
-
// Listen for order history snapshots (RithmicOrderNotification with is_snapshot=true)
|
|
587
|
-
const onOrderNotification = (data) => {
|
|
588
|
-
try {
|
|
589
|
-
// Check if this is a historical order (snapshot)
|
|
590
|
-
if (data.isSnapshot || data.status === 'complete' || data.status === 'Complete') {
|
|
591
|
-
const order = {
|
|
592
|
-
orderId: data.orderId || data.orderTag,
|
|
593
|
-
accountId: data.accountId,
|
|
594
|
-
symbol: data.symbol,
|
|
595
|
-
exchange: data.exchange,
|
|
596
|
-
side: data.transactionType === 1 ? 0 : 1, // 1=BUY->0, 2=SELL->1
|
|
597
|
-
quantity: data.quantity || data.totalFillQuantity || 0,
|
|
598
|
-
fillPrice: data.avgFillPrice || data.lastFillPrice || 0,
|
|
599
|
-
timestamp: data.ssboe ? data.ssboe * 1000 : Date.now(),
|
|
600
|
-
status: data.status,
|
|
601
|
-
};
|
|
602
|
-
|
|
603
|
-
if (order.quantity > 0 && order.fillPrice > 0) {
|
|
604
|
-
historyOrders.push(order);
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
} catch (e) {
|
|
608
|
-
// Ignore parse errors
|
|
609
|
-
}
|
|
610
|
-
};
|
|
611
|
-
|
|
612
|
-
// Listen for history complete response
|
|
613
|
-
const onHistoryComplete = () => {
|
|
614
|
-
if (!resolved) {
|
|
615
|
-
resolved = true;
|
|
616
|
-
cleanup();
|
|
617
|
-
const allTrades = [...this._processHistoryToTrades(historyOrders), ...this.completedTrades];
|
|
618
|
-
resolve({ success: true, trades: allTrades });
|
|
619
|
-
}
|
|
620
|
-
};
|
|
621
|
-
|
|
622
|
-
const cleanup = () => {
|
|
623
|
-
clearTimeout(timeout);
|
|
624
|
-
this.removeListener('orderNotification', onOrderNotification);
|
|
625
|
-
this.removeListener('orderHistoryComplete', onHistoryComplete);
|
|
626
|
-
};
|
|
627
|
-
|
|
628
|
-
this.on('orderNotification', onOrderNotification);
|
|
629
|
-
this.on('orderHistoryComplete', onHistoryComplete);
|
|
630
|
-
|
|
631
|
-
// Request order history for each account
|
|
632
|
-
try {
|
|
633
|
-
const accounts = accountId
|
|
634
|
-
? this.accounts.filter(a => a.accountId === accountId)
|
|
635
|
-
: this.accounts;
|
|
636
|
-
|
|
637
|
-
for (const acc of accounts) {
|
|
638
|
-
this.orderConn.send('RequestShowOrderHistory', {
|
|
639
|
-
templateId: 324, // REQ.SHOW_ORDER_HISTORY
|
|
640
|
-
userMsg: ['HQX-HISTORY'],
|
|
641
|
-
fcmId: acc.fcmId || this.loginInfo.fcmId,
|
|
642
|
-
ibId: acc.ibId || this.loginInfo.ibId,
|
|
643
|
-
accountId: acc.accountId,
|
|
644
|
-
});
|
|
645
|
-
}
|
|
646
|
-
} catch (e) {
|
|
647
|
-
if (!resolved) {
|
|
648
|
-
resolved = true;
|
|
649
|
-
cleanup();
|
|
650
|
-
resolve({ success: false, error: e.message, trades: this.completedTrades || [] });
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
});
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
/**
|
|
657
|
-
* Process historical orders into trades with P&L
|
|
658
|
-
* Matches entries and exits to calculate P&L
|
|
659
|
-
* @private
|
|
660
|
-
*/
|
|
661
|
-
_processHistoryToTrades(orders) {
|
|
662
|
-
const trades = [];
|
|
663
|
-
const openPositions = new Map(); // key: accountId:symbol
|
|
664
|
-
|
|
665
|
-
// Sort by timestamp (oldest first)
|
|
666
|
-
const sorted = [...orders].sort((a, b) => a.timestamp - b.timestamp);
|
|
667
|
-
|
|
668
|
-
for (const order of sorted) {
|
|
669
|
-
const key = `${order.accountId}:${order.symbol}`;
|
|
670
|
-
const open = openPositions.get(key);
|
|
671
|
-
|
|
672
|
-
if (open && open.side !== order.side) {
|
|
673
|
-
// Closing trade - calculate P&L
|
|
674
|
-
const closeQty = Math.min(order.quantity, open.quantity);
|
|
675
|
-
let pnl;
|
|
676
|
-
|
|
677
|
-
if (open.side === 0) {
|
|
678
|
-
// Was long (bought), now selling
|
|
679
|
-
pnl = (order.fillPrice - open.price) * closeQty;
|
|
680
|
-
} else {
|
|
681
|
-
// Was short (sold), now buying
|
|
682
|
-
pnl = (open.price - order.fillPrice) * closeQty;
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
// Estimate tick value (futures typically $12.50-$50 per tick)
|
|
686
|
-
// For indices like ES/NQ, multiply by contract multiplier
|
|
687
|
-
const tickMultiplier = this._getTickMultiplier(order.symbol);
|
|
688
|
-
pnl = pnl * tickMultiplier;
|
|
689
|
-
|
|
690
|
-
trades.push({
|
|
691
|
-
id: order.orderId,
|
|
692
|
-
accountId: order.accountId,
|
|
693
|
-
symbol: order.symbol,
|
|
694
|
-
exchange: order.exchange,
|
|
695
|
-
side: open.side,
|
|
696
|
-
size: closeQty,
|
|
697
|
-
entryPrice: open.price,
|
|
698
|
-
exitPrice: order.fillPrice,
|
|
699
|
-
price: order.fillPrice,
|
|
700
|
-
timestamp: order.timestamp,
|
|
701
|
-
creationTimestamp: new Date(order.timestamp).toISOString(),
|
|
702
|
-
status: 'CLOSED',
|
|
703
|
-
profitAndLoss: pnl,
|
|
704
|
-
pnl: pnl,
|
|
705
|
-
fees: 0,
|
|
706
|
-
});
|
|
707
|
-
|
|
708
|
-
// Update or remove open position
|
|
709
|
-
const remaining = open.quantity - closeQty;
|
|
710
|
-
if (remaining <= 0) {
|
|
711
|
-
openPositions.delete(key);
|
|
712
|
-
} else {
|
|
713
|
-
open.quantity = remaining;
|
|
714
|
-
}
|
|
715
|
-
} else if (open && open.side === order.side) {
|
|
716
|
-
// Adding to position
|
|
717
|
-
const totalQty = open.quantity + order.quantity;
|
|
718
|
-
open.price = ((open.price * open.quantity) + (order.fillPrice * order.quantity)) / totalQty;
|
|
719
|
-
open.quantity = totalQty;
|
|
720
|
-
} else {
|
|
721
|
-
// Opening new position
|
|
722
|
-
openPositions.set(key, {
|
|
723
|
-
side: order.side,
|
|
724
|
-
quantity: order.quantity,
|
|
725
|
-
price: order.fillPrice,
|
|
726
|
-
timestamp: order.timestamp,
|
|
727
|
-
});
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
return trades;
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
/**
|
|
735
|
-
* Get tick multiplier for P&L calculation
|
|
736
|
-
* @private
|
|
737
|
-
*/
|
|
738
|
-
_getTickMultiplier(symbol) {
|
|
739
|
-
const sym = (symbol || '').toUpperCase();
|
|
740
|
-
if (sym.startsWith('ES')) return 50; // E-mini S&P 500: $50 per point
|
|
741
|
-
if (sym.startsWith('NQ')) return 20; // E-mini Nasdaq: $20 per point
|
|
742
|
-
if (sym.startsWith('YM')) return 5; // E-mini Dow: $5 per point
|
|
743
|
-
if (sym.startsWith('RTY')) return 50; // E-mini Russell: $50 per point
|
|
744
|
-
if (sym.startsWith('MES')) return 5; // Micro E-mini S&P: $5 per point
|
|
745
|
-
if (sym.startsWith('MNQ')) return 2; // Micro E-mini Nasdaq: $2 per point
|
|
746
|
-
if (sym.startsWith('GC')) return 100; // Gold: $100 per point
|
|
747
|
-
if (sym.startsWith('SI')) return 5000; // Silver: $5000 per point
|
|
748
|
-
if (sym.startsWith('CL')) return 1000; // Crude Oil: $1000 per point
|
|
749
|
-
if (sym.startsWith('NG')) return 10000; // Natural Gas: $10000 per point
|
|
750
|
-
if (sym.startsWith('ZB') || sym.startsWith('ZN')) return 1000; // Bonds
|
|
751
|
-
if (sym.startsWith('6E')) return 125000; // Euro FX
|
|
752
|
-
if (sym.startsWith('6J')) return 12500000; // Japanese Yen
|
|
753
|
-
return 1; // Default
|
|
754
|
-
}
|
|
316
|
+
async getTradeHistory(accountId, days = 30) { return getTradeHistory(this, accountId, days); }
|
|
755
317
|
|
|
756
318
|
async getMarketStatus() {
|
|
757
|
-
const status =
|
|
319
|
+
const status = checkMarketHours();
|
|
758
320
|
return { success: true, isOpen: status.isOpen, message: status.message };
|
|
759
321
|
}
|
|
760
322
|
|
|
323
|
+
checkMarketHours() { return checkMarketHours(); }
|
|
761
324
|
getToken() { return this.loginInfo ? 'connected' : null; }
|
|
762
325
|
getPropfirm() { return this.propfirmKey || 'apex'; }
|
|
763
326
|
|
|
@@ -773,39 +336,25 @@ class RithmicService extends EventEmitter {
|
|
|
773
336
|
|
|
774
337
|
// ==================== CONTRACTS ====================
|
|
775
338
|
|
|
776
|
-
/**
|
|
777
|
-
* Get all available contracts from Rithmic API
|
|
778
|
-
* @returns {Promise<{success: boolean, contracts: Array, source?: string, error?: string}>}
|
|
779
|
-
*/
|
|
780
339
|
async getContracts() {
|
|
781
|
-
// Check cache
|
|
782
340
|
if (this._contractsCache && Date.now() - this._contractsCacheTime < CACHE.CONTRACTS_TTL) {
|
|
783
341
|
return { success: true, contracts: this._contractsCache, source: 'cache' };
|
|
784
342
|
}
|
|
785
343
|
|
|
786
|
-
if (!this.credentials) {
|
|
787
|
-
return { success: false, error: 'Not logged in' };
|
|
788
|
-
}
|
|
344
|
+
if (!this.credentials) return { success: false, error: 'Not logged in' };
|
|
789
345
|
|
|
790
346
|
try {
|
|
791
|
-
// Connect to TICKER_PLANT if needed
|
|
792
347
|
if (!this.tickerConn) {
|
|
793
348
|
const connected = await this.connectTicker(this.credentials.username, this.credentials.password);
|
|
794
|
-
if (!connected) {
|
|
795
|
-
return { success: false, error: 'Failed to connect to TICKER_PLANT' };
|
|
796
|
-
}
|
|
349
|
+
if (!connected) return { success: false, error: 'Failed to connect to TICKER_PLANT' };
|
|
797
350
|
}
|
|
798
351
|
|
|
799
352
|
this.tickerConn.setMaxListeners(5000);
|
|
800
|
-
|
|
801
353
|
log.debug('Fetching contracts from Rithmic API');
|
|
802
354
|
const contracts = await this._fetchAllFrontMonths();
|
|
803
355
|
|
|
804
|
-
if (!contracts.length) {
|
|
805
|
-
return { success: false, error: 'No tradeable contracts found' };
|
|
806
|
-
}
|
|
356
|
+
if (!contracts.length) return { success: false, error: 'No tradeable contracts found' };
|
|
807
357
|
|
|
808
|
-
// Cache results
|
|
809
358
|
this._contractsCache = contracts;
|
|
810
359
|
this._contractsCacheTime = Date.now();
|
|
811
360
|
|
|
@@ -816,11 +365,6 @@ class RithmicService extends EventEmitter {
|
|
|
816
365
|
}
|
|
817
366
|
}
|
|
818
367
|
|
|
819
|
-
/**
|
|
820
|
-
* Search contracts
|
|
821
|
-
* @param {string} searchText - Search text
|
|
822
|
-
* @returns {Promise<Array>}
|
|
823
|
-
*/
|
|
824
368
|
async searchContracts(searchText) {
|
|
825
369
|
const result = await this.getContracts();
|
|
826
370
|
if (!searchText || !result.success) return result.contracts || [];
|
|
@@ -832,20 +376,13 @@ class RithmicService extends EventEmitter {
|
|
|
832
376
|
);
|
|
833
377
|
}
|
|
834
378
|
|
|
835
|
-
/**
|
|
836
|
-
* Fetch all front month contracts from API
|
|
837
|
-
* @private
|
|
838
|
-
*/
|
|
839
379
|
async _fetchAllFrontMonths() {
|
|
840
|
-
if (!this.tickerConn)
|
|
841
|
-
throw new Error('TICKER_PLANT not connected');
|
|
842
|
-
}
|
|
380
|
+
if (!this.tickerConn) throw new Error('TICKER_PLANT not connected');
|
|
843
381
|
|
|
844
382
|
return new Promise((resolve) => {
|
|
845
383
|
const contracts = new Map();
|
|
846
384
|
const productsToCheck = new Map();
|
|
847
385
|
|
|
848
|
-
// Handler for ProductCodes responses
|
|
849
386
|
const productHandler = (msg) => {
|
|
850
387
|
if (msg.templateId !== 112) return;
|
|
851
388
|
|
|
@@ -868,7 +405,6 @@ class RithmicService extends EventEmitter {
|
|
|
868
405
|
}
|
|
869
406
|
};
|
|
870
407
|
|
|
871
|
-
// Handler for FrontMonth responses
|
|
872
408
|
const frontMonthHandler = (msg) => {
|
|
873
409
|
if (msg.templateId !== 114) return;
|
|
874
410
|
|
|
@@ -885,13 +421,11 @@ class RithmicService extends EventEmitter {
|
|
|
885
421
|
this.tickerConn.on('message', productHandler);
|
|
886
422
|
this.tickerConn.on('message', frontMonthHandler);
|
|
887
423
|
|
|
888
|
-
// Request all product codes
|
|
889
424
|
this.tickerConn.send('RequestProductCodes', {
|
|
890
425
|
templateId: 111,
|
|
891
426
|
userMsg: ['get-products'],
|
|
892
427
|
});
|
|
893
428
|
|
|
894
|
-
// After timeout, request front months
|
|
895
429
|
setTimeout(() => {
|
|
896
430
|
this.tickerConn.removeListener('message', productHandler);
|
|
897
431
|
log.debug('Collected products', { count: productsToCheck.size });
|
|
@@ -905,7 +439,6 @@ class RithmicService extends EventEmitter {
|
|
|
905
439
|
});
|
|
906
440
|
}
|
|
907
441
|
|
|
908
|
-
// Collect results after timeout
|
|
909
442
|
setTimeout(() => {
|
|
910
443
|
this.tickerConn.removeListener('message', frontMonthHandler);
|
|
911
444
|
|
|
@@ -913,27 +446,21 @@ class RithmicService extends EventEmitter {
|
|
|
913
446
|
for (const [baseSymbol, contract] of contracts) {
|
|
914
447
|
const productKey = `${baseSymbol}:${contract.exchange}`;
|
|
915
448
|
const product = productsToCheck.get(productKey);
|
|
916
|
-
|
|
917
|
-
// API data + CME contract specs (technical constants)
|
|
918
449
|
const specs = CME_CONTRACT_SPECS[baseSymbol] || null;
|
|
919
|
-
// Use CME spec name first, then API productName, then baseSymbol as fallback
|
|
920
450
|
const productName = specs?.name || product?.productName || baseSymbol;
|
|
921
451
|
|
|
922
452
|
results.push({
|
|
923
453
|
symbol: contract.symbol,
|
|
924
454
|
baseSymbol,
|
|
925
|
-
name: contract.symbol,
|
|
926
|
-
description: productName,
|
|
455
|
+
name: contract.symbol,
|
|
456
|
+
description: productName,
|
|
927
457
|
exchange: contract.exchange,
|
|
928
|
-
// CME contract specifications (official exchange constants)
|
|
929
458
|
tickSize: specs?.tickSize ?? null,
|
|
930
459
|
tickValue: specs?.tickValue ?? null,
|
|
931
460
|
});
|
|
932
461
|
}
|
|
933
462
|
|
|
934
|
-
// Sort alphabetically by base symbol
|
|
935
463
|
results.sort((a, b) => a.baseSymbol.localeCompare(b.baseSymbol));
|
|
936
|
-
|
|
937
464
|
log.debug('Got contracts from API', { count: results.length });
|
|
938
465
|
resolve(results);
|
|
939
466
|
}, TIMEOUTS.RITHMIC_PRODUCTS);
|
|
@@ -941,17 +468,12 @@ class RithmicService extends EventEmitter {
|
|
|
941
468
|
});
|
|
942
469
|
}
|
|
943
470
|
|
|
944
|
-
/**
|
|
945
|
-
* Decode ProductCodes response
|
|
946
|
-
* @private
|
|
947
|
-
*/
|
|
948
471
|
_decodeProductCodes(buffer) {
|
|
949
472
|
const result = {};
|
|
950
473
|
let offset = 0;
|
|
951
474
|
|
|
952
475
|
const readVarint = (buf, off) => {
|
|
953
|
-
let value = 0;
|
|
954
|
-
let shift = 0;
|
|
476
|
+
let value = 0, shift = 0;
|
|
955
477
|
while (off < buf.length) {
|
|
956
478
|
const byte = buf[off++];
|
|
957
479
|
value |= (byte & 0x7F) << shift;
|
|
@@ -985,52 +507,20 @@ class RithmicService extends EventEmitter {
|
|
|
985
507
|
} else {
|
|
986
508
|
break;
|
|
987
509
|
}
|
|
988
|
-
} catch {
|
|
989
|
-
break;
|
|
990
|
-
}
|
|
510
|
+
} catch { break; }
|
|
991
511
|
}
|
|
992
512
|
|
|
993
513
|
return result;
|
|
994
514
|
}
|
|
995
515
|
|
|
996
|
-
// ==================== MARKET HOURS ====================
|
|
997
|
-
|
|
998
|
-
checkMarketHours() {
|
|
999
|
-
const now = new Date();
|
|
1000
|
-
const utcDay = now.getUTCDay();
|
|
1001
|
-
const utcHour = now.getUTCHours();
|
|
1002
|
-
|
|
1003
|
-
const isDST = now.getTimezoneOffset() < Math.max(
|
|
1004
|
-
new Date(now.getFullYear(), 0, 1).getTimezoneOffset(),
|
|
1005
|
-
new Date(now.getFullYear(), 6, 1).getTimezoneOffset()
|
|
1006
|
-
);
|
|
1007
|
-
const ctOffset = isDST ? 5 : 6;
|
|
1008
|
-
const ctHour = (utcHour - ctOffset + 24) % 24;
|
|
1009
|
-
const ctDay = utcHour < ctOffset ? (utcDay + 6) % 7 : utcDay;
|
|
1010
|
-
|
|
1011
|
-
if (ctDay === 6) return { isOpen: false, message: 'Market closed (Saturday)' };
|
|
1012
|
-
if (ctDay === 0 && ctHour < 17) return { isOpen: false, message: 'Market opens Sunday 5:00 PM CT' };
|
|
1013
|
-
if (ctDay === 5 && ctHour >= 16) return { isOpen: false, message: 'Market closed (Friday after 4PM CT)' };
|
|
1014
|
-
if (ctHour === 16 && ctDay >= 1 && ctDay <= 4) return { isOpen: false, message: 'Daily maintenance (4:00-5:00 PM CT)' };
|
|
1015
|
-
|
|
1016
|
-
return { isOpen: true, message: 'Market is open' };
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
516
|
// ==================== CLEANUP ====================
|
|
1020
517
|
|
|
1021
|
-
/**
|
|
1022
|
-
* Disconnect all connections
|
|
1023
|
-
*/
|
|
1024
518
|
async disconnect() {
|
|
1025
519
|
const connections = [this.orderConn, this.pnlConn, this.tickerConn];
|
|
1026
520
|
|
|
1027
521
|
for (const conn of connections) {
|
|
1028
522
|
if (conn) {
|
|
1029
|
-
try {
|
|
1030
|
-
await conn.disconnect();
|
|
1031
|
-
} catch (err) {
|
|
1032
|
-
log.warn('Disconnect error', { error: err.message });
|
|
1033
|
-
}
|
|
523
|
+
try { await conn.disconnect(); } catch (err) { log.warn('Disconnect error', { error: err.message }); }
|
|
1034
524
|
}
|
|
1035
525
|
}
|
|
1036
526
|
|