hedgequantx 2.6.161 → 2.6.162

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/package.json +1 -1
  2. package/src/menus/ai-agent-connect.js +181 -0
  3. package/src/menus/ai-agent-models.js +219 -0
  4. package/src/menus/ai-agent-oauth.js +292 -0
  5. package/src/menus/ai-agent-ui.js +141 -0
  6. package/src/menus/ai-agent.js +88 -1489
  7. package/src/pages/algo/copy-engine.js +449 -0
  8. package/src/pages/algo/copy-trading.js +11 -543
  9. package/src/pages/algo/smart-logs-data.js +218 -0
  10. package/src/pages/algo/smart-logs.js +9 -214
  11. package/src/pages/algo/ui-constants.js +144 -0
  12. package/src/pages/algo/ui-summary.js +184 -0
  13. package/src/pages/algo/ui.js +42 -526
  14. package/src/pages/stats-calculations.js +191 -0
  15. package/src/pages/stats-ui.js +381 -0
  16. package/src/pages/stats.js +14 -507
  17. package/src/services/ai/client-analysis.js +194 -0
  18. package/src/services/ai/client-models.js +333 -0
  19. package/src/services/ai/client.js +6 -489
  20. package/src/services/ai/index.js +2 -257
  21. package/src/services/ai/proxy-install.js +249 -0
  22. package/src/services/ai/proxy-manager.js +29 -411
  23. package/src/services/ai/proxy-remote.js +161 -0
  24. package/src/services/ai/supervisor-optimize.js +215 -0
  25. package/src/services/ai/supervisor-sync.js +178 -0
  26. package/src/services/ai/supervisor.js +50 -515
  27. package/src/services/ai/validation.js +250 -0
  28. package/src/services/hqx-server-events.js +110 -0
  29. package/src/services/hqx-server-handlers.js +217 -0
  30. package/src/services/hqx-server-latency.js +136 -0
  31. package/src/services/hqx-server.js +51 -403
  32. package/src/services/position-constants.js +28 -0
  33. package/src/services/position-manager.js +105 -554
  34. package/src/services/position-momentum.js +206 -0
  35. package/src/services/projectx/accounts.js +142 -0
  36. package/src/services/projectx/index.js +40 -289
  37. package/src/services/projectx/trading.js +180 -0
  38. package/src/services/rithmic/handlers.js +2 -208
  39. package/src/services/rithmic/index.js +32 -542
  40. package/src/services/rithmic/latency-tracker.js +182 -0
  41. package/src/services/rithmic/specs.js +146 -0
  42. package/src/services/rithmic/trade-history.js +254 -0
@@ -9,112 +9,44 @@
9
9
  * - 60 second failsafe exit (NON-NEGOTIABLE)
10
10
  * - VPIN protection filter
11
11
  *
12
- * USES EXISTING MATH MODELS from HQX Ultra Scalping Strategy:
13
- * - OFI (Order Flow Imbalance)
14
- * - Kalman Filter with velocity tracking
15
- * - Z-Score Mean Reversion
16
- * - VPIN for toxicity detection
17
- *
18
12
  * Data source: Rithmic ORDER_PLANT (fills), PNL_PLANT (positions), TICKER_PLANT (prices)
19
13
  */
20
14
 
21
15
  const EventEmitter = require('events');
22
- const { performance } = require('perf_hooks');
23
16
  const { FAST_SCALPING } = require('../config/settings');
24
17
  const { logger } = require('../utils/logger');
18
+ const { MOMENTUM, WEIGHTS } = require('./position-constants');
19
+ const { calculateMomentum, getVPIN } = require('./position-momentum');
25
20
 
26
21
  const log = logger.scope('PositionMgr');
27
22
 
28
- // =============================================================================
29
- // MOMENTUM THRESHOLDS (from analysis)
30
- // =============================================================================
31
- const MOMENTUM = {
32
- STRONG_FAVORABLE: 0.5, // momentum > 0.5 + profit → HOLD
33
- WEAK_THRESHOLD: 0.2, // momentum < 0.2 + profit → EXIT with profit
34
- ADVERSE_THRESHOLD: -0.3, // momentum < -0.3 → EXIT immediately
35
- VPIN_DANGER: 0.7, // VPIN > 0.7 → informed traders = EXIT
36
- };
37
-
38
- // Momentum weights
39
- const WEIGHTS = {
40
- OFI: 0.50, // Order Flow Imbalance - 50%
41
- KALMAN: 0.25, // Kalman Velocity - 25%
42
- ZSCORE: 0.25, // Z-Score progression - 25%
43
- };
44
-
45
- /**
46
- * Position state for tracking
47
- * @typedef {Object} ManagedPosition
48
- * @property {string} orderTag - Entry order correlation ID
49
- * @property {string} accountId - Account ID
50
- * @property {string} symbol - Trading symbol (e.g., NQH5)
51
- * @property {string} exchange - Exchange (e.g., CME)
52
- * @property {number} side - 0=Long, 1=Short
53
- * @property {number} size - Position size
54
- * @property {number} entryPrice - Average fill price
55
- * @property {number} entryTime - Entry timestamp (ms)
56
- * @property {number} fillTime - When fill was confirmed (ms)
57
- * @property {number} highWaterMark - Highest price since entry (for trailing)
58
- * @property {number} lowWaterMark - Lowest price since entry (for trailing)
59
- * @property {string} status - 'pending' | 'active' | 'holding' | 'exiting' | 'closed'
60
- * @property {boolean} holdComplete - True after MIN_HOLD_MS elapsed
61
- * @property {boolean} breakevenActive - True after BE threshold reached
62
- * @property {number|null} breakevenPrice - Price at which BE stop is set
63
- * @property {Object|null} exitReason - Why position was exited
64
- * @property {number} tickSize - Tick size from API
65
- * @property {number} tickValue - Tick value from API
66
- * @property {string} contractId - Contract ID for strategy lookups
67
- */
68
-
69
23
  /**
70
24
  * Position Manager Service
71
25
  * Handles position lifecycle for fast scalping strategy
72
26
  */
73
27
  class PositionManager extends EventEmitter {
74
- /**
75
- * @param {RithmicService} rithmicService - Connected Rithmic service
76
- * @param {Object} strategy - HQX Ultra Scalping strategy instance (M1)
77
- */
78
28
  constructor(rithmicService, strategy = null) {
79
29
  super();
80
30
  this.rithmic = rithmicService;
81
- this.strategy = strategy; // Reference to HQX Ultra Scalping strategy
31
+ this.strategy = strategy;
82
32
 
83
- /** @type {Map<string, ManagedPosition>} orderTag -> position */
84
33
  this.positions = new Map();
85
-
86
- /** @type {Map<string, number>} symbol -> latest price */
87
34
  this.latestPrices = new Map();
88
-
89
- /** @type {Map<string, Object>} symbol -> contract info (tickSize, tickValue) */
90
35
  this.contractInfo = new Map();
91
36
 
92
- /** @type {NodeJS.Timer|null} */
93
37
  this._monitorInterval = null;
94
-
95
- /** @type {boolean} */
96
38
  this._isRunning = false;
97
39
 
98
- // Bind event handlers
99
40
  this._onOrderFilled = this._onOrderFilled.bind(this);
100
41
  this._onPriceUpdate = this._onPriceUpdate.bind(this);
101
42
  this._onPositionUpdate = this._onPositionUpdate.bind(this);
102
43
  }
103
44
 
104
- /**
105
- * Set the strategy reference (for accessing math models)
106
- * @param {Object} strategy - HQX Ultra Scalping strategy instance
107
- */
108
45
  setStrategy(strategy) {
109
46
  this.strategy = strategy;
110
47
  log.debug('Strategy reference set');
111
48
  }
112
49
 
113
- /**
114
- * Set contract info from API (tick size, tick value)
115
- * @param {string} symbol - Trading symbol
116
- * @param {Object} info - { tickSize, tickValue, contractId }
117
- */
118
50
  setContractInfo(symbol, info) {
119
51
  this.contractInfo.set(symbol, {
120
52
  tickSize: info.tickSize,
@@ -124,10 +56,6 @@ class PositionManager extends EventEmitter {
124
56
  log.debug('Contract info set', { symbol, tickSize: info.tickSize, tickValue: info.tickValue });
125
57
  }
126
58
 
127
- /**
128
- * Start the position manager
129
- * Attaches to Rithmic service events
130
- */
131
59
  start() {
132
60
  if (this._isRunning) return;
133
61
 
@@ -139,12 +67,10 @@ class PositionManager extends EventEmitter {
139
67
  momentumWeights: WEIGHTS,
140
68
  });
141
69
 
142
- // Subscribe to Rithmic events
143
70
  this.rithmic.on('orderFilled', this._onOrderFilled);
144
71
  this.rithmic.on('priceUpdate', this._onPriceUpdate);
145
72
  this.rithmic.on('positionUpdate', this._onPositionUpdate);
146
73
 
147
- // Start monitoring loop
148
74
  this._monitorInterval = setInterval(() => {
149
75
  this._monitorPositions();
150
76
  }, FAST_SCALPING.MONITOR_INTERVAL_MS);
@@ -153,21 +79,15 @@ class PositionManager extends EventEmitter {
153
79
  log.debug('Position manager started');
154
80
  }
155
81
 
156
- /**
157
- * Stop the position manager
158
- * Removes event listeners and stops monitoring
159
- */
160
82
  stop() {
161
83
  if (!this._isRunning) return;
162
84
 
163
85
  log.info('Stopping position manager');
164
86
 
165
- // Remove event listeners
166
87
  this.rithmic.off('orderFilled', this._onOrderFilled);
167
88
  this.rithmic.off('priceUpdate', this._onPriceUpdate);
168
89
  this.rithmic.off('positionUpdate', this._onPositionUpdate);
169
90
 
170
- // Stop monitoring
171
91
  if (this._monitorInterval) {
172
92
  clearInterval(this._monitorInterval);
173
93
  this._monitorInterval = null;
@@ -177,15 +97,6 @@ class PositionManager extends EventEmitter {
177
97
  log.debug('Position manager stopped');
178
98
  }
179
99
 
180
- /**
181
- * Register a new entry order
182
- * Called immediately after fastEntry() to track the position
183
- *
184
- * @param {Object} entryResult - Result from fastEntry()
185
- * @param {Object} orderData - Original order data
186
- * @param {Object} contractInfo - { tickSize, tickValue, contractId } from API
187
- * @returns {string} orderTag for tracking
188
- */
189
100
  registerEntry(entryResult, orderData, contractInfo = null) {
190
101
  if (!entryResult.success) {
191
102
  log.warn('Cannot register failed entry', { error: entryResult.error });
@@ -194,33 +105,30 @@ class PositionManager extends EventEmitter {
194
105
 
195
106
  const { orderTag, entryTime, latencyMs } = entryResult;
196
107
 
197
- // Get contract info from cache or parameter
198
108
  const info = contractInfo || this.contractInfo.get(orderData.symbol) || {
199
109
  tickSize: null,
200
110
  tickValue: null,
201
111
  contractId: orderData.contractId || orderData.symbol,
202
112
  };
203
113
 
204
- /** @type {ManagedPosition} */
205
114
  const position = {
206
115
  orderTag,
207
116
  accountId: orderData.accountId,
208
117
  symbol: orderData.symbol,
209
118
  exchange: orderData.exchange || 'CME',
210
- side: orderData.side, // 0=Long, 1=Short
119
+ side: orderData.side,
211
120
  size: orderData.size,
212
- entryPrice: null, // Will be filled from order notification (async)
121
+ entryPrice: null,
213
122
  entryTime,
214
123
  fillTime: null,
215
124
  highWaterMark: null,
216
125
  lowWaterMark: null,
217
- status: 'pending', // Waiting for fill confirmation
126
+ status: 'pending',
218
127
  holdComplete: false,
219
- breakevenActive: false, // BE not yet activated
220
- breakevenPrice: null, // Will be set when BE activates
128
+ breakevenActive: false,
129
+ breakevenPrice: null,
221
130
  exitReason: null,
222
131
  latencyMs,
223
- // Contract info from API (NOT hardcoded)
224
132
  tickSize: info.tickSize,
225
133
  tickValue: info.tickValue,
226
134
  contractId: info.contractId,
@@ -240,45 +148,17 @@ class PositionManager extends EventEmitter {
240
148
  return orderTag;
241
149
  }
242
150
 
243
- /**
244
- * Handle order fill notification from Rithmic (templateId: 351)
245
- * This is ASYNC - does not block fastEntry()
246
- * @private
247
- */
248
151
  _onOrderFilled(fillInfo) {
249
- const { orderTag, avgFillPrice, totalFillQuantity, symbol, transactionType, localTimestamp } = fillInfo;
152
+ const { orderTag, avgFillPrice, symbol } = fillInfo;
250
153
 
251
154
  if (!orderTag) return;
252
155
 
253
156
  const position = this.positions.get(orderTag);
254
157
  if (!position) {
255
- // Could be an exit order fill - check if any position is exiting
158
+ // Check if this is an exit fill
256
159
  for (const [tag, pos] of this.positions) {
257
160
  if (pos.status === 'exiting' && pos.symbol === symbol) {
258
- // This is likely our exit fill
259
- pos.status = 'closed';
260
- const holdDuration = Date.now() - pos.fillTime;
261
- const pnlTicks = this._calculatePnlTicks(pos, avgFillPrice);
262
-
263
- log.info('EXIT FILLED', {
264
- orderTag: tag,
265
- symbol,
266
- exitPrice: avgFillPrice,
267
- entryPrice: pos.entryPrice,
268
- pnlTicks,
269
- holdDurationMs: holdDuration,
270
- reason: pos.exitReason,
271
- });
272
-
273
- this.emit('exitFilled', {
274
- orderTag: tag,
275
- position: pos,
276
- exitPrice: avgFillPrice,
277
- pnlTicks,
278
- holdDurationMs: holdDuration,
279
- });
280
-
281
- this.positions.delete(tag);
161
+ this._handleExitFill(tag, pos, avgFillPrice);
282
162
  return;
283
163
  }
284
164
  }
@@ -287,90 +167,72 @@ class PositionManager extends EventEmitter {
287
167
  }
288
168
 
289
169
  if (position.status === 'pending') {
290
- // Entry fill confirmed - UPDATE with real fill price
291
- position.entryPrice = avgFillPrice;
292
- position.fillTime = Date.now();
293
- position.highWaterMark = avgFillPrice;
294
- position.lowWaterMark = avgFillPrice;
295
- position.status = 'holding'; // Now in holding period
296
-
297
- const fillLatency = position.fillTime - position.entryTime;
298
-
299
- log.info('ENTRY FILLED', {
300
- orderTag,
301
- symbol,
302
- side: position.side === 0 ? 'LONG' : 'SHORT',
303
- size: position.size,
304
- price: avgFillPrice,
305
- entryLatencyMs: position.latencyMs,
306
- fillLatencyMs: fillLatency,
307
- });
308
-
309
- this.emit('entryFilled', {
310
- orderTag,
311
- position,
312
- fillLatencyMs: fillLatency,
313
- });
314
-
315
- // Schedule hold completion check
316
- setTimeout(() => {
317
- this._onHoldComplete(orderTag);
318
- }, FAST_SCALPING.MIN_HOLD_MS);
319
-
320
- // Schedule 60s failsafe (NON-NEGOTIABLE)
321
- setTimeout(() => {
322
- this._failsafeExit(orderTag);
323
- }, FAST_SCALPING.MAX_HOLD_MS);
324
-
170
+ this._handleEntryFill(orderTag, position, avgFillPrice);
325
171
  } else if (position.status === 'exiting') {
326
- // Exit fill confirmed
327
- position.status = 'closed';
328
-
329
- const holdDuration = Date.now() - position.fillTime;
330
- const pnlTicks = this._calculatePnlTicks(position, avgFillPrice);
331
-
332
- log.info('EXIT FILLED', {
333
- orderTag,
334
- symbol,
335
- exitPrice: avgFillPrice,
336
- entryPrice: position.entryPrice,
337
- pnlTicks,
338
- holdDurationMs: holdDuration,
339
- reason: position.exitReason,
340
- });
341
-
342
- this.emit('exitFilled', {
343
- orderTag,
344
- position,
345
- exitPrice: avgFillPrice,
346
- pnlTicks,
347
- holdDurationMs: holdDuration,
348
- });
349
-
350
- // Clean up
351
- this.positions.delete(orderTag);
172
+ this._handleExitFill(orderTag, position, avgFillPrice);
352
173
  }
353
174
  }
354
175
 
355
- /**
356
- * 60 second failsafe exit (NON-NEGOTIABLE)
357
- * Forces market exit if position still open
358
- * @private
359
- */
176
+ _handleEntryFill(orderTag, position, avgFillPrice) {
177
+ position.entryPrice = avgFillPrice;
178
+ position.fillTime = Date.now();
179
+ position.highWaterMark = avgFillPrice;
180
+ position.lowWaterMark = avgFillPrice;
181
+ position.status = 'holding';
182
+
183
+ const fillLatency = position.fillTime - position.entryTime;
184
+
185
+ log.info('ENTRY FILLED', {
186
+ orderTag,
187
+ symbol: position.symbol,
188
+ side: position.side === 0 ? 'LONG' : 'SHORT',
189
+ size: position.size,
190
+ price: avgFillPrice,
191
+ entryLatencyMs: position.latencyMs,
192
+ fillLatencyMs: fillLatency,
193
+ });
194
+
195
+ this.emit('entryFilled', { orderTag, position, fillLatencyMs: fillLatency });
196
+
197
+ setTimeout(() => this._onHoldComplete(orderTag), FAST_SCALPING.MIN_HOLD_MS);
198
+ setTimeout(() => this._failsafeExit(orderTag), FAST_SCALPING.MAX_HOLD_MS);
199
+ }
200
+
201
+ _handleExitFill(orderTag, position, avgFillPrice) {
202
+ position.status = 'closed';
203
+ const holdDuration = Date.now() - position.fillTime;
204
+ const pnlTicks = this._calculatePnlTicks(position, avgFillPrice);
205
+
206
+ log.info('EXIT FILLED', {
207
+ orderTag,
208
+ symbol: position.symbol,
209
+ exitPrice: avgFillPrice,
210
+ entryPrice: position.entryPrice,
211
+ pnlTicks,
212
+ holdDurationMs: holdDuration,
213
+ reason: position.exitReason,
214
+ });
215
+
216
+ this.emit('exitFilled', {
217
+ orderTag,
218
+ position,
219
+ exitPrice: avgFillPrice,
220
+ pnlTicks,
221
+ holdDurationMs: holdDuration,
222
+ });
223
+
224
+ this.positions.delete(orderTag);
225
+ }
226
+
360
227
  _failsafeExit(orderTag) {
361
228
  const position = this.positions.get(orderTag);
362
229
  if (!position) return;
363
230
 
364
- // Only force exit if still active (not already exiting/closed)
365
231
  if (position.status === 'holding' || position.status === 'active') {
366
232
  const currentPrice = this.latestPrices.get(position.symbol);
367
233
  const pnlTicks = currentPrice ? this._calculatePnlTicks(position, currentPrice) : 0;
368
234
 
369
- log.warn('FAILSAFE EXIT - 60s max hold exceeded', {
370
- orderTag,
371
- symbol: position.symbol,
372
- pnlTicks,
373
- });
235
+ log.warn('FAILSAFE EXIT - 60s max hold exceeded', { orderTag, symbol: position.symbol, pnlTicks });
374
236
 
375
237
  this._executeExit(orderTag, {
376
238
  type: 'failsafe',
@@ -380,49 +242,33 @@ class PositionManager extends EventEmitter {
380
242
  }
381
243
  }
382
244
 
383
- /**
384
- * Handle price update from market data
385
- * @private
386
- */
387
245
  _onPriceUpdate(priceData) {
388
- const { symbol, price, timestamp } = priceData;
246
+ const { symbol, price } = priceData;
389
247
 
390
248
  this.latestPrices.set(symbol, price);
391
249
 
392
- // Update high/low water marks for active positions
393
- for (const [orderTag, position] of this.positions) {
250
+ for (const [, position] of this.positions) {
394
251
  if (position.symbol === symbol && (position.status === 'holding' || position.status === 'active') && position.entryPrice) {
395
- if (position.side === 0) { // Long
252
+ if (position.side === 0) {
396
253
  position.highWaterMark = Math.max(position.highWaterMark, price);
397
- } else { // Short
254
+ } else {
398
255
  position.lowWaterMark = Math.min(position.lowWaterMark, price);
399
256
  }
400
257
  }
401
258
  }
402
259
  }
403
260
 
404
- /**
405
- * Handle position update from PNL_PLANT
406
- * @private
407
- */
408
261
  _onPositionUpdate(posData) {
409
- log.debug('Position update from PNL_PLANT', {
410
- symbol: posData?.symbol,
411
- qty: posData?.quantity,
412
- });
262
+ log.debug('Position update from PNL_PLANT', { symbol: posData?.symbol, qty: posData?.quantity });
413
263
  }
414
264
 
415
- /**
416
- * Called when minimum hold period is complete
417
- * @private
418
- */
419
265
  _onHoldComplete(orderTag) {
420
266
  const position = this.positions.get(orderTag);
421
267
  if (!position) return;
422
268
 
423
269
  if (position.status === 'holding') {
424
270
  position.holdComplete = true;
425
- position.status = 'active'; // Now eligible for exit
271
+ position.status = 'active';
426
272
 
427
273
  log.info('Hold complete - now monitoring for exit', {
428
274
  orderTag,
@@ -434,15 +280,10 @@ class PositionManager extends EventEmitter {
434
280
  }
435
281
  }
436
282
 
437
- /**
438
- * Main monitoring loop - runs every MONITOR_INTERVAL_MS
439
- * @private
440
- */
441
283
  _monitorPositions() {
442
284
  const now = Date.now();
443
285
 
444
286
  for (const [orderTag, position] of this.positions) {
445
- // Skip if not ready to exit
446
287
  if (position.status !== 'active') continue;
447
288
 
448
289
  const currentPrice = this.latestPrices.get(position.symbol);
@@ -451,7 +292,6 @@ class PositionManager extends EventEmitter {
451
292
  const holdDuration = now - position.fillTime;
452
293
  const pnlTicks = this._calculatePnlTicks(position, currentPrice);
453
294
 
454
- // Check exit conditions
455
295
  const exitReason = this._checkExitConditions(position, currentPrice, pnlTicks, holdDuration);
456
296
 
457
297
  if (exitReason) {
@@ -460,66 +300,42 @@ class PositionManager extends EventEmitter {
460
300
  }
461
301
  }
462
302
 
463
- /**
464
- * Check if position should be exited
465
- * Uses momentum calculation with OFI/Kalman/Z-Score from strategy
466
- *
467
- * Data sources:
468
- * - pnlTicks: Calculated from entryPrice (Rithmic fill) and currentPrice (market data)
469
- * - VPIN: Strategy's computeVPIN()
470
- * - Momentum: Strategy's OFI, Kalman, Z-Score
471
- *
472
- * @private
473
- * @returns {Object|null} Exit reason or null
474
- */
475
- _checkExitConditions(position, currentPrice, pnlTicks, holdDuration) {
476
- // Cannot evaluate exit conditions without PnL data
477
- if (pnlTicks === null) {
478
- log.debug('Cannot check exit - no PnL data', { symbol: position.symbol });
479
- return null;
480
- }
303
+ _checkExitConditions(position, currentPrice, pnlTicks) {
304
+ if (pnlTicks === null) return null;
481
305
 
482
306
  const targetTicks = FAST_SCALPING.TARGET_TICKS;
483
307
  const stopTicks = FAST_SCALPING.STOP_TICKS;
484
308
 
485
- // 1. TARGET HIT - Always exit at target
309
+ // 1. TARGET HIT
486
310
  if (pnlTicks >= targetTicks) {
487
311
  return { type: 'target', reason: 'Target reached', pnlTicks };
488
312
  }
489
313
 
490
- // 2. BREAKEVEN CHECK - If BE is active, use BE price as stop
314
+ // 2. BREAKEVEN CHECK
491
315
  if (position.breakevenActive && position.breakevenPrice !== null) {
492
316
  const tickSize = this._getTickSize(position);
493
317
  if (tickSize) {
494
- // Check if price hit breakeven stop
495
- if (position.side === 0) { // Long
496
- if (currentPrice <= position.breakevenPrice) {
497
- return { type: 'breakeven', reason: 'Breakeven stop hit', pnlTicks };
498
- }
499
- } else { // Short
500
- if (currentPrice >= position.breakevenPrice) {
501
- return { type: 'breakeven', reason: 'Breakeven stop hit', pnlTicks };
502
- }
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 };
503
322
  }
504
323
  }
505
324
  }
506
325
 
507
- // 3. STOP HIT - Only if BE not active (original stop)
326
+ // 3. STOP HIT (only if BE not active)
508
327
  if (!position.breakevenActive && pnlTicks <= -stopTicks) {
509
328
  return { type: 'stop', reason: 'Stop loss hit', pnlTicks };
510
329
  }
511
330
 
512
- // 4. ACTIVATE BREAKEVEN - Move stop to entry after profit threshold
331
+ // 4. ACTIVATE BREAKEVEN
513
332
  if (!position.breakevenActive && pnlTicks >= FAST_SCALPING.BREAKEVEN_ACTIVATION_TICKS) {
514
333
  const tickSize = this._getTickSize(position);
515
334
  if (tickSize && position.entryPrice) {
516
- // Set BE price at entry + offset (small profit lock)
517
335
  const offset = FAST_SCALPING.BREAKEVEN_OFFSET_TICKS * tickSize;
518
- if (position.side === 0) { // Long
519
- position.breakevenPrice = position.entryPrice + offset;
520
- } else { // Short
521
- position.breakevenPrice = position.entryPrice - offset;
522
- }
336
+ position.breakevenPrice = position.side === 0
337
+ ? position.entryPrice + offset
338
+ : position.entryPrice - offset;
523
339
  position.breakevenActive = true;
524
340
 
525
341
  log.info('BREAKEVEN ACTIVATED', {
@@ -538,13 +354,13 @@ class PositionManager extends EventEmitter {
538
354
  }
539
355
  }
540
356
 
541
- // 5. VPIN DANGER - Informed traders detected (from strategy)
542
- const vpin = this._getVPIN(position);
357
+ // 5. VPIN DANGER
358
+ const vpin = getVPIN(this.strategy, position.contractId || position.symbol);
543
359
  if (vpin !== null && vpin > MOMENTUM.VPIN_DANGER) {
544
360
  return { type: 'vpin', reason: `VPIN spike ${(vpin * 100).toFixed(0)}% - informed traders`, pnlTicks, vpin };
545
361
  }
546
362
 
547
- // 6. TRAILING STOP (only if in profit above threshold)
363
+ // 6. TRAILING STOP
548
364
  if (pnlTicks >= FAST_SCALPING.TRAILING_ACTIVATION_TICKS) {
549
365
  const trailingPnl = this._calculateTrailingPnl(position, currentPrice);
550
366
  if (trailingPnl !== null && trailingPnl <= -FAST_SCALPING.TRAILING_DISTANCE_TICKS) {
@@ -552,22 +368,24 @@ class PositionManager extends EventEmitter {
552
368
  }
553
369
  }
554
370
 
555
- // 7. MOMENTUM-BASED EXIT (using strategy's math models)
556
- const momentum = this._calculateMomentum(position);
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
+ });
557
379
 
558
380
  if (momentum !== null) {
559
- // Strong favorable momentum + profit → HOLD (let it run)
560
381
  if (momentum > MOMENTUM.STRONG_FAVORABLE && pnlTicks > 4) {
561
- // Don't exit - momentum is strong in our favor
562
- return null;
382
+ return null; // Don't exit - momentum is strong
563
383
  }
564
384
 
565
- // Weak momentum + profit → EXIT (secure profit)
566
385
  if (momentum < MOMENTUM.WEAK_THRESHOLD && pnlTicks > 0) {
567
386
  return { type: 'momentum_weak', reason: 'Weak momentum - securing profit', pnlTicks, momentum };
568
387
  }
569
388
 
570
- // Adverse momentum → EXIT immediately
571
389
  if (momentum < MOMENTUM.ADVERSE_THRESHOLD) {
572
390
  return { type: 'momentum_adverse', reason: 'Adverse momentum detected', pnlTicks, momentum };
573
391
  }
@@ -576,210 +394,6 @@ class PositionManager extends EventEmitter {
576
394
  return null;
577
395
  }
578
396
 
579
- /**
580
- * Calculate momentum score using strategy's existing math models
581
- * Weighted: OFI (50%) + Kalman Velocity (25%) + Z-Score (25%)
582
- *
583
- * Data sources:
584
- * - OFI: Strategy's computeOrderFlowImbalance()
585
- * - Kalman: Strategy's kalmanStates
586
- * - Z-Score: Strategy's computeZScore()
587
- *
588
- * @private
589
- * @param {ManagedPosition} position
590
- * @returns {number|null} Momentum score [-1 to 1], positive = favorable, null if insufficient data
591
- */
592
- _calculateMomentum(position) {
593
- if (!this.strategy) {
594
- return null;
595
- }
596
-
597
- // Get individual model values (all from API/strategy, not invented)
598
- const ofi = this._getOFI(position);
599
- const velocity = this._getKalmanVelocity(position);
600
- const zscore = this._getZScore(position);
601
-
602
- // Count how many models have data
603
- let availableModels = 0;
604
- let totalWeight = 0;
605
- let weightedSum = 0;
606
-
607
- // 1. OFI (50%) - Order Flow Imbalance
608
- if (ofi !== null) {
609
- // For long: positive OFI = favorable, For short: negative OFI = favorable
610
- const favorableOfi = position.side === 0 ? ofi : -ofi;
611
- const ofiScore = Math.min(1, Math.max(-1, favorableOfi));
612
- weightedSum += ofiScore * WEIGHTS.OFI;
613
- totalWeight += WEIGHTS.OFI;
614
- availableModels++;
615
- }
616
-
617
- // 2. Kalman Velocity (25%)
618
- if (velocity !== null) {
619
- const tickSize = this._getTickSize(position);
620
- if (tickSize !== null) {
621
- // Normalize velocity: favorable direction = positive
622
- const favorableVelocity = position.side === 0 ? velocity : -velocity;
623
- // Normalize to [-1, 1]: 4 ticks of velocity = 1.0 score
624
- const normalizedVelocity = favorableVelocity / (tickSize * 4);
625
- const velocityScore = Math.min(1, Math.max(-1, normalizedVelocity));
626
- weightedSum += velocityScore * WEIGHTS.KALMAN;
627
- totalWeight += WEIGHTS.KALMAN;
628
- availableModels++;
629
- }
630
- }
631
-
632
- // 3. Z-Score (25%) - Progression toward mean
633
- if (zscore !== null) {
634
- let zscoreScore;
635
- if (position.side === 0) {
636
- // Long: entered when Z < -threshold, improving = Z moving toward 0
637
- // Z > -0.5 means close to mean = favorable
638
- zscoreScore = zscore > -0.5 ? 0.5 : -0.5;
639
- } else {
640
- // Short: entered when Z > threshold, improving = Z moving toward 0
641
- // Z < 0.5 means close to mean = favorable
642
- zscoreScore = zscore < 0.5 ? 0.5 : -0.5;
643
- }
644
- weightedSum += zscoreScore * WEIGHTS.ZSCORE;
645
- totalWeight += WEIGHTS.ZSCORE;
646
- availableModels++;
647
- }
648
-
649
- // Need at least 1 model with data to calculate momentum
650
- if (availableModels === 0 || totalWeight === 0) {
651
- return null;
652
- }
653
-
654
- // Normalize by actual total weight (in case some models unavailable)
655
- const momentum = weightedSum / totalWeight;
656
-
657
- // Clamp to [-1, 1]
658
- return Math.min(1, Math.max(-1, momentum));
659
- }
660
-
661
- /**
662
- * Get OFI (Order Flow Imbalance) from strategy
663
- * Data source: Strategy's computeOrderFlowImbalance() or getModelValues()
664
- * @private
665
- * @returns {number|null} OFI value [-1, 1] or null if unavailable
666
- */
667
- _getOFI(position) {
668
- if (!this.strategy) return null;
669
-
670
- const contractId = position.contractId || position.symbol;
671
-
672
- // Try strategy's computeOrderFlowImbalance (direct calculation from bars)
673
- if (typeof this.strategy.computeOrderFlowImbalance === 'function') {
674
- const bars = this.strategy.getBarHistory?.(contractId);
675
- if (bars && bars.length >= 20) {
676
- try {
677
- return this.strategy.computeOrderFlowImbalance(bars);
678
- } catch (error) {
679
- log.debug('OFI calculation failed', { error: error.message });
680
- }
681
- }
682
- }
683
-
684
- // Try getModelValues (pre-calculated values)
685
- const modelValues = this.strategy.getModelValues?.(contractId);
686
- if (modelValues && modelValues.rawOfi !== undefined) {
687
- return modelValues.rawOfi;
688
- }
689
-
690
- return null;
691
- }
692
-
693
- /**
694
- * Get Kalman velocity from strategy's Kalman filter
695
- * Data source: Strategy's kalmanStates or kalmanFilterManager
696
- * @private
697
- * @returns {number|null} Velocity value or null if unavailable
698
- */
699
- _getKalmanVelocity(position) {
700
- if (!this.strategy) return null;
701
-
702
- const contractId = position.contractId || position.symbol;
703
-
704
- // Try to access kalmanStates from strategy
705
- if (this.strategy.kalmanStates) {
706
- const state = this.strategy.kalmanStates.get(contractId);
707
- if (state && typeof state.estimate === 'number') {
708
- const currentPrice = this.latestPrices.get(position.symbol);
709
- if (currentPrice !== undefined && currentPrice !== null) {
710
- // Velocity = price difference from Kalman estimate
711
- // Positive = price above estimate (upward momentum)
712
- return currentPrice - state.estimate;
713
- }
714
- }
715
- }
716
-
717
- return null;
718
- }
719
-
720
- /**
721
- * Get Z-Score from strategy
722
- * Data source: Strategy's computeZScore() or priceBuffer
723
- * @private
724
- * @returns {number|null} Z-Score value or null if unavailable
725
- */
726
- _getZScore(position) {
727
- if (!this.strategy) return null;
728
-
729
- const contractId = position.contractId || position.symbol;
730
-
731
- // Try strategy's computeZScore (direct calculation from price buffer)
732
- if (typeof this.strategy.computeZScore === 'function') {
733
- const prices = this.strategy.priceBuffer?.get(contractId);
734
- if (prices && prices.length >= 50) {
735
- try {
736
- return this.strategy.computeZScore(prices);
737
- } catch (error) {
738
- log.debug('Z-Score calculation failed', { error: error.message });
739
- }
740
- }
741
- }
742
-
743
- return null;
744
- }
745
-
746
- /**
747
- * Get VPIN from strategy
748
- * Data source: Strategy's computeVPIN() or volumeBuffer
749
- * @private
750
- * @returns {number|null} VPIN value [0, 1] or null if unavailable
751
- */
752
- _getVPIN(position) {
753
- if (!this.strategy) return null;
754
-
755
- const contractId = position.contractId || position.symbol;
756
-
757
- // Try strategy's computeVPIN (direct calculation from volume buffer)
758
- if (typeof this.strategy.computeVPIN === 'function') {
759
- const volumes = this.strategy.volumeBuffer?.get(contractId);
760
- if (volumes && volumes.length >= 50) {
761
- try {
762
- return this.strategy.computeVPIN(volumes);
763
- } catch (error) {
764
- log.debug('VPIN calculation failed', { error: error.message });
765
- }
766
- }
767
- }
768
-
769
- // Try getModelValues (pre-calculated, stored as 1 - vpin for scoring)
770
- const modelValues = this.strategy.getModelValues?.(contractId);
771
- if (modelValues && typeof modelValues.vpin === 'number') {
772
- // modelValues.vpin is normalized score (1 - vpin), convert back
773
- return 1 - modelValues.vpin;
774
- }
775
-
776
- return null;
777
- }
778
-
779
- /**
780
- * Execute exit order
781
- * @private
782
- */
783
397
  _executeExit(orderTag, exitReason) {
784
398
  const position = this.positions.get(orderTag);
785
399
  if (!position || position.status === 'exiting' || position.status === 'closed') return;
@@ -795,8 +409,7 @@ class PositionManager extends EventEmitter {
795
409
  momentum: exitReason.momentum,
796
410
  });
797
411
 
798
- // Fire exit order (opposite side)
799
- const exitSide = position.side === 0 ? 1 : 0; // Reverse: Long->Sell, Short->Buy
412
+ const exitSide = position.side === 0 ? 1 : 0;
800
413
 
801
414
  const exitResult = this.rithmic.fastExit({
802
415
  accountId: position.accountId,
@@ -820,41 +433,26 @@ class PositionManager extends EventEmitter {
820
433
  latencyMs: exitResult.latencyMs,
821
434
  });
822
435
  } else {
823
- log.error('Exit order FAILED', {
824
- orderTag,
825
- error: exitResult.error,
826
- });
827
- // Reset status to try again next cycle
436
+ log.error('Exit order FAILED', { orderTag, error: exitResult.error });
828
437
  position.status = 'active';
829
438
  position.exitReason = null;
830
439
  }
831
440
  }
832
441
 
833
- /**
834
- * Get tick size for position (from API, not hardcoded)
835
- * @private
836
- */
837
442
  _getTickSize(position) {
838
- // First try position's stored tickSize (from API)
839
443
  if (position.tickSize !== null && position.tickSize !== undefined) {
840
444
  return position.tickSize;
841
445
  }
842
446
 
843
- // Then try contract info cache
844
447
  const info = this.contractInfo.get(position.symbol);
845
448
  if (info && info.tickSize) {
846
449
  return info.tickSize;
847
450
  }
848
451
 
849
- // Last resort: log warning and return null (will cause issues)
850
452
  log.warn('No tick size available for symbol', { symbol: position.symbol });
851
453
  return null;
852
454
  }
853
455
 
854
- /**
855
- * Get tick value for position (from API, not hardcoded)
856
- * @private
857
- */
858
456
  _getTickValue(position) {
859
457
  if (position.tickValue !== null && position.tickValue !== undefined) {
860
458
  return position.tickValue;
@@ -869,23 +467,11 @@ class PositionManager extends EventEmitter {
869
467
  return null;
870
468
  }
871
469
 
872
- /**
873
- * Calculate P&L in ticks from entry
874
- * Data source: position.entryPrice (from Rithmic fill), tickSize (from API)
875
- * @private
876
- * @returns {number|null} PnL in ticks, null if cannot calculate
877
- */
878
470
  _calculatePnlTicks(position, currentPrice) {
879
- if (position.entryPrice === null || position.entryPrice === undefined) {
880
- return null;
881
- }
882
-
883
- if (currentPrice === null || currentPrice === undefined) {
884
- return null;
885
- }
471
+ if (position.entryPrice === null || currentPrice === null) return null;
886
472
 
887
473
  const tickSize = this._getTickSize(position);
888
- if (tickSize === null || tickSize === undefined) {
474
+ if (tickSize === null) {
889
475
  log.error('Cannot calculate PnL - no tick size from API', { symbol: position.symbol });
890
476
  return null;
891
477
  }
@@ -896,58 +482,31 @@ class PositionManager extends EventEmitter {
896
482
  return Math.round(signedDiff / tickSize);
897
483
  }
898
484
 
899
- /**
900
- * Calculate trailing P&L from high/low water mark
901
- * Data source: position water marks (from price updates), tickSize (from API)
902
- * @private
903
- * @returns {number|null} Trailing PnL in ticks, null if cannot calculate
904
- */
905
485
  _calculateTrailingPnl(position, currentPrice) {
906
486
  const tickSize = this._getTickSize(position);
907
- if (tickSize === null || tickSize === undefined) {
908
- return null;
909
- }
487
+ if (tickSize === null) return null;
910
488
 
911
- if (position.side === 0) { // Long
912
- if (position.highWaterMark === null || position.highWaterMark === undefined) {
913
- return null;
914
- }
489
+ if (position.side === 0) {
490
+ if (position.highWaterMark === null) return null;
915
491
  const dropFromHigh = position.highWaterMark - currentPrice;
916
492
  return -Math.round(dropFromHigh / tickSize);
917
- } else { // Short
918
- if (position.lowWaterMark === null || position.lowWaterMark === undefined) {
919
- return null;
920
- }
493
+ } else {
494
+ if (position.lowWaterMark === null) return null;
921
495
  const riseFromLow = currentPrice - position.lowWaterMark;
922
496
  return -Math.round(riseFromLow / tickSize);
923
497
  }
924
498
  }
925
499
 
926
- /**
927
- * Get all active positions
928
- * @returns {Array<ManagedPosition>}
929
- */
930
500
  getActivePositions() {
931
501
  return Array.from(this.positions.values()).filter(
932
502
  p => p.status === 'holding' || p.status === 'active'
933
503
  );
934
504
  }
935
505
 
936
- /**
937
- * Get position by order tag
938
- * @param {string} orderTag
939
- * @returns {ManagedPosition|null}
940
- */
941
506
  getPosition(orderTag) {
942
507
  return this.positions.get(orderTag) || null;
943
508
  }
944
509
 
945
- /**
946
- * Check if we can enter a new position
947
- * (no existing position in same symbol - 1 position at a time)
948
- * @param {string} symbol
949
- * @returns {boolean}
950
- */
951
510
  canEnter(symbol) {
952
511
  for (const position of this.positions.values()) {
953
512
  if (position.symbol === symbol && position.status !== 'closed') {
@@ -957,18 +516,10 @@ class PositionManager extends EventEmitter {
957
516
  return true;
958
517
  }
959
518
 
960
- /**
961
- * Get momentum thresholds (for UI display)
962
- * @returns {Object}
963
- */
964
519
  getMomentumThresholds() {
965
520
  return { ...MOMENTUM };
966
521
  }
967
522
 
968
- /**
969
- * Get momentum weights (for UI display)
970
- * @returns {Object}
971
- */
972
523
  getMomentumWeights() {
973
524
  return { ...WEIGHTS };
974
525
  }