hedgequantx 2.5.44 → 2.6.1

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/bin/cli.js CHANGED
@@ -51,6 +51,17 @@ program
51
51
  console.log(`HedgeQuantX CLI v${pkg.version}`);
52
52
  });
53
53
 
54
+ program
55
+ .command('test-latency')
56
+ .description('Test order latency (requires active Rithmic connection)')
57
+ .option('-s, --symbol <symbol>', 'Symbol to test (e.g., NQ, ES, MNQ)', 'NQ')
58
+ .option('-c, --count <count>', 'Number of test iterations', '10')
59
+ .option('-p, --propfirm <propfirm>', 'Propfirm to use', 'apex')
60
+ .action(async (options) => {
61
+ const { runLatencyTest } = require('../src/commands/test-latency');
62
+ await runLatencyTest(options);
63
+ });
64
+
54
65
  // Handle -u flag before parsing commands
55
66
  if (process.argv.includes('-u') || process.argv.includes('--update')) {
56
67
  const { execSync } = require('child_process');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hedgequantx",
3
- "version": "2.5.44",
3
+ "version": "2.6.1",
4
4
  "description": "HedgeQuantX - Prop Futures Trading CLI",
5
5
  "main": "src/app.js",
6
6
  "bin": {
package/src/app.js CHANGED
@@ -256,6 +256,17 @@ const run = async () => {
256
256
  log.info('Starting HQX CLI');
257
257
  await bannerClosed();
258
258
 
259
+ // PRE-LOAD: Initialize Rithmic protobuf definitions at startup
260
+ // This removes ~100ms latency from first order
261
+ const { proto } = require('./services/rithmic/protobuf');
262
+ const protoSpinner = ora({ text: 'INITIALIZING TRADING ENGINE...', color: 'cyan' }).start();
263
+ try {
264
+ await proto.load();
265
+ protoSpinner.succeed('TRADING ENGINE READY');
266
+ } catch (e) {
267
+ protoSpinner.warn('Trading engine partial init');
268
+ }
269
+
259
270
  // Restore session
260
271
  const spinner = ora({ text: 'RESTORING SESSION...', color: 'yellow' }).start();
261
272
  const restored = await connections.restoreFromStorage();
@@ -136,6 +136,34 @@ const CACHE = {
136
136
  STATS_TTL: 60000, // 1 minute
137
137
  };
138
138
 
139
+ // ==================== FAST SCALPING (Ultra-Low Latency) ====================
140
+ const FAST_SCALPING = {
141
+ // Hold constraints (prop firm rules - NON-NEGOTIABLE)
142
+ MIN_HOLD_MS: 10000, // 10 seconds minimum hold
143
+ MAX_HOLD_MS: 60000, // 60 seconds failsafe (force exit if stuck)
144
+
145
+ // Exit targets (in ticks) - defaults, override per symbol
146
+ TARGET_TICKS: 16, // Take profit target
147
+ STOP_TICKS: 20, // Stop loss
148
+
149
+ // Trailing stop (activates after MIN_HOLD + profit threshold)
150
+ TRAILING_ACTIVATION_TICKS: 8, // Start trailing after +8 ticks profit
151
+ TRAILING_DISTANCE_TICKS: 4, // Trail 4 ticks behind high/low
152
+
153
+ // Position monitoring
154
+ MONITOR_INTERVAL_MS: 100, // Check position every 100ms after hold
155
+
156
+ // Momentum thresholds (cumulative delta over 5 seconds)
157
+ MOMENTUM_STRONG_THRESHOLD: 50, // Delta > 50 = strong momentum, HOLD
158
+ MOMENTUM_WEAK_THRESHOLD: 20, // Delta < 20 = weak momentum, consider EXIT
159
+ MOMENTUM_WINDOW_MS: 5000, // 5 second window for momentum calc
160
+
161
+ // Latency monitoring
162
+ LOG_LATENCY: true,
163
+ LATENCY_TARGET_MS: 50, // Target entry latency
164
+ LATENCY_WARN_MS: 100, // Warn if entry takes > 100ms
165
+ };
166
+
139
167
  // ==================== DEBUG ====================
140
168
  const DEBUG = {
141
169
  get enabled() {
@@ -152,4 +180,5 @@ module.exports = {
152
180
  HQX_SERVER,
153
181
  CACHE,
154
182
  DEBUG,
183
+ FAST_SCALPING,
155
184
  };
@@ -1,5 +1,8 @@
1
1
  /**
2
2
  * One Account Mode - HQX Ultra Scalping
3
+ *
4
+ * FAST PATH: Rithmic direct execution (~10-50ms latency)
5
+ * SLOW PATH: ProjectX/Tradovate HTTP REST (~50-150ms latency)
3
6
  */
4
7
 
5
8
  const chalk = require('chalk');
@@ -10,6 +13,8 @@ const { connections } = require('../../services');
10
13
  const { AlgoUI, renderSessionSummary } = require('./ui');
11
14
  const { prompts } = require('../../utils');
12
15
  const { checkMarketHours } = require('../../services/projectx/market');
16
+ const { FAST_SCALPING } = require('../../config/settings');
17
+ const { PositionManager } = require('../../services/position-manager');
13
18
 
14
19
  // Strategy & Market Data (obfuscated)
15
20
  const { M1 } = require('../../../dist/lib/m/s1');
@@ -168,23 +173,42 @@ const configureAlgo = async (account, contract) => {
168
173
  return { contracts, dailyTarget, maxRisk, showName };
169
174
  };
170
175
 
176
+ /**
177
+ * Check if service supports fast path (Rithmic direct)
178
+ * @param {Object} service - Trading service
179
+ * @returns {boolean}
180
+ */
181
+ const isRithmicFastPath = (service) => {
182
+ return typeof service.fastEntry === 'function' &&
183
+ typeof service.fastExit === 'function' &&
184
+ service.orderConn?.isConnected;
185
+ };
186
+
171
187
  /**
172
188
  * Launch algo trading - HQX Ultra Scalping Strategy
173
189
  * Real-time market data + Strategy signals + Auto order execution
174
190
  * AI Supervision: All connected agents monitor and supervise trading
191
+ *
192
+ * FAST PATH (Rithmic): Uses fastEntry() for ~10-50ms latency
193
+ * SLOW PATH (ProjectX): Uses placeOrder() for ~50-150ms latency
175
194
  */
176
195
  const launchAlgo = async (service, account, contract, config) => {
177
196
  const { contracts, dailyTarget, maxRisk, showName } = config;
178
197
 
179
- // Use RAW API fields
198
+ // Use RAW API fields only - NO hardcoded fallbacks
180
199
  const accountName = showName
181
200
  ? (account.accountName || account.rithmicAccountId || account.accountId)
182
201
  : 'HQX *****';
183
202
  const symbolName = contract.name;
184
203
  const contractId = contract.id;
185
204
  const connectionType = account.platform || 'ProjectX';
186
- const tickSize = contract.tickSize || 0.25;
187
- const tickValue = contract.tickValue || 5.0;
205
+
206
+ // Tick size/value from API - null if not available (RULES.md compliant)
207
+ const tickSize = contract.tickSize ?? null;
208
+ const tickValue = contract.tickValue ?? null;
209
+
210
+ // Determine execution path
211
+ const useFastPath = isRithmicFastPath(service);
188
212
 
189
213
  const ui = new AlgoUI({ subtitle: 'HQX ULTRA SCALPING', mode: 'one-account' });
190
214
 
@@ -204,7 +228,12 @@ const launchAlgo = async (service, account, contract, config) => {
204
228
  connected: false,
205
229
  startTime: Date.now(),
206
230
  aiSupervision: false,
207
- aiMode: null
231
+ aiMode: null,
232
+ // Fast path stats
233
+ fastPath: useFastPath,
234
+ avgEntryLatency: 0,
235
+ avgFillLatency: 0,
236
+ entryLatencies: [],
208
237
  };
209
238
 
210
239
  let running = true;
@@ -216,9 +245,76 @@ const launchAlgo = async (service, account, contract, config) => {
216
245
  let lastTradeCount = 0; // Track number of trades from API
217
246
  let lastPositionQty = 0; // Track position changes
218
247
 
219
- // Initialize Strategy (M1 is singleton instance)
248
+ // Initialize Strategy FIRST (M1 is singleton instance)
249
+ // Strategy needs to be initialized before PositionManager so it can access math models
220
250
  const strategy = M1;
221
- strategy.initialize(contractId, tickSize, tickValue);
251
+
252
+ // Only initialize strategy if we have tick data from API
253
+ if (tickSize !== null && tickValue !== null) {
254
+ strategy.initialize(contractId, tickSize, tickValue);
255
+ } else {
256
+ algoLogger.warn(ui, 'WARNING', 'Tick size/value not available from API');
257
+ }
258
+
259
+ // Initialize Position Manager for fast path
260
+ let positionManager = null;
261
+ if (useFastPath) {
262
+ // Pass strategy reference so PositionManager can access math models
263
+ positionManager = new PositionManager(service, strategy);
264
+
265
+ // Set contract info from API (NOT hardcoded)
266
+ if (tickSize !== null && tickValue !== null) {
267
+ positionManager.setContractInfo(symbolName, {
268
+ tickSize,
269
+ tickValue,
270
+ contractId,
271
+ });
272
+ }
273
+
274
+ positionManager.start();
275
+
276
+ // Listen for position manager events
277
+ positionManager.on('entryFilled', ({ orderTag, position, fillLatencyMs }) => {
278
+ stats.entryLatencies.push(fillLatencyMs);
279
+ stats.avgFillLatency = stats.entryLatencies.reduce((a, b) => a + b, 0) / stats.entryLatencies.length;
280
+ algoLogger.info(ui, 'FAST FILL', `${fillLatencyMs}ms | avg=${stats.avgFillLatency.toFixed(1)}ms`);
281
+ });
282
+
283
+ positionManager.on('exitFilled', ({ orderTag, exitPrice, pnlTicks, holdDurationMs }) => {
284
+ // Calculate PnL in dollars only if tickValue is available from API
285
+ if (pnlTicks !== null && tickValue !== null) {
286
+ const pnlDollars = pnlTicks * tickValue;
287
+ if (pnlDollars >= 0) {
288
+ stats.wins++;
289
+ algoLogger.targetHit(ui, symbolName, exitPrice, pnlDollars);
290
+ } else {
291
+ stats.losses++;
292
+ algoLogger.stopHit(ui, symbolName, exitPrice, Math.abs(pnlDollars));
293
+ }
294
+ } else {
295
+ // Log with ticks only if tickValue unavailable
296
+ if (pnlTicks !== null && pnlTicks >= 0) {
297
+ stats.wins++;
298
+ algoLogger.info(ui, 'TARGET', `+${pnlTicks} ticks`);
299
+ } else if (pnlTicks !== null) {
300
+ stats.losses++;
301
+ algoLogger.info(ui, 'STOP', `${pnlTicks} ticks`);
302
+ }
303
+ }
304
+ stats.trades++;
305
+ currentPosition = 0;
306
+ pendingOrder = false;
307
+ algoLogger.info(ui, 'HOLD TIME', `${(holdDurationMs / 1000).toFixed(1)}s`);
308
+ });
309
+
310
+ positionManager.on('holdComplete', ({ orderTag, position }) => {
311
+ algoLogger.info(ui, 'HOLD COMPLETE', `${FAST_SCALPING.MIN_HOLD_MS / 1000}s minimum reached`);
312
+ });
313
+
314
+ positionManager.on('exitOrderFired', ({ orderTag, exitReason, latencyMs }) => {
315
+ algoLogger.info(ui, 'EXIT FIRED', `${exitReason.reason} | ${latencyMs.toFixed(1)}ms`);
316
+ });
317
+ }
222
318
 
223
319
  // Initialize AI Strategy Supervisor - agents observe, learn & optimize
224
320
  const aiAgents = aiService.getAgents();
@@ -245,10 +341,23 @@ const launchAlgo = async (service, account, contract, config) => {
245
341
  algoLogger.info(ui, 'AI SUPERVISION', `${aiAgents.length} agent(s) - ${stats.aiMode} mode - LEARNING ACTIVE`);
246
342
  }
247
343
 
344
+ // Log execution path
345
+ if (useFastPath) {
346
+ algoLogger.info(ui, 'FAST PATH', `Rithmic direct | Target <${FAST_SCALPING.LATENCY_TARGET_MS}ms | Hold ${FAST_SCALPING.MIN_HOLD_MS / 1000}s`);
347
+ } else {
348
+ algoLogger.info(ui, 'SLOW PATH', `HTTP REST | Bracket orders enabled`);
349
+ }
350
+
248
351
  // Handle strategy signals
249
352
  strategy.on('signal', async (signal) => {
250
353
  if (!running || pendingOrder || currentPosition !== 0) return;
251
354
 
355
+ // Fast path: check if position manager allows new entry
356
+ if (useFastPath && positionManager && !positionManager.canEnter(symbolName)) {
357
+ algoLogger.info(ui, 'BLOCKED', 'Existing position in symbol');
358
+ return;
359
+ }
360
+
252
361
  const { side, direction, entry, stopLoss, takeProfit, confidence } = signal;
253
362
 
254
363
  // Feed signal to AI supervisor (agents observe the signal)
@@ -282,58 +391,116 @@ const launchAlgo = async (service, account, contract, config) => {
282
391
 
283
392
  // Place order via API
284
393
  pendingOrder = true;
394
+ const orderSide = direction === 'long' ? 0 : 1; // 0=Buy, 1=Sell
395
+
285
396
  try {
286
- const orderSide = direction === 'long' ? 0 : 1; // 0=Buy, 1=Sell
287
- const orderResult = await service.placeOrder({
288
- accountId: account.accountId,
289
- contractId: contractId,
290
- type: 2, // Market order
291
- side: orderSide,
292
- size: contracts
293
- });
294
-
295
- if (orderResult.success) {
296
- currentPosition = direction === 'long' ? contracts : -contracts;
297
- stats.trades++;
298
- const sideStr = direction === 'long' ? 'BUY' : 'SELL';
299
- const positionSide = direction === 'long' ? 'LONG' : 'SHORT';
397
+ // ═══════════════════════════════════════════════════════════════
398
+ // FAST PATH: Rithmic direct execution (~10-50ms)
399
+ // ═══════════════════════════════════════════════════════════════
400
+ if (useFastPath && positionManager) {
401
+ const orderData = {
402
+ accountId: account.accountId,
403
+ symbol: symbolName,
404
+ exchange: contract.exchange || 'CME',
405
+ size: contracts,
406
+ side: orderSide,
407
+ };
300
408
 
301
- algoLogger.orderSubmitted(ui, symbolName, sideStr, contracts, entry);
302
- algoLogger.orderFilled(ui, symbolName, sideStr, contracts, entry);
303
- algoLogger.positionOpened(ui, symbolName, positionSide, contracts, entry);
304
- algoLogger.entryConfirmed(ui, sideStr, contracts, symbolName, entry);
409
+ // Fire-and-forget entry (no await on fill)
410
+ const entryResult = service.fastEntry(orderData);
305
411
 
306
- // Place bracket orders (SL/TP)
307
- if (stopLoss && takeProfit) {
308
- // Stop Loss
309
- await service.placeOrder({
310
- accountId: account.accountId,
311
- contractId: contractId,
312
- type: 4, // Stop order
313
- side: direction === 'long' ? 1 : 0, // Opposite side
314
- size: contracts,
315
- stopPrice: stopLoss
316
- });
412
+ if (entryResult.success) {
413
+ // Register with position manager for lifecycle tracking
414
+ // Pass contract info from API (NOT hardcoded)
415
+ const contractInfo = {
416
+ tickSize,
417
+ tickValue,
418
+ contractId,
419
+ };
420
+ positionManager.registerEntry(entryResult, orderData, contractInfo);
421
+
422
+ currentPosition = direction === 'long' ? contracts : -contracts;
423
+ const sideStr = direction === 'long' ? 'BUY' : 'SELL';
317
424
 
318
- // Take Profit
319
- await service.placeOrder({
320
- accountId: account.accountId,
321
- contractId: contractId,
322
- type: 1, // Limit order
323
- side: direction === 'long' ? 1 : 0,
324
- size: contracts,
325
- limitPrice: takeProfit
326
- });
425
+ // Log with latency
426
+ const latencyColor = entryResult.latencyMs < FAST_SCALPING.LATENCY_TARGET_MS
427
+ ? chalk.green
428
+ : entryResult.latencyMs < FAST_SCALPING.LATENCY_WARN_MS
429
+ ? chalk.yellow
430
+ : chalk.red;
327
431
 
328
- algoLogger.stopsSet(ui, stopLoss, takeProfit);
432
+ stats.avgEntryLatency = stats.entryLatencies.length > 0
433
+ ? (stats.avgEntryLatency * stats.entryLatencies.length + entryResult.latencyMs) / (stats.entryLatencies.length + 1)
434
+ : entryResult.latencyMs;
435
+
436
+ algoLogger.info(ui, 'FAST ENTRY', `${sideStr} ${contracts}x ${symbolName} | ${latencyColor(entryResult.latencyMs.toFixed(2) + 'ms')}`);
437
+ algoLogger.info(ui, 'HOLD START', `Min ${FAST_SCALPING.MIN_HOLD_MS / 1000}s before exit`);
438
+
439
+ // Note: NO bracket orders in fast path
440
+ // PositionManager handles exit logic after 10s hold
441
+
442
+ } else {
443
+ algoLogger.orderRejected(ui, symbolName, entryResult.error || 'Fast entry failed');
444
+ pendingOrder = false;
329
445
  }
446
+
447
+ // ═══════════════════════════════════════════════════════════════
448
+ // SLOW PATH: ProjectX/Tradovate HTTP REST (~50-150ms)
449
+ // ═══════════════════════════════════════════════════════════════
330
450
  } else {
331
- algoLogger.orderRejected(ui, symbolName, orderResult.error || 'Unknown error');
451
+ const orderResult = await service.placeOrder({
452
+ accountId: account.accountId,
453
+ contractId: contractId,
454
+ type: 2, // Market order
455
+ side: orderSide,
456
+ size: contracts
457
+ });
458
+
459
+ if (orderResult.success) {
460
+ currentPosition = direction === 'long' ? contracts : -contracts;
461
+ stats.trades++;
462
+ const sideStr = direction === 'long' ? 'BUY' : 'SELL';
463
+ const positionSide = direction === 'long' ? 'LONG' : 'SHORT';
464
+
465
+ algoLogger.orderSubmitted(ui, symbolName, sideStr, contracts, entry);
466
+ algoLogger.orderFilled(ui, symbolName, sideStr, contracts, entry);
467
+ algoLogger.positionOpened(ui, symbolName, positionSide, contracts, entry);
468
+ algoLogger.entryConfirmed(ui, sideStr, contracts, symbolName, entry);
469
+
470
+ // Place bracket orders (SL/TP) - SLOW PATH ONLY
471
+ if (stopLoss && takeProfit) {
472
+ // Stop Loss
473
+ await service.placeOrder({
474
+ accountId: account.accountId,
475
+ contractId: contractId,
476
+ type: 4, // Stop order
477
+ side: direction === 'long' ? 1 : 0, // Opposite side
478
+ size: contracts,
479
+ stopPrice: stopLoss
480
+ });
481
+
482
+ // Take Profit
483
+ await service.placeOrder({
484
+ accountId: account.accountId,
485
+ contractId: contractId,
486
+ type: 1, // Limit order
487
+ side: direction === 'long' ? 1 : 0,
488
+ size: contracts,
489
+ limitPrice: takeProfit
490
+ });
491
+
492
+ algoLogger.stopsSet(ui, stopLoss, takeProfit);
493
+ }
494
+ pendingOrder = false;
495
+ } else {
496
+ algoLogger.orderRejected(ui, symbolName, orderResult.error || 'Unknown error');
497
+ pendingOrder = false;
498
+ }
332
499
  }
333
500
  } catch (e) {
334
501
  algoLogger.error(ui, 'ORDER ERROR', e.message);
502
+ pendingOrder = false;
335
503
  }
336
- pendingOrder = false;
337
504
  });
338
505
 
339
506
  // Handle market data ticks
@@ -368,6 +535,27 @@ const launchAlgo = async (service, account, contract, config) => {
368
535
 
369
536
  strategy.processTick(tickData);
370
537
 
538
+ // Feed price to position manager for exit monitoring (fast path)
539
+ if (useFastPath && positionManager) {
540
+ // Update latest price for position monitoring
541
+ service.emit('priceUpdate', {
542
+ symbol: symbolName,
543
+ price: tickData.price,
544
+ timestamp: tickData.timestamp,
545
+ });
546
+
547
+ // Get momentum data from strategy if available
548
+ const modelValues = strategy.getModelValues?.(contractId);
549
+ if (modelValues) {
550
+ positionManager.updateMomentum(symbolName, {
551
+ ofi: modelValues.ofi || 0,
552
+ zscore: modelValues.zscore || 0,
553
+ delta: modelValues.delta || 0,
554
+ timestamp: tickData.timestamp,
555
+ });
556
+ }
557
+ }
558
+
371
559
  stats.latency = Date.now() - latencyStart;
372
560
 
373
561
  // Heartbeat every 30 seconds (smart log instead of tick count)
@@ -576,6 +764,12 @@ const launchAlgo = async (service, account, contract, config) => {
576
764
  clearInterval(refreshInterval);
577
765
  clearInterval(pnlInterval);
578
766
 
767
+ // Stop Position Manager (fast path)
768
+ if (positionManager) {
769
+ positionManager.stop();
770
+ positionManager = null;
771
+ }
772
+
579
773
  // Stop AI Supervisor and get learning summary
580
774
  if (stats.aiSupervision) {
581
775
  const aiSummary = StrategySupervisor.stop();