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.
@@ -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 { calculateMomentum, getVPIN } = require('./position-momentum');
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
- const exitReason = this._checkExitConditions(position, currentPrice, pnlTicks, holdDuration);
296
-
297
- if (exitReason) {
298
- this._executeExit(orderTag, exitReason);
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
- if (momentum < MOMENTUM.WEAK_THRESHOLD && pnlTicks > 0) {
386
- return { type: 'momentum_weak', reason: 'Weak momentum - securing profit', pnlTicks, momentum };
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 (momentum < MOMENTUM.ADVERSE_THRESHOLD) {
390
- return { type: 'momentum_adverse', reason: 'Adverse momentum detected', pnlTicks, momentum };
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
- if (position.entryPrice === null || currentPrice === null) return null;
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
+ };