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.
@@ -6,15 +6,12 @@
6
6
  const protobuf = require('protobufjs');
7
7
  const path = require('path');
8
8
  const { PROTO_FILES } = require('./constants');
9
+ const {
10
+ readVarint, readLengthDelimited, skipField,
11
+ decodeAccountPnL, decodeInstrumentPnL, decodeProductCodes, decodeFrontMonthContract
12
+ } = require('./proto-decoders');
9
13
 
10
- // ==================== BUFFER POOL ====================
11
- // Pre-allocated buffer pool for zero-allocation hot path
12
- // Avoids GC pressure during high-frequency trading
13
-
14
- /**
15
- * High-performance buffer pool for zero-allocation encoding
16
- * Uses ring buffer pattern for O(1) acquire/release
17
- */
14
+ /** High-performance buffer pool for zero-allocation encoding */
18
15
  class BufferPool {
19
16
  constructor(poolSize = 16, bufferSize = 512) {
20
17
  this._pool = new Array(poolSize);
@@ -24,370 +21,32 @@ class BufferPool {
24
21
  this._head = 0;
25
22
  this._tail = 0;
26
23
  this._count = poolSize;
27
-
28
- // Pre-allocate all buffers
29
24
  for (let i = 0; i < poolSize; i++) {
30
25
  this._pool[i] = Buffer.allocUnsafe(bufferSize);
31
26
  this._available[i] = i;
32
27
  }
33
28
  }
34
29
 
35
- /**
36
- * Acquire a buffer from the pool
37
- * @returns {Buffer|null} Buffer or null if pool exhausted
38
- */
39
30
  acquire() {
40
- if (this._count === 0) {
41
- // Pool exhausted - allocate new (fallback)
42
- return Buffer.allocUnsafe(this._bufferSize);
43
- }
31
+ if (this._count === 0) return Buffer.allocUnsafe(this._bufferSize);
44
32
  const idx = this._available[this._head];
45
33
  this._head = (this._head + 1) % this._size;
46
34
  this._count--;
47
35
  return this._pool[idx];
48
36
  }
49
37
 
50
- /**
51
- * Release a buffer back to pool
52
- * Only releases buffers that belong to the pool
53
- * @param {Buffer} buffer
54
- */
55
38
  release(buffer) {
56
- // Find if this buffer is from our pool
57
39
  const idx = this._pool.indexOf(buffer);
58
40
  if (idx !== -1 && this._count < this._size) {
59
41
  this._available[this._tail] = idx;
60
42
  this._tail = (this._tail + 1) % this._size;
61
43
  this._count++;
62
44
  }
63
- // If not from pool, let GC handle it
64
45
  }
65
46
 
66
- /**
67
- * Get pool stats
68
- */
69
47
  getStats() {
70
- return {
71
- size: this._size,
72
- available: this._count,
73
- bufferSize: this._bufferSize,
74
- };
75
- }
76
- }
77
-
78
- // PnL field IDs (Rithmic uses very large field IDs)
79
- const PNL_FIELDS = {
80
- TEMPLATE_ID: 154467,
81
- IS_SNAPSHOT: 110121,
82
- FCM_ID: 154013,
83
- IB_ID: 154014,
84
- ACCOUNT_ID: 154008,
85
- ACCOUNT_BALANCE: 156970,
86
- CASH_ON_HAND: 156971,
87
- MARGIN_BALANCE: 156977,
88
- MIN_ACCOUNT_BALANCE: 156968,
89
- OPEN_POSITION_PNL: 156961,
90
- CLOSED_POSITION_PNL: 156963,
91
- DAY_PNL: 157956,
92
- DAY_OPEN_PNL: 157954,
93
- DAY_CLOSED_PNL: 157955,
94
- AVAILABLE_BUYING_POWER: 157015,
95
- SSBOE: 150100,
96
- USECS: 150101,
97
- };
98
-
99
- // Symbol/Contract field IDs (ResponseProductCodes, ResponseFrontMonthContract)
100
- const SYMBOL_FIELDS = {
101
- TEMPLATE_ID: 154467,
102
- RP_CODE: 132766,
103
- EXCHANGE: 110101,
104
- PRODUCT_CODE: 110102, // Base symbol (ES, NQ, MNQ)
105
- PRODUCT_NAME: 110103, // Product name
106
- SYMBOL: 110100, // Full contract symbol (ESH26)
107
- TRADING_SYMBOL: 157095, // Trading symbol
108
- DESCRIPTION: 110114, // Contract description
109
- USER_MSG: 132760,
110
- };
111
-
112
- // Instrument PnL Position Update field IDs
113
- const INSTRUMENT_PNL_FIELDS = {
114
- TEMPLATE_ID: 154467,
115
- IS_SNAPSHOT: 110121,
116
- FCM_ID: 154013,
117
- IB_ID: 154014,
118
- ACCOUNT_ID: 154008,
119
- SYMBOL: 110100,
120
- EXCHANGE: 110101,
121
- PRODUCT_CODE: 100749,
122
- INSTRUMENT_TYPE: 110116,
123
- FILL_BUY_QTY: 154041,
124
- FILL_SELL_QTY: 154042,
125
- ORDER_BUY_QTY: 154037,
126
- ORDER_SELL_QTY: 154038,
127
- BUY_QTY: 154260,
128
- SELL_QTY: 154261,
129
- AVG_OPEN_FILL_PRICE: 154434,
130
- DAY_OPEN_PNL: 157954,
131
- DAY_CLOSED_PNL: 157955,
132
- DAY_PNL: 157956,
133
- OPEN_POSITION_PNL: 156961,
134
- OPEN_POSITION_QUANTITY: 156962,
135
- CLOSED_POSITION_PNL: 156963,
136
- CLOSED_POSITION_QUANTITY: 156964,
137
- NET_QUANTITY: 156967,
138
- SSBOE: 150100,
139
- USECS: 150101,
140
- };
141
-
142
- /**
143
- * Read a varint from buffer
144
- */
145
- function readVarint(buffer, offset) {
146
- let result = BigInt(0);
147
- let shift = BigInt(0);
148
- let pos = offset;
149
-
150
- while (pos < buffer.length) {
151
- const byte = buffer[pos++];
152
- result |= BigInt(byte & 0x7f) << shift;
153
- if ((byte & 0x80) === 0) {
154
- return [Number(result), pos];
155
- }
156
- shift += BigInt(7);
157
- if (shift > BigInt(63)) {
158
- throw new Error('Varint too large');
159
- }
160
- }
161
- throw new Error('Incomplete varint');
162
- }
163
-
164
- /**
165
- * Read a length-delimited field (string/bytes)
166
- */
167
- function readLengthDelimited(buffer, offset) {
168
- const [length, newOffset] = readVarint(buffer, offset);
169
- const value = buffer.slice(newOffset, newOffset + length).toString('utf8');
170
- return [value, newOffset + length];
171
- }
172
-
173
- /**
174
- * Skip a field based on wire type
175
- */
176
- function skipField(buffer, offset, wireType) {
177
- switch (wireType) {
178
- case 0: // Varint
179
- const [, newOffset] = readVarint(buffer, offset);
180
- return newOffset;
181
- case 1: // 64-bit
182
- return offset + 8;
183
- case 2: // Length-delimited
184
- const [length, lenOffset] = readVarint(buffer, offset);
185
- return lenOffset + length;
186
- case 5: // 32-bit
187
- return offset + 4;
188
- default:
189
- throw new Error(`Unknown wire type: ${wireType}`);
190
- }
191
- }
192
-
193
- /**
194
- * Manually decode AccountPnL from raw bytes
195
- */
196
- function decodeAccountPnL(buffer) {
197
- const result = {};
198
- let offset = 0;
199
-
200
- while (offset < buffer.length) {
201
- try {
202
- const [tag, tagOffset] = readVarint(buffer, offset);
203
- const wireType = tag & 0x7;
204
- const fieldNumber = tag >>> 3;
205
- offset = tagOffset;
206
-
207
- switch (fieldNumber) {
208
- case PNL_FIELDS.TEMPLATE_ID:
209
- [result.templateId, offset] = readVarint(buffer, offset);
210
- break;
211
- case PNL_FIELDS.IS_SNAPSHOT:
212
- const [isSnap, snapOffset] = readVarint(buffer, offset);
213
- result.isSnapshot = isSnap !== 0;
214
- offset = snapOffset;
215
- break;
216
- case PNL_FIELDS.FCM_ID:
217
- [result.fcmId, offset] = readLengthDelimited(buffer, offset);
218
- break;
219
- case PNL_FIELDS.IB_ID:
220
- [result.ibId, offset] = readLengthDelimited(buffer, offset);
221
- break;
222
- case PNL_FIELDS.ACCOUNT_ID:
223
- [result.accountId, offset] = readLengthDelimited(buffer, offset);
224
- break;
225
- case PNL_FIELDS.ACCOUNT_BALANCE:
226
- [result.accountBalance, offset] = readLengthDelimited(buffer, offset);
227
- break;
228
- case PNL_FIELDS.CASH_ON_HAND:
229
- [result.cashOnHand, offset] = readLengthDelimited(buffer, offset);
230
- break;
231
- case PNL_FIELDS.MARGIN_BALANCE:
232
- [result.marginBalance, offset] = readLengthDelimited(buffer, offset);
233
- break;
234
- case PNL_FIELDS.MIN_ACCOUNT_BALANCE:
235
- [result.minAccountBalance, offset] = readLengthDelimited(buffer, offset);
236
- break;
237
- case PNL_FIELDS.OPEN_POSITION_PNL:
238
- [result.openPositionPnl, offset] = readLengthDelimited(buffer, offset);
239
- break;
240
- case PNL_FIELDS.CLOSED_POSITION_PNL:
241
- [result.closedPositionPnl, offset] = readLengthDelimited(buffer, offset);
242
- break;
243
- case PNL_FIELDS.DAY_PNL:
244
- [result.dayPnl, offset] = readLengthDelimited(buffer, offset);
245
- break;
246
- case PNL_FIELDS.DAY_OPEN_PNL:
247
- [result.dayOpenPnl, offset] = readLengthDelimited(buffer, offset);
248
- break;
249
- case PNL_FIELDS.DAY_CLOSED_PNL:
250
- [result.dayClosedPnl, offset] = readLengthDelimited(buffer, offset);
251
- break;
252
- case PNL_FIELDS.AVAILABLE_BUYING_POWER:
253
- [result.availableBuyingPower, offset] = readLengthDelimited(buffer, offset);
254
- break;
255
- case PNL_FIELDS.SSBOE:
256
- [result.ssboe, offset] = readVarint(buffer, offset);
257
- break;
258
- case PNL_FIELDS.USECS:
259
- [result.usecs, offset] = readVarint(buffer, offset);
260
- break;
261
- default:
262
- offset = skipField(buffer, offset, wireType);
263
- }
264
- } catch (error) {
265
- break;
266
- }
48
+ return { size: this._size, available: this._count, bufferSize: this._bufferSize };
267
49
  }
268
-
269
- return result;
270
- }
271
-
272
- /**
273
- * Manually decode InstrumentPnLPositionUpdate from raw bytes
274
- */
275
- function decodeInstrumentPnL(buffer) {
276
- const result = {};
277
- let offset = 0;
278
-
279
- while (offset < buffer.length) {
280
- try {
281
- const [tag, tagOffset] = readVarint(buffer, offset);
282
- const wireType = tag & 0x7;
283
- const fieldNumber = tag >>> 3;
284
- offset = tagOffset;
285
-
286
- switch (fieldNumber) {
287
- case INSTRUMENT_PNL_FIELDS.TEMPLATE_ID:
288
- [result.templateId, offset] = readVarint(buffer, offset);
289
- break;
290
- case INSTRUMENT_PNL_FIELDS.IS_SNAPSHOT:
291
- const [isSnap, snapOffset] = readVarint(buffer, offset);
292
- result.isSnapshot = isSnap !== 0;
293
- offset = snapOffset;
294
- break;
295
- case INSTRUMENT_PNL_FIELDS.FCM_ID:
296
- [result.fcmId, offset] = readLengthDelimited(buffer, offset);
297
- break;
298
- case INSTRUMENT_PNL_FIELDS.IB_ID:
299
- [result.ibId, offset] = readLengthDelimited(buffer, offset);
300
- break;
301
- case INSTRUMENT_PNL_FIELDS.ACCOUNT_ID:
302
- [result.accountId, offset] = readLengthDelimited(buffer, offset);
303
- break;
304
- case INSTRUMENT_PNL_FIELDS.SYMBOL:
305
- [result.symbol, offset] = readLengthDelimited(buffer, offset);
306
- break;
307
- case INSTRUMENT_PNL_FIELDS.EXCHANGE:
308
- [result.exchange, offset] = readLengthDelimited(buffer, offset);
309
- break;
310
- case INSTRUMENT_PNL_FIELDS.PRODUCT_CODE:
311
- [result.productCode, offset] = readLengthDelimited(buffer, offset);
312
- break;
313
- case INSTRUMENT_PNL_FIELDS.BUY_QTY:
314
- [result.buyQty, offset] = readVarint(buffer, offset);
315
- break;
316
- case INSTRUMENT_PNL_FIELDS.SELL_QTY:
317
- [result.sellQty, offset] = readVarint(buffer, offset);
318
- break;
319
- case INSTRUMENT_PNL_FIELDS.FILL_BUY_QTY:
320
- [result.fillBuyQty, offset] = readVarint(buffer, offset);
321
- break;
322
- case INSTRUMENT_PNL_FIELDS.FILL_SELL_QTY:
323
- [result.fillSellQty, offset] = readVarint(buffer, offset);
324
- break;
325
- case INSTRUMENT_PNL_FIELDS.NET_QUANTITY:
326
- [result.netQuantity, offset] = readVarint(buffer, offset);
327
- break;
328
- case INSTRUMENT_PNL_FIELDS.OPEN_POSITION_QUANTITY:
329
- [result.openPositionQuantity, offset] = readVarint(buffer, offset);
330
- break;
331
- case INSTRUMENT_PNL_FIELDS.AVG_OPEN_FILL_PRICE:
332
- // Double is 64-bit fixed
333
- if (wireType === 1) {
334
- result.avgOpenFillPrice = buffer.readDoubleLE(offset);
335
- offset += 8;
336
- } else {
337
- offset = skipField(buffer, offset, wireType);
338
- }
339
- break;
340
- case INSTRUMENT_PNL_FIELDS.OPEN_POSITION_PNL:
341
- [result.openPositionPnl, offset] = readLengthDelimited(buffer, offset);
342
- break;
343
- case INSTRUMENT_PNL_FIELDS.CLOSED_POSITION_PNL:
344
- [result.closedPositionPnl, offset] = readLengthDelimited(buffer, offset);
345
- break;
346
- case INSTRUMENT_PNL_FIELDS.DAY_PNL:
347
- // DAY_PNL is a double (wireType 1 = 64-bit fixed)
348
- if (wireType === 1) {
349
- result.dayPnl = buffer.readDoubleLE(offset);
350
- offset += 8;
351
- } else {
352
- // Fallback: try string
353
- [result.dayPnl, offset] = readLengthDelimited(buffer, offset);
354
- }
355
- break;
356
- case INSTRUMENT_PNL_FIELDS.DAY_OPEN_PNL:
357
- // DAY_OPEN_PNL is a double (wireType 1 = 64-bit fixed)
358
- if (wireType === 1) {
359
- result.dayOpenPnl = buffer.readDoubleLE(offset);
360
- offset += 8;
361
- } else {
362
- // Fallback: try string
363
- [result.dayOpenPnl, offset] = readLengthDelimited(buffer, offset);
364
- }
365
- break;
366
- case INSTRUMENT_PNL_FIELDS.DAY_CLOSED_PNL:
367
- // DAY_CLOSED_PNL is a double (wireType 1 = 64-bit fixed)
368
- if (wireType === 1) {
369
- result.dayClosedPnl = buffer.readDoubleLE(offset);
370
- offset += 8;
371
- } else {
372
- // Fallback: try string
373
- [result.dayClosedPnl, offset] = readLengthDelimited(buffer, offset);
374
- }
375
- break;
376
- case INSTRUMENT_PNL_FIELDS.SSBOE:
377
- [result.ssboe, offset] = readVarint(buffer, offset);
378
- break;
379
- case INSTRUMENT_PNL_FIELDS.USECS:
380
- [result.usecs, offset] = readVarint(buffer, offset);
381
- break;
382
- default:
383
- offset = skipField(buffer, offset, wireType);
384
- }
385
- } catch (error) {
386
- break;
387
- }
388
- }
389
-
390
- return result;
391
50
  }
392
51
 
393
52
  /**
@@ -580,101 +239,6 @@ class ProtobufHandler {
580
239
  }
581
240
  }
582
241
 
583
- /**
584
- * Decode ResponseProductCodes (template 112) - list of available symbols
585
- */
586
- function decodeProductCodes(buffer) {
587
- const result = { rpCode: [] };
588
- let offset = 0;
589
-
590
- while (offset < buffer.length) {
591
- try {
592
- const [tag, tagOffset] = readVarint(buffer, offset);
593
- const wireType = tag & 0x7;
594
- const fieldNumber = tag >>> 3;
595
- offset = tagOffset;
596
-
597
- switch (fieldNumber) {
598
- case SYMBOL_FIELDS.TEMPLATE_ID:
599
- [result.templateId, offset] = readVarint(buffer, offset);
600
- break;
601
- case SYMBOL_FIELDS.RP_CODE:
602
- let rpCode;
603
- [rpCode, offset] = readLengthDelimited(buffer, offset);
604
- result.rpCode.push(rpCode);
605
- break;
606
- case SYMBOL_FIELDS.EXCHANGE:
607
- [result.exchange, offset] = readLengthDelimited(buffer, offset);
608
- break;
609
- case SYMBOL_FIELDS.PRODUCT_CODE:
610
- [result.productCode, offset] = readLengthDelimited(buffer, offset);
611
- break;
612
- case SYMBOL_FIELDS.PRODUCT_NAME:
613
- [result.productName, offset] = readLengthDelimited(buffer, offset);
614
- break;
615
- case SYMBOL_FIELDS.USER_MSG:
616
- [result.userMsg, offset] = readLengthDelimited(buffer, offset);
617
- break;
618
- default:
619
- offset = skipField(buffer, offset, wireType);
620
- }
621
- } catch (error) {
622
- break;
623
- }
624
- }
625
-
626
- return result;
627
- }
628
-
629
- /**
630
- * Decode ResponseFrontMonthContract (template 114) - current tradeable contract
631
- */
632
- function decodeFrontMonthContract(buffer) {
633
- const result = { rpCode: [] };
634
- let offset = 0;
635
-
636
- while (offset < buffer.length) {
637
- try {
638
- const [tag, tagOffset] = readVarint(buffer, offset);
639
- const wireType = tag & 0x7;
640
- const fieldNumber = tag >>> 3;
641
- offset = tagOffset;
642
-
643
- switch (fieldNumber) {
644
- case SYMBOL_FIELDS.TEMPLATE_ID:
645
- [result.templateId, offset] = readVarint(buffer, offset);
646
- break;
647
- case SYMBOL_FIELDS.RP_CODE:
648
- let rpCode;
649
- [rpCode, offset] = readLengthDelimited(buffer, offset);
650
- result.rpCode.push(rpCode);
651
- break;
652
- case SYMBOL_FIELDS.SYMBOL:
653
- [result.symbol, offset] = readLengthDelimited(buffer, offset);
654
- break;
655
- case SYMBOL_FIELDS.EXCHANGE:
656
- [result.exchange, offset] = readLengthDelimited(buffer, offset);
657
- break;
658
- case SYMBOL_FIELDS.TRADING_SYMBOL:
659
- [result.tradingSymbol, offset] = readLengthDelimited(buffer, offset);
660
- break;
661
- case SYMBOL_FIELDS.DESCRIPTION:
662
- [result.description, offset] = readLengthDelimited(buffer, offset);
663
- break;
664
- case SYMBOL_FIELDS.USER_MSG:
665
- [result.userMsg, offset] = readLengthDelimited(buffer, offset);
666
- break;
667
- default:
668
- offset = skipField(buffer, offset, wireType);
669
- }
670
- } catch (error) {
671
- break;
672
- }
673
- }
674
-
675
- return result;
676
- }
677
-
678
242
  // Singleton
679
243
  const proto = new ProtobufHandler();
680
244
 
@@ -0,0 +1,147 @@
1
+ /**
2
+ * HFT Signal Calculation
3
+ * @module services/strategy/hft-signal-calc
4
+ *
5
+ * Signal generation and building logic for HFT strategy
6
+ */
7
+
8
+ /**
9
+ * Calculate trading signal from all models
10
+ * @param {Object} state - Strategy state
11
+ * @param {Object} config - Strategy config
12
+ * @returns {Object} {direction, confidence, scores}
13
+ */
14
+ function calculateSignal(state, config) {
15
+ let direction = 'none';
16
+ let confidence = 0;
17
+
18
+ const { ofiValue, zscore, momentum, buyVolume, sellVolume, cumulativeDelta } = state;
19
+ const { ofiThreshold, zscoreThreshold, momentumThreshold, minConfidence } = config;
20
+
21
+ const scores = {
22
+ ofi: 0,
23
+ zscore: 0,
24
+ momentum: 0,
25
+ delta: 0,
26
+ composite: 0,
27
+ };
28
+
29
+ // === MODEL 1: OFI ===
30
+ const absOfi = Math.abs(ofiValue);
31
+ if (absOfi > ofiThreshold) {
32
+ scores.ofi = Math.min(1.0, absOfi / 0.6);
33
+ }
34
+
35
+ // === MODEL 2: Z-Score Mean Reversion ===
36
+ const absZ = Math.abs(zscore);
37
+ if (absZ > zscoreThreshold) {
38
+ scores.zscore = Math.min(1.0, absZ / 3.0);
39
+ }
40
+
41
+ // === MODEL 3: Momentum ===
42
+ const absMom = Math.abs(momentum);
43
+ if (absMom > momentumThreshold) {
44
+ scores.momentum = Math.min(1.0, absMom / 3.0);
45
+ }
46
+
47
+ // === MODEL 4: Delta ===
48
+ const totalVol = buyVolume + sellVolume;
49
+ if (totalVol > 0) {
50
+ const deltaRatio = cumulativeDelta / totalVol;
51
+ scores.delta = Math.min(1.0, Math.abs(deltaRatio) * 2);
52
+ }
53
+
54
+ // === COMPOSITE SCORE ===
55
+ scores.composite =
56
+ scores.ofi * 0.35 + // OFI: 35%
57
+ scores.zscore * 0.25 + // Z-Score: 25%
58
+ scores.momentum * 0.20 + // Momentum: 20%
59
+ scores.delta * 0.20; // Delta: 20%
60
+
61
+ confidence = scores.composite;
62
+
63
+ // === DETERMINE DIRECTION ===
64
+ if (scores.composite >= minConfidence) {
65
+ // Primary: Mean reversion
66
+ if (absZ > zscoreThreshold) {
67
+ direction = zscore > 0 ? 'short' : 'long';
68
+
69
+ // Confirm with OFI
70
+ const ofiConfirms =
71
+ (direction === 'long' && ofiValue > 0) ||
72
+ (direction === 'short' && ofiValue < 0);
73
+
74
+ if (ofiConfirms) {
75
+ confidence += 0.1;
76
+ } else if (Math.abs(ofiValue) > 0.2) {
77
+ confidence -= 0.15;
78
+ }
79
+ }
80
+ // Fallback: Momentum breakout
81
+ else if (absMom > momentumThreshold * 2 && absOfi > ofiThreshold) {
82
+ direction = momentum > 0 ? 'long' : 'short';
83
+ if ((direction === 'long' && ofiValue < 0) ||
84
+ (direction === 'short' && ofiValue > 0)) {
85
+ direction = 'none';
86
+ }
87
+ }
88
+ }
89
+
90
+ confidence = Math.min(1.0, Math.max(0, confidence));
91
+
92
+ return { direction, confidence, scores };
93
+ }
94
+
95
+ /**
96
+ * Build signal object
97
+ * @param {string} direction
98
+ * @param {number} confidence
99
+ * @param {Object} scores
100
+ * @param {Object} tick
101
+ * @param {Object} state
102
+ * @param {Object} config
103
+ * @returns {Object} Signal object
104
+ */
105
+ function buildSignal(direction, confidence, scores, tick, state, config) {
106
+ const entry = tick.price;
107
+ const isLong = direction === 'long';
108
+
109
+ const { std, ofiValue, zscore, momentum, cumulativeDelta, tickCount, signalCount, contractId, tickSize } = state;
110
+ const { baseStopTicks, baseTargetTicks } = config;
111
+
112
+ // Adaptive stops based on volatility
113
+ const volMult = Math.max(0.5, Math.min(2.0, std / tickSize / 4));
114
+ const stopTicks = Math.round(baseStopTicks * volMult);
115
+ const targetTicks = Math.round(baseTargetTicks * volMult);
116
+
117
+ const stopLoss = isLong
118
+ ? entry - stopTicks * tickSize
119
+ : entry + stopTicks * tickSize;
120
+
121
+ const takeProfit = isLong
122
+ ? entry + targetTicks * tickSize
123
+ : entry - targetTicks * tickSize;
124
+
125
+ return {
126
+ id: `hft-${Date.now()}-${signalCount}`,
127
+ timestamp: Date.now(),
128
+ contractId,
129
+ direction,
130
+ side: isLong ? 0 : 1,
131
+ entry,
132
+ stopLoss,
133
+ takeProfit,
134
+ confidence,
135
+ scores,
136
+ ofi: ofiValue,
137
+ zscore,
138
+ momentum,
139
+ delta: cumulativeDelta,
140
+ tickCount,
141
+ };
142
+ }
143
+
144
+ module.exports = {
145
+ calculateSignal,
146
+ buildSignal,
147
+ };