hedgequantx 2.9.215 → 2.9.217
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 +56 -7
- package/package.json +1 -1
- package/src/lib/smart-logs-engine.js +80 -19
- package/src/services/daemon/client.js +413 -0
- package/src/services/daemon/constants.js +104 -0
- package/src/services/daemon/handlers.js +381 -0
- package/src/services/daemon/index.js +258 -0
- package/src/services/daemon/protocol.js +197 -0
- package/src/services/daemon/proxy.js +408 -0
- package/src/services/daemon/server.js +340 -0
package/bin/cli.js
CHANGED
|
@@ -3,7 +3,13 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* HedgeQuantX CLI - Entry Point
|
|
5
5
|
* Prop Futures Algo Trading with Protected Strategy
|
|
6
|
-
*
|
|
6
|
+
*
|
|
7
|
+
* Modes:
|
|
8
|
+
* hqx - Start TUI (connects to daemon if available, or standalone)
|
|
9
|
+
* hqx --daemon - Start daemon in foreground (persistent Rithmic connection)
|
|
10
|
+
* hqx --stop - Stop running daemon
|
|
11
|
+
* hqx --status - Check daemon status
|
|
12
|
+
* hqx -u - Update HQX to latest version
|
|
7
13
|
*/
|
|
8
14
|
|
|
9
15
|
'use strict';
|
|
@@ -34,7 +40,10 @@ program
|
|
|
34
40
|
.name('hqx')
|
|
35
41
|
.description('HedgeQuantX - Prop Futures Algo Trading CLI')
|
|
36
42
|
.version(pkg.version)
|
|
37
|
-
.option('-u, --update', 'Update HQX to latest version')
|
|
43
|
+
.option('-u, --update', 'Update HQX to latest version')
|
|
44
|
+
.option('-d, --daemon', 'Start daemon (persistent Rithmic connection)')
|
|
45
|
+
.option('--stop', 'Stop running daemon')
|
|
46
|
+
.option('--status', 'Check daemon status');
|
|
38
47
|
|
|
39
48
|
program
|
|
40
49
|
.command('start', { isDefault: true })
|
|
@@ -51,12 +60,23 @@ program
|
|
|
51
60
|
console.log(`HedgeQuantX CLI v${pkg.version}`);
|
|
52
61
|
});
|
|
53
62
|
|
|
54
|
-
|
|
55
|
-
|
|
63
|
+
program
|
|
64
|
+
.command('daemon')
|
|
65
|
+
.description('Start daemon in foreground')
|
|
66
|
+
.action(async () => {
|
|
67
|
+
const { startDaemonForeground } = require('../src/services/daemon');
|
|
68
|
+
await startDaemonForeground();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Handle special flags before parsing
|
|
72
|
+
const args = process.argv;
|
|
73
|
+
|
|
74
|
+
// Handle -u flag
|
|
75
|
+
if (args.includes('-u') || args.includes('--update')) {
|
|
56
76
|
const { execSync } = require('child_process');
|
|
57
77
|
console.log('Updating HedgeQuantX...');
|
|
58
78
|
try {
|
|
59
|
-
execSync('npm
|
|
79
|
+
execSync('npm update -g hedgequantx', { stdio: 'inherit' });
|
|
60
80
|
console.log('Update complete! Run "hqx" to start.');
|
|
61
81
|
} catch (e) {
|
|
62
82
|
console.error('Update failed:', e.message);
|
|
@@ -64,5 +84,34 @@ if (process.argv.includes('-u') || process.argv.includes('--update')) {
|
|
|
64
84
|
process.exit(0);
|
|
65
85
|
}
|
|
66
86
|
|
|
67
|
-
//
|
|
68
|
-
|
|
87
|
+
// Handle --daemon flag
|
|
88
|
+
if (args.includes('-d') || args.includes('--daemon')) {
|
|
89
|
+
const { startDaemonForeground } = require('../src/services/daemon');
|
|
90
|
+
startDaemonForeground().catch((err) => {
|
|
91
|
+
console.error('Daemon error:', err.message);
|
|
92
|
+
process.exit(1);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
// Handle --stop flag
|
|
96
|
+
else if (args.includes('--stop')) {
|
|
97
|
+
const { stopDaemon } = require('../src/services/daemon');
|
|
98
|
+
stopDaemon();
|
|
99
|
+
process.exit(0);
|
|
100
|
+
}
|
|
101
|
+
// Handle --status flag
|
|
102
|
+
else if (args.includes('--status')) {
|
|
103
|
+
const { isDaemonRunning, getDaemonPid, SOCKET_PATH } = require('../src/services/daemon');
|
|
104
|
+
|
|
105
|
+
if (isDaemonRunning()) {
|
|
106
|
+
console.log('Daemon Status: RUNNING');
|
|
107
|
+
console.log(' PID:', getDaemonPid());
|
|
108
|
+
console.log(' Socket:', SOCKET_PATH);
|
|
109
|
+
} else {
|
|
110
|
+
console.log('Daemon Status: NOT RUNNING');
|
|
111
|
+
}
|
|
112
|
+
process.exit(0);
|
|
113
|
+
}
|
|
114
|
+
// Normal TUI startup
|
|
115
|
+
else {
|
|
116
|
+
program.parse(process.argv);
|
|
117
|
+
}
|
package/package.json
CHANGED
|
@@ -8,6 +8,16 @@
|
|
|
8
8
|
* - Uses smartLogs.getLiveAnalysisLog() for varied, non-repetitive messages
|
|
9
9
|
*
|
|
10
10
|
* Only logs when something SIGNIFICANT happens - no spam, no repetitive messages
|
|
11
|
+
*
|
|
12
|
+
* COLOR SCHEME:
|
|
13
|
+
* - Symbols: cyan (NQ, ES, CL, GC)
|
|
14
|
+
* - Prices: white bold
|
|
15
|
+
* - Bullish/Long: green
|
|
16
|
+
* - Bearish/Short: red
|
|
17
|
+
* - Neutral/System: gray/dim
|
|
18
|
+
* - Signals: yellow/magenta
|
|
19
|
+
* - Risk/Warnings: red bold
|
|
20
|
+
* - Values: blue (Z-Score, VPIN, OFI numbers)
|
|
11
21
|
*/
|
|
12
22
|
|
|
13
23
|
'use strict';
|
|
@@ -16,6 +26,39 @@ const chalk = require('chalk');
|
|
|
16
26
|
const smartLogs = require('./smart-logs');
|
|
17
27
|
const { getContextualMessage } = require('./smart-logs-context');
|
|
18
28
|
|
|
29
|
+
// Color helpers for consistent styling
|
|
30
|
+
const C = {
|
|
31
|
+
// Symbols & identifiers
|
|
32
|
+
sym: (s) => chalk.cyan.bold(s),
|
|
33
|
+
|
|
34
|
+
// Prices
|
|
35
|
+
price: (p) => chalk.white.bold(p),
|
|
36
|
+
|
|
37
|
+
// Direction
|
|
38
|
+
long: (s) => chalk.green.bold(s),
|
|
39
|
+
short: (s) => chalk.red.bold(s),
|
|
40
|
+
bull: (s) => chalk.green(s),
|
|
41
|
+
bear: (s) => chalk.red(s),
|
|
42
|
+
|
|
43
|
+
// Values & metrics
|
|
44
|
+
val: (v) => chalk.blue(v),
|
|
45
|
+
valHigh: (v) => chalk.magenta.bold(v),
|
|
46
|
+
|
|
47
|
+
// Status
|
|
48
|
+
ok: (s) => chalk.green(s),
|
|
49
|
+
warn: (s) => chalk.yellow(s),
|
|
50
|
+
danger: (s) => chalk.red.bold(s),
|
|
51
|
+
|
|
52
|
+
// System/neutral
|
|
53
|
+
dim: (s) => chalk.dim(s),
|
|
54
|
+
info: (s) => chalk.gray(s),
|
|
55
|
+
|
|
56
|
+
// Special
|
|
57
|
+
signal: (s) => chalk.yellow.bold(s),
|
|
58
|
+
zone: (s) => chalk.magenta(s),
|
|
59
|
+
regime: (s) => chalk.cyan(s),
|
|
60
|
+
};
|
|
61
|
+
|
|
19
62
|
const CONFIG = {
|
|
20
63
|
SESSION_LOG_INTERVAL: 10,
|
|
21
64
|
// HQX-2B thresholds
|
|
@@ -84,11 +127,14 @@ class SmartLogsEngine {
|
|
|
84
127
|
|
|
85
128
|
// Active position - same for all strategies
|
|
86
129
|
if (position !== 0) {
|
|
87
|
-
const
|
|
88
|
-
const
|
|
130
|
+
const isLong = position > 0;
|
|
131
|
+
const side = isLong ? C.long('LONG') : C.short('SHORT');
|
|
132
|
+
const flowFavor = (isLong && delta > 0) || (!isLong && delta < 0);
|
|
133
|
+
const flowLabel = flowFavor ? C.ok('FAVOR') : C.danger('ADVERSE');
|
|
134
|
+
const deltaStr = delta > 0 ? C.bull(`+${delta}`) : C.bear(`${delta}`);
|
|
89
135
|
return {
|
|
90
136
|
type: 'trade',
|
|
91
|
-
message: `[${sym}] ${side} ACTIVE @ ${price} | Delta: ${
|
|
137
|
+
message: `[${C.sym(sym)}] ${side} ACTIVE @ ${C.price(price)} | Delta: ${deltaStr} | Flow: ${flowLabel}`,
|
|
92
138
|
logToSession: true
|
|
93
139
|
};
|
|
94
140
|
}
|
|
@@ -116,35 +162,37 @@ class SmartLogsEngine {
|
|
|
116
162
|
this.warmupLogged = true;
|
|
117
163
|
event = 'warmup';
|
|
118
164
|
const warmupMsg = getContextualMessage(this.symbolCode, this.strategyId, 'warmup');
|
|
119
|
-
message = `[${sym}] 2B ready | ${bars} bars | ${warmupMsg}`;
|
|
165
|
+
message = `[${C.sym(sym)}] ${C.ok('2B ready')} | ${C.val(bars)} bars | ${C.dim(warmupMsg)}`;
|
|
120
166
|
logType = 'system';
|
|
121
167
|
}
|
|
122
168
|
// EVENT 2: New zone created
|
|
123
169
|
else if (zones > this.lastZones && zones > 0) {
|
|
124
170
|
event = 'new_zone';
|
|
125
171
|
const signalMsg = getContextualMessage(this.symbolCode, this.strategyId, 'signal');
|
|
126
|
-
message = `[${sym}] ${price} | Zone
|
|
172
|
+
message = `[${C.sym(sym)}] ${C.price(price)} | ${C.zone('Zone #' + zones)} | ${C.signal(signalMsg)}`;
|
|
127
173
|
logType = 'signal';
|
|
128
174
|
}
|
|
129
175
|
// EVENT 3: New swing detected
|
|
130
176
|
else if (swings > this.lastSwings && swings > 0) {
|
|
131
177
|
event = 'new_swing';
|
|
132
178
|
const scanMsg = getContextualMessage(this.symbolCode, this.strategyId, 'scanning');
|
|
133
|
-
message = `[${sym}] ${price} | Swing
|
|
179
|
+
message = `[${C.sym(sym)}] ${C.price(price)} | ${C.info('Swing #' + swings)} | ${C.dim(scanMsg)}`;
|
|
134
180
|
}
|
|
135
181
|
// EVENT 4: Zone approach (price near zone)
|
|
136
182
|
else if (nearZone && !this.lastNearZone && zones > 0) {
|
|
137
183
|
event = 'zone_approach';
|
|
138
184
|
const signalMsg = getContextualMessage(this.symbolCode, this.strategyId, 'signal');
|
|
139
|
-
message = `[${sym}] ${price} | Zone approach | ${signalMsg}`;
|
|
185
|
+
message = `[${C.sym(sym)}] ${C.price(price)} | ${C.warn('Zone approach')} | ${C.signal(signalMsg)}`;
|
|
140
186
|
logType = 'signal';
|
|
141
187
|
}
|
|
142
188
|
// EVENT 5: Bias flip
|
|
143
189
|
else if (this.lastBias && trend !== this.lastBias && trend !== 'neutral' && this.lastBias !== 'neutral') {
|
|
144
190
|
event = 'bias_flip';
|
|
145
|
-
const arrow = trend === 'bullish' ?
|
|
191
|
+
const arrow = trend === 'bullish' ? C.bull('▲') : C.bear('▼');
|
|
192
|
+
const oldBias = this.lastBias === 'bullish' ? C.bull(this.lastBias) : C.bear(this.lastBias);
|
|
193
|
+
const newBias = trend === 'bullish' ? C.bull(trend) : C.bear(trend);
|
|
146
194
|
const flipMsg = getContextualMessage(this.symbolCode, this.strategyId, trend);
|
|
147
|
-
message = `[${sym}] ${arrow} ${
|
|
195
|
+
message = `[${C.sym(sym)}] ${arrow} ${oldBias} → ${newBias} | ${C.dim(flipMsg)}`;
|
|
148
196
|
}
|
|
149
197
|
|
|
150
198
|
// Update state tracking
|
|
@@ -176,12 +224,22 @@ class SmartLogsEngine {
|
|
|
176
224
|
let logType = 'analysis';
|
|
177
225
|
let message = null;
|
|
178
226
|
|
|
227
|
+
// Helper for Z-Score color
|
|
228
|
+
const zColor = (z) => {
|
|
229
|
+
const absVal = Math.abs(z);
|
|
230
|
+
const formatted = `${z.toFixed(1)}σ`;
|
|
231
|
+
if (absVal >= CONFIG.Z_EXTREME) return C.valHigh(formatted);
|
|
232
|
+
if (absVal >= CONFIG.Z_HIGH) return C.warn(formatted);
|
|
233
|
+
if (absVal >= CONFIG.Z_BUILDING) return C.val(formatted);
|
|
234
|
+
return C.dim(formatted);
|
|
235
|
+
};
|
|
236
|
+
|
|
179
237
|
// EVENT 1: Warmup complete (250 ticks for QUANT models)
|
|
180
238
|
if (ticks >= CONFIG.QUANT_WARMUP_TICKS && !this.warmupLogged) {
|
|
181
239
|
this.warmupLogged = true;
|
|
182
240
|
event = 'warmup';
|
|
183
241
|
const warmupMsg = getContextualMessage(this.symbolCode, this.strategyId, 'warmup');
|
|
184
|
-
message = `[${sym}] QUANT ready | ${ticks} ticks | ${warmupMsg}`;
|
|
242
|
+
message = `[${C.sym(sym)}] ${C.ok('QUANT ready')} | ${C.val(ticks)} ticks | ${C.dim(warmupMsg)}`;
|
|
185
243
|
logType = 'system';
|
|
186
244
|
}
|
|
187
245
|
// EVENT 2: Z-Score regime change
|
|
@@ -193,34 +251,37 @@ class SmartLogsEngine {
|
|
|
193
251
|
|
|
194
252
|
if (zRegime === 'extreme') {
|
|
195
253
|
logType = 'signal';
|
|
196
|
-
const dir = zScore < 0 ? 'LONG' : 'SHORT';
|
|
254
|
+
const dir = zScore < 0 ? C.long('LONG') : C.short('SHORT');
|
|
197
255
|
const signalMsg = getContextualMessage(this.symbolCode, this.strategyId, 'signal');
|
|
198
|
-
message = `[${sym}] ${price} | Z: ${zScore.
|
|
256
|
+
message = `[${C.sym(sym)}] ${C.price(price)} | Z: ${zColor(zScore)} ${C.signal('EXTREME')} | ${dir} | ${C.signal(signalMsg)}`;
|
|
199
257
|
} else if (zRegime === 'high') {
|
|
200
258
|
logType = 'signal';
|
|
201
|
-
message = `[${sym}] ${price} | Z: ${zScore.
|
|
259
|
+
message = `[${C.sym(sym)}] ${C.price(price)} | Z: ${zColor(zScore)} ${C.warn('HIGH')} | ${C.dim(instrumentMsg)}`;
|
|
202
260
|
} else if (zRegime === 'building') {
|
|
203
|
-
message = `[${sym}] ${price} | Z
|
|
261
|
+
message = `[${C.sym(sym)}] ${C.price(price)} | Z: ${zColor(zScore)} ${C.info('building')} | ${C.dim(instrumentMsg)}`;
|
|
204
262
|
} else {
|
|
205
263
|
const scanMsg = getContextualMessage(this.symbolCode, this.strategyId, 'scanning');
|
|
206
|
-
message = `[${sym}] ${price} | Z normalized | ${scanMsg}`;
|
|
264
|
+
message = `[${C.sym(sym)}] ${C.price(price)} | Z: ${C.ok('normalized')} | ${C.dim(scanMsg)}`;
|
|
207
265
|
}
|
|
208
266
|
}
|
|
209
267
|
// EVENT 3: Bias flip (OFI direction change)
|
|
210
268
|
else if (this.lastBias !== null && bias !== this.lastBias && bias !== 'neutral' && this.lastBias !== 'neutral') {
|
|
211
269
|
event = 'bias_flip';
|
|
212
|
-
const arrow = bias === 'bullish' ?
|
|
270
|
+
const arrow = bias === 'bullish' ? C.bull('▲') : C.bear('▼');
|
|
271
|
+
const oldBias = this.lastBias === 'bullish' ? C.bull(this.lastBias) : C.bear(this.lastBias);
|
|
272
|
+
const newBias = bias === 'bullish' ? C.bull(bias) : C.bear(bias);
|
|
213
273
|
const flipMsg = getContextualMessage(this.symbolCode, this.strategyId, bias);
|
|
214
|
-
message = `[${sym}] ${arrow} OFI: ${
|
|
274
|
+
message = `[${C.sym(sym)}] ${arrow} OFI: ${oldBias} → ${newBias} | ${C.dim(flipMsg)}`;
|
|
215
275
|
}
|
|
216
276
|
// EVENT 4: VPIN toxicity change
|
|
217
277
|
else if (this.lastVpinToxic !== null && vpinToxic !== this.lastVpinToxic) {
|
|
218
278
|
event = 'vpin';
|
|
279
|
+
const vpinPct = (vpin * 100).toFixed(0);
|
|
219
280
|
if (vpinToxic) {
|
|
220
|
-
message = `[${sym}] ${price} | VPIN
|
|
281
|
+
message = `[${C.sym(sym)}] ${C.price(price)} | VPIN: ${C.danger(vpinPct + '%')} ${C.danger('TOXIC')} - informed flow`;
|
|
221
282
|
logType = 'risk';
|
|
222
283
|
} else {
|
|
223
|
-
message = `[${sym}] ${price} | VPIN
|
|
284
|
+
message = `[${C.sym(sym)}] ${C.price(price)} | VPIN: ${C.ok(vpinPct + '%')} ${C.ok('clean')} - normal flow`;
|
|
224
285
|
}
|
|
225
286
|
}
|
|
226
287
|
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Daemon Client - TUI connection to daemon
|
|
3
|
+
* @module services/daemon/client
|
|
4
|
+
*
|
|
5
|
+
* Connects to the HQX daemon via Unix socket.
|
|
6
|
+
* Provides async request/response API for TUI.
|
|
7
|
+
*
|
|
8
|
+
* NO MOCK DATA - All data comes from daemon (which gets it from Rithmic)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const net = require('net');
|
|
14
|
+
const EventEmitter = require('events');
|
|
15
|
+
const { SOCKET_PATH, MSG_TYPE, TIMEOUTS } = require('./constants');
|
|
16
|
+
const { createMessage, encode, MessageParser, RequestHandler } = require('./protocol');
|
|
17
|
+
const { logger } = require('../../utils/logger');
|
|
18
|
+
|
|
19
|
+
const log = logger.scope('DaemonClient');
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Daemon Client for TUI
|
|
23
|
+
* Connects to daemon and provides async API
|
|
24
|
+
*/
|
|
25
|
+
class DaemonClient extends EventEmitter {
|
|
26
|
+
constructor() {
|
|
27
|
+
super();
|
|
28
|
+
|
|
29
|
+
/** @type {net.Socket|null} */
|
|
30
|
+
this.socket = null;
|
|
31
|
+
|
|
32
|
+
/** @type {MessageParser} */
|
|
33
|
+
this.parser = new MessageParser();
|
|
34
|
+
|
|
35
|
+
/** @type {RequestHandler} */
|
|
36
|
+
this.requests = new RequestHandler();
|
|
37
|
+
|
|
38
|
+
/** @type {boolean} */
|
|
39
|
+
this.connected = false;
|
|
40
|
+
|
|
41
|
+
/** @type {NodeJS.Timeout|null} */
|
|
42
|
+
this.pingInterval = null;
|
|
43
|
+
|
|
44
|
+
/** @type {Object|null} Cached daemon info */
|
|
45
|
+
this.daemonInfo = null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Connect to daemon
|
|
50
|
+
* @returns {Promise<boolean>}
|
|
51
|
+
*/
|
|
52
|
+
async connect() {
|
|
53
|
+
if (this.connected) return true;
|
|
54
|
+
|
|
55
|
+
return new Promise((resolve) => {
|
|
56
|
+
this.socket = net.createConnection(SOCKET_PATH);
|
|
57
|
+
|
|
58
|
+
this.socket.on('connect', async () => {
|
|
59
|
+
log.debug('Connected to daemon');
|
|
60
|
+
this.connected = true;
|
|
61
|
+
|
|
62
|
+
// Perform handshake
|
|
63
|
+
try {
|
|
64
|
+
this.daemonInfo = await this._request(MSG_TYPE.HANDSHAKE, null, TIMEOUTS.HANDSHAKE);
|
|
65
|
+
log.debug('Handshake complete', this.daemonInfo);
|
|
66
|
+
|
|
67
|
+
// Start ping interval
|
|
68
|
+
this._startPing();
|
|
69
|
+
|
|
70
|
+
resolve(true);
|
|
71
|
+
} catch (err) {
|
|
72
|
+
log.error('Handshake failed', { error: err.message });
|
|
73
|
+
this.disconnect();
|
|
74
|
+
resolve(false);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
this.socket.on('data', (data) => {
|
|
79
|
+
const messages = this.parser.feed(data);
|
|
80
|
+
for (const msg of messages) {
|
|
81
|
+
this._handleMessage(msg);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
this.socket.on('close', () => {
|
|
86
|
+
log.debug('Disconnected from daemon');
|
|
87
|
+
this._cleanup();
|
|
88
|
+
this.emit('disconnected');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
this.socket.on('error', (err) => {
|
|
92
|
+
if (err.code === 'ENOENT') {
|
|
93
|
+
log.debug('Daemon not running');
|
|
94
|
+
} else if (err.code === 'ECONNREFUSED') {
|
|
95
|
+
log.debug('Daemon connection refused');
|
|
96
|
+
} else {
|
|
97
|
+
log.warn('Socket error', { error: err.message });
|
|
98
|
+
}
|
|
99
|
+
this._cleanup();
|
|
100
|
+
resolve(false);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Check if daemon is available
|
|
107
|
+
* @returns {Promise<boolean>}
|
|
108
|
+
*/
|
|
109
|
+
async isAvailable() {
|
|
110
|
+
const connected = await this.connect();
|
|
111
|
+
return connected;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Disconnect from daemon
|
|
116
|
+
*/
|
|
117
|
+
disconnect() {
|
|
118
|
+
if (this.socket) {
|
|
119
|
+
this.socket.destroy();
|
|
120
|
+
}
|
|
121
|
+
this._cleanup();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Cleanup state
|
|
126
|
+
*/
|
|
127
|
+
_cleanup() {
|
|
128
|
+
this.connected = false;
|
|
129
|
+
this.requests.clear();
|
|
130
|
+
this.parser.reset();
|
|
131
|
+
|
|
132
|
+
if (this.pingInterval) {
|
|
133
|
+
clearInterval(this.pingInterval);
|
|
134
|
+
this.pingInterval = null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
this.socket = null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Start ping interval
|
|
142
|
+
*/
|
|
143
|
+
_startPing() {
|
|
144
|
+
this.pingInterval = setInterval(async () => {
|
|
145
|
+
try {
|
|
146
|
+
await this._request(MSG_TYPE.PING, null, TIMEOUTS.PING_TIMEOUT);
|
|
147
|
+
} catch (err) {
|
|
148
|
+
log.warn('Ping failed, disconnecting');
|
|
149
|
+
this.disconnect();
|
|
150
|
+
}
|
|
151
|
+
}, TIMEOUTS.PING_INTERVAL);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Handle incoming message
|
|
156
|
+
* @param {Object} msg
|
|
157
|
+
*/
|
|
158
|
+
_handleMessage(msg) {
|
|
159
|
+
const { type, data, replyTo } = msg;
|
|
160
|
+
|
|
161
|
+
// Check if this is a response to a pending request
|
|
162
|
+
if (replyTo && this.requests.resolve(replyTo, data)) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Handle push events from daemon
|
|
167
|
+
switch (type) {
|
|
168
|
+
case MSG_TYPE.EVENT_ORDER_UPDATE:
|
|
169
|
+
this.emit('orderUpdate', data);
|
|
170
|
+
break;
|
|
171
|
+
|
|
172
|
+
case MSG_TYPE.EVENT_POSITION_UPDATE:
|
|
173
|
+
this.emit('positionUpdate', data);
|
|
174
|
+
break;
|
|
175
|
+
|
|
176
|
+
case MSG_TYPE.EVENT_PNL_UPDATE:
|
|
177
|
+
this.emit('pnlUpdate', data);
|
|
178
|
+
break;
|
|
179
|
+
|
|
180
|
+
case MSG_TYPE.EVENT_FILL:
|
|
181
|
+
this.emit('fill', data);
|
|
182
|
+
break;
|
|
183
|
+
|
|
184
|
+
case MSG_TYPE.EVENT_DISCONNECTED:
|
|
185
|
+
this.emit('rithmicDisconnected', data);
|
|
186
|
+
break;
|
|
187
|
+
|
|
188
|
+
case MSG_TYPE.EVENT_RECONNECTED:
|
|
189
|
+
this.emit('rithmicReconnected', data);
|
|
190
|
+
break;
|
|
191
|
+
|
|
192
|
+
case MSG_TYPE.MARKET_DATA:
|
|
193
|
+
case MSG_TYPE.TICK:
|
|
194
|
+
this.emit('marketData', data);
|
|
195
|
+
break;
|
|
196
|
+
|
|
197
|
+
case MSG_TYPE.ALGO_LOG:
|
|
198
|
+
this.emit('algoLog', data);
|
|
199
|
+
break;
|
|
200
|
+
|
|
201
|
+
case MSG_TYPE.PONG:
|
|
202
|
+
// Handled by request handler
|
|
203
|
+
break;
|
|
204
|
+
|
|
205
|
+
default:
|
|
206
|
+
log.debug('Unhandled message', { type });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Send request and wait for response
|
|
212
|
+
* @param {string} type - Message type
|
|
213
|
+
* @param {any} data - Request data
|
|
214
|
+
* @param {number} [timeout] - Timeout in ms
|
|
215
|
+
* @returns {Promise<any>} Response data
|
|
216
|
+
*/
|
|
217
|
+
async _request(type, data, timeout = TIMEOUTS.REQUEST) {
|
|
218
|
+
if (!this.connected || !this.socket) {
|
|
219
|
+
throw new Error('Not connected to daemon');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const msg = createMessage(type, data);
|
|
223
|
+
const promise = this.requests.createRequest(msg.id, timeout);
|
|
224
|
+
|
|
225
|
+
this.socket.write(encode(msg));
|
|
226
|
+
|
|
227
|
+
return promise;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ==================== PUBLIC API ====================
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Get daemon status
|
|
234
|
+
* @returns {Promise<Object>}
|
|
235
|
+
*/
|
|
236
|
+
async getStatus() {
|
|
237
|
+
return this._request(MSG_TYPE.GET_STATUS);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Login to Rithmic via daemon
|
|
242
|
+
* @param {string} propfirmKey
|
|
243
|
+
* @param {string} username
|
|
244
|
+
* @param {string} password
|
|
245
|
+
* @returns {Promise<Object>}
|
|
246
|
+
*/
|
|
247
|
+
async login(propfirmKey, username, password) {
|
|
248
|
+
return this._request(MSG_TYPE.LOGIN, { propfirmKey, username, password }, TIMEOUTS.LOGIN);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Restore session from storage
|
|
253
|
+
* @returns {Promise<Object>}
|
|
254
|
+
*/
|
|
255
|
+
async restoreSession() {
|
|
256
|
+
return this._request(MSG_TYPE.RESTORE_SESSION, null, TIMEOUTS.LOGIN);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Logout
|
|
261
|
+
* @returns {Promise<Object>}
|
|
262
|
+
*/
|
|
263
|
+
async logout() {
|
|
264
|
+
return this._request(MSG_TYPE.LOGOUT);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Get trading accounts
|
|
269
|
+
* @returns {Promise<Object>}
|
|
270
|
+
*/
|
|
271
|
+
async getTradingAccounts() {
|
|
272
|
+
return this._request(MSG_TYPE.GET_ACCOUNTS);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Get positions
|
|
277
|
+
* @returns {Promise<Object>}
|
|
278
|
+
*/
|
|
279
|
+
async getPositions() {
|
|
280
|
+
return this._request(MSG_TYPE.GET_POSITIONS);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Get orders
|
|
285
|
+
* @returns {Promise<Object>}
|
|
286
|
+
*/
|
|
287
|
+
async getOrders() {
|
|
288
|
+
return this._request(MSG_TYPE.GET_ORDERS);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Get P&L for account
|
|
293
|
+
* @param {string} accountId
|
|
294
|
+
* @returns {Promise<Object>}
|
|
295
|
+
*/
|
|
296
|
+
async getPnL(accountId) {
|
|
297
|
+
return this._request(MSG_TYPE.GET_PNL, { accountId });
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Place order
|
|
302
|
+
* @param {Object} orderData
|
|
303
|
+
* @returns {Promise<Object>}
|
|
304
|
+
*/
|
|
305
|
+
async placeOrder(orderData) {
|
|
306
|
+
return this._request(MSG_TYPE.PLACE_ORDER, orderData);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Cancel order
|
|
311
|
+
* @param {string} orderId
|
|
312
|
+
* @returns {Promise<Object>}
|
|
313
|
+
*/
|
|
314
|
+
async cancelOrder(orderId) {
|
|
315
|
+
return this._request(MSG_TYPE.CANCEL_ORDER, { orderId });
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Cancel all orders for account
|
|
320
|
+
* @param {string} accountId
|
|
321
|
+
* @returns {Promise<Object>}
|
|
322
|
+
*/
|
|
323
|
+
async cancelAllOrders(accountId) {
|
|
324
|
+
return this._request(MSG_TYPE.CANCEL_ALL, { accountId });
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Close position
|
|
329
|
+
* @param {string} accountId
|
|
330
|
+
* @param {string} symbol
|
|
331
|
+
* @returns {Promise<Object>}
|
|
332
|
+
*/
|
|
333
|
+
async closePosition(accountId, symbol) {
|
|
334
|
+
return this._request(MSG_TYPE.CLOSE_POSITION, { accountId, symbol });
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Get contracts
|
|
339
|
+
* @returns {Promise<Object>}
|
|
340
|
+
*/
|
|
341
|
+
async getContracts() {
|
|
342
|
+
return this._request(MSG_TYPE.GET_CONTRACTS);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Search contracts
|
|
347
|
+
* @param {string} search
|
|
348
|
+
* @returns {Promise<Object>}
|
|
349
|
+
*/
|
|
350
|
+
async searchContracts(search) {
|
|
351
|
+
return this._request(MSG_TYPE.SEARCH_CONTRACTS, { search });
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Subscribe to market data
|
|
356
|
+
* @param {string} symbol
|
|
357
|
+
* @returns {Promise<Object>}
|
|
358
|
+
*/
|
|
359
|
+
async subscribeMarket(symbol) {
|
|
360
|
+
return this._request(MSG_TYPE.SUBSCRIBE_MARKET, { symbol });
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Unsubscribe from market data
|
|
365
|
+
* @param {string} symbol
|
|
366
|
+
* @returns {Promise<Object>}
|
|
367
|
+
*/
|
|
368
|
+
async unsubscribeMarket(symbol) {
|
|
369
|
+
return this._request(MSG_TYPE.UNSUBSCRIBE_MARKET, { symbol });
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Start algo trading
|
|
374
|
+
* @param {Object} config
|
|
375
|
+
* @returns {Promise<Object>}
|
|
376
|
+
*/
|
|
377
|
+
async startAlgo(config) {
|
|
378
|
+
return this._request(MSG_TYPE.START_ALGO, config);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Stop algo trading
|
|
383
|
+
* @param {string} algoId
|
|
384
|
+
* @returns {Promise<Object>}
|
|
385
|
+
*/
|
|
386
|
+
async stopAlgo(algoId) {
|
|
387
|
+
return this._request(MSG_TYPE.STOP_ALGO, { algoId });
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Shutdown daemon
|
|
392
|
+
* @returns {Promise<Object>}
|
|
393
|
+
*/
|
|
394
|
+
async shutdown() {
|
|
395
|
+
return this._request(MSG_TYPE.SHUTDOWN);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Singleton instance
|
|
400
|
+
let instance = null;
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Get daemon client instance
|
|
404
|
+
* @returns {DaemonClient}
|
|
405
|
+
*/
|
|
406
|
+
function getDaemonClient() {
|
|
407
|
+
if (!instance) {
|
|
408
|
+
instance = new DaemonClient();
|
|
409
|
+
}
|
|
410
|
+
return instance;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
module.exports = { DaemonClient, getDaemonClient };
|