hedgequantx 2.6.162 → 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/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/position-exit-logic.js +174 -0
- package/src/services/position-manager.js +18 -108
- package/src/services/rithmic/contracts.js +218 -0
- package/src/services/rithmic/index.js +5 -179
- 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/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
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Position Exit Logic
|
|
3
|
+
* @module services/position-exit-logic
|
|
4
|
+
*
|
|
5
|
+
* Exit condition evaluation for fast scalping positions
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { FAST_SCALPING } = require('../config/settings');
|
|
9
|
+
const { MOMENTUM, WEIGHTS } = require('./position-constants');
|
|
10
|
+
const { calculateMomentum, getVPIN } = require('./position-momentum');
|
|
11
|
+
const { logger } = require('../utils/logger');
|
|
12
|
+
|
|
13
|
+
const log = logger.scope('PositionExit');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Check all exit conditions for a position
|
|
17
|
+
* @param {Object} params
|
|
18
|
+
* @param {Object} params.position - Position data
|
|
19
|
+
* @param {number} params.currentPrice - Current market price
|
|
20
|
+
* @param {number} params.pnlTicks - Current P&L in ticks
|
|
21
|
+
* @param {number} params.holdDuration - Time held in ms
|
|
22
|
+
* @param {Object} params.strategy - Strategy instance
|
|
23
|
+
* @param {Function} params.getTickSize - Function to get tick size
|
|
24
|
+
* @param {Map} params.latestPrices - Latest prices map
|
|
25
|
+
* @returns {Object|null} - Exit reason or null
|
|
26
|
+
*/
|
|
27
|
+
function checkExitConditions({
|
|
28
|
+
position,
|
|
29
|
+
currentPrice,
|
|
30
|
+
pnlTicks,
|
|
31
|
+
holdDuration,
|
|
32
|
+
strategy,
|
|
33
|
+
getTickSize,
|
|
34
|
+
latestPrices,
|
|
35
|
+
}) {
|
|
36
|
+
if (pnlTicks === null) return null;
|
|
37
|
+
|
|
38
|
+
const targetTicks = FAST_SCALPING.TARGET_TICKS;
|
|
39
|
+
const stopTicks = FAST_SCALPING.STOP_TICKS;
|
|
40
|
+
|
|
41
|
+
// 1. TARGET HIT
|
|
42
|
+
if (pnlTicks >= targetTicks) {
|
|
43
|
+
return { type: 'target', reason: 'Target reached', pnlTicks };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 2. BREAKEVEN CHECK
|
|
47
|
+
if (position.breakevenActive && position.breakevenPrice !== null) {
|
|
48
|
+
const tickSize = getTickSize(position);
|
|
49
|
+
if (tickSize) {
|
|
50
|
+
if (position.side === 0 && currentPrice <= position.breakevenPrice) {
|
|
51
|
+
return { type: 'breakeven', reason: 'Breakeven stop hit', pnlTicks };
|
|
52
|
+
} else if (position.side === 1 && currentPrice >= position.breakevenPrice) {
|
|
53
|
+
return { type: 'breakeven', reason: 'Breakeven stop hit', pnlTicks };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 3. STOP HIT (only if BE not active)
|
|
59
|
+
if (!position.breakevenActive && pnlTicks <= -stopTicks) {
|
|
60
|
+
return { type: 'stop', reason: 'Stop loss hit', pnlTicks };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 4. VPIN DANGER
|
|
64
|
+
const vpin = getVPIN(strategy, position.contractId || position.symbol);
|
|
65
|
+
if (vpin !== null && vpin > MOMENTUM.VPIN_DANGER) {
|
|
66
|
+
return { type: 'vpin', reason: `VPIN spike ${(vpin * 100).toFixed(0)}% - informed traders`, pnlTicks, vpin };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 5. TRAILING STOP
|
|
70
|
+
if (pnlTicks >= FAST_SCALPING.TRAILING_ACTIVATION_TICKS) {
|
|
71
|
+
const trailingPnl = calculateTrailingPnl(position, currentPrice, getTickSize);
|
|
72
|
+
if (trailingPnl !== null && trailingPnl <= -FAST_SCALPING.TRAILING_DISTANCE_TICKS) {
|
|
73
|
+
return { type: 'trailing', reason: 'Trailing stop triggered', pnlTicks, trailingPnl };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 6. MOMENTUM-BASED EXIT
|
|
78
|
+
const momentum = calculateMomentum({
|
|
79
|
+
strategy: strategy,
|
|
80
|
+
contractId: position.contractId || position.symbol,
|
|
81
|
+
side: position.side,
|
|
82
|
+
currentPrice: latestPrices.get(position.symbol),
|
|
83
|
+
tickSize: getTickSize(position),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (momentum !== null) {
|
|
87
|
+
if (momentum > MOMENTUM.STRONG_FAVORABLE && pnlTicks > 4) {
|
|
88
|
+
return null; // Don't exit - momentum is strong
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (momentum < MOMENTUM.WEAK_THRESHOLD && pnlTicks > 0) {
|
|
92
|
+
return { type: 'momentum_weak', reason: 'Weak momentum - securing profit', pnlTicks, momentum };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (momentum < MOMENTUM.ADVERSE_THRESHOLD) {
|
|
96
|
+
return { type: 'momentum_adverse', reason: 'Adverse momentum detected', pnlTicks, momentum };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Check and activate breakeven if conditions met
|
|
105
|
+
* @param {Object} position
|
|
106
|
+
* @param {number} pnlTicks
|
|
107
|
+
* @param {Function} getTickSize
|
|
108
|
+
* @returns {Object|null} - Breakeven activation info or null
|
|
109
|
+
*/
|
|
110
|
+
function checkBreakevenActivation(position, pnlTicks, getTickSize) {
|
|
111
|
+
if (position.breakevenActive) return null;
|
|
112
|
+
if (pnlTicks < FAST_SCALPING.BREAKEVEN_ACTIVATION_TICKS) return null;
|
|
113
|
+
|
|
114
|
+
const tickSize = getTickSize(position);
|
|
115
|
+
if (!tickSize || !position.entryPrice) return null;
|
|
116
|
+
|
|
117
|
+
const offset = FAST_SCALPING.BREAKEVEN_OFFSET_TICKS * tickSize;
|
|
118
|
+
const breakevenPrice = position.side === 0
|
|
119
|
+
? position.entryPrice + offset
|
|
120
|
+
: position.entryPrice - offset;
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
breakevenPrice,
|
|
124
|
+
pnlTicks,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Calculate trailing P&L from high/low water mark
|
|
130
|
+
* @param {Object} position
|
|
131
|
+
* @param {number} currentPrice
|
|
132
|
+
* @param {Function} getTickSize
|
|
133
|
+
* @returns {number|null}
|
|
134
|
+
*/
|
|
135
|
+
function calculateTrailingPnl(position, currentPrice, getTickSize) {
|
|
136
|
+
const tickSize = getTickSize(position);
|
|
137
|
+
if (tickSize === null) return null;
|
|
138
|
+
|
|
139
|
+
if (position.side === 0) {
|
|
140
|
+
if (position.highWaterMark === null) return null;
|
|
141
|
+
const dropFromHigh = position.highWaterMark - currentPrice;
|
|
142
|
+
return -Math.round(dropFromHigh / tickSize);
|
|
143
|
+
} else {
|
|
144
|
+
if (position.lowWaterMark === null) return null;
|
|
145
|
+
const riseFromLow = currentPrice - position.lowWaterMark;
|
|
146
|
+
return -Math.round(riseFromLow / tickSize);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Calculate P&L in ticks
|
|
152
|
+
* @param {Object} position
|
|
153
|
+
* @param {number} currentPrice
|
|
154
|
+
* @param {Function} getTickSize
|
|
155
|
+
* @returns {number|null}
|
|
156
|
+
*/
|
|
157
|
+
function calculatePnlTicks(position, currentPrice, getTickSize) {
|
|
158
|
+
if (position.entryPrice === null || currentPrice === null) return null;
|
|
159
|
+
|
|
160
|
+
const tickSize = getTickSize(position);
|
|
161
|
+
if (tickSize === null) return null;
|
|
162
|
+
|
|
163
|
+
const priceDiff = currentPrice - position.entryPrice;
|
|
164
|
+
const signedDiff = position.side === 0 ? priceDiff : -priceDiff;
|
|
165
|
+
|
|
166
|
+
return Math.round(signedDiff / tickSize);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
module.exports = {
|
|
170
|
+
checkExitConditions,
|
|
171
|
+
checkBreakevenActivation,
|
|
172
|
+
calculateTrailingPnl,
|
|
173
|
+
calculatePnlTicks,
|
|
174
|
+
};
|
|
@@ -16,7 +16,7 @@ const EventEmitter = require('events');
|
|
|
16
16
|
const { FAST_SCALPING } = require('../config/settings');
|
|
17
17
|
const { logger } = require('../utils/logger');
|
|
18
18
|
const { MOMENTUM, WEIGHTS } = require('./position-constants');
|
|
19
|
-
const {
|
|
19
|
+
const { checkExitConditions, checkBreakevenActivation, calculatePnlTicks } = require('./position-exit-logic');
|
|
20
20
|
|
|
21
21
|
const log = logger.scope('PositionMgr');
|
|
22
22
|
|
|
@@ -292,50 +292,10 @@ class PositionManager extends EventEmitter {
|
|
|
292
292
|
const holdDuration = now - position.fillTime;
|
|
293
293
|
const pnlTicks = this._calculatePnlTicks(position, currentPrice);
|
|
294
294
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
if (
|
|
298
|
-
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
_checkExitConditions(position, currentPrice, pnlTicks) {
|
|
304
|
-
if (pnlTicks === null) return null;
|
|
305
|
-
|
|
306
|
-
const targetTicks = FAST_SCALPING.TARGET_TICKS;
|
|
307
|
-
const stopTicks = FAST_SCALPING.STOP_TICKS;
|
|
308
|
-
|
|
309
|
-
// 1. TARGET HIT
|
|
310
|
-
if (pnlTicks >= targetTicks) {
|
|
311
|
-
return { type: 'target', reason: 'Target reached', pnlTicks };
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// 2. BREAKEVEN CHECK
|
|
315
|
-
if (position.breakevenActive && position.breakevenPrice !== null) {
|
|
316
|
-
const tickSize = this._getTickSize(position);
|
|
317
|
-
if (tickSize) {
|
|
318
|
-
if (position.side === 0 && currentPrice <= position.breakevenPrice) {
|
|
319
|
-
return { type: 'breakeven', reason: 'Breakeven stop hit', pnlTicks };
|
|
320
|
-
} else if (position.side === 1 && currentPrice >= position.breakevenPrice) {
|
|
321
|
-
return { type: 'breakeven', reason: 'Breakeven stop hit', pnlTicks };
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// 3. STOP HIT (only if BE not active)
|
|
327
|
-
if (!position.breakevenActive && pnlTicks <= -stopTicks) {
|
|
328
|
-
return { type: 'stop', reason: 'Stop loss hit', pnlTicks };
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
// 4. ACTIVATE BREAKEVEN
|
|
332
|
-
if (!position.breakevenActive && pnlTicks >= FAST_SCALPING.BREAKEVEN_ACTIVATION_TICKS) {
|
|
333
|
-
const tickSize = this._getTickSize(position);
|
|
334
|
-
if (tickSize && position.entryPrice) {
|
|
335
|
-
const offset = FAST_SCALPING.BREAKEVEN_OFFSET_TICKS * tickSize;
|
|
336
|
-
position.breakevenPrice = position.side === 0
|
|
337
|
-
? position.entryPrice + offset
|
|
338
|
-
: position.entryPrice - offset;
|
|
295
|
+
// Check for breakeven activation
|
|
296
|
+
const beActivation = checkBreakevenActivation(position, pnlTicks, (p) => this._getTickSize(p));
|
|
297
|
+
if (beActivation) {
|
|
298
|
+
position.breakevenPrice = beActivation.breakevenPrice;
|
|
339
299
|
position.breakevenActive = true;
|
|
340
300
|
|
|
341
301
|
log.info('BREAKEVEN ACTIVATED', {
|
|
@@ -352,46 +312,22 @@ class PositionManager extends EventEmitter {
|
|
|
352
312
|
pnlTicks,
|
|
353
313
|
});
|
|
354
314
|
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// 5. VPIN DANGER
|
|
358
|
-
const vpin = getVPIN(this.strategy, position.contractId || position.symbol);
|
|
359
|
-
if (vpin !== null && vpin > MOMENTUM.VPIN_DANGER) {
|
|
360
|
-
return { type: 'vpin', reason: `VPIN spike ${(vpin * 100).toFixed(0)}% - informed traders`, pnlTicks, vpin };
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
// 6. TRAILING STOP
|
|
364
|
-
if (pnlTicks >= FAST_SCALPING.TRAILING_ACTIVATION_TICKS) {
|
|
365
|
-
const trailingPnl = this._calculateTrailingPnl(position, currentPrice);
|
|
366
|
-
if (trailingPnl !== null && trailingPnl <= -FAST_SCALPING.TRAILING_DISTANCE_TICKS) {
|
|
367
|
-
return { type: 'trailing', reason: 'Trailing stop triggered', pnlTicks, trailingPnl };
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// 7. MOMENTUM-BASED EXIT
|
|
372
|
-
const momentum = calculateMomentum({
|
|
373
|
-
strategy: this.strategy,
|
|
374
|
-
contractId: position.contractId || position.symbol,
|
|
375
|
-
side: position.side,
|
|
376
|
-
currentPrice: this.latestPrices.get(position.symbol),
|
|
377
|
-
tickSize: this._getTickSize(position),
|
|
378
|
-
});
|
|
379
|
-
|
|
380
|
-
if (momentum !== null) {
|
|
381
|
-
if (momentum > MOMENTUM.STRONG_FAVORABLE && pnlTicks > 4) {
|
|
382
|
-
return null; // Don't exit - momentum is strong
|
|
383
|
-
}
|
|
384
315
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
316
|
+
// Check exit conditions
|
|
317
|
+
const exitReason = checkExitConditions({
|
|
318
|
+
position,
|
|
319
|
+
currentPrice,
|
|
320
|
+
pnlTicks,
|
|
321
|
+
holdDuration,
|
|
322
|
+
strategy: this.strategy,
|
|
323
|
+
getTickSize: (p) => this._getTickSize(p),
|
|
324
|
+
latestPrices: this.latestPrices,
|
|
325
|
+
});
|
|
388
326
|
|
|
389
|
-
if (
|
|
390
|
-
|
|
327
|
+
if (exitReason) {
|
|
328
|
+
this._executeExit(orderTag, exitReason);
|
|
391
329
|
}
|
|
392
330
|
}
|
|
393
|
-
|
|
394
|
-
return null;
|
|
395
331
|
}
|
|
396
332
|
|
|
397
333
|
_executeExit(orderTag, exitReason) {
|
|
@@ -468,33 +404,7 @@ class PositionManager extends EventEmitter {
|
|
|
468
404
|
}
|
|
469
405
|
|
|
470
406
|
_calculatePnlTicks(position, currentPrice) {
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
const tickSize = this._getTickSize(position);
|
|
474
|
-
if (tickSize === null) {
|
|
475
|
-
log.error('Cannot calculate PnL - no tick size from API', { symbol: position.symbol });
|
|
476
|
-
return null;
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
const priceDiff = currentPrice - position.entryPrice;
|
|
480
|
-
const signedDiff = position.side === 0 ? priceDiff : -priceDiff;
|
|
481
|
-
|
|
482
|
-
return Math.round(signedDiff / tickSize);
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
_calculateTrailingPnl(position, currentPrice) {
|
|
486
|
-
const tickSize = this._getTickSize(position);
|
|
487
|
-
if (tickSize === null) return null;
|
|
488
|
-
|
|
489
|
-
if (position.side === 0) {
|
|
490
|
-
if (position.highWaterMark === null) return null;
|
|
491
|
-
const dropFromHigh = position.highWaterMark - currentPrice;
|
|
492
|
-
return -Math.round(dropFromHigh / tickSize);
|
|
493
|
-
} else {
|
|
494
|
-
if (position.lowWaterMark === null) return null;
|
|
495
|
-
const riseFromLow = currentPrice - position.lowWaterMark;
|
|
496
|
-
return -Math.round(riseFromLow / tickSize);
|
|
497
|
-
}
|
|
407
|
+
return calculatePnlTicks(position, currentPrice, (p) => this._getTickSize(p));
|
|
498
408
|
}
|
|
499
409
|
|
|
500
410
|
getActivePositions() {
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rithmic Contracts Module
|
|
3
|
+
* @module services/rithmic/contracts
|
|
4
|
+
*
|
|
5
|
+
* Contract lookup and front month discovery
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { decodeFrontMonthContract } = require('./protobuf');
|
|
9
|
+
const { CME_CONTRACT_SPECS } = require('./specs');
|
|
10
|
+
const { TIMEOUTS, CACHE } = require('../../config/settings');
|
|
11
|
+
const { logger } = require('../../utils/logger');
|
|
12
|
+
|
|
13
|
+
const log = logger.scope('RithmicContracts');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Decode product codes from raw protobuf buffer
|
|
17
|
+
* @param {Buffer} buffer
|
|
18
|
+
* @returns {Object}
|
|
19
|
+
*/
|
|
20
|
+
function decodeProductCodes(buffer) {
|
|
21
|
+
const result = {};
|
|
22
|
+
let offset = 0;
|
|
23
|
+
|
|
24
|
+
const readVarint = (buf, off) => {
|
|
25
|
+
let value = 0, shift = 0;
|
|
26
|
+
while (off < buf.length) {
|
|
27
|
+
const byte = buf[off++];
|
|
28
|
+
value |= (byte & 0x7F) << shift;
|
|
29
|
+
if (!(byte & 0x80)) break;
|
|
30
|
+
shift += 7;
|
|
31
|
+
}
|
|
32
|
+
return [value, off];
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const readString = (buf, off) => {
|
|
36
|
+
const [len, newOff] = readVarint(buf, off);
|
|
37
|
+
return [buf.slice(newOff, newOff + len).toString('utf8'), newOff + len];
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
while (offset < buffer.length) {
|
|
41
|
+
try {
|
|
42
|
+
const [tag, tagOff] = readVarint(buffer, offset);
|
|
43
|
+
const wireType = tag & 0x7;
|
|
44
|
+
const fieldNumber = tag >>> 3;
|
|
45
|
+
offset = tagOff;
|
|
46
|
+
|
|
47
|
+
if (wireType === 0) {
|
|
48
|
+
const [, newOff] = readVarint(buffer, offset);
|
|
49
|
+
offset = newOff;
|
|
50
|
+
} else if (wireType === 2) {
|
|
51
|
+
const [val, newOff] = readString(buffer, offset);
|
|
52
|
+
offset = newOff;
|
|
53
|
+
if (fieldNumber === 110101) result.exchange = val;
|
|
54
|
+
if (fieldNumber === 100749) result.productCode = val;
|
|
55
|
+
if (fieldNumber === 100003) result.productName = val;
|
|
56
|
+
} else {
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
} catch { break; }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Fetch all front month contracts from TICKER_PLANT
|
|
67
|
+
* @param {RithmicService} service
|
|
68
|
+
* @returns {Promise<Array>}
|
|
69
|
+
*/
|
|
70
|
+
async function fetchAllFrontMonths(service) {
|
|
71
|
+
if (!service.tickerConn) throw new Error('TICKER_PLANT not connected');
|
|
72
|
+
|
|
73
|
+
return new Promise((resolve) => {
|
|
74
|
+
const contracts = new Map();
|
|
75
|
+
const productsToCheck = new Map();
|
|
76
|
+
|
|
77
|
+
const productHandler = (msg) => {
|
|
78
|
+
if (msg.templateId !== 112) return;
|
|
79
|
+
|
|
80
|
+
const decoded = decodeProductCodes(msg.data);
|
|
81
|
+
if (!decoded.productCode || !decoded.exchange) return;
|
|
82
|
+
|
|
83
|
+
const validExchanges = ['CME', 'CBOT', 'NYMEX', 'COMEX', 'NYBOT', 'CFE'];
|
|
84
|
+
if (!validExchanges.includes(decoded.exchange)) return;
|
|
85
|
+
|
|
86
|
+
const name = (decoded.productName || '').toLowerCase();
|
|
87
|
+
if (name.includes('option') || name.includes('swap') || name.includes('spread')) return;
|
|
88
|
+
|
|
89
|
+
const key = `${decoded.productCode}:${decoded.exchange}`;
|
|
90
|
+
if (!productsToCheck.has(key)) {
|
|
91
|
+
productsToCheck.set(key, {
|
|
92
|
+
productCode: decoded.productCode,
|
|
93
|
+
productName: decoded.productName || decoded.productCode,
|
|
94
|
+
exchange: decoded.exchange,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const frontMonthHandler = (msg) => {
|
|
100
|
+
if (msg.templateId !== 114) return;
|
|
101
|
+
|
|
102
|
+
const decoded = decodeFrontMonthContract(msg.data);
|
|
103
|
+
if (decoded.rpCode[0] === '0' && decoded.tradingSymbol) {
|
|
104
|
+
contracts.set(decoded.userMsg, {
|
|
105
|
+
symbol: decoded.tradingSymbol,
|
|
106
|
+
baseSymbol: decoded.userMsg,
|
|
107
|
+
exchange: decoded.exchange,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
service.tickerConn.on('message', productHandler);
|
|
113
|
+
service.tickerConn.on('message', frontMonthHandler);
|
|
114
|
+
|
|
115
|
+
service.tickerConn.send('RequestProductCodes', {
|
|
116
|
+
templateId: 111,
|
|
117
|
+
userMsg: ['get-products'],
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
setTimeout(() => {
|
|
121
|
+
service.tickerConn.removeListener('message', productHandler);
|
|
122
|
+
log.debug('Collected products', { count: productsToCheck.size });
|
|
123
|
+
|
|
124
|
+
for (const product of productsToCheck.values()) {
|
|
125
|
+
service.tickerConn.send('RequestFrontMonthContract', {
|
|
126
|
+
templateId: 113,
|
|
127
|
+
userMsg: [product.productCode],
|
|
128
|
+
symbol: product.productCode,
|
|
129
|
+
exchange: product.exchange,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
setTimeout(() => {
|
|
134
|
+
service.tickerConn.removeListener('message', frontMonthHandler);
|
|
135
|
+
|
|
136
|
+
const results = [];
|
|
137
|
+
for (const [baseSymbol, contract] of contracts) {
|
|
138
|
+
const productKey = `${baseSymbol}:${contract.exchange}`;
|
|
139
|
+
const product = productsToCheck.get(productKey);
|
|
140
|
+
const specs = CME_CONTRACT_SPECS[baseSymbol] || null;
|
|
141
|
+
const productName = specs?.name || product?.productName || baseSymbol;
|
|
142
|
+
|
|
143
|
+
results.push({
|
|
144
|
+
symbol: contract.symbol,
|
|
145
|
+
baseSymbol,
|
|
146
|
+
name: contract.symbol,
|
|
147
|
+
description: productName,
|
|
148
|
+
exchange: contract.exchange,
|
|
149
|
+
tickSize: specs?.tickSize ?? null,
|
|
150
|
+
tickValue: specs?.tickValue ?? null,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
results.sort((a, b) => a.baseSymbol.localeCompare(b.baseSymbol));
|
|
155
|
+
log.debug('Got contracts from API', { count: results.length });
|
|
156
|
+
resolve(results);
|
|
157
|
+
}, TIMEOUTS.RITHMIC_PRODUCTS);
|
|
158
|
+
}, TIMEOUTS.RITHMIC_CONTRACTS);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Get contracts with caching
|
|
164
|
+
* @param {RithmicService} service
|
|
165
|
+
* @returns {Promise<Object>}
|
|
166
|
+
*/
|
|
167
|
+
async function getContracts(service) {
|
|
168
|
+
if (service._contractsCache && Date.now() - service._contractsCacheTime < CACHE.CONTRACTS_TTL) {
|
|
169
|
+
return { success: true, contracts: service._contractsCache, source: 'cache' };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!service.credentials) return { success: false, error: 'Not logged in' };
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
if (!service.tickerConn) {
|
|
176
|
+
const connected = await service.connectTicker(service.credentials.username, service.credentials.password);
|
|
177
|
+
if (!connected) return { success: false, error: 'Failed to connect to TICKER_PLANT' };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
service.tickerConn.setMaxListeners(5000);
|
|
181
|
+
log.debug('Fetching contracts from Rithmic API');
|
|
182
|
+
const contracts = await fetchAllFrontMonths(service);
|
|
183
|
+
|
|
184
|
+
if (!contracts.length) return { success: false, error: 'No tradeable contracts found' };
|
|
185
|
+
|
|
186
|
+
service._contractsCache = contracts;
|
|
187
|
+
service._contractsCacheTime = Date.now();
|
|
188
|
+
|
|
189
|
+
return { success: true, contracts, source: 'api' };
|
|
190
|
+
} catch (err) {
|
|
191
|
+
log.error('getContracts error', { error: err.message });
|
|
192
|
+
return { success: false, error: err.message };
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Search contracts by text
|
|
198
|
+
* @param {RithmicService} service
|
|
199
|
+
* @param {string} searchText
|
|
200
|
+
* @returns {Promise<Array>}
|
|
201
|
+
*/
|
|
202
|
+
async function searchContracts(service, searchText) {
|
|
203
|
+
const result = await getContracts(service);
|
|
204
|
+
if (!searchText || !result.success) return result.contracts || [];
|
|
205
|
+
|
|
206
|
+
const search = searchText.toUpperCase();
|
|
207
|
+
return result.contracts.filter(c =>
|
|
208
|
+
c.symbol.toUpperCase().includes(search) ||
|
|
209
|
+
c.name.toUpperCase().includes(search)
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
module.exports = {
|
|
214
|
+
decodeProductCodes,
|
|
215
|
+
fetchAllFrontMonths,
|
|
216
|
+
getContracts,
|
|
217
|
+
searchContracts,
|
|
218
|
+
};
|