hedgequantx 2.6.161 → 2.6.163

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) 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/providers/direct-providers.js +323 -0
  22. package/src/services/ai/providers/index.js +8 -472
  23. package/src/services/ai/providers/other-providers.js +104 -0
  24. package/src/services/ai/proxy-install.js +249 -0
  25. package/src/services/ai/proxy-manager.js +29 -411
  26. package/src/services/ai/proxy-remote.js +161 -0
  27. package/src/services/ai/supervisor-optimize.js +215 -0
  28. package/src/services/ai/supervisor-sync.js +178 -0
  29. package/src/services/ai/supervisor.js +50 -515
  30. package/src/services/ai/validation.js +250 -0
  31. package/src/services/hqx-server-events.js +110 -0
  32. package/src/services/hqx-server-handlers.js +217 -0
  33. package/src/services/hqx-server-latency.js +136 -0
  34. package/src/services/hqx-server.js +51 -403
  35. package/src/services/position-constants.js +28 -0
  36. package/src/services/position-exit-logic.js +174 -0
  37. package/src/services/position-manager.js +90 -629
  38. package/src/services/position-momentum.js +206 -0
  39. package/src/services/projectx/accounts.js +142 -0
  40. package/src/services/projectx/index.js +40 -289
  41. package/src/services/projectx/trading.js +180 -0
  42. package/src/services/rithmic/contracts.js +218 -0
  43. package/src/services/rithmic/handlers.js +2 -208
  44. package/src/services/rithmic/index.js +28 -712
  45. package/src/services/rithmic/latency-tracker.js +182 -0
  46. package/src/services/rithmic/market-data-decoders.js +229 -0
  47. package/src/services/rithmic/market-data.js +1 -278
  48. package/src/services/rithmic/orders-fast.js +246 -0
  49. package/src/services/rithmic/orders.js +1 -251
  50. package/src/services/rithmic/proto-decoders.js +403 -0
  51. package/src/services/rithmic/protobuf.js +7 -443
  52. package/src/services/rithmic/specs.js +146 -0
  53. package/src/services/rithmic/trade-history.js +254 -0
  54. package/src/services/strategy/hft-signal-calc.js +147 -0
  55. package/src/services/strategy/hft-tick.js +33 -133
  56. package/src/services/tradovate/index.js +6 -119
  57. package/src/services/tradovate/orders.js +145 -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 { checkExitConditions, checkBreakevenActivation, calculatePnlTicks } = require('./position-exit-logic');
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,75 +292,10 @@ 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
- const exitReason = this._checkExitConditions(position, currentPrice, pnlTicks, holdDuration);
456
-
457
- if (exitReason) {
458
- this._executeExit(orderTag, exitReason);
459
- }
460
- }
461
- }
462
-
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
- }
481
-
482
- const targetTicks = FAST_SCALPING.TARGET_TICKS;
483
- const stopTicks = FAST_SCALPING.STOP_TICKS;
484
-
485
- // 1. TARGET HIT - Always exit at target
486
- if (pnlTicks >= targetTicks) {
487
- return { type: 'target', reason: 'Target reached', pnlTicks };
488
- }
489
-
490
- // 2. BREAKEVEN CHECK - If BE is active, use BE price as stop
491
- if (position.breakevenActive && position.breakevenPrice !== null) {
492
- const tickSize = this._getTickSize(position);
493
- 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
- }
503
- }
504
- }
505
- }
506
-
507
- // 3. STOP HIT - Only if BE not active (original stop)
508
- if (!position.breakevenActive && pnlTicks <= -stopTicks) {
509
- return { type: 'stop', reason: 'Stop loss hit', pnlTicks };
510
- }
511
-
512
- // 4. ACTIVATE BREAKEVEN - Move stop to entry after profit threshold
513
- if (!position.breakevenActive && pnlTicks >= FAST_SCALPING.BREAKEVEN_ACTIVATION_TICKS) {
514
- const tickSize = this._getTickSize(position);
515
- if (tickSize && position.entryPrice) {
516
- // Set BE price at entry + offset (small profit lock)
517
- 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
- }
295
+ // Check for breakeven activation
296
+ const beActivation = checkBreakevenActivation(position, pnlTicks, (p) => this._getTickSize(p));
297
+ if (beActivation) {
298
+ position.breakevenPrice = beActivation.breakevenPrice;
523
299
  position.breakevenActive = true;
524
300
 
525
301
  log.info('BREAKEVEN ACTIVATED', {
@@ -536,250 +312,24 @@ class PositionManager extends EventEmitter {
536
312
  pnlTicks,
537
313
  });
538
314
  }
539
- }
540
-
541
- // 5. VPIN DANGER - Informed traders detected (from strategy)
542
- const vpin = this._getVPIN(position);
543
- if (vpin !== null && vpin > MOMENTUM.VPIN_DANGER) {
544
- return { type: 'vpin', reason: `VPIN spike ${(vpin * 100).toFixed(0)}% - informed traders`, pnlTicks, vpin };
545
- }
546
-
547
- // 6. TRAILING STOP (only if in profit above threshold)
548
- if (pnlTicks >= FAST_SCALPING.TRAILING_ACTIVATION_TICKS) {
549
- const trailingPnl = this._calculateTrailingPnl(position, currentPrice);
550
- if (trailingPnl !== null && trailingPnl <= -FAST_SCALPING.TRAILING_DISTANCE_TICKS) {
551
- return { type: 'trailing', reason: 'Trailing stop triggered', pnlTicks, trailingPnl };
552
- }
553
- }
554
-
555
- // 7. MOMENTUM-BASED EXIT (using strategy's math models)
556
- const momentum = this._calculateMomentum(position);
557
-
558
- if (momentum !== null) {
559
- // Strong favorable momentum + profit → HOLD (let it run)
560
- if (momentum > MOMENTUM.STRONG_FAVORABLE && pnlTicks > 4) {
561
- // Don't exit - momentum is strong in our favor
562
- return null;
563
- }
564
315
 
565
- // Weak momentum + profit → EXIT (secure profit)
566
- if (momentum < MOMENTUM.WEAK_THRESHOLD && pnlTicks > 0) {
567
- return { type: 'momentum_weak', reason: 'Weak momentum - securing profit', pnlTicks, momentum };
568
- }
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
+ });
569
326
 
570
- // Adverse momentum → EXIT immediately
571
- if (momentum < MOMENTUM.ADVERSE_THRESHOLD) {
572
- return { type: 'momentum_adverse', reason: 'Adverse momentum detected', pnlTicks, momentum };
573
- }
574
- }
575
-
576
- return null;
577
- }
578
-
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
- }
327
+ if (exitReason) {
328
+ this._executeExit(orderTag, exitReason);
766
329
  }
767
330
  }
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
331
  }
778
332
 
779
- /**
780
- * Execute exit order
781
- * @private
782
- */
783
333
  _executeExit(orderTag, exitReason) {
784
334
  const position = this.positions.get(orderTag);
785
335
  if (!position || position.status === 'exiting' || position.status === 'closed') return;
@@ -795,8 +345,7 @@ class PositionManager extends EventEmitter {
795
345
  momentum: exitReason.momentum,
796
346
  });
797
347
 
798
- // Fire exit order (opposite side)
799
- const exitSide = position.side === 0 ? 1 : 0; // Reverse: Long->Sell, Short->Buy
348
+ const exitSide = position.side === 0 ? 1 : 0;
800
349
 
801
350
  const exitResult = this.rithmic.fastExit({
802
351
  accountId: position.accountId,
@@ -820,41 +369,26 @@ class PositionManager extends EventEmitter {
820
369
  latencyMs: exitResult.latencyMs,
821
370
  });
822
371
  } else {
823
- log.error('Exit order FAILED', {
824
- orderTag,
825
- error: exitResult.error,
826
- });
827
- // Reset status to try again next cycle
372
+ log.error('Exit order FAILED', { orderTag, error: exitResult.error });
828
373
  position.status = 'active';
829
374
  position.exitReason = null;
830
375
  }
831
376
  }
832
377
 
833
- /**
834
- * Get tick size for position (from API, not hardcoded)
835
- * @private
836
- */
837
378
  _getTickSize(position) {
838
- // First try position's stored tickSize (from API)
839
379
  if (position.tickSize !== null && position.tickSize !== undefined) {
840
380
  return position.tickSize;
841
381
  }
842
382
 
843
- // Then try contract info cache
844
383
  const info = this.contractInfo.get(position.symbol);
845
384
  if (info && info.tickSize) {
846
385
  return info.tickSize;
847
386
  }
848
387
 
849
- // Last resort: log warning and return null (will cause issues)
850
388
  log.warn('No tick size available for symbol', { symbol: position.symbol });
851
389
  return null;
852
390
  }
853
391
 
854
- /**
855
- * Get tick value for position (from API, not hardcoded)
856
- * @private
857
- */
858
392
  _getTickValue(position) {
859
393
  if (position.tickValue !== null && position.tickValue !== undefined) {
860
394
  return position.tickValue;
@@ -869,85 +403,20 @@ class PositionManager extends EventEmitter {
869
403
  return null;
870
404
  }
871
405
 
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
406
  _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
- }
886
-
887
- const tickSize = this._getTickSize(position);
888
- if (tickSize === null || tickSize === undefined) {
889
- log.error('Cannot calculate PnL - no tick size from API', { symbol: position.symbol });
890
- return null;
891
- }
892
-
893
- const priceDiff = currentPrice - position.entryPrice;
894
- const signedDiff = position.side === 0 ? priceDiff : -priceDiff;
895
-
896
- return Math.round(signedDiff / tickSize);
897
- }
898
-
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
- _calculateTrailingPnl(position, currentPrice) {
906
- const tickSize = this._getTickSize(position);
907
- if (tickSize === null || tickSize === undefined) {
908
- return null;
909
- }
910
-
911
- if (position.side === 0) { // Long
912
- if (position.highWaterMark === null || position.highWaterMark === undefined) {
913
- return null;
914
- }
915
- const dropFromHigh = position.highWaterMark - currentPrice;
916
- return -Math.round(dropFromHigh / tickSize);
917
- } else { // Short
918
- if (position.lowWaterMark === null || position.lowWaterMark === undefined) {
919
- return null;
920
- }
921
- const riseFromLow = currentPrice - position.lowWaterMark;
922
- return -Math.round(riseFromLow / tickSize);
923
- }
407
+ return calculatePnlTicks(position, currentPrice, (p) => this._getTickSize(p));
924
408
  }
925
409
 
926
- /**
927
- * Get all active positions
928
- * @returns {Array<ManagedPosition>}
929
- */
930
410
  getActivePositions() {
931
411
  return Array.from(this.positions.values()).filter(
932
412
  p => p.status === 'holding' || p.status === 'active'
933
413
  );
934
414
  }
935
415
 
936
- /**
937
- * Get position by order tag
938
- * @param {string} orderTag
939
- * @returns {ManagedPosition|null}
940
- */
941
416
  getPosition(orderTag) {
942
417
  return this.positions.get(orderTag) || null;
943
418
  }
944
419
 
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
420
  canEnter(symbol) {
952
421
  for (const position of this.positions.values()) {
953
422
  if (position.symbol === symbol && position.status !== 'closed') {
@@ -957,18 +426,10 @@ class PositionManager extends EventEmitter {
957
426
  return true;
958
427
  }
959
428
 
960
- /**
961
- * Get momentum thresholds (for UI display)
962
- * @returns {Object}
963
- */
964
429
  getMomentumThresholds() {
965
430
  return { ...MOMENTUM };
966
431
  }
967
432
 
968
- /**
969
- * Get momentum weights (for UI display)
970
- * @returns {Object}
971
- */
972
433
  getMomentumWeights() {
973
434
  return { ...WEIGHTS };
974
435
  }