hedgequantx 2.6.162 → 2.7.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 +15 -88
- package/bin/cli.js +0 -11
- package/dist/lib/api.jsc +0 -0
- package/dist/lib/api2.jsc +0 -0
- package/dist/lib/core.jsc +0 -0
- package/dist/lib/core2.jsc +0 -0
- package/dist/lib/data.js +1 -1
- package/dist/lib/data.jsc +0 -0
- package/dist/lib/data2.jsc +0 -0
- package/dist/lib/decoder.jsc +0 -0
- package/dist/lib/m/mod1.jsc +0 -0
- package/dist/lib/m/mod2.jsc +0 -0
- package/dist/lib/n/r1.jsc +0 -0
- package/dist/lib/n/r2.jsc +0 -0
- package/dist/lib/n/r3.jsc +0 -0
- package/dist/lib/n/r4.jsc +0 -0
- package/dist/lib/n/r5.jsc +0 -0
- package/dist/lib/n/r6.jsc +0 -0
- package/dist/lib/n/r7.jsc +0 -0
- package/dist/lib/o/util1.jsc +0 -0
- package/dist/lib/o/util2.jsc +0 -0
- package/package.json +6 -3
- package/src/app.js +40 -162
- package/src/config/constants.js +31 -33
- package/src/config/propfirms.js +13 -217
- package/src/config/settings.js +0 -43
- package/src/lib/api.js +198 -0
- package/src/lib/api2.js +353 -0
- package/src/lib/core.js +539 -0
- package/src/lib/core2.js +341 -0
- package/src/lib/data.js +555 -0
- package/src/lib/data2.js +492 -0
- package/src/lib/decoder.js +599 -0
- package/src/lib/m/s1.js +804 -0
- package/src/lib/m/s2.js +34 -0
- package/src/lib/n/r1.js +454 -0
- package/src/lib/n/r2.js +514 -0
- package/src/lib/n/r3.js +631 -0
- package/src/lib/n/r4.js +401 -0
- package/src/lib/n/r5.js +335 -0
- package/src/lib/n/r6.js +425 -0
- package/src/lib/n/r7.js +530 -0
- package/src/lib/o/l1.js +44 -0
- package/src/lib/o/l2.js +427 -0
- package/src/lib/python-bridge.js +206 -0
- package/src/menus/connect.js +14 -176
- package/src/menus/dashboard.js +65 -110
- package/src/pages/accounts.js +18 -18
- package/src/pages/algo/copy-trading.js +210 -240
- package/src/pages/algo/index.js +41 -104
- package/src/pages/algo/one-account.js +386 -33
- package/src/pages/algo/ui.js +312 -151
- package/src/pages/orders.js +3 -3
- package/src/pages/positions.js +3 -3
- package/src/pages/stats/chart.js +74 -0
- package/src/pages/stats/display.js +228 -0
- package/src/pages/stats/index.js +236 -0
- package/src/pages/stats/metrics.js +213 -0
- package/src/pages/user.js +6 -6
- package/src/services/hqx-server/constants.js +55 -0
- package/src/services/hqx-server/index.js +401 -0
- package/src/services/hqx-server/latency.js +81 -0
- package/src/services/index.js +12 -3
- package/src/services/rithmic/accounts.js +7 -32
- package/src/services/rithmic/connection.js +1 -204
- package/src/services/rithmic/contracts.js +235 -0
- package/src/services/rithmic/handlers.js +21 -196
- package/src/services/rithmic/index.js +60 -291
- package/src/services/rithmic/market.js +31 -0
- package/src/services/rithmic/orders.js +5 -361
- package/src/services/rithmic/protobuf.js +5 -195
- package/src/services/session.js +22 -173
- package/src/ui/box.js +10 -18
- package/src/ui/index.js +1 -3
- package/src/ui/menu.js +1 -1
- package/src/utils/prompts.js +2 -2
- package/dist/lib/m/s1.js +0 -1
- package/src/menus/ai-agent-connect.js +0 -181
- package/src/menus/ai-agent-models.js +0 -219
- package/src/menus/ai-agent-oauth.js +0 -292
- package/src/menus/ai-agent-ui.js +0 -141
- package/src/menus/ai-agent.js +0 -484
- package/src/pages/algo/algo-config.js +0 -195
- package/src/pages/algo/algo-multi.js +0 -801
- package/src/pages/algo/algo-utils.js +0 -58
- package/src/pages/algo/copy-engine.js +0 -449
- package/src/pages/algo/custom-strategy.js +0 -459
- package/src/pages/algo/logger.js +0 -245
- package/src/pages/algo/smart-logs-data.js +0 -218
- package/src/pages/algo/smart-logs.js +0 -387
- package/src/pages/algo/ui-constants.js +0 -144
- package/src/pages/algo/ui-summary.js +0 -184
- package/src/pages/stats-calculations.js +0 -191
- package/src/pages/stats-ui.js +0 -381
- package/src/pages/stats.js +0 -339
- package/src/services/ai/client-analysis.js +0 -194
- package/src/services/ai/client-models.js +0 -333
- package/src/services/ai/client.js +0 -343
- package/src/services/ai/index.js +0 -384
- package/src/services/ai/oauth-anthropic.js +0 -265
- package/src/services/ai/oauth-gemini.js +0 -223
- package/src/services/ai/oauth-iflow.js +0 -269
- package/src/services/ai/oauth-openai.js +0 -233
- package/src/services/ai/oauth-qwen.js +0 -279
- package/src/services/ai/providers/index.js +0 -526
- package/src/services/ai/proxy-install.js +0 -249
- package/src/services/ai/proxy-manager.js +0 -494
- package/src/services/ai/proxy-remote.js +0 -161
- package/src/services/ai/strategy-supervisor.js +0 -1312
- package/src/services/ai/supervisor-data.js +0 -195
- package/src/services/ai/supervisor-optimize.js +0 -215
- package/src/services/ai/supervisor-sync.js +0 -178
- package/src/services/ai/supervisor-utils.js +0 -158
- package/src/services/ai/supervisor.js +0 -484
- package/src/services/ai/validation.js +0 -250
- package/src/services/hqx-server-events.js +0 -110
- package/src/services/hqx-server-handlers.js +0 -217
- package/src/services/hqx-server-latency.js +0 -136
- package/src/services/hqx-server.js +0 -403
- package/src/services/position-constants.js +0 -28
- package/src/services/position-manager.js +0 -528
- package/src/services/position-momentum.js +0 -206
- package/src/services/projectx/accounts.js +0 -142
- package/src/services/projectx/index.js +0 -443
- package/src/services/projectx/market.js +0 -172
- package/src/services/projectx/stats.js +0 -110
- package/src/services/projectx/trading.js +0 -180
- package/src/services/rithmic/latency-tracker.js +0 -182
- package/src/services/rithmic/market-data.js +0 -549
- package/src/services/rithmic/specs.js +0 -146
- package/src/services/rithmic/trade-history.js +0 -254
- package/src/services/session-history.js +0 -475
- package/src/services/strategy/hft-tick.js +0 -507
- package/src/services/strategy/recovery-math.js +0 -402
- package/src/services/tradovate/constants.js +0 -109
- package/src/services/tradovate/index.js +0 -505
- package/src/services/tradovate/market.js +0 -47
- package/src/services/tradovate/websocket.js +0 -97
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Latency tracking and adaptive heartbeat
|
|
3
|
+
* @module services/hqx-server/latency
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Latency tracker with adaptive heartbeat
|
|
8
|
+
*/
|
|
9
|
+
class LatencyTracker {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.latency = 0;
|
|
12
|
+
this.minLatency = Infinity;
|
|
13
|
+
this.maxLatency = 0;
|
|
14
|
+
this.avgLatency = 0;
|
|
15
|
+
this.latencySamples = [];
|
|
16
|
+
this.adaptiveHeartbeat = 1000;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Update latency with new sample
|
|
21
|
+
* @param {number} latency - Latency in ms
|
|
22
|
+
*/
|
|
23
|
+
update(latency) {
|
|
24
|
+
this.latency = latency;
|
|
25
|
+
this.minLatency = Math.min(this.minLatency, latency);
|
|
26
|
+
this.maxLatency = Math.max(this.maxLatency, latency);
|
|
27
|
+
|
|
28
|
+
// Rolling average (last 100 samples)
|
|
29
|
+
this.latencySamples.push(latency);
|
|
30
|
+
if (this.latencySamples.length > 100) {
|
|
31
|
+
this.latencySamples.shift();
|
|
32
|
+
}
|
|
33
|
+
this.avgLatency = this.latencySamples.reduce((a, b) => a + b, 0) / this.latencySamples.length;
|
|
34
|
+
|
|
35
|
+
this._adaptHeartbeat();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Adapt heartbeat interval based on connection quality
|
|
40
|
+
* @private
|
|
41
|
+
*/
|
|
42
|
+
_adaptHeartbeat() {
|
|
43
|
+
if (this.avgLatency < 10) {
|
|
44
|
+
this.adaptiveHeartbeat = 2000; // <10ms: 2s heartbeat
|
|
45
|
+
} else if (this.avgLatency < 50) {
|
|
46
|
+
this.adaptiveHeartbeat = 1000; // <50ms: 1s heartbeat
|
|
47
|
+
} else if (this.avgLatency < 100) {
|
|
48
|
+
this.adaptiveHeartbeat = 500; // <100ms: 500ms heartbeat
|
|
49
|
+
} else {
|
|
50
|
+
this.adaptiveHeartbeat = 250; // High latency: 250ms heartbeat
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get latency statistics
|
|
56
|
+
* @returns {Object}
|
|
57
|
+
*/
|
|
58
|
+
getStats() {
|
|
59
|
+
return {
|
|
60
|
+
current: this.latency,
|
|
61
|
+
min: this.minLatency === Infinity ? 0 : this.minLatency,
|
|
62
|
+
max: this.maxLatency,
|
|
63
|
+
avg: this.avgLatency,
|
|
64
|
+
samples: this.latencySamples.length,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Reset all statistics
|
|
70
|
+
*/
|
|
71
|
+
reset() {
|
|
72
|
+
this.latency = 0;
|
|
73
|
+
this.minLatency = Infinity;
|
|
74
|
+
this.maxLatency = 0;
|
|
75
|
+
this.avgLatency = 0;
|
|
76
|
+
this.latencySamples = [];
|
|
77
|
+
this.adaptiveHeartbeat = 1000;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = { LatencyTracker };
|
package/src/services/index.js
CHANGED
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Services module exports
|
|
3
3
|
* @module services
|
|
4
|
+
*
|
|
5
|
+
* Rithmic-only service hub
|
|
4
6
|
*/
|
|
5
7
|
|
|
6
|
-
const {
|
|
8
|
+
const { RithmicService } = require('./rithmic/index');
|
|
9
|
+
const { HQXServerService } = require('./hqx-server/index');
|
|
7
10
|
const { storage, connections } = require('./session');
|
|
8
11
|
|
|
9
12
|
module.exports = {
|
|
10
|
-
|
|
13
|
+
// Platform Service (Rithmic only)
|
|
14
|
+
RithmicService,
|
|
15
|
+
|
|
16
|
+
// HQX Algo Server
|
|
17
|
+
HQXServerService,
|
|
18
|
+
|
|
19
|
+
// Session Management
|
|
11
20
|
storage,
|
|
12
|
-
connections
|
|
21
|
+
connections,
|
|
13
22
|
};
|
|
@@ -109,46 +109,21 @@ const getTradingAccounts = async (service) => {
|
|
|
109
109
|
debug(`Account ${acc.accountId} pnlData:`, JSON.stringify(pnlData));
|
|
110
110
|
debug(` accountPnL map size:`, service.accountPnL.size);
|
|
111
111
|
|
|
112
|
-
// REAL DATA FROM RITHMIC ONLY - NO DEFAULTS
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
const
|
|
116
|
-
const
|
|
117
|
-
const dayPnL = pnlData.dayPnl !== undefined ? parseFloat(pnlData.dayPnl) : null;
|
|
118
|
-
|
|
119
|
-
// R Trader additional metrics (from ACCOUNT_PNL_UPDATE 451)
|
|
120
|
-
const buyingPower = pnlData.availableBuyingPower !== undefined ? parseFloat(pnlData.availableBuyingPower) : null;
|
|
121
|
-
const margin = pnlData.marginBalance !== undefined ? parseFloat(pnlData.marginBalance) : null;
|
|
122
|
-
const cashOnHand = pnlData.cashOnHand !== undefined ? parseFloat(pnlData.cashOnHand) : null;
|
|
123
|
-
|
|
124
|
-
// Calculate P&L like R Trader: openPositionPnl + closedPositionPnl
|
|
125
|
-
// This matches what R Trader shows as "Today's P&L"
|
|
126
|
-
const totalPnL = (openPnL !== null || closedPnL !== null)
|
|
127
|
-
? (openPnL || 0) + (closedPnL || 0)
|
|
128
|
-
: null;
|
|
129
|
-
|
|
130
|
-
// Net Liquidation Value = Account Balance + Open P&L (same as R Trader)
|
|
131
|
-
const netLiquidation = (accountBalance !== null || openPnL !== null)
|
|
132
|
-
? (accountBalance || 0) + (openPnL || 0)
|
|
133
|
-
: null;
|
|
112
|
+
// REAL DATA FROM RITHMIC ONLY - NO DEFAULTS
|
|
113
|
+
const accountBalance = pnlData.accountBalance ? parseFloat(pnlData.accountBalance) : null;
|
|
114
|
+
const openPnL = pnlData.openPositionPnl ? parseFloat(pnlData.openPositionPnl) : null;
|
|
115
|
+
const closedPnL = pnlData.closedPositionPnl ? parseFloat(pnlData.closedPositionPnl) : null;
|
|
116
|
+
const dayPnL = pnlData.dayPnl ? parseFloat(pnlData.dayPnl) : null;
|
|
134
117
|
|
|
135
118
|
return {
|
|
136
119
|
accountId: hashAccountId(acc.accountId),
|
|
137
120
|
rithmicAccountId: acc.accountId,
|
|
138
121
|
accountName: acc.accountId, // Never expose real name - only account ID
|
|
139
122
|
name: acc.accountId, // Never expose real name - only account ID
|
|
140
|
-
// Core metrics (same as R Trader)
|
|
141
123
|
balance: accountBalance,
|
|
142
|
-
profitAndLoss:
|
|
124
|
+
profitAndLoss: dayPnL !== null ? dayPnL : (openPnL !== null || closedPnL !== null ? (openPnL || 0) + (closedPnL || 0) : null),
|
|
143
125
|
openPnL: openPnL,
|
|
144
|
-
|
|
145
|
-
dayPnL: dayPnL,
|
|
146
|
-
// R Trader additional metrics
|
|
147
|
-
buyingPower: buyingPower,
|
|
148
|
-
margin: margin,
|
|
149
|
-
cashOnHand: cashOnHand,
|
|
150
|
-
netLiquidation: netLiquidation,
|
|
151
|
-
// Meta
|
|
126
|
+
todayPnL: closedPnL,
|
|
152
127
|
status: 0,
|
|
153
128
|
platform: 'Rithmic',
|
|
154
129
|
propfirm: service.propfirm.name,
|
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Rithmic Connection Manager
|
|
3
3
|
* Handles WebSocket connection and heartbeat
|
|
4
|
-
*
|
|
5
|
-
* OPTIMIZED FOR ULTRA-LOW LATENCY:
|
|
6
|
-
* - TCP_NODELAY enabled (disable Nagle's algorithm)
|
|
7
|
-
* - Compression disabled
|
|
8
|
-
* - Skip UTF8 validation for binary
|
|
9
4
|
*/
|
|
10
5
|
|
|
11
6
|
const WebSocket = require('ws');
|
|
@@ -20,7 +15,6 @@ class RithmicConnection extends EventEmitter {
|
|
|
20
15
|
this.config = null;
|
|
21
16
|
this.state = 'DISCONNECTED';
|
|
22
17
|
this.heartbeatTimer = null;
|
|
23
|
-
this._socket = null; // Direct socket reference for fast access
|
|
24
18
|
}
|
|
25
19
|
|
|
26
20
|
get isConnected() {
|
|
@@ -33,7 +27,6 @@ class RithmicConnection extends EventEmitter {
|
|
|
33
27
|
|
|
34
28
|
/**
|
|
35
29
|
* Connect to Rithmic server
|
|
36
|
-
* OPTIMIZED: TCP_NODELAY, no compression, skip UTF8 validation
|
|
37
30
|
*/
|
|
38
31
|
async connect(config) {
|
|
39
32
|
this.config = config;
|
|
@@ -42,24 +35,10 @@ class RithmicConnection extends EventEmitter {
|
|
|
42
35
|
await proto.load();
|
|
43
36
|
|
|
44
37
|
return new Promise((resolve, reject) => {
|
|
45
|
-
|
|
46
|
-
this.ws = new WebSocket(config.uri, {
|
|
47
|
-
rejectUnauthorized: false,
|
|
48
|
-
perMessageDeflate: false, // CRITICAL: Disable compression
|
|
49
|
-
skipUTF8Validation: true, // Skip validation for binary protobuf
|
|
50
|
-
maxPayload: 64 * 1024, // 64KB max (orders are small)
|
|
51
|
-
});
|
|
38
|
+
this.ws = new WebSocket(config.uri, { rejectUnauthorized: false });
|
|
52
39
|
|
|
53
40
|
this.ws.on('open', () => {
|
|
54
41
|
this.state = 'CONNECTED';
|
|
55
|
-
|
|
56
|
-
// CRITICAL: Disable Nagle's algorithm for low latency
|
|
57
|
-
// This sends packets immediately instead of buffering
|
|
58
|
-
if (this.ws._socket) {
|
|
59
|
-
this.ws._socket.setNoDelay(true);
|
|
60
|
-
this._socket = this.ws._socket; // Cache for fast access
|
|
61
|
-
}
|
|
62
|
-
|
|
63
42
|
this.emit('connected');
|
|
64
43
|
resolve(true);
|
|
65
44
|
});
|
|
@@ -113,122 +92,6 @@ class RithmicConnection extends EventEmitter {
|
|
|
113
92
|
this.ws.send(buffer);
|
|
114
93
|
}
|
|
115
94
|
|
|
116
|
-
/**
|
|
117
|
-
* Fast send - bypasses some ws overhead for hot path
|
|
118
|
-
* Use for time-critical order messages
|
|
119
|
-
* @param {Buffer} buffer - Pre-encoded protobuf buffer
|
|
120
|
-
* @returns {boolean} true if sent, false if connection not open
|
|
121
|
-
*/
|
|
122
|
-
fastSend(buffer) {
|
|
123
|
-
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
124
|
-
this.ws.send(buffer);
|
|
125
|
-
return true;
|
|
126
|
-
}
|
|
127
|
-
return false;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Ultra-fast send - direct socket write with WebSocket framing
|
|
132
|
-
* MAXIMUM PERFORMANCE: Bypasses ws library overhead entirely
|
|
133
|
-
* Only use for pre-encoded binary protobuf messages
|
|
134
|
-
*
|
|
135
|
-
* @param {Buffer} payload - Pre-encoded protobuf buffer
|
|
136
|
-
* @returns {boolean} true if sent successfully
|
|
137
|
-
*/
|
|
138
|
-
ultraSend(payload) {
|
|
139
|
-
// Require cached socket reference
|
|
140
|
-
if (!this._socket || !this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
141
|
-
return false;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
try {
|
|
145
|
-
// Build WebSocket frame manually for binary message
|
|
146
|
-
// This avoids all ws library overhead (callbacks, validation, etc.)
|
|
147
|
-
const frame = this._buildBinaryFrame(payload);
|
|
148
|
-
|
|
149
|
-
// Direct socket write - bypasses ws entirely
|
|
150
|
-
this._socket.write(frame);
|
|
151
|
-
return true;
|
|
152
|
-
} catch (e) {
|
|
153
|
-
// Fallback to standard send on error
|
|
154
|
-
this.ws.send(payload);
|
|
155
|
-
return true;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Build WebSocket binary frame manually
|
|
161
|
-
* Format: [opcode] [length] [payload]
|
|
162
|
-
* @private
|
|
163
|
-
* @param {Buffer} payload
|
|
164
|
-
* @returns {Buffer}
|
|
165
|
-
*/
|
|
166
|
-
_buildBinaryFrame(payload) {
|
|
167
|
-
const len = payload.length;
|
|
168
|
-
let frame;
|
|
169
|
-
|
|
170
|
-
if (len < 126) {
|
|
171
|
-
// 2-byte header: FIN + opcode (0x82 = final binary), length
|
|
172
|
-
frame = Buffer.allocUnsafe(2 + len);
|
|
173
|
-
frame[0] = 0x82; // FIN=1, opcode=2 (binary)
|
|
174
|
-
frame[1] = len; // No mask (server->client would need mask, but we're client)
|
|
175
|
-
payload.copy(frame, 2);
|
|
176
|
-
} else if (len < 65536) {
|
|
177
|
-
// 4-byte header for medium messages
|
|
178
|
-
frame = Buffer.allocUnsafe(4 + len);
|
|
179
|
-
frame[0] = 0x82;
|
|
180
|
-
frame[1] = 126;
|
|
181
|
-
frame.writeUInt16BE(len, 2);
|
|
182
|
-
payload.copy(frame, 4);
|
|
183
|
-
} else {
|
|
184
|
-
// 10-byte header for large messages (unlikely for orders)
|
|
185
|
-
frame = Buffer.allocUnsafe(10 + len);
|
|
186
|
-
frame[0] = 0x82;
|
|
187
|
-
frame[1] = 127;
|
|
188
|
-
frame.writeBigUInt64BE(BigInt(len), 2);
|
|
189
|
-
payload.copy(frame, 10);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Client frames MUST be masked per RFC 6455
|
|
193
|
-
// Apply masking key
|
|
194
|
-
return this._applyMask(frame, len < 126 ? 2 : (len < 65536 ? 4 : 10));
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Apply WebSocket client masking
|
|
199
|
-
* @private
|
|
200
|
-
* @param {Buffer} frame - Frame with unmasked payload
|
|
201
|
-
* @param {number} headerLen - Length of header before payload
|
|
202
|
-
* @returns {Buffer} - New frame with mask applied
|
|
203
|
-
*/
|
|
204
|
-
_applyMask(frame, headerLen) {
|
|
205
|
-
const payloadLen = frame.length - headerLen;
|
|
206
|
-
|
|
207
|
-
// Generate 4-byte mask key
|
|
208
|
-
const mask = Buffer.allocUnsafe(4);
|
|
209
|
-
mask[0] = (Math.random() * 256) | 0;
|
|
210
|
-
mask[1] = (Math.random() * 256) | 0;
|
|
211
|
-
mask[2] = (Math.random() * 256) | 0;
|
|
212
|
-
mask[3] = (Math.random() * 256) | 0;
|
|
213
|
-
|
|
214
|
-
// Create new frame with mask bit set and mask key inserted
|
|
215
|
-
const maskedFrame = Buffer.allocUnsafe(headerLen + 4 + payloadLen);
|
|
216
|
-
|
|
217
|
-
// Copy header, set mask bit
|
|
218
|
-
frame.copy(maskedFrame, 0, 0, headerLen);
|
|
219
|
-
maskedFrame[1] |= 0x80; // Set MASK bit
|
|
220
|
-
|
|
221
|
-
// Insert mask key after length
|
|
222
|
-
mask.copy(maskedFrame, headerLen);
|
|
223
|
-
|
|
224
|
-
// Copy and mask payload
|
|
225
|
-
for (let i = 0; i < payloadLen; i++) {
|
|
226
|
-
maskedFrame[headerLen + 4 + i] = frame[headerLen + i] ^ mask[i & 3];
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
return maskedFrame;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
95
|
/**
|
|
233
96
|
* Login to system
|
|
234
97
|
*/
|
|
@@ -335,72 +198,6 @@ class RithmicConnection extends EventEmitter {
|
|
|
335
198
|
this.heartbeatTimer = null;
|
|
336
199
|
}
|
|
337
200
|
}
|
|
338
|
-
|
|
339
|
-
/**
|
|
340
|
-
* Warmup connection for minimum latency on first order
|
|
341
|
-
* Call after login but before trading starts
|
|
342
|
-
*
|
|
343
|
-
* OPTIMIZATIONS:
|
|
344
|
-
* - Pre-load protobuf types
|
|
345
|
-
* - Keep TCP connection "hot" with small pings
|
|
346
|
-
* - Configure socket for trading
|
|
347
|
-
*/
|
|
348
|
-
async warmup() {
|
|
349
|
-
if (!this._socket) return false;
|
|
350
|
-
|
|
351
|
-
try {
|
|
352
|
-
// Ensure TCP_NODELAY is set
|
|
353
|
-
this._socket.setNoDelay(true);
|
|
354
|
-
|
|
355
|
-
// Set socket keep-alive to prevent idle disconnection
|
|
356
|
-
// Aggressive keep-alive: probe every 10 seconds
|
|
357
|
-
this._socket.setKeepAlive(true, 10000);
|
|
358
|
-
|
|
359
|
-
// Pre-allocate socket buffer space
|
|
360
|
-
if (this._socket.setRecvBufferSize) {
|
|
361
|
-
this._socket.setRecvBufferSize(65536); // 64KB receive buffer
|
|
362
|
-
}
|
|
363
|
-
if (this._socket.setSendBufferSize) {
|
|
364
|
-
this._socket.setSendBufferSize(65536); // 64KB send buffer
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
// Send a heartbeat to "warm up" the connection
|
|
368
|
-
this.send('RequestHeartbeat', { templateId: REQ.HEARTBEAT });
|
|
369
|
-
|
|
370
|
-
this.emit('warmedUp');
|
|
371
|
-
return true;
|
|
372
|
-
} catch (e) {
|
|
373
|
-
return false;
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
/**
|
|
378
|
-
* Get connection diagnostics
|
|
379
|
-
* @returns {Object}
|
|
380
|
-
*/
|
|
381
|
-
getDiagnostics() {
|
|
382
|
-
const diag = {
|
|
383
|
-
state: this.state,
|
|
384
|
-
isConnected: this.isConnected,
|
|
385
|
-
hasSocket: !!this._socket,
|
|
386
|
-
socketState: null,
|
|
387
|
-
};
|
|
388
|
-
|
|
389
|
-
if (this._socket) {
|
|
390
|
-
diag.socketState = {
|
|
391
|
-
readable: this._socket.readable,
|
|
392
|
-
writable: this._socket.writable,
|
|
393
|
-
bytesRead: this._socket.bytesRead,
|
|
394
|
-
bytesWritten: this._socket.bytesWritten,
|
|
395
|
-
localAddress: this._socket.localAddress,
|
|
396
|
-
localPort: this._socket.localPort,
|
|
397
|
-
remoteAddress: this._socket.remoteAddress,
|
|
398
|
-
remotePort: this._socket.remotePort,
|
|
399
|
-
};
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
return diag;
|
|
403
|
-
}
|
|
404
201
|
}
|
|
405
202
|
|
|
406
203
|
module.exports = { RithmicConnection };
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Rithmic contract methods
|
|
3
|
+
* @module services/rithmic/contracts
|
|
4
|
+
*
|
|
5
|
+
* NO FAKE DATA - Only real values from Rithmic API
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { decodeFrontMonthContract } = require('./protobuf');
|
|
9
|
+
const { TIMEOUTS, CACHE } = require('../../config/settings');
|
|
10
|
+
const { logger } = require('../../utils/logger');
|
|
11
|
+
|
|
12
|
+
const log = logger.scope('Rithmic:Contracts');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get all available contracts from Rithmic API
|
|
16
|
+
* @param {RithmicService} service - Service instance
|
|
17
|
+
* @returns {Promise<{success: boolean, contracts: Array, source?: string, error?: string}>}
|
|
18
|
+
*/
|
|
19
|
+
const getContracts = async (service) => {
|
|
20
|
+
// Check cache
|
|
21
|
+
if (service._contractsCache && Date.now() - service._contractsCacheTime < CACHE.CONTRACTS_TTL) {
|
|
22
|
+
return { success: true, contracts: service._contractsCache, source: 'cache' };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (!service.credentials) {
|
|
26
|
+
return { success: false, error: 'Not logged in' };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
// Connect to TICKER_PLANT if needed
|
|
31
|
+
if (!service.tickerConn) {
|
|
32
|
+
const connected = await service.connectTicker(service.credentials.username, service.credentials.password);
|
|
33
|
+
if (!connected) {
|
|
34
|
+
return { success: false, error: 'Failed to connect to TICKER_PLANT' };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
service.tickerConn.setMaxListeners(5000);
|
|
39
|
+
|
|
40
|
+
log.debug('Fetching contracts from Rithmic API');
|
|
41
|
+
const contracts = await fetchAllFrontMonths(service);
|
|
42
|
+
|
|
43
|
+
if (!contracts.length) {
|
|
44
|
+
return { success: false, error: 'No tradeable contracts found' };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Cache results
|
|
48
|
+
service._contractsCache = contracts;
|
|
49
|
+
service._contractsCacheTime = Date.now();
|
|
50
|
+
|
|
51
|
+
return { success: true, contracts, source: 'api' };
|
|
52
|
+
} catch (err) {
|
|
53
|
+
log.error('getContracts error', { error: err.message });
|
|
54
|
+
return { success: false, error: err.message };
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Search contracts
|
|
60
|
+
* @param {RithmicService} service - Service instance
|
|
61
|
+
* @param {string} searchText - Search text
|
|
62
|
+
* @returns {Promise<Array>}
|
|
63
|
+
*/
|
|
64
|
+
const searchContracts = async (service, searchText) => {
|
|
65
|
+
const result = await getContracts(service);
|
|
66
|
+
if (!searchText || !result.success) return result.contracts || [];
|
|
67
|
+
|
|
68
|
+
const search = searchText.toUpperCase();
|
|
69
|
+
return result.contracts.filter(c =>
|
|
70
|
+
c.symbol.toUpperCase().includes(search) ||
|
|
71
|
+
c.name.toUpperCase().includes(search)
|
|
72
|
+
);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Fetch all front month contracts from API
|
|
77
|
+
* @param {RithmicService} service - Service instance
|
|
78
|
+
* @returns {Promise<Array>}
|
|
79
|
+
*/
|
|
80
|
+
const fetchAllFrontMonths = (service) => {
|
|
81
|
+
if (!service.tickerConn) {
|
|
82
|
+
throw new Error('TICKER_PLANT not connected');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return new Promise((resolve) => {
|
|
86
|
+
const contracts = new Map();
|
|
87
|
+
const productsToCheck = new Map();
|
|
88
|
+
|
|
89
|
+
// Handler for ProductCodes responses
|
|
90
|
+
const productHandler = (msg) => {
|
|
91
|
+
if (msg.templateId !== 112) return;
|
|
92
|
+
|
|
93
|
+
const decoded = decodeProductCodes(msg.data);
|
|
94
|
+
if (!decoded.productCode || !decoded.exchange) return;
|
|
95
|
+
|
|
96
|
+
const validExchanges = ['CME', 'CBOT', 'NYMEX', 'COMEX', 'NYBOT', 'CFE'];
|
|
97
|
+
if (!validExchanges.includes(decoded.exchange)) return;
|
|
98
|
+
|
|
99
|
+
const name = (decoded.productName || '').toLowerCase();
|
|
100
|
+
if (name.includes('option') || name.includes('swap') || name.includes('spread')) return;
|
|
101
|
+
|
|
102
|
+
const key = `${decoded.productCode}:${decoded.exchange}`;
|
|
103
|
+
if (!productsToCheck.has(key)) {
|
|
104
|
+
productsToCheck.set(key, {
|
|
105
|
+
productCode: decoded.productCode,
|
|
106
|
+
productName: decoded.productName || decoded.productCode,
|
|
107
|
+
exchange: decoded.exchange,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// Handler for FrontMonth responses
|
|
113
|
+
const frontMonthHandler = (msg) => {
|
|
114
|
+
if (msg.templateId !== 114) return;
|
|
115
|
+
|
|
116
|
+
const decoded = decodeFrontMonthContract(msg.data);
|
|
117
|
+
if (decoded.rpCode[0] === '0' && decoded.tradingSymbol) {
|
|
118
|
+
contracts.set(decoded.userMsg, {
|
|
119
|
+
symbol: decoded.tradingSymbol,
|
|
120
|
+
baseSymbol: decoded.userMsg,
|
|
121
|
+
exchange: decoded.exchange,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
service.tickerConn.on('message', productHandler);
|
|
127
|
+
service.tickerConn.on('message', frontMonthHandler);
|
|
128
|
+
|
|
129
|
+
// Request all product codes
|
|
130
|
+
service.tickerConn.send('RequestProductCodes', {
|
|
131
|
+
templateId: 111,
|
|
132
|
+
userMsg: ['get-products'],
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// After timeout, request front months
|
|
136
|
+
setTimeout(() => {
|
|
137
|
+
service.tickerConn.removeListener('message', productHandler);
|
|
138
|
+
log.debug('Collected products', { count: productsToCheck.size });
|
|
139
|
+
|
|
140
|
+
for (const product of productsToCheck.values()) {
|
|
141
|
+
service.tickerConn.send('RequestFrontMonthContract', {
|
|
142
|
+
templateId: 113,
|
|
143
|
+
userMsg: [product.productCode],
|
|
144
|
+
symbol: product.productCode,
|
|
145
|
+
exchange: product.exchange,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Collect results after timeout
|
|
150
|
+
setTimeout(() => {
|
|
151
|
+
service.tickerConn.removeListener('message', frontMonthHandler);
|
|
152
|
+
|
|
153
|
+
const results = [];
|
|
154
|
+
for (const [baseSymbol, contract] of contracts) {
|
|
155
|
+
const productKey = `${baseSymbol}:${contract.exchange}`;
|
|
156
|
+
const product = productsToCheck.get(productKey);
|
|
157
|
+
|
|
158
|
+
// 100% API data - no static symbol info
|
|
159
|
+
results.push({
|
|
160
|
+
symbol: contract.symbol,
|
|
161
|
+
baseSymbol,
|
|
162
|
+
name: product?.productName || baseSymbol,
|
|
163
|
+
exchange: contract.exchange,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Sort alphabetically by base symbol
|
|
168
|
+
results.sort((a, b) => a.baseSymbol.localeCompare(b.baseSymbol));
|
|
169
|
+
|
|
170
|
+
log.debug('Got contracts from API', { count: results.length });
|
|
171
|
+
resolve(results);
|
|
172
|
+
}, TIMEOUTS.RITHMIC_PRODUCTS);
|
|
173
|
+
}, TIMEOUTS.RITHMIC_CONTRACTS);
|
|
174
|
+
});
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Decode ProductCodes response
|
|
179
|
+
* @param {Buffer} buffer - Protobuf buffer
|
|
180
|
+
* @returns {Object} Decoded product data
|
|
181
|
+
*/
|
|
182
|
+
const decodeProductCodes = (buffer) => {
|
|
183
|
+
const result = {};
|
|
184
|
+
let offset = 0;
|
|
185
|
+
|
|
186
|
+
const readVarint = (buf, off) => {
|
|
187
|
+
let value = 0;
|
|
188
|
+
let shift = 0;
|
|
189
|
+
while (off < buf.length) {
|
|
190
|
+
const byte = buf[off++];
|
|
191
|
+
value |= (byte & 0x7F) << shift;
|
|
192
|
+
if (!(byte & 0x80)) break;
|
|
193
|
+
shift += 7;
|
|
194
|
+
}
|
|
195
|
+
return [value, off];
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const readString = (buf, off) => {
|
|
199
|
+
const [len, newOff] = readVarint(buf, off);
|
|
200
|
+
return [buf.slice(newOff, newOff + len).toString('utf8'), newOff + len];
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
while (offset < buffer.length) {
|
|
204
|
+
try {
|
|
205
|
+
const [tag, tagOff] = readVarint(buffer, offset);
|
|
206
|
+
const wireType = tag & 0x7;
|
|
207
|
+
const fieldNumber = tag >>> 3;
|
|
208
|
+
offset = tagOff;
|
|
209
|
+
|
|
210
|
+
if (wireType === 0) {
|
|
211
|
+
const [, newOff] = readVarint(buffer, offset);
|
|
212
|
+
offset = newOff;
|
|
213
|
+
} else if (wireType === 2) {
|
|
214
|
+
const [val, newOff] = readString(buffer, offset);
|
|
215
|
+
offset = newOff;
|
|
216
|
+
if (fieldNumber === 110101) result.exchange = val;
|
|
217
|
+
if (fieldNumber === 100749) result.productCode = val;
|
|
218
|
+
if (fieldNumber === 100003) result.productName = val;
|
|
219
|
+
} else {
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
} catch {
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return result;
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
module.exports = {
|
|
231
|
+
getContracts,
|
|
232
|
+
searchContracts,
|
|
233
|
+
fetchAllFrontMonths,
|
|
234
|
+
decodeProductCodes,
|
|
235
|
+
};
|