hedgequantx 2.6.161 → 2.6.163
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/providers/direct-providers.js +323 -0
- package/src/services/ai/providers/index.js +8 -472
- package/src/services/ai/providers/other-providers.js +104 -0
- 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/supervisor-optimize.js +215 -0
- package/src/services/ai/supervisor-sync.js +178 -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-exit-logic.js +174 -0
- package/src/services/position-manager.js +90 -629
- 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/contracts.js +218 -0
- package/src/services/rithmic/handlers.js +2 -208
- package/src/services/rithmic/index.js +28 -712
- package/src/services/rithmic/latency-tracker.js +182 -0
- package/src/services/rithmic/market-data-decoders.js +229 -0
- package/src/services/rithmic/market-data.js +1 -278
- package/src/services/rithmic/orders-fast.js +246 -0
- package/src/services/rithmic/orders.js +1 -251
- package/src/services/rithmic/proto-decoders.js +403 -0
- package/src/services/rithmic/protobuf.js +7 -443
- package/src/services/rithmic/specs.js +146 -0
- package/src/services/rithmic/trade-history.js +254 -0
- package/src/services/strategy/hft-signal-calc.js +147 -0
- package/src/services/strategy/hft-tick.js +33 -133
- package/src/services/tradovate/index.js +6 -119
- package/src/services/tradovate/orders.js +145 -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
|
-
const {
|
|
22
|
-
const { TIMEOUTS, CACHE } = require('../../config/settings');
|
|
14
|
+
const { TIMEOUTS } = require('../../config/settings');
|
|
23
15
|
const { logger } = require('../../utils/logger');
|
|
24
16
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
};
|
|
17
|
+
// Extracted modules
|
|
18
|
+
const { PROPFIRM_CONFIGS, checkMarketHours } = require('./specs');
|
|
19
|
+
const { getTradeHistory, setupOrderFillListener } = require('./trade-history');
|
|
20
|
+
const { getContracts, searchContracts } = require('./contracts');
|
|
87
21
|
|
|
88
|
-
|
|
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,264 +336,17 @@ class RithmicService extends EventEmitter {
|
|
|
773
336
|
|
|
774
337
|
// ==================== CONTRACTS ====================
|
|
775
338
|
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
* @returns {Promise<{success: boolean, contracts: Array, source?: string, error?: string}>}
|
|
779
|
-
*/
|
|
780
|
-
async getContracts() {
|
|
781
|
-
// Check cache
|
|
782
|
-
if (this._contractsCache && Date.now() - this._contractsCacheTime < CACHE.CONTRACTS_TTL) {
|
|
783
|
-
return { success: true, contracts: this._contractsCache, source: 'cache' };
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
if (!this.credentials) {
|
|
787
|
-
return { success: false, error: 'Not logged in' };
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
try {
|
|
791
|
-
// Connect to TICKER_PLANT if needed
|
|
792
|
-
if (!this.tickerConn) {
|
|
793
|
-
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
|
-
}
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
this.tickerConn.setMaxListeners(5000);
|
|
800
|
-
|
|
801
|
-
log.debug('Fetching contracts from Rithmic API');
|
|
802
|
-
const contracts = await this._fetchAllFrontMonths();
|
|
803
|
-
|
|
804
|
-
if (!contracts.length) {
|
|
805
|
-
return { success: false, error: 'No tradeable contracts found' };
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
// Cache results
|
|
809
|
-
this._contractsCache = contracts;
|
|
810
|
-
this._contractsCacheTime = Date.now();
|
|
811
|
-
|
|
812
|
-
return { success: true, contracts, source: 'api' };
|
|
813
|
-
} catch (err) {
|
|
814
|
-
log.error('getContracts error', { error: err.message });
|
|
815
|
-
return { success: false, error: err.message };
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
/**
|
|
820
|
-
* Search contracts
|
|
821
|
-
* @param {string} searchText - Search text
|
|
822
|
-
* @returns {Promise<Array>}
|
|
823
|
-
*/
|
|
824
|
-
async searchContracts(searchText) {
|
|
825
|
-
const result = await this.getContracts();
|
|
826
|
-
if (!searchText || !result.success) return result.contracts || [];
|
|
827
|
-
|
|
828
|
-
const search = searchText.toUpperCase();
|
|
829
|
-
return result.contracts.filter(c =>
|
|
830
|
-
c.symbol.toUpperCase().includes(search) ||
|
|
831
|
-
c.name.toUpperCase().includes(search)
|
|
832
|
-
);
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
/**
|
|
836
|
-
* Fetch all front month contracts from API
|
|
837
|
-
* @private
|
|
838
|
-
*/
|
|
839
|
-
async _fetchAllFrontMonths() {
|
|
840
|
-
if (!this.tickerConn) {
|
|
841
|
-
throw new Error('TICKER_PLANT not connected');
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
return new Promise((resolve) => {
|
|
845
|
-
const contracts = new Map();
|
|
846
|
-
const productsToCheck = new Map();
|
|
847
|
-
|
|
848
|
-
// Handler for ProductCodes responses
|
|
849
|
-
const productHandler = (msg) => {
|
|
850
|
-
if (msg.templateId !== 112) return;
|
|
851
|
-
|
|
852
|
-
const decoded = this._decodeProductCodes(msg.data);
|
|
853
|
-
if (!decoded.productCode || !decoded.exchange) return;
|
|
854
|
-
|
|
855
|
-
const validExchanges = ['CME', 'CBOT', 'NYMEX', 'COMEX', 'NYBOT', 'CFE'];
|
|
856
|
-
if (!validExchanges.includes(decoded.exchange)) return;
|
|
857
|
-
|
|
858
|
-
const name = (decoded.productName || '').toLowerCase();
|
|
859
|
-
if (name.includes('option') || name.includes('swap') || name.includes('spread')) return;
|
|
860
|
-
|
|
861
|
-
const key = `${decoded.productCode}:${decoded.exchange}`;
|
|
862
|
-
if (!productsToCheck.has(key)) {
|
|
863
|
-
productsToCheck.set(key, {
|
|
864
|
-
productCode: decoded.productCode,
|
|
865
|
-
productName: decoded.productName || decoded.productCode,
|
|
866
|
-
exchange: decoded.exchange,
|
|
867
|
-
});
|
|
868
|
-
}
|
|
869
|
-
};
|
|
870
|
-
|
|
871
|
-
// Handler for FrontMonth responses
|
|
872
|
-
const frontMonthHandler = (msg) => {
|
|
873
|
-
if (msg.templateId !== 114) return;
|
|
874
|
-
|
|
875
|
-
const decoded = decodeFrontMonthContract(msg.data);
|
|
876
|
-
if (decoded.rpCode[0] === '0' && decoded.tradingSymbol) {
|
|
877
|
-
contracts.set(decoded.userMsg, {
|
|
878
|
-
symbol: decoded.tradingSymbol,
|
|
879
|
-
baseSymbol: decoded.userMsg,
|
|
880
|
-
exchange: decoded.exchange,
|
|
881
|
-
});
|
|
882
|
-
}
|
|
883
|
-
};
|
|
884
|
-
|
|
885
|
-
this.tickerConn.on('message', productHandler);
|
|
886
|
-
this.tickerConn.on('message', frontMonthHandler);
|
|
887
|
-
|
|
888
|
-
// Request all product codes
|
|
889
|
-
this.tickerConn.send('RequestProductCodes', {
|
|
890
|
-
templateId: 111,
|
|
891
|
-
userMsg: ['get-products'],
|
|
892
|
-
});
|
|
893
|
-
|
|
894
|
-
// After timeout, request front months
|
|
895
|
-
setTimeout(() => {
|
|
896
|
-
this.tickerConn.removeListener('message', productHandler);
|
|
897
|
-
log.debug('Collected products', { count: productsToCheck.size });
|
|
898
|
-
|
|
899
|
-
for (const product of productsToCheck.values()) {
|
|
900
|
-
this.tickerConn.send('RequestFrontMonthContract', {
|
|
901
|
-
templateId: 113,
|
|
902
|
-
userMsg: [product.productCode],
|
|
903
|
-
symbol: product.productCode,
|
|
904
|
-
exchange: product.exchange,
|
|
905
|
-
});
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
// Collect results after timeout
|
|
909
|
-
setTimeout(() => {
|
|
910
|
-
this.tickerConn.removeListener('message', frontMonthHandler);
|
|
911
|
-
|
|
912
|
-
const results = [];
|
|
913
|
-
for (const [baseSymbol, contract] of contracts) {
|
|
914
|
-
const productKey = `${baseSymbol}:${contract.exchange}`;
|
|
915
|
-
const product = productsToCheck.get(productKey);
|
|
916
|
-
|
|
917
|
-
// API data + CME contract specs (technical constants)
|
|
918
|
-
const specs = CME_CONTRACT_SPECS[baseSymbol] || null;
|
|
919
|
-
// Use CME spec name first, then API productName, then baseSymbol as fallback
|
|
920
|
-
const productName = specs?.name || product?.productName || baseSymbol;
|
|
921
|
-
|
|
922
|
-
results.push({
|
|
923
|
-
symbol: contract.symbol,
|
|
924
|
-
baseSymbol,
|
|
925
|
-
name: contract.symbol, // Use trading symbol as name
|
|
926
|
-
description: productName, // Product name as description
|
|
927
|
-
exchange: contract.exchange,
|
|
928
|
-
// CME contract specifications (official exchange constants)
|
|
929
|
-
tickSize: specs?.tickSize ?? null,
|
|
930
|
-
tickValue: specs?.tickValue ?? null,
|
|
931
|
-
});
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
// Sort alphabetically by base symbol
|
|
935
|
-
results.sort((a, b) => a.baseSymbol.localeCompare(b.baseSymbol));
|
|
936
|
-
|
|
937
|
-
log.debug('Got contracts from API', { count: results.length });
|
|
938
|
-
resolve(results);
|
|
939
|
-
}, TIMEOUTS.RITHMIC_PRODUCTS);
|
|
940
|
-
}, TIMEOUTS.RITHMIC_CONTRACTS);
|
|
941
|
-
});
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
/**
|
|
945
|
-
* Decode ProductCodes response
|
|
946
|
-
* @private
|
|
947
|
-
*/
|
|
948
|
-
_decodeProductCodes(buffer) {
|
|
949
|
-
const result = {};
|
|
950
|
-
let offset = 0;
|
|
951
|
-
|
|
952
|
-
const readVarint = (buf, off) => {
|
|
953
|
-
let value = 0;
|
|
954
|
-
let shift = 0;
|
|
955
|
-
while (off < buf.length) {
|
|
956
|
-
const byte = buf[off++];
|
|
957
|
-
value |= (byte & 0x7F) << shift;
|
|
958
|
-
if (!(byte & 0x80)) break;
|
|
959
|
-
shift += 7;
|
|
960
|
-
}
|
|
961
|
-
return [value, off];
|
|
962
|
-
};
|
|
963
|
-
|
|
964
|
-
const readString = (buf, off) => {
|
|
965
|
-
const [len, newOff] = readVarint(buf, off);
|
|
966
|
-
return [buf.slice(newOff, newOff + len).toString('utf8'), newOff + len];
|
|
967
|
-
};
|
|
968
|
-
|
|
969
|
-
while (offset < buffer.length) {
|
|
970
|
-
try {
|
|
971
|
-
const [tag, tagOff] = readVarint(buffer, offset);
|
|
972
|
-
const wireType = tag & 0x7;
|
|
973
|
-
const fieldNumber = tag >>> 3;
|
|
974
|
-
offset = tagOff;
|
|
975
|
-
|
|
976
|
-
if (wireType === 0) {
|
|
977
|
-
const [, newOff] = readVarint(buffer, offset);
|
|
978
|
-
offset = newOff;
|
|
979
|
-
} else if (wireType === 2) {
|
|
980
|
-
const [val, newOff] = readString(buffer, offset);
|
|
981
|
-
offset = newOff;
|
|
982
|
-
if (fieldNumber === 110101) result.exchange = val;
|
|
983
|
-
if (fieldNumber === 100749) result.productCode = val;
|
|
984
|
-
if (fieldNumber === 100003) result.productName = val;
|
|
985
|
-
} else {
|
|
986
|
-
break;
|
|
987
|
-
}
|
|
988
|
-
} catch {
|
|
989
|
-
break;
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
return result;
|
|
994
|
-
}
|
|
995
|
-
|
|
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
|
-
}
|
|
339
|
+
async getContracts() { return getContracts(this); }
|
|
340
|
+
async searchContracts(searchText) { return searchContracts(this, searchText); }
|
|
1018
341
|
|
|
1019
342
|
// ==================== CLEANUP ====================
|
|
1020
343
|
|
|
1021
|
-
/**
|
|
1022
|
-
* Disconnect all connections
|
|
1023
|
-
*/
|
|
1024
344
|
async disconnect() {
|
|
1025
345
|
const connections = [this.orderConn, this.pnlConn, this.tickerConn];
|
|
1026
346
|
|
|
1027
347
|
for (const conn of connections) {
|
|
1028
348
|
if (conn) {
|
|
1029
|
-
try {
|
|
1030
|
-
await conn.disconnect();
|
|
1031
|
-
} catch (err) {
|
|
1032
|
-
log.warn('Disconnect error', { error: err.message });
|
|
1033
|
-
}
|
|
349
|
+
try { await conn.disconnect(); } catch (err) { log.warn('Disconnect error', { error: err.message }); }
|
|
1034
350
|
}
|
|
1035
351
|
}
|
|
1036
352
|
|