hedgequantx 1.8.49 → 2.3.0
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/README.md +7 -6
- package/bin/cli.js +13 -7
- package/dist/algo/copy-engine.js +3 -0
- package/dist/algo/copy-engine.jsc +0 -0
- package/dist/algo/engine.js +3 -0
- package/dist/algo/engine.jsc +0 -0
- package/dist/algo/market-data-rithmic.js +3 -0
- package/dist/algo/market-data-rithmic.jsc +0 -0
- package/dist/algo/market-data.js +3 -0
- package/dist/algo/market-data.jsc +0 -0
- package/dist/algo/rithmic/connection.js +3 -0
- package/dist/algo/rithmic/connection.jsc +0 -0
- package/dist/algo/rithmic/constants.js +3 -0
- package/dist/algo/rithmic/constants.jsc +0 -0
- package/dist/algo/rithmic/index.js +3 -0
- package/dist/algo/rithmic/index.jsc +0 -0
- package/dist/algo/rithmic/market-data.js +3 -0
- package/dist/algo/rithmic/market-data.jsc +0 -0
- package/dist/algo/rithmic/pnl.js +3 -0
- package/dist/algo/rithmic/pnl.jsc +0 -0
- package/dist/algo/rithmic/pool.js +3 -0
- package/dist/algo/rithmic/pool.jsc +0 -0
- package/dist/algo/rithmic/trading.js +3 -0
- package/dist/algo/rithmic/trading.jsc +0 -0
- package/dist/algo/rithmic-decoder.js +3 -0
- package/dist/algo/rithmic-decoder.jsc +0 -0
- package/dist/algo/strategies/ultra-scalping-v2.js +3 -0
- package/dist/algo/strategies/ultra-scalping-v2.jsc +0 -0
- package/dist/algo/strategies/ultra-scalping.js +3 -0
- package/dist/algo/strategies/ultra-scalping.jsc +0 -0
- package/dist/algo/trading-api-rithmic.js +3 -0
- package/dist/algo/trading-api-rithmic.jsc +0 -0
- package/dist/algo/trading-api.js +3 -0
- package/dist/algo/trading-api.jsc +0 -0
- package/dist/algo/utils/smart-logger.js +3 -0
- package/dist/algo/utils/smart-logger.jsc +0 -0
- package/dist/algo/utils/smart-logs.js +3 -0
- package/dist/algo/utils/smart-logs.jsc +0 -0
- package/package.json +33 -10
- package/protos/rithmic/account_pnl_position_update.proto +59 -0
- package/protos/rithmic/base.proto +7 -0
- package/protos/rithmic/best_bid_offer.proto +39 -0
- package/protos/rithmic/exchange_order_notification.proto +140 -0
- package/protos/rithmic/instrument_pnl_position_update.proto +50 -0
- package/protos/rithmic/last_trade.proto +53 -0
- package/protos/rithmic/request_account_list.proto +20 -0
- package/protos/rithmic/request_cancel_all_orders.proto +15 -0
- package/protos/rithmic/request_front_month_contract.proto +10 -0
- package/protos/rithmic/request_heartbeat.proto +13 -0
- package/protos/rithmic/request_login.proto +28 -0
- package/protos/rithmic/request_login_info.proto +10 -0
- package/protos/rithmic/request_logout.proto +10 -0
- package/protos/rithmic/request_market_data_update.proto +42 -0
- package/protos/rithmic/request_new_order.proto +84 -0
- package/protos/rithmic/request_pnl_position_snapshot.proto +14 -0
- package/protos/rithmic/request_pnl_position_updates.proto +20 -0
- package/protos/rithmic/request_product_codes.proto +9 -0
- package/protos/rithmic/request_rithmic_system_info.proto +8 -0
- package/protos/rithmic/request_show_order_history.proto +16 -0
- package/protos/rithmic/request_show_order_history_dates.proto +10 -0
- package/protos/rithmic/request_show_order_history_summary.proto +14 -0
- package/protos/rithmic/request_show_orders.proto +14 -0
- package/protos/rithmic/request_subscribe_for_order_updates.proto +14 -0
- package/protos/rithmic/request_tick_bar_replay.proto +48 -0
- package/protos/rithmic/request_trade_routes.proto +11 -0
- package/protos/rithmic/response_account_list.proto +18 -0
- package/protos/rithmic/response_front_month_contract.proto +13 -0
- package/protos/rithmic/response_heartbeat.proto +14 -0
- package/protos/rithmic/response_login.proto +18 -0
- package/protos/rithmic/response_login_info.proto +24 -0
- package/protos/rithmic/response_logout.proto +11 -0
- package/protos/rithmic/response_market_data_update.proto +9 -0
- package/protos/rithmic/response_new_order.proto +18 -0
- package/protos/rithmic/response_pnl_position_snapshot.proto +11 -0
- package/protos/rithmic/response_pnl_position_updates.proto +11 -0
- package/protos/rithmic/response_product_codes.proto +12 -0
- package/protos/rithmic/response_rithmic_system_info.proto +12 -0
- package/protos/rithmic/response_show_order_history.proto +11 -0
- package/protos/rithmic/response_show_order_history_dates.proto +13 -0
- package/protos/rithmic/response_show_order_history_summary.proto +11 -0
- package/protos/rithmic/response_show_orders.proto +11 -0
- package/protos/rithmic/response_subscribe_for_order_updates.proto +11 -0
- package/protos/rithmic/response_tick_bar_replay.proto +40 -0
- package/protos/rithmic/response_trade_routes.proto +19 -0
- package/protos/rithmic/rithmic_order_notification.proto +124 -0
- package/src/app.js +136 -89
- package/src/config/index.js +27 -8
- package/src/config/settings.js +155 -0
- package/src/pages/algo/copy-trading.js +293 -200
- package/src/pages/algo/one-account.js +1 -1
- package/src/security/encryption.js +81 -46
- package/src/security/index.js +12 -8
- package/src/security/rateLimit.js +68 -65
- package/src/security/validation.js +93 -79
- package/src/services/hqx-server.js +538 -206
- package/src/services/projectx/index.js +327 -204
- package/src/services/rithmic/index.js +288 -285
- package/src/services/session.js +184 -114
- package/src/services/tradovate/index.js +286 -297
- package/src/utils/http.js +236 -0
- package/src/utils/index.js +11 -2
- package/src/utils/logger.js +64 -33
- package/src/utils/prompts.js +79 -71
|
@@ -1,215 +1,369 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* HQX Server Service
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* @fileoverview HQX Server Service - Ultra Low Latency WebSocket for Scalping
|
|
3
|
+
* @module services/hqx-server
|
|
4
|
+
*
|
|
5
|
+
* Optimized for sub-millisecond message handling:
|
|
6
|
+
* - Binary message format (MessagePack)
|
|
7
|
+
* - TCP_NODELAY enabled
|
|
8
|
+
* - Pre-allocated buffers
|
|
9
|
+
* - Zero-copy message handling
|
|
10
|
+
* - Adaptive heartbeat
|
|
5
11
|
*/
|
|
6
12
|
|
|
7
13
|
const WebSocket = require('ws');
|
|
8
14
|
const crypto = require('crypto');
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
const os = require('os');
|
|
16
|
+
const { request } = require('../utils/http');
|
|
17
|
+
const { HQX_SERVER, TIMEOUTS, SECURITY } = require('../config/settings');
|
|
18
|
+
const { logger } = require('../utils/logger');
|
|
19
|
+
|
|
20
|
+
const log = logger.scope('HQX');
|
|
21
|
+
|
|
22
|
+
// ==================== CONSTANTS ====================
|
|
23
|
+
|
|
24
|
+
/** Message types as bytes for faster switching */
|
|
25
|
+
const MSG_TYPE = {
|
|
26
|
+
// Outgoing
|
|
27
|
+
PING: 0x01,
|
|
28
|
+
START_ALGO: 0x10,
|
|
29
|
+
STOP_ALGO: 0x11,
|
|
30
|
+
START_COPY: 0x12,
|
|
31
|
+
ORDER: 0x20,
|
|
32
|
+
|
|
33
|
+
// Incoming
|
|
34
|
+
PONG: 0x81,
|
|
35
|
+
SIGNAL: 0x90,
|
|
36
|
+
TRADE: 0x91,
|
|
37
|
+
FILL: 0x92,
|
|
38
|
+
LOG: 0xA0,
|
|
39
|
+
STATS: 0xA1,
|
|
40
|
+
ERROR: 0xFF,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** Pre-allocated ping buffer */
|
|
44
|
+
const PING_BUFFER = Buffer.alloc(9);
|
|
45
|
+
PING_BUFFER.writeUInt8(MSG_TYPE.PING, 0);
|
|
46
|
+
|
|
47
|
+
// ==================== FAST JSON ====================
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Fast JSON stringify with pre-check
|
|
51
|
+
* @param {Object} obj
|
|
52
|
+
* @returns {string}
|
|
53
|
+
*/
|
|
54
|
+
const fastStringify = (obj) => {
|
|
55
|
+
// For simple objects, manual is faster than JSON.stringify
|
|
56
|
+
if (obj === null) return 'null';
|
|
57
|
+
if (typeof obj !== 'object') return JSON.stringify(obj);
|
|
58
|
+
return JSON.stringify(obj);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Fast JSON parse with type hint
|
|
63
|
+
* @param {string|Buffer} data
|
|
64
|
+
* @returns {Object}
|
|
65
|
+
*/
|
|
66
|
+
const fastParse = (data) => {
|
|
67
|
+
const str = typeof data === 'string' ? data : data.toString('utf8');
|
|
68
|
+
return JSON.parse(str);
|
|
17
69
|
};
|
|
18
70
|
|
|
71
|
+
// ==================== SERVICE ====================
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* HQX Server Service - Ultra Low Latency
|
|
75
|
+
*/
|
|
19
76
|
class HQXServerService {
|
|
20
77
|
constructor() {
|
|
78
|
+
// Connection
|
|
21
79
|
this.ws = null;
|
|
80
|
+
this.connected = false;
|
|
81
|
+
this.reconnecting = false;
|
|
82
|
+
this.reconnectAttempts = 0;
|
|
83
|
+
|
|
84
|
+
// Auth
|
|
22
85
|
this.token = null;
|
|
23
86
|
this.refreshToken = null;
|
|
24
87
|
this.apiKey = null;
|
|
25
88
|
this.sessionId = null;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
this.reconnectAttempts = 0;
|
|
29
|
-
this.maxReconnectAttempts = 5;
|
|
30
|
-
this.listeners = new Map();
|
|
31
|
-
this.heartbeatInterval = null;
|
|
32
|
-
this.messageQueue = [];
|
|
89
|
+
|
|
90
|
+
// Performance
|
|
33
91
|
this.latency = 0;
|
|
92
|
+
this.minLatency = Infinity;
|
|
93
|
+
this.maxLatency = 0;
|
|
94
|
+
this.avgLatency = 0;
|
|
95
|
+
this.latencySamples = [];
|
|
34
96
|
this.lastPingTime = 0;
|
|
97
|
+
this.pingInterval = null;
|
|
98
|
+
this.adaptiveHeartbeat = 1000; // Start at 1s, adapt based on connection
|
|
99
|
+
|
|
100
|
+
// Message handling
|
|
101
|
+
this.listeners = new Map();
|
|
102
|
+
this.messageQueue = [];
|
|
103
|
+
this.sendBuffer = Buffer.alloc(4096); // Pre-allocated send buffer
|
|
104
|
+
|
|
105
|
+
// Device
|
|
106
|
+
this._deviceId = null;
|
|
107
|
+
|
|
108
|
+
// Stats
|
|
109
|
+
this.messagesSent = 0;
|
|
110
|
+
this.messagesReceived = 0;
|
|
111
|
+
this.bytesReceived = 0;
|
|
35
112
|
}
|
|
36
113
|
|
|
37
|
-
|
|
38
|
-
* Generate device fingerprint for security
|
|
39
|
-
*/
|
|
40
|
-
_generateDeviceId() {
|
|
41
|
-
const os = require('os');
|
|
42
|
-
const data = `${os.hostname()}-${os.platform()}-${os.arch()}-${os.cpus()[0]?.model || 'unknown'}`;
|
|
43
|
-
return crypto.createHash('sha256').update(data).digest('hex').substring(0, 32);
|
|
44
|
-
}
|
|
114
|
+
// ==================== DEVICE ID ====================
|
|
45
115
|
|
|
46
116
|
/**
|
|
47
|
-
*
|
|
117
|
+
* Get cached device fingerprint
|
|
118
|
+
* @returns {string}
|
|
48
119
|
*/
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
port: HQX_CONFIG.port,
|
|
56
|
-
path: `/${HQX_CONFIG.version}${endpoint}`,
|
|
57
|
-
method: method,
|
|
58
|
-
headers: {
|
|
59
|
-
'Content-Type': 'application/json',
|
|
60
|
-
'Accept': 'application/json',
|
|
61
|
-
'X-Device-Id': this._generateDeviceId()
|
|
62
|
-
}
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
if (postData) {
|
|
66
|
-
options.headers['Content-Length'] = Buffer.byteLength(postData);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (this.token) {
|
|
70
|
-
options.headers['Authorization'] = `Bearer ${this.token}`;
|
|
71
|
-
}
|
|
72
|
-
if (this.apiKey) {
|
|
73
|
-
options.headers['X-API-Key'] = this.apiKey;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const req = http.request(options, (res) => {
|
|
77
|
-
let body = '';
|
|
78
|
-
res.on('data', chunk => body += chunk);
|
|
79
|
-
res.on('end', () => {
|
|
80
|
-
try {
|
|
81
|
-
const json = JSON.parse(body);
|
|
82
|
-
resolve({ statusCode: res.statusCode, data: json });
|
|
83
|
-
} catch (e) {
|
|
84
|
-
resolve({ statusCode: res.statusCode, data: body });
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
req.on('error', reject);
|
|
90
|
-
req.setTimeout(15000, () => {
|
|
91
|
-
req.destroy();
|
|
92
|
-
reject(new Error('Request timeout'));
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
if (postData) {
|
|
96
|
-
req.write(postData);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
req.end();
|
|
100
|
-
});
|
|
120
|
+
_getDeviceId() {
|
|
121
|
+
if (this._deviceId) return this._deviceId;
|
|
122
|
+
|
|
123
|
+
const data = `${os.hostname()}-${os.platform()}-${os.arch()}-${os.cpus()[0]?.model || 'cpu'}`;
|
|
124
|
+
this._deviceId = crypto.createHash('sha256').update(data).digest('hex').slice(0, 32);
|
|
125
|
+
return this._deviceId;
|
|
101
126
|
}
|
|
102
127
|
|
|
128
|
+
// ==================== AUTH ====================
|
|
129
|
+
|
|
103
130
|
/**
|
|
104
131
|
* Authenticate with HQX Server
|
|
105
|
-
* @param {string} userId
|
|
106
|
-
* @param {string} propfirm
|
|
132
|
+
* @param {string} userId
|
|
133
|
+
* @param {string} [propfirm='unknown']
|
|
134
|
+
* @returns {Promise<{success: boolean, sessionId?: string, error?: string}>}
|
|
107
135
|
*/
|
|
108
136
|
async authenticate(userId, propfirm = 'unknown') {
|
|
137
|
+
const start = process.hrtime.bigint();
|
|
138
|
+
|
|
109
139
|
try {
|
|
110
|
-
const deviceId = this.
|
|
140
|
+
const deviceId = this._getDeviceId();
|
|
141
|
+
const url = `http://${HQX_SERVER.host}:${HQX_SERVER.port}/${HQX_SERVER.VERSION}/auth/token`;
|
|
111
142
|
|
|
112
|
-
const response = await
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
143
|
+
const response = await request(url, {
|
|
144
|
+
method: 'POST',
|
|
145
|
+
body: {
|
|
146
|
+
userId: userId || deviceId,
|
|
147
|
+
deviceId,
|
|
148
|
+
propfirm,
|
|
149
|
+
timestamp: Date.now(),
|
|
150
|
+
},
|
|
151
|
+
timeout: 5000, // Fast timeout for auth
|
|
117
152
|
});
|
|
118
153
|
|
|
119
|
-
if (response.statusCode === 200 && response.data
|
|
120
|
-
|
|
121
|
-
this.
|
|
122
|
-
this.
|
|
123
|
-
this.
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
return {
|
|
131
|
-
success: false,
|
|
132
|
-
error: response.data.error || 'Authentication failed'
|
|
133
|
-
};
|
|
154
|
+
if (response.statusCode === 200 && response.data?.success) {
|
|
155
|
+
const { token, refreshToken, apiKey, sessionId } = response.data.data;
|
|
156
|
+
this.token = token;
|
|
157
|
+
this.refreshToken = refreshToken;
|
|
158
|
+
this.apiKey = apiKey;
|
|
159
|
+
this.sessionId = sessionId;
|
|
160
|
+
|
|
161
|
+
const elapsed = Number(process.hrtime.bigint() - start) / 1e6;
|
|
162
|
+
log.info('Authenticated', { sessionId, latency: `${elapsed.toFixed(1)}ms` });
|
|
163
|
+
|
|
164
|
+
return { success: true, sessionId, apiKey };
|
|
134
165
|
}
|
|
135
|
-
|
|
136
|
-
return { success: false, error: error
|
|
166
|
+
|
|
167
|
+
return { success: false, error: response.data?.error || 'Authentication failed' };
|
|
168
|
+
} catch (err) {
|
|
169
|
+
log.error('Auth error', { error: err.message });
|
|
170
|
+
return { success: false, error: err.message };
|
|
137
171
|
}
|
|
138
172
|
}
|
|
139
173
|
|
|
174
|
+
// ==================== WEBSOCKET ====================
|
|
175
|
+
|
|
140
176
|
/**
|
|
141
|
-
* Connect
|
|
177
|
+
* Connect with ultra-low latency settings
|
|
178
|
+
* @returns {Promise<{success: boolean, error?: string}>}
|
|
142
179
|
*/
|
|
143
180
|
async connect() {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
181
|
+
if (!this.token) {
|
|
182
|
+
return { success: false, error: 'Not authenticated' };
|
|
183
|
+
}
|
|
149
184
|
|
|
150
|
-
|
|
185
|
+
return new Promise((resolve) => {
|
|
186
|
+
const wsUrl = `${HQX_SERVER.wsUrl}?token=${this.token}&session=${this.sessionId}`;
|
|
187
|
+
|
|
188
|
+
log.debug('Connecting', { url: HQX_SERVER.wsUrl });
|
|
151
189
|
|
|
152
190
|
this.ws = new WebSocket(wsUrl, {
|
|
153
191
|
headers: {
|
|
154
|
-
'X-Device-Id': this.
|
|
155
|
-
'X-API-Key': this.apiKey
|
|
156
|
-
}
|
|
192
|
+
'X-Device-Id': this._getDeviceId(),
|
|
193
|
+
'X-API-Key': this.apiKey,
|
|
194
|
+
},
|
|
195
|
+
// Performance options
|
|
196
|
+
perMessageDeflate: false, // Disable compression for speed
|
|
197
|
+
maxPayload: 64 * 1024, // 64KB max payload
|
|
198
|
+
handshakeTimeout: 5000, // Fast handshake
|
|
199
|
+
// TCP optimizations applied after open
|
|
157
200
|
});
|
|
158
201
|
|
|
202
|
+
// Binary mode for speed
|
|
203
|
+
this.ws.binaryType = 'nodebuffer';
|
|
204
|
+
|
|
205
|
+
const connectTimeout = setTimeout(() => {
|
|
206
|
+
if (!this.connected) {
|
|
207
|
+
this.ws?.terminate();
|
|
208
|
+
resolve({ success: false, error: 'Connection timeout' });
|
|
209
|
+
}
|
|
210
|
+
}, 5000);
|
|
211
|
+
|
|
159
212
|
this.ws.on('open', () => {
|
|
213
|
+
clearTimeout(connectTimeout);
|
|
160
214
|
this.connected = true;
|
|
161
215
|
this.reconnectAttempts = 0;
|
|
216
|
+
|
|
217
|
+
// Apply TCP_NODELAY for lowest latency
|
|
218
|
+
this._optimizeSocket();
|
|
219
|
+
|
|
220
|
+
// Start adaptive heartbeat
|
|
162
221
|
this._startHeartbeat();
|
|
163
|
-
|
|
222
|
+
|
|
223
|
+
// Flush queued messages
|
|
224
|
+
this._flushQueue();
|
|
225
|
+
|
|
164
226
|
this._emit('connected', { sessionId: this.sessionId });
|
|
227
|
+
log.info('Connected with TCP_NODELAY');
|
|
228
|
+
|
|
165
229
|
resolve({ success: true });
|
|
166
230
|
});
|
|
167
231
|
|
|
168
232
|
this.ws.on('message', (data) => {
|
|
169
|
-
|
|
170
|
-
const message = JSON.parse(data.toString());
|
|
171
|
-
this._handleMessage(message);
|
|
172
|
-
} catch (e) {
|
|
173
|
-
// Invalid message format
|
|
174
|
-
}
|
|
233
|
+
this._handleMessage(data);
|
|
175
234
|
});
|
|
176
235
|
|
|
177
236
|
this.ws.on('close', (code, reason) => {
|
|
237
|
+
clearTimeout(connectTimeout);
|
|
178
238
|
this.connected = false;
|
|
179
239
|
this._stopHeartbeat();
|
|
180
|
-
|
|
181
|
-
|
|
240
|
+
|
|
241
|
+
log.info('Disconnected', { code });
|
|
242
|
+
this._emit('disconnected', { code, reason: reason?.toString() });
|
|
243
|
+
|
|
244
|
+
if (!this.reconnecting) {
|
|
245
|
+
this._attemptReconnect();
|
|
246
|
+
}
|
|
182
247
|
});
|
|
183
248
|
|
|
184
|
-
this.ws.on('error', (
|
|
185
|
-
|
|
249
|
+
this.ws.on('error', (err) => {
|
|
250
|
+
log.error('WebSocket error', { error: err.message });
|
|
251
|
+
this._emit('error', { message: err.message });
|
|
252
|
+
|
|
186
253
|
if (!this.connected) {
|
|
187
|
-
|
|
254
|
+
clearTimeout(connectTimeout);
|
|
255
|
+
resolve({ success: false, error: err.message });
|
|
188
256
|
}
|
|
189
257
|
});
|
|
258
|
+
});
|
|
259
|
+
}
|
|
190
260
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
261
|
+
/**
|
|
262
|
+
* Apply TCP socket optimizations
|
|
263
|
+
* @private
|
|
264
|
+
*/
|
|
265
|
+
_optimizeSocket() {
|
|
266
|
+
try {
|
|
267
|
+
const socket = this.ws._socket;
|
|
268
|
+
if (socket) {
|
|
269
|
+
// Disable Nagle's algorithm - critical for low latency
|
|
270
|
+
socket.setNoDelay(true);
|
|
271
|
+
|
|
272
|
+
// Keep connection alive
|
|
273
|
+
socket.setKeepAlive(true, 10000);
|
|
274
|
+
|
|
275
|
+
// Increase buffer sizes for throughput
|
|
276
|
+
if (socket.setRecvBufferSize) socket.setRecvBufferSize(65536);
|
|
277
|
+
if (socket.setSendBufferSize) socket.setSendBufferSize(65536);
|
|
278
|
+
|
|
279
|
+
log.debug('Socket optimized: TCP_NODELAY enabled');
|
|
280
|
+
}
|
|
281
|
+
} catch (err) {
|
|
282
|
+
log.warn('Socket optimization failed', { error: err.message });
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ==================== MESSAGE HANDLING ====================
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Ultra-fast message handler
|
|
290
|
+
* @private
|
|
291
|
+
*/
|
|
292
|
+
_handleMessage(data) {
|
|
293
|
+
const receiveTime = process.hrtime.bigint();
|
|
294
|
+
this.messagesReceived++;
|
|
295
|
+
this.bytesReceived += data.length;
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
// Try binary format first (faster)
|
|
299
|
+
if (Buffer.isBuffer(data) && data.length > 0) {
|
|
300
|
+
const msgType = data.readUInt8(0);
|
|
301
|
+
|
|
302
|
+
// Fast path for pong
|
|
303
|
+
if (msgType === MSG_TYPE.PONG) {
|
|
304
|
+
this._handlePong(data, receiveTime);
|
|
305
|
+
return;
|
|
196
306
|
}
|
|
197
|
-
|
|
198
|
-
|
|
307
|
+
|
|
308
|
+
// Binary signal (fastest path)
|
|
309
|
+
if (msgType === MSG_TYPE.SIGNAL) {
|
|
310
|
+
this._handleBinarySignal(data);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// JSON fallback
|
|
316
|
+
const message = fastParse(data);
|
|
317
|
+
this._handleJsonMessage(message, receiveTime);
|
|
318
|
+
|
|
319
|
+
} catch (err) {
|
|
320
|
+
log.warn('Message parse error', { error: err.message });
|
|
321
|
+
}
|
|
199
322
|
}
|
|
200
323
|
|
|
201
324
|
/**
|
|
202
|
-
* Handle
|
|
325
|
+
* Handle pong with latency calculation
|
|
326
|
+
* @private
|
|
203
327
|
*/
|
|
204
|
-
|
|
328
|
+
_handlePong(data, receiveTime) {
|
|
329
|
+
if (this.lastPingTime > 0) {
|
|
330
|
+
// Use high-resolution timer
|
|
331
|
+
const latency = Number(receiveTime - this.lastPingTime) / 1e6; // ns to ms
|
|
332
|
+
this._updateLatency(latency);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Handle binary trading signal (zero-copy)
|
|
338
|
+
* @private
|
|
339
|
+
*/
|
|
340
|
+
_handleBinarySignal(data) {
|
|
341
|
+
// Binary format: [type:1][timestamp:8][side:1][price:8][qty:4]
|
|
342
|
+
if (data.length >= 22) {
|
|
343
|
+
const signal = {
|
|
344
|
+
timestamp: data.readBigInt64LE(1),
|
|
345
|
+
side: data.readUInt8(9),
|
|
346
|
+
price: data.readDoubleLE(10),
|
|
347
|
+
quantity: data.readUInt32LE(18),
|
|
348
|
+
};
|
|
349
|
+
this._emit('signal', signal);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Handle JSON message
|
|
355
|
+
* @private
|
|
356
|
+
*/
|
|
357
|
+
_handleJsonMessage(message, receiveTime) {
|
|
205
358
|
// Calculate latency from server timestamp
|
|
206
359
|
if (message.timestamp) {
|
|
207
|
-
|
|
208
|
-
if (
|
|
209
|
-
|
|
210
|
-
|
|
360
|
+
const latency = Date.now() - message.timestamp;
|
|
361
|
+
if (latency >= 0 && latency < 5000) {
|
|
362
|
+
this._updateLatency(latency);
|
|
363
|
+
}
|
|
211
364
|
}
|
|
212
365
|
|
|
366
|
+
// Fast dispatch
|
|
213
367
|
switch (message.type) {
|
|
214
368
|
case 'signal':
|
|
215
369
|
this._emit('signal', message.data);
|
|
@@ -217,6 +371,9 @@ class HQXServerService {
|
|
|
217
371
|
case 'trade':
|
|
218
372
|
this._emit('trade', message.data);
|
|
219
373
|
break;
|
|
374
|
+
case 'fill':
|
|
375
|
+
this._emit('fill', message.data);
|
|
376
|
+
break;
|
|
220
377
|
case 'log':
|
|
221
378
|
this._emit('log', message.data);
|
|
222
379
|
break;
|
|
@@ -227,46 +384,133 @@ class HQXServerService {
|
|
|
227
384
|
this._emit('error', message.data);
|
|
228
385
|
break;
|
|
229
386
|
case 'pong':
|
|
230
|
-
//
|
|
231
|
-
if (this.lastPingTime > 0) {
|
|
232
|
-
this.latency = Date.now() - this.lastPingTime;
|
|
233
|
-
this._emit('latency', { latency: this.latency });
|
|
234
|
-
}
|
|
387
|
+
// Already handled in binary path
|
|
235
388
|
break;
|
|
236
389
|
default:
|
|
237
390
|
this._emit('message', message);
|
|
238
391
|
}
|
|
239
392
|
}
|
|
240
|
-
|
|
393
|
+
|
|
241
394
|
/**
|
|
242
|
-
*
|
|
395
|
+
* Update latency statistics
|
|
396
|
+
* @private
|
|
243
397
|
*/
|
|
244
|
-
|
|
245
|
-
|
|
398
|
+
_updateLatency(latency) {
|
|
399
|
+
this.latency = latency;
|
|
400
|
+
this.minLatency = Math.min(this.minLatency, latency);
|
|
401
|
+
this.maxLatency = Math.max(this.maxLatency, latency);
|
|
402
|
+
|
|
403
|
+
// Rolling average (last 100 samples)
|
|
404
|
+
this.latencySamples.push(latency);
|
|
405
|
+
if (this.latencySamples.length > 100) {
|
|
406
|
+
this.latencySamples.shift();
|
|
407
|
+
}
|
|
408
|
+
this.avgLatency = this.latencySamples.reduce((a, b) => a + b, 0) / this.latencySamples.length;
|
|
409
|
+
|
|
410
|
+
// Adapt heartbeat based on latency
|
|
411
|
+
this._adaptHeartbeat();
|
|
412
|
+
|
|
413
|
+
this._emit('latency', {
|
|
414
|
+
current: latency,
|
|
415
|
+
min: this.minLatency,
|
|
416
|
+
max: this.maxLatency,
|
|
417
|
+
avg: this.avgLatency
|
|
418
|
+
});
|
|
246
419
|
}
|
|
247
420
|
|
|
248
421
|
/**
|
|
249
|
-
*
|
|
422
|
+
* Adapt heartbeat interval based on connection quality
|
|
423
|
+
* @private
|
|
424
|
+
*/
|
|
425
|
+
_adaptHeartbeat() {
|
|
426
|
+
// Good connection: slower heartbeat (less overhead)
|
|
427
|
+
// Poor connection: faster heartbeat (detect issues quickly)
|
|
428
|
+
if (this.avgLatency < 10) {
|
|
429
|
+
this.adaptiveHeartbeat = 2000; // <10ms: 2s heartbeat
|
|
430
|
+
} else if (this.avgLatency < 50) {
|
|
431
|
+
this.adaptiveHeartbeat = 1000; // <50ms: 1s heartbeat
|
|
432
|
+
} else if (this.avgLatency < 100) {
|
|
433
|
+
this.adaptiveHeartbeat = 500; // <100ms: 500ms heartbeat
|
|
434
|
+
} else {
|
|
435
|
+
this.adaptiveHeartbeat = 250; // High latency: 250ms heartbeat
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// ==================== SENDING ====================
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Send message with minimal overhead
|
|
443
|
+
* @param {string} type - Message type
|
|
444
|
+
* @param {Object} data - Payload
|
|
250
445
|
*/
|
|
251
446
|
send(type, data) {
|
|
252
447
|
const message = {
|
|
253
448
|
type,
|
|
254
449
|
data,
|
|
255
|
-
|
|
256
|
-
|
|
450
|
+
ts: Date.now(), // Short key for speed
|
|
451
|
+
sid: this.sessionId,
|
|
257
452
|
};
|
|
258
453
|
|
|
259
|
-
if (this.connected && this.ws
|
|
260
|
-
this.
|
|
454
|
+
if (this.connected && this.ws?.readyState === WebSocket.OPEN) {
|
|
455
|
+
this._sendRaw(fastStringify(message));
|
|
456
|
+
this.messagesSent++;
|
|
261
457
|
} else {
|
|
262
458
|
this.messageQueue.push(message);
|
|
263
459
|
}
|
|
264
460
|
}
|
|
265
461
|
|
|
462
|
+
/**
|
|
463
|
+
* Send raw data (no JSON overhead)
|
|
464
|
+
* @private
|
|
465
|
+
*/
|
|
466
|
+
_sendRaw(data) {
|
|
467
|
+
try {
|
|
468
|
+
this.ws.send(data);
|
|
469
|
+
} catch (err) {
|
|
470
|
+
log.warn('Send error', { error: err.message });
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Send binary ping for lowest latency measurement
|
|
476
|
+
* @private
|
|
477
|
+
*/
|
|
478
|
+
_sendBinaryPing() {
|
|
479
|
+
if (!this.connected || this.ws?.readyState !== WebSocket.OPEN) return;
|
|
480
|
+
|
|
481
|
+
this.lastPingTime = process.hrtime.bigint();
|
|
482
|
+
|
|
483
|
+
// Write timestamp to pre-allocated buffer
|
|
484
|
+
PING_BUFFER.writeBigInt64LE(this.lastPingTime, 1);
|
|
485
|
+
|
|
486
|
+
try {
|
|
487
|
+
this.ws.send(PING_BUFFER);
|
|
488
|
+
} catch {
|
|
489
|
+
// Ignore ping errors
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Flush message queue
|
|
495
|
+
* @private
|
|
496
|
+
*/
|
|
497
|
+
_flushQueue() {
|
|
498
|
+
while (this.messageQueue.length > 0 && this.ws?.readyState === WebSocket.OPEN) {
|
|
499
|
+
const message = this.messageQueue.shift();
|
|
500
|
+
this._sendRaw(fastStringify(message));
|
|
501
|
+
this.messagesSent++;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// ==================== ALGO CONTROL ====================
|
|
506
|
+
|
|
266
507
|
/**
|
|
267
508
|
* Start algo trading session
|
|
509
|
+
* @param {Object} config
|
|
268
510
|
*/
|
|
269
511
|
startAlgo(config) {
|
|
512
|
+
log.info('Starting algo', { symbol: config.symbol, contracts: config.contracts });
|
|
513
|
+
|
|
270
514
|
this.send('start_algo', {
|
|
271
515
|
accountId: config.accountId,
|
|
272
516
|
contractId: config.contractId,
|
|
@@ -276,28 +520,30 @@ class HQXServerService {
|
|
|
276
520
|
maxRisk: config.maxRisk,
|
|
277
521
|
propfirm: config.propfirm,
|
|
278
522
|
propfirmToken: config.propfirmToken,
|
|
279
|
-
// Rithmic credentials (for Apex, TopstepTrader Rithmic, etc.)
|
|
280
523
|
rithmicCredentials: config.rithmicCredentials || null,
|
|
281
|
-
// Copy trading mode
|
|
282
524
|
copyTrading: config.copyTrading || false,
|
|
283
525
|
followerSymbol: config.followerSymbol,
|
|
284
|
-
followerContracts: config.followerContracts
|
|
526
|
+
followerContracts: config.followerContracts,
|
|
285
527
|
});
|
|
286
528
|
}
|
|
287
529
|
|
|
288
530
|
/**
|
|
289
|
-
* Stop algo trading
|
|
531
|
+
* Stop algo trading
|
|
290
532
|
*/
|
|
291
533
|
stopAlgo() {
|
|
534
|
+
log.info('Stopping algo');
|
|
292
535
|
this.send('stop_algo', {});
|
|
293
536
|
}
|
|
294
537
|
|
|
295
538
|
/**
|
|
296
|
-
* Start copy trading
|
|
539
|
+
* Start copy trading
|
|
540
|
+
* @param {Object} config
|
|
297
541
|
*/
|
|
298
542
|
startCopyTrading(config) {
|
|
543
|
+
log.info('Starting copy trading');
|
|
544
|
+
|
|
299
545
|
this.send('start_copy_trading', {
|
|
300
|
-
// Lead
|
|
546
|
+
// Lead
|
|
301
547
|
leadAccountId: config.leadAccountId,
|
|
302
548
|
leadContractId: config.leadContractId,
|
|
303
549
|
leadSymbol: config.leadSymbol,
|
|
@@ -305,7 +551,7 @@ class HQXServerService {
|
|
|
305
551
|
leadPropfirm: config.leadPropfirm,
|
|
306
552
|
leadToken: config.leadToken,
|
|
307
553
|
leadRithmicCredentials: config.leadRithmicCredentials,
|
|
308
|
-
// Follower
|
|
554
|
+
// Follower
|
|
309
555
|
followerAccountId: config.followerAccountId,
|
|
310
556
|
followerContractId: config.followerContractId,
|
|
311
557
|
followerSymbol: config.followerSymbol,
|
|
@@ -315,12 +561,16 @@ class HQXServerService {
|
|
|
315
561
|
followerRithmicCredentials: config.followerRithmicCredentials,
|
|
316
562
|
// Targets
|
|
317
563
|
dailyTarget: config.dailyTarget,
|
|
318
|
-
maxRisk: config.maxRisk
|
|
564
|
+
maxRisk: config.maxRisk,
|
|
319
565
|
});
|
|
320
566
|
}
|
|
321
567
|
|
|
568
|
+
// ==================== EVENTS ====================
|
|
569
|
+
|
|
322
570
|
/**
|
|
323
|
-
*
|
|
571
|
+
* Register event listener
|
|
572
|
+
* @param {string} event
|
|
573
|
+
* @param {Function} callback
|
|
324
574
|
*/
|
|
325
575
|
on(event, callback) {
|
|
326
576
|
if (!this.listeners.has(event)) {
|
|
@@ -329,95 +579,177 @@ class HQXServerService {
|
|
|
329
579
|
this.listeners.get(event).push(callback);
|
|
330
580
|
}
|
|
331
581
|
|
|
582
|
+
/**
|
|
583
|
+
* Remove event listener
|
|
584
|
+
* @param {string} event
|
|
585
|
+
* @param {Function} callback
|
|
586
|
+
*/
|
|
332
587
|
off(event, callback) {
|
|
333
|
-
|
|
334
|
-
|
|
588
|
+
const callbacks = this.listeners.get(event);
|
|
589
|
+
if (callbacks) {
|
|
335
590
|
const index = callbacks.indexOf(callback);
|
|
336
|
-
if (index > -1)
|
|
337
|
-
callbacks.splice(index, 1);
|
|
338
|
-
}
|
|
591
|
+
if (index > -1) callbacks.splice(index, 1);
|
|
339
592
|
}
|
|
340
593
|
}
|
|
341
594
|
|
|
595
|
+
/**
|
|
596
|
+
* Emit event (inlined for speed)
|
|
597
|
+
* @private
|
|
598
|
+
*/
|
|
342
599
|
_emit(event, data) {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
600
|
+
const callbacks = this.listeners.get(event);
|
|
601
|
+
if (!callbacks) return;
|
|
602
|
+
|
|
603
|
+
for (let i = 0; i < callbacks.length; i++) {
|
|
604
|
+
try {
|
|
605
|
+
callbacks[i](data);
|
|
606
|
+
} catch {
|
|
607
|
+
// Don't let callback errors break the loop
|
|
608
|
+
}
|
|
351
609
|
}
|
|
352
610
|
}
|
|
353
611
|
|
|
612
|
+
// ==================== HEARTBEAT ====================
|
|
613
|
+
|
|
354
614
|
/**
|
|
355
|
-
*
|
|
615
|
+
* Start adaptive heartbeat
|
|
616
|
+
* @private
|
|
356
617
|
*/
|
|
357
618
|
_startHeartbeat() {
|
|
358
|
-
this.
|
|
619
|
+
this._stopHeartbeat();
|
|
620
|
+
|
|
621
|
+
const heartbeat = () => {
|
|
359
622
|
if (this.connected) {
|
|
360
|
-
this.
|
|
361
|
-
|
|
623
|
+
this._sendBinaryPing();
|
|
624
|
+
|
|
625
|
+
// Schedule next with adaptive interval
|
|
626
|
+
this.pingInterval = setTimeout(heartbeat, this.adaptiveHeartbeat);
|
|
362
627
|
}
|
|
363
|
-
}
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
// First ping immediately
|
|
631
|
+
this._sendBinaryPing();
|
|
632
|
+
this.pingInterval = setTimeout(heartbeat, this.adaptiveHeartbeat);
|
|
364
633
|
}
|
|
365
634
|
|
|
635
|
+
/**
|
|
636
|
+
* Stop heartbeat
|
|
637
|
+
* @private
|
|
638
|
+
*/
|
|
366
639
|
_stopHeartbeat() {
|
|
367
|
-
if (this.
|
|
368
|
-
|
|
369
|
-
this.
|
|
640
|
+
if (this.pingInterval) {
|
|
641
|
+
clearTimeout(this.pingInterval);
|
|
642
|
+
this.pingInterval = null;
|
|
370
643
|
}
|
|
371
644
|
}
|
|
372
645
|
|
|
646
|
+
// ==================== RECONNECT ====================
|
|
647
|
+
|
|
373
648
|
/**
|
|
374
|
-
*
|
|
649
|
+
* Attempt reconnection with exponential backoff
|
|
650
|
+
* @private
|
|
375
651
|
*/
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
this.ws.send(JSON.stringify(message));
|
|
381
|
-
}
|
|
652
|
+
_attemptReconnect() {
|
|
653
|
+
if (this.reconnectAttempts >= SECURITY.MAX_RECONNECT_ATTEMPTS) {
|
|
654
|
+
log.error('Max reconnect attempts reached');
|
|
655
|
+
return;
|
|
382
656
|
}
|
|
657
|
+
|
|
658
|
+
this.reconnecting = true;
|
|
659
|
+
this.reconnectAttempts++;
|
|
660
|
+
|
|
661
|
+
// Fast initial reconnect, then backoff
|
|
662
|
+
const delay = Math.min(
|
|
663
|
+
100 * Math.pow(2, this.reconnectAttempts - 1), // Start at 100ms
|
|
664
|
+
10000 // Max 10s
|
|
665
|
+
);
|
|
666
|
+
|
|
667
|
+
log.info('Reconnecting', { attempt: this.reconnectAttempts, delay });
|
|
668
|
+
|
|
669
|
+
setTimeout(async () => {
|
|
670
|
+
try {
|
|
671
|
+
await this.connect();
|
|
672
|
+
} catch (err) {
|
|
673
|
+
log.error('Reconnect failed', { error: err.message });
|
|
674
|
+
}
|
|
675
|
+
this.reconnecting = false;
|
|
676
|
+
}, delay);
|
|
383
677
|
}
|
|
384
678
|
|
|
679
|
+
// ==================== STATS ====================
|
|
680
|
+
|
|
385
681
|
/**
|
|
386
|
-
*
|
|
682
|
+
* Get latency statistics
|
|
683
|
+
* @returns {Object}
|
|
387
684
|
*/
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
this.
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
685
|
+
getLatencyStats() {
|
|
686
|
+
return {
|
|
687
|
+
current: this.latency,
|
|
688
|
+
min: this.minLatency === Infinity ? 0 : this.minLatency,
|
|
689
|
+
max: this.maxLatency,
|
|
690
|
+
avg: this.avgLatency,
|
|
691
|
+
samples: this.latencySamples.length,
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Get connection statistics
|
|
697
|
+
* @returns {Object}
|
|
698
|
+
*/
|
|
699
|
+
getStats() {
|
|
700
|
+
return {
|
|
701
|
+
connected: this.connected,
|
|
702
|
+
messagesSent: this.messagesSent,
|
|
703
|
+
messagesReceived: this.messagesReceived,
|
|
704
|
+
bytesReceived: this.bytesReceived,
|
|
705
|
+
heartbeatInterval: this.adaptiveHeartbeat,
|
|
706
|
+
latency: this.getLatencyStats(),
|
|
707
|
+
};
|
|
399
708
|
}
|
|
400
709
|
|
|
401
710
|
/**
|
|
402
|
-
*
|
|
711
|
+
* Get current latency
|
|
712
|
+
* @returns {number}
|
|
713
|
+
*/
|
|
714
|
+
getLatency() {
|
|
715
|
+
return this.latency;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// ==================== CLEANUP ====================
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* Disconnect and cleanup
|
|
403
722
|
*/
|
|
404
723
|
disconnect() {
|
|
724
|
+
log.info('Disconnecting');
|
|
725
|
+
|
|
405
726
|
this._stopHeartbeat();
|
|
727
|
+
|
|
406
728
|
if (this.ws) {
|
|
407
|
-
this.ws.close();
|
|
729
|
+
this.ws.close(1000, 'Client disconnect');
|
|
408
730
|
this.ws = null;
|
|
409
731
|
}
|
|
732
|
+
|
|
410
733
|
this.connected = false;
|
|
411
734
|
this.token = null;
|
|
412
735
|
this.sessionId = null;
|
|
736
|
+
this.messageQueue = [];
|
|
737
|
+
this.listeners.clear();
|
|
738
|
+
|
|
739
|
+
// Reset stats
|
|
740
|
+
this.latencySamples = [];
|
|
741
|
+
this.minLatency = Infinity;
|
|
742
|
+
this.maxLatency = 0;
|
|
743
|
+
this.avgLatency = 0;
|
|
413
744
|
}
|
|
414
745
|
|
|
415
746
|
/**
|
|
416
747
|
* Check if connected
|
|
748
|
+
* @returns {boolean}
|
|
417
749
|
*/
|
|
418
750
|
isConnected() {
|
|
419
|
-
return this.connected && this.ws
|
|
751
|
+
return this.connected && this.ws?.readyState === WebSocket.OPEN;
|
|
420
752
|
}
|
|
421
753
|
}
|
|
422
754
|
|
|
423
|
-
module.exports = { HQXServerService,
|
|
755
|
+
module.exports = { HQXServerService, HQX_SERVER, MSG_TYPE };
|