hedgequantx 2.9.226 → 2.9.228

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.
@@ -151,9 +151,18 @@ class HQXUltraScalpingStrategy extends EventEmitter {
151
151
  // Add tick to buffer
152
152
  let ticks = this.tickBuffer.get(contractId);
153
153
  ticks.push(tick);
154
+
155
+ // Track total ticks for status log
156
+ this._totalTicks = (this._totalTicks || 0) + 1;
157
+
158
+ // Emit status log every second
159
+ const now = Date.now();
160
+ if (!this._lastStatusLog || now - this._lastStatusLog >= 1000) {
161
+ this._lastStatusLog = now;
162
+ this._emitStatusLog(contractId, tick.price);
163
+ }
154
164
 
155
165
  // Check if we should form a new bar
156
- const now = Date.now();
157
166
  const lastBar = this.lastBarTime.get(contractId);
158
167
 
159
168
  if (now - lastBar >= this.barIntervalMs && ticks.length > 0) {
@@ -171,6 +180,72 @@ class HQXUltraScalpingStrategy extends EventEmitter {
171
180
  }
172
181
  return null;
173
182
  }
183
+
184
+ /**
185
+ * Emit status log with QUANT metrics
186
+ */
187
+ _emitStatusLog(contractId, currentPrice) {
188
+ const prices = this.priceBuffer.get(contractId) || [];
189
+ const volumes = this.volumeBuffer.get(contractId) || [];
190
+ const bars = this.barHistory.get(contractId) || [];
191
+
192
+ if (prices.length < 20) return; // Not enough data yet
193
+
194
+ // Compute current metrics
195
+ const zscore = computeZScore(prices);
196
+ const vpin = volumes.length >= 10 ? computeVPIN(volumes, this.vpinWindow) : 0;
197
+ const ofi = bars.length >= 10 ? computeOrderFlowImbalance(bars, this.ofiLookback) : 0;
198
+
199
+ // Determine market state
200
+ const absZ = Math.abs(zscore);
201
+ let zState = 'normal';
202
+ if (absZ >= 2.0) zState = 'EXTREME';
203
+ else if (absZ >= 1.5) zState = 'HIGH';
204
+ else if (absZ >= 1.0) zState = 'building';
205
+
206
+ // Determine direction bias
207
+ let bias = 'neutral';
208
+ if (zscore < -1.5 && ofi > 0.1) bias = 'LONG setup';
209
+ else if (zscore > 1.5 && ofi < -0.1) bias = 'SHORT setup';
210
+ else if (zscore < -1.0) bias = 'oversold';
211
+ else if (zscore > 1.0) bias = 'overbought';
212
+
213
+ // Extract symbol
214
+ const sym = (contractId || '').replace(/[A-Z]\d+$/, '');
215
+
216
+ // Build message based on state
217
+ let message;
218
+ if (!this.tradingEnabled) {
219
+ message = `[${sym}] ${currentPrice.toFixed(2)} | PAUSED (${this.lossStreak} losses) | Cooldown active`;
220
+ } else if (absZ >= 2.0) {
221
+ const dir = zscore < 0 ? 'LONG' : 'SHORT';
222
+ const ofiPct = (Math.abs(ofi) * 100).toFixed(0);
223
+ const ofiConfirm = (zscore < 0 && ofi > 0.15) || (zscore > 0 && ofi < -0.15);
224
+ if (ofiConfirm) {
225
+ message = `[${sym}] ${currentPrice.toFixed(2)} | Z: ${zscore.toFixed(1)}σ ${zState} | ${dir} | OFI ${ofiPct}% confirms`;
226
+ } else {
227
+ message = `[${sym}] ${currentPrice.toFixed(2)} | Z: ${zscore.toFixed(1)}σ ${zState} | ${dir} signal | OFI ${ofiPct}% pending`;
228
+ }
229
+ } else if (absZ >= 1.5) {
230
+ message = `[${sym}] ${currentPrice.toFixed(2)} | Z: ${zscore.toFixed(1)}σ ${zState} | ${bias} | Monitoring`;
231
+ } else if (absZ >= 1.0) {
232
+ message = `[${sym}] ${currentPrice.toFixed(2)} | Z: ${zscore.toFixed(1)}σ ${zState} | Awaiting extremity`;
233
+ } else {
234
+ // Normal state - show different context messages
235
+ const vpinPct = (vpin * 100).toFixed(0);
236
+ const contexts = [
237
+ `VPIN ${vpinPct}% | Mean reversion scan`,
238
+ `OFI balanced | Price discovery`,
239
+ `Z normalized | Statistical scan`,
240
+ `Tick flow stable | Edge detection`,
241
+ `Volatility normal | Alpha scan`,
242
+ ];
243
+ const ctx = contexts[Math.floor(Date.now() / 5000) % contexts.length];
244
+ message = `[${sym}] ${currentPrice.toFixed(2)} | Z: ${zscore.toFixed(1)}σ | ${ctx}`;
245
+ }
246
+
247
+ this.emit('log', { type: 'info', message });
248
+ }
174
249
 
175
250
  /**
176
251
  * Aggregate ticks into a bar
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hedgequantx",
3
- "version": "2.9.226",
3
+ "version": "2.9.228",
4
4
  "description": "HedgeQuantX - Prop Futures Trading CLI",
5
5
  "main": "src/app.js",
6
6
  "bin": {
@@ -35,7 +35,8 @@
35
35
  'use strict';
36
36
 
37
37
  const fs = require('fs');
38
- const { spawn } = require('child_process');
38
+ const path = require('path');
39
+ const { spawn, execSync } = require('child_process');
39
40
  const { SOCKET_PATH, PID_FILE, SOCKET_DIR } = require('./constants');
40
41
  const { DaemonServer } = require('./server');
41
42
  const { DaemonClient, getDaemonClient } = require('./client');
@@ -43,6 +44,11 @@ const { logger } = require('../../utils/logger');
43
44
 
44
45
  const log = logger.scope('Daemon');
45
46
 
47
+ /** Isolated daemon directory - survives npm updates */
48
+ const DAEMON_INSTALL_DIR = path.join(SOCKET_DIR, 'daemon');
49
+ const DAEMON_ENTRY = path.join(DAEMON_INSTALL_DIR, 'cli-daemon.js');
50
+ const DAEMON_VERSION_FILE = path.join(DAEMON_INSTALL_DIR, 'version.txt');
51
+
46
52
  /**
47
53
  * Check if daemon is running
48
54
  * @returns {boolean}
@@ -86,6 +92,150 @@ function getDaemonPid() {
86
92
  }
87
93
  }
88
94
 
95
+ /**
96
+ * Get current package version
97
+ * @returns {string}
98
+ */
99
+ function getCurrentVersion() {
100
+ try {
101
+ const pkg = require('../../../package.json');
102
+ return pkg.version;
103
+ } catch (_) {
104
+ return '0.0.0';
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Get installed daemon version
110
+ * @returns {string|null}
111
+ */
112
+ function getInstalledDaemonVersion() {
113
+ try {
114
+ if (fs.existsSync(DAEMON_VERSION_FILE)) {
115
+ return fs.readFileSync(DAEMON_VERSION_FILE, 'utf8').trim();
116
+ }
117
+ } catch (_) {}
118
+ return null;
119
+ }
120
+
121
+ /**
122
+ * Install daemon to isolated directory
123
+ * This copies all necessary files so daemon survives npm updates
124
+ * @returns {boolean}
125
+ */
126
+ function installDaemon() {
127
+ const currentVersion = getCurrentVersion();
128
+ const installedVersion = getInstalledDaemonVersion();
129
+
130
+ // Skip if already installed with same version
131
+ if (installedVersion === currentVersion && fs.existsSync(DAEMON_ENTRY)) {
132
+ log.debug('Daemon already installed', { version: currentVersion });
133
+ return true;
134
+ }
135
+
136
+ log.info('Installing daemon to isolated directory', { version: currentVersion });
137
+
138
+ try {
139
+ // Create daemon directory
140
+ if (!fs.existsSync(DAEMON_INSTALL_DIR)) {
141
+ fs.mkdirSync(DAEMON_INSTALL_DIR, { recursive: true, mode: 0o700 });
142
+ }
143
+
144
+ // Get source directory (where this package is installed)
145
+ const srcDir = path.resolve(__dirname, '../../..');
146
+
147
+ // Files/directories to copy for daemon to work
148
+ const filesToCopy = [
149
+ 'src/cli-daemon.js',
150
+ 'src/services/daemon',
151
+ 'src/services/rithmic',
152
+ 'src/services/session.js',
153
+ 'src/services/index.js',
154
+ 'src/config',
155
+ 'src/security',
156
+ 'src/utils',
157
+ 'package.json',
158
+ ];
159
+
160
+ // Copy each file/directory
161
+ for (const file of filesToCopy) {
162
+ const srcPath = path.join(srcDir, file);
163
+ const destPath = path.join(DAEMON_INSTALL_DIR, file);
164
+
165
+ if (!fs.existsSync(srcPath)) {
166
+ log.warn('Source not found', { file });
167
+ continue;
168
+ }
169
+
170
+ // Create parent directory
171
+ const destDir = path.dirname(destPath);
172
+ if (!fs.existsSync(destDir)) {
173
+ fs.mkdirSync(destDir, { recursive: true });
174
+ }
175
+
176
+ // Copy file or directory
177
+ if (fs.statSync(srcPath).isDirectory()) {
178
+ copyDirSync(srcPath, destPath);
179
+ } else {
180
+ fs.copyFileSync(srcPath, destPath);
181
+ }
182
+ }
183
+
184
+ // Copy node_modules that daemon needs
185
+ const nodeModulesDir = path.join(srcDir, 'node_modules');
186
+ const destNodeModules = path.join(DAEMON_INSTALL_DIR, 'node_modules');
187
+
188
+ // Essential dependencies for daemon
189
+ const deps = ['protobufjs', 'ws', 'long', 'signale', 'chalk', 'figures'];
190
+
191
+ if (!fs.existsSync(destNodeModules)) {
192
+ fs.mkdirSync(destNodeModules, { recursive: true });
193
+ }
194
+
195
+ for (const dep of deps) {
196
+ const srcDep = path.join(nodeModulesDir, dep);
197
+ const destDep = path.join(destNodeModules, dep);
198
+
199
+ if (fs.existsSync(srcDep) && !fs.existsSync(destDep)) {
200
+ copyDirSync(srcDep, destDep);
201
+ }
202
+ }
203
+
204
+ // Write version file
205
+ fs.writeFileSync(DAEMON_VERSION_FILE, currentVersion);
206
+
207
+ log.info('Daemon installed successfully');
208
+ return true;
209
+ } catch (err) {
210
+ log.error('Failed to install daemon', { error: err.message });
211
+ return false;
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Recursively copy directory
217
+ * @param {string} src
218
+ * @param {string} dest
219
+ */
220
+ function copyDirSync(src, dest) {
221
+ if (!fs.existsSync(dest)) {
222
+ fs.mkdirSync(dest, { recursive: true });
223
+ }
224
+
225
+ const entries = fs.readdirSync(src, { withFileTypes: true });
226
+
227
+ for (const entry of entries) {
228
+ const srcPath = path.join(src, entry.name);
229
+ const destPath = path.join(dest, entry.name);
230
+
231
+ if (entry.isDirectory()) {
232
+ copyDirSync(srcPath, destPath);
233
+ } else {
234
+ fs.copyFileSync(srcPath, destPath);
235
+ }
236
+ }
237
+ }
238
+
89
239
  /**
90
240
  * Start daemon in foreground (blocking)
91
241
  * Used when running `hqx --daemon`
@@ -147,7 +297,7 @@ async function startDaemonForeground() {
147
297
  }
148
298
 
149
299
  /**
150
- * Start daemon in background
300
+ * Start daemon in background (from isolated directory)
151
301
  * @returns {Promise<boolean>}
152
302
  */
153
303
  async function startDaemonBackground() {
@@ -156,8 +306,60 @@ async function startDaemonBackground() {
156
306
  return true;
157
307
  }
158
308
 
309
+ // Install daemon to isolated directory first
310
+ const installed = installDaemon();
311
+ if (!installed) {
312
+ log.error('Failed to install daemon');
313
+ return false;
314
+ }
315
+
316
+ // Check if isolated daemon entry exists
317
+ if (!fs.existsSync(DAEMON_ENTRY)) {
318
+ log.error('Daemon entry not found', { path: DAEMON_ENTRY });
319
+ // Fallback to direct execution
320
+ return startDaemonDirect();
321
+ }
322
+
323
+ return new Promise((resolve) => {
324
+ // Spawn daemon from isolated directory (survives npm updates)
325
+ const daemon = spawn(process.execPath, [DAEMON_ENTRY], {
326
+ detached: true,
327
+ stdio: 'ignore',
328
+ cwd: DAEMON_INSTALL_DIR,
329
+ env: {
330
+ ...process.env,
331
+ NODE_PATH: path.join(DAEMON_INSTALL_DIR, 'node_modules'),
332
+ },
333
+ });
334
+
335
+ daemon.unref();
336
+
337
+ // Wait for daemon to start
338
+ let attempts = 0;
339
+ const maxAttempts = 30;
340
+
341
+ const check = setInterval(() => {
342
+ attempts++;
343
+
344
+ if (isDaemonRunning()) {
345
+ clearInterval(check);
346
+ log.debug('Daemon started from isolated directory');
347
+ resolve(true);
348
+ } else if (attempts >= maxAttempts) {
349
+ clearInterval(check);
350
+ log.error('Daemon failed to start');
351
+ resolve(false);
352
+ }
353
+ }, 100);
354
+ });
355
+ }
356
+
357
+ /**
358
+ * Start daemon directly (fallback if isolation fails)
359
+ * @returns {Promise<boolean>}
360
+ */
361
+ function startDaemonDirect() {
159
362
  return new Promise((resolve) => {
160
- // Spawn daemon process
161
363
  const daemon = spawn(process.execPath, [
162
364
  require.resolve('../../cli-daemon'),
163
365
  ], {
@@ -167,7 +369,6 @@ async function startDaemonBackground() {
167
369
 
168
370
  daemon.unref();
169
371
 
170
- // Wait for daemon to start
171
372
  let attempts = 0;
172
373
  const maxAttempts = 20;
173
374
 
@@ -176,7 +377,7 @@ async function startDaemonBackground() {
176
377
 
177
378
  if (isDaemonRunning()) {
178
379
  clearInterval(check);
179
- log.debug('Daemon started');
380
+ log.debug('Daemon started (direct mode)');
180
381
  resolve(true);
181
382
  } else if (attempts >= maxAttempts) {
182
383
  clearInterval(check);
@@ -248,6 +449,11 @@ module.exports = {
248
449
  getDaemonClient,
249
450
  ensureDaemon,
250
451
 
452
+ // Installation
453
+ installDaemon,
454
+ getCurrentVersion,
455
+ getInstalledDaemonVersion,
456
+
251
457
  // Utilities
252
458
  isDaemonRunning,
253
459
  getDaemonPid,
@@ -255,4 +461,5 @@ module.exports = {
255
461
  // Constants
256
462
  SOCKET_PATH,
257
463
  PID_FILE,
464
+ DAEMON_INSTALL_DIR,
258
465
  };