hedgequantx 2.6.58 → 2.6.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hedgequantx",
3
- "version": "2.6.58",
3
+ "version": "2.6.60",
4
4
  "description": "HedgeQuantX - Prop Futures Trading CLI",
5
5
  "main": "src/app.js",
6
6
  "bin": {
@@ -143,11 +143,15 @@ const FAST_SCALPING = {
143
143
  MAX_HOLD_MS: 60000, // 60 seconds failsafe (force exit if stuck)
144
144
 
145
145
  // Exit targets (in ticks) - defaults, override per symbol
146
- TARGET_TICKS: 16, // Take profit target
147
- STOP_TICKS: 20, // Stop loss
146
+ TARGET_TICKS: 16, // Take profit target (+$80 on NQ)
147
+ STOP_TICKS: 20, // Stop loss (-$100 on NQ)
148
+
149
+ // Breakeven (BE) - move SL to entry price after profit threshold
150
+ BREAKEVEN_ACTIVATION_TICKS: 6, // Activate BE after +6 ticks profit (+$30 on NQ)
151
+ BREAKEVEN_OFFSET_TICKS: 1, // BE at entry + 1 tick (small profit lock)
148
152
 
149
153
  // Trailing stop (activates after MIN_HOLD + profit threshold)
150
- TRAILING_ACTIVATION_TICKS: 8, // Start trailing after +8 ticks profit
154
+ TRAILING_ACTIVATION_TICKS: 8, // Start trailing after +8 ticks profit (+$40 on NQ)
151
155
  TRAILING_DISTANCE_TICKS: 4, // Trail 4 ticks behind high/low
152
156
 
153
157
  // Position monitoring
@@ -315,68 +315,57 @@ const launchAlgo = async (service, account, contract, config) => {
315
315
 
316
316
  positionManager.start();
317
317
 
318
- // Listen for position manager events
318
+ // Listen for position manager events - CLEAN LOGS (no verbose order status)
319
319
  positionManager.on('entryFilled', ({ orderTag, position, fillLatencyMs }) => {
320
320
  stats.entryLatencies.push(fillLatencyMs);
321
321
  stats.avgFillLatency = stats.entryLatencies.reduce((a, b) => a + b, 0) / stats.entryLatencies.length;
322
- algoLogger.info(ui, 'FAST FILL', `${fillLatencyMs}ms | avg=${stats.avgFillLatency.toFixed(1)}ms`);
322
+ const side = position.side === 0 ? 'LONG' : 'SHORT';
323
+ algoLogger.info(ui, 'FILLED', `${side} ${position.size}x ${symbolName} @ ${position.entryPrice} | ${fillLatencyMs}ms`);
323
324
  });
324
325
 
325
326
  positionManager.on('exitFilled', ({ orderTag, exitPrice, pnlTicks, holdDurationMs }) => {
327
+ const holdSec = (holdDurationMs / 1000).toFixed(1);
326
328
  // Calculate PnL in dollars only if tickValue is available from API
327
329
  if (pnlTicks !== null && tickValue !== null) {
328
330
  const pnlDollars = pnlTicks * tickValue;
329
331
  stats.sessionPnl += pnlDollars; // Track session P&L
330
332
  if (pnlDollars >= 0) {
331
333
  stats.wins++;
332
- algoLogger.targetHit(ui, symbolName, exitPrice, pnlDollars);
334
+ algoLogger.info(ui, 'WIN', `+$${pnlDollars.toFixed(2)} @ ${exitPrice} | ${holdSec}s`);
333
335
  } else {
334
336
  stats.losses++;
335
- algoLogger.stopHit(ui, symbolName, exitPrice, Math.abs(pnlDollars));
337
+ algoLogger.info(ui, 'LOSS', `-$${Math.abs(pnlDollars).toFixed(2)} @ ${exitPrice} | ${holdSec}s`);
336
338
  }
337
339
  } else {
338
340
  // Log with ticks only if tickValue unavailable
339
341
  if (pnlTicks !== null && pnlTicks >= 0) {
340
342
  stats.wins++;
341
- algoLogger.info(ui, 'TARGET', `+${pnlTicks} ticks`);
343
+ algoLogger.info(ui, 'WIN', `+${pnlTicks} ticks | ${holdSec}s`);
342
344
  } else if (pnlTicks !== null) {
343
345
  stats.losses++;
344
- algoLogger.info(ui, 'STOP', `${pnlTicks} ticks`);
346
+ algoLogger.info(ui, 'LOSS', `${pnlTicks} ticks | ${holdSec}s`);
345
347
  }
346
348
  }
347
349
  stats.trades++;
348
350
  currentPosition = 0;
349
351
  stats.position = 0; // Reset UI position display
350
352
  pendingOrder = false;
351
- algoLogger.info(ui, 'HOLD TIME', `${(holdDurationMs / 1000).toFixed(1)}s`);
352
353
  });
353
354
 
354
355
  positionManager.on('holdComplete', ({ orderTag, position }) => {
355
- algoLogger.info(ui, 'HOLD COMPLETE', `${FAST_SCALPING.MIN_HOLD_MS / 1000}s minimum reached`);
356
+ algoLogger.info(ui, 'READY', `Hold complete - monitoring exit`);
356
357
  });
357
358
 
358
- positionManager.on('exitOrderFired', ({ orderTag, exitReason, latencyMs }) => {
359
- algoLogger.info(ui, 'EXIT FIRED', `${exitReason.reason} | ${latencyMs.toFixed(1)}ms`);
360
- });
361
-
362
- // DEBUG: Listen for raw order notifications from Rithmic
363
- service.on('orderAccepted', (data) => {
364
- algoLogger.info(ui, 'ORDER ACCEPTED', `tag=${data.orderTag} basket=${data.basketId}`);
359
+ positionManager.on('breakevenActivated', ({ orderTag, position, breakevenPrice, pnlTicks }) => {
360
+ algoLogger.info(ui, 'BE', `Breakeven activated @ ${breakevenPrice} | +${pnlTicks} ticks`);
365
361
  });
366
362
 
367
- service.on('orderNotification', (data) => {
368
- const status = data.status || 'unknown';
369
- const fillQty = data.fillQuantity || data.totalFillQuantity || 0;
370
- if (fillQty > 0) {
371
- algoLogger.info(ui, 'ORDER FILL', `tag=${data.orderTag} qty=${fillQty} @ ${data.avgFillPrice}`);
372
- } else {
373
- algoLogger.info(ui, 'ORDER STATUS', `tag=${data.orderTag} status=${status}`);
374
- }
363
+ positionManager.on('exitOrderFired', ({ orderTag, exitReason, latencyMs }) => {
364
+ // Don't log here - exitFilled will log the result
375
365
  });
376
366
 
377
- service.on('orderFilled', (data) => {
378
- algoLogger.info(ui, 'FILL CONFIRMED', `${data.transactionType === 1 ? 'BUY' : 'SELL'} ${data.fillQuantity}x @ ${data.avgFillPrice}`);
379
- });
367
+ // NOTE: Removed verbose DEBUG logs (ORDER ACCEPTED, ORDER STATUS, ORDER FILL, FILL CONFIRMED)
368
+ // Entry/Exit fills are logged by positionManager events (entryFilled, exitFilled)
380
369
  }
381
370
 
382
371
  // ═══════════════════════════════════════════════════════════════════════════
@@ -539,11 +528,11 @@ const launchAlgo = async (service, account, contract, config) => {
539
528
  }
540
529
 
541
530
  const riskPct = Math.round((riskAmount / maxRisk) * 100);
542
- algoLogger.positionSized(ui, contracts, kelly, riskAmount, riskPct);
543
531
 
544
532
  // Place order via API
545
533
  pendingOrder = true;
546
534
  const orderSide = direction === 'long' ? 0 : 1; // 0=Buy, 1=Sell
535
+ const sideStr = direction === 'long' ? 'LONG' : 'SHORT';
547
536
 
548
537
  try {
549
538
  // ═══════════════════════════════════════════════════════════════
@@ -559,49 +548,29 @@ const launchAlgo = async (service, account, contract, config) => {
559
548
  };
560
549
 
561
550
  // CRITICAL: Use rithmicAccountId (original) not accountId (hash) for Rithmic orders
562
- // The accountId field is hashed for display, but Rithmic API needs the original
563
551
  if (account.rithmicAccountId) {
564
552
  orderData.accountId = account.rithmicAccountId;
565
553
  }
566
554
 
567
- // Log order details before sending
568
- algoLogger.info(ui, 'ORDER DATA', `acct=${orderData.accountId} sym=${orderData.symbol} side=${orderData.side} qty=${orderData.size}`);
555
+ // Log entry attempt (single line)
556
+ algoLogger.info(ui, 'ENTRY', `${sideStr} ${contracts}x ${symbolName} | risk: $${riskAmount} (${riskPct}%)`);
569
557
 
570
558
  // Fire-and-forget entry (no await on fill)
571
559
  const entryResult = service.fastEntry(orderData);
572
560
 
573
- // Log the order tag for tracking
574
- algoLogger.info(ui, 'ORDER TAG', `${entryResult.orderTag} | success=${entryResult.success}`);
575
-
576
561
  if (entryResult.success) {
577
562
  // Register with position manager for lifecycle tracking
578
- // Pass contract info from API (NOT hardcoded)
579
- const contractInfo = {
580
- tickSize,
581
- tickValue,
582
- contractId,
583
- };
563
+ const contractInfo = { tickSize, tickValue, contractId };
584
564
  positionManager.registerEntry(entryResult, orderData, contractInfo);
585
565
 
586
566
  currentPosition = direction === 'long' ? contracts : -contracts;
587
- const sideStr = direction === 'long' ? 'BUY' : 'SELL';
588
-
589
- // Log with latency
590
- const latencyColor = entryResult.latencyMs < FAST_SCALPING.LATENCY_TARGET_MS
591
- ? chalk.green
592
- : entryResult.latencyMs < FAST_SCALPING.LATENCY_WARN_MS
593
- ? chalk.yellow
594
- : chalk.red;
595
567
 
568
+ // Update avg entry latency
596
569
  stats.avgEntryLatency = stats.entryLatencies.length > 0
597
570
  ? (stats.avgEntryLatency * stats.entryLatencies.length + entryResult.latencyMs) / (stats.entryLatencies.length + 1)
598
571
  : entryResult.latencyMs;
599
572
 
600
- algoLogger.info(ui, 'FAST ENTRY', `${sideStr} ${contracts}x ${symbolName} | ${latencyColor(entryResult.latencyMs.toFixed(2) + 'ms')}`);
601
- algoLogger.info(ui, 'HOLD START', `Min ${FAST_SCALPING.MIN_HOLD_MS / 1000}s before exit`);
602
-
603
- // Note: NO bracket orders in fast path
604
- // PositionManager handles exit logic after 10s hold
573
+ // Note: Fill confirmation logged by positionManager.on('entryFilled')
605
574
 
606
575
  } else {
607
576
  algoLogger.orderRejected(ui, symbolName, entryResult.error || 'Fast entry failed');
@@ -58,6 +58,8 @@ const WEIGHTS = {
58
58
  * @property {number} lowWaterMark - Lowest price since entry (for trailing)
59
59
  * @property {string} status - 'pending' | 'active' | 'holding' | 'exiting' | 'closed'
60
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
61
63
  * @property {Object|null} exitReason - Why position was exited
62
64
  * @property {number} tickSize - Tick size from API
63
65
  * @property {number} tickValue - Tick value from API
@@ -214,6 +216,8 @@ class PositionManager extends EventEmitter {
214
216
  lowWaterMark: null,
215
217
  status: 'pending', // Waiting for fill confirmation
216
218
  holdComplete: false,
219
+ breakevenActive: false, // BE not yet activated
220
+ breakevenPrice: null, // Will be set when BE activates
217
221
  exitReason: null,
218
222
  latencyMs,
219
223
  // Contract info from API (NOT hardcoded)
@@ -483,18 +487,64 @@ class PositionManager extends EventEmitter {
483
487
  return { type: 'target', reason: 'Target reached', pnlTicks };
484
488
  }
485
489
 
486
- // 2. STOP HIT - Always exit at stop
487
- if (pnlTicks <= -stopTicks) {
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) {
488
509
  return { type: 'stop', reason: 'Stop loss hit', pnlTicks };
489
510
  }
490
511
 
491
- // 3. VPIN DANGER - Informed traders detected (from strategy)
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
+ }
523
+ position.breakevenActive = true;
524
+
525
+ log.info('BREAKEVEN ACTIVATED', {
526
+ symbol: position.symbol,
527
+ entryPrice: position.entryPrice,
528
+ bePrice: position.breakevenPrice,
529
+ pnlTicks,
530
+ });
531
+
532
+ this.emit('breakevenActivated', {
533
+ orderTag: position.orderTag,
534
+ position,
535
+ breakevenPrice: position.breakevenPrice,
536
+ pnlTicks,
537
+ });
538
+ }
539
+ }
540
+
541
+ // 5. VPIN DANGER - Informed traders detected (from strategy)
492
542
  const vpin = this._getVPIN(position);
493
543
  if (vpin !== null && vpin > MOMENTUM.VPIN_DANGER) {
494
544
  return { type: 'vpin', reason: `VPIN spike ${(vpin * 100).toFixed(0)}% - informed traders`, pnlTicks, vpin };
495
545
  }
496
546
 
497
- // 4. TRAILING STOP (only if in profit above threshold)
547
+ // 6. TRAILING STOP (only if in profit above threshold)
498
548
  if (pnlTicks >= FAST_SCALPING.TRAILING_ACTIVATION_TICKS) {
499
549
  const trailingPnl = this._calculateTrailingPnl(position, currentPrice);
500
550
  if (trailingPnl !== null && trailingPnl <= -FAST_SCALPING.TRAILING_DISTANCE_TICKS) {
@@ -502,7 +552,7 @@ class PositionManager extends EventEmitter {
502
552
  }
503
553
  }
504
554
 
505
- // 5. MOMENTUM-BASED EXIT (using strategy's math models)
555
+ // 7. MOMENTUM-BASED EXIT (using strategy's math models)
506
556
  const momentum = this._calculateMomentum(position);
507
557
 
508
558
  if (momentum !== null) {