hedgequantx 2.6.161 → 2.6.163
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/package.json +1 -1
- package/src/menus/ai-agent-connect.js +181 -0
- package/src/menus/ai-agent-models.js +219 -0
- package/src/menus/ai-agent-oauth.js +292 -0
- package/src/menus/ai-agent-ui.js +141 -0
- package/src/menus/ai-agent.js +88 -1489
- package/src/pages/algo/copy-engine.js +449 -0
- package/src/pages/algo/copy-trading.js +11 -543
- package/src/pages/algo/smart-logs-data.js +218 -0
- package/src/pages/algo/smart-logs.js +9 -214
- package/src/pages/algo/ui-constants.js +144 -0
- package/src/pages/algo/ui-summary.js +184 -0
- package/src/pages/algo/ui.js +42 -526
- package/src/pages/stats-calculations.js +191 -0
- package/src/pages/stats-ui.js +381 -0
- package/src/pages/stats.js +14 -507
- package/src/services/ai/client-analysis.js +194 -0
- package/src/services/ai/client-models.js +333 -0
- package/src/services/ai/client.js +6 -489
- package/src/services/ai/index.js +2 -257
- package/src/services/ai/providers/direct-providers.js +323 -0
- package/src/services/ai/providers/index.js +8 -472
- package/src/services/ai/providers/other-providers.js +104 -0
- package/src/services/ai/proxy-install.js +249 -0
- package/src/services/ai/proxy-manager.js +29 -411
- package/src/services/ai/proxy-remote.js +161 -0
- package/src/services/ai/supervisor-optimize.js +215 -0
- package/src/services/ai/supervisor-sync.js +178 -0
- package/src/services/ai/supervisor.js +50 -515
- package/src/services/ai/validation.js +250 -0
- package/src/services/hqx-server-events.js +110 -0
- package/src/services/hqx-server-handlers.js +217 -0
- package/src/services/hqx-server-latency.js +136 -0
- package/src/services/hqx-server.js +51 -403
- package/src/services/position-constants.js +28 -0
- package/src/services/position-exit-logic.js +174 -0
- package/src/services/position-manager.js +90 -629
- package/src/services/position-momentum.js +206 -0
- package/src/services/projectx/accounts.js +142 -0
- package/src/services/projectx/index.js +40 -289
- package/src/services/projectx/trading.js +180 -0
- package/src/services/rithmic/contracts.js +218 -0
- package/src/services/rithmic/handlers.js +2 -208
- package/src/services/rithmic/index.js +28 -712
- package/src/services/rithmic/latency-tracker.js +182 -0
- package/src/services/rithmic/market-data-decoders.js +229 -0
- package/src/services/rithmic/market-data.js +1 -278
- package/src/services/rithmic/orders-fast.js +246 -0
- package/src/services/rithmic/orders.js +1 -251
- package/src/services/rithmic/proto-decoders.js +403 -0
- package/src/services/rithmic/protobuf.js +7 -443
- package/src/services/rithmic/specs.js +146 -0
- package/src/services/rithmic/trade-history.js +254 -0
- package/src/services/strategy/hft-signal-calc.js +147 -0
- package/src/services/strategy/hft-tick.js +33 -133
- package/src/services/tradovate/index.js +6 -119
- package/src/services/tradovate/orders.js +145 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rithmic Latency Tracking
|
|
3
|
+
* @module services/rithmic/latency-tracker
|
|
4
|
+
*
|
|
5
|
+
* High-precision order-to-fill latency tracking.
|
|
6
|
+
* Uses circular buffer and high-resolution timing.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Get high-resolution timestamp in nanoseconds
|
|
11
|
+
* @returns {bigint}
|
|
12
|
+
*/
|
|
13
|
+
const hrNow = () => process.hrtime.bigint();
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Convert nanoseconds to milliseconds with precision
|
|
17
|
+
* @param {bigint} ns
|
|
18
|
+
* @returns {number}
|
|
19
|
+
*/
|
|
20
|
+
const nsToMs = (ns) => Number(ns) / 1_000_000;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Latency tracker with circular buffer for O(1) operations
|
|
24
|
+
*/
|
|
25
|
+
const LatencyTracker = {
|
|
26
|
+
_pending: new Map(), // orderTag -> entryTime (bigint nanoseconds)
|
|
27
|
+
_samples: null, // Pre-allocated Float64Array circular buffer
|
|
28
|
+
_maxSamples: 100,
|
|
29
|
+
_head: 0, // Next write position
|
|
30
|
+
_count: 0, // Number of valid samples
|
|
31
|
+
_initialized: false,
|
|
32
|
+
|
|
33
|
+
/** Initialize circular buffer (lazy init) */
|
|
34
|
+
_init() {
|
|
35
|
+
if (this._initialized) return;
|
|
36
|
+
this._samples = new Float64Array(this._maxSamples);
|
|
37
|
+
this._initialized = true;
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
/** Record order sent time with high-resolution timestamp */
|
|
41
|
+
recordEntry(orderTag, entryTimeMs) {
|
|
42
|
+
this._pending.set(orderTag, hrNow());
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
/** Record fill received, calculate latency with sub-ms precision */
|
|
46
|
+
recordFill(orderTag) {
|
|
47
|
+
const entryTime = this._pending.get(orderTag);
|
|
48
|
+
if (!entryTime) return null;
|
|
49
|
+
|
|
50
|
+
this._pending.delete(orderTag);
|
|
51
|
+
const latencyNs = hrNow() - entryTime;
|
|
52
|
+
const latencyMs = nsToMs(latencyNs);
|
|
53
|
+
|
|
54
|
+
// Store in circular buffer (no shift, O(1))
|
|
55
|
+
this._init();
|
|
56
|
+
this._samples[this._head] = latencyMs;
|
|
57
|
+
this._head = (this._head + 1) % this._maxSamples;
|
|
58
|
+
if (this._count < this._maxSamples) this._count++;
|
|
59
|
+
|
|
60
|
+
return latencyMs;
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
/** Get average latency */
|
|
64
|
+
getAverage() {
|
|
65
|
+
if (this._count === 0) return null;
|
|
66
|
+
let sum = 0;
|
|
67
|
+
for (let i = 0; i < this._count; i++) {
|
|
68
|
+
sum += this._samples[i];
|
|
69
|
+
}
|
|
70
|
+
return sum / this._count;
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
/** Get min/max/avg stats with high precision */
|
|
74
|
+
getStats() {
|
|
75
|
+
if (this._count === 0) {
|
|
76
|
+
return { min: null, max: null, avg: null, p50: null, p99: null, samples: 0 };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const valid = [];
|
|
80
|
+
for (let i = 0; i < this._count; i++) {
|
|
81
|
+
valid.push(this._samples[i]);
|
|
82
|
+
}
|
|
83
|
+
valid.sort((a, b) => a - b);
|
|
84
|
+
|
|
85
|
+
const sum = valid.reduce((a, b) => a + b, 0);
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
min: valid[0],
|
|
89
|
+
max: valid[valid.length - 1],
|
|
90
|
+
avg: sum / valid.length,
|
|
91
|
+
p50: valid[Math.floor(valid.length * 0.5)],
|
|
92
|
+
p99: valid[Math.floor(valid.length * 0.99)] || valid[valid.length - 1],
|
|
93
|
+
samples: this._count,
|
|
94
|
+
};
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
/** Get last N latency samples */
|
|
98
|
+
getRecent(n = 10) {
|
|
99
|
+
if (this._count === 0) return [];
|
|
100
|
+
const result = [];
|
|
101
|
+
const start = this._count < this._maxSamples ? 0 : this._head;
|
|
102
|
+
for (let i = 0; i < Math.min(n, this._count); i++) {
|
|
103
|
+
const idx = (start + this._count - 1 - i + this._maxSamples) % this._maxSamples;
|
|
104
|
+
result.push(this._samples[idx]);
|
|
105
|
+
}
|
|
106
|
+
return result;
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
/** Clear all tracking data */
|
|
110
|
+
clear() {
|
|
111
|
+
this._pending.clear();
|
|
112
|
+
this._head = 0;
|
|
113
|
+
this._count = 0;
|
|
114
|
+
if (this._samples) {
|
|
115
|
+
this._samples.fill(0);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Pre-allocated fill info pool for zero-allocation in hot path
|
|
122
|
+
*/
|
|
123
|
+
const FillInfoPool = {
|
|
124
|
+
_template: {
|
|
125
|
+
orderTag: null,
|
|
126
|
+
basketId: null,
|
|
127
|
+
orderId: null,
|
|
128
|
+
status: null,
|
|
129
|
+
symbol: null,
|
|
130
|
+
exchange: null,
|
|
131
|
+
accountId: null,
|
|
132
|
+
fillQuantity: 0,
|
|
133
|
+
totalFillQuantity: 0,
|
|
134
|
+
remainingQuantity: 0,
|
|
135
|
+
avgFillPrice: 0,
|
|
136
|
+
lastFillPrice: 0,
|
|
137
|
+
transactionType: 0,
|
|
138
|
+
orderType: 0,
|
|
139
|
+
quantity: 0,
|
|
140
|
+
ssboe: 0,
|
|
141
|
+
usecs: 0,
|
|
142
|
+
localTimestamp: 0,
|
|
143
|
+
roundTripLatencyMs: null,
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
/** Fill template with notification data */
|
|
147
|
+
fill(notif, receiveTime, latency) {
|
|
148
|
+
const o = this._template;
|
|
149
|
+
o.orderTag = notif.userTag || null;
|
|
150
|
+
o.basketId = notif.basketId;
|
|
151
|
+
o.orderId = notif.exchangeOrderId || notif.orderId;
|
|
152
|
+
o.status = notif.status;
|
|
153
|
+
o.symbol = notif.symbol;
|
|
154
|
+
o.exchange = notif.exchange;
|
|
155
|
+
o.accountId = notif.accountId;
|
|
156
|
+
o.fillQuantity = notif.totalFillSize || notif.fillQuantity || 0;
|
|
157
|
+
o.totalFillQuantity = notif.totalFillSize || notif.totalFillQuantity || 0;
|
|
158
|
+
o.remainingQuantity = notif.totalUnfilledSize || notif.remainingQuantity || 0;
|
|
159
|
+
o.avgFillPrice = parseFloat(notif.avgFillPrice || 0);
|
|
160
|
+
o.lastFillPrice = parseFloat(notif.price || notif.fillPrice || 0);
|
|
161
|
+
o.transactionType = notif.transactionType;
|
|
162
|
+
o.orderType = notif.priceType || notif.orderType;
|
|
163
|
+
o.quantity = notif.quantity;
|
|
164
|
+
o.ssboe = notif.ssboe;
|
|
165
|
+
o.usecs = notif.usecs;
|
|
166
|
+
o.localTimestamp = receiveTime;
|
|
167
|
+
o.roundTripLatencyMs = latency;
|
|
168
|
+
return o;
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
/** Create a copy for async operations */
|
|
172
|
+
clone(fillInfo) {
|
|
173
|
+
return { ...fillInfo };
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
module.exports = {
|
|
178
|
+
hrNow,
|
|
179
|
+
nsToMs,
|
|
180
|
+
LatencyTracker,
|
|
181
|
+
FillInfoPool,
|
|
182
|
+
};
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rithmic Market Data Decoders
|
|
3
|
+
* @module services/rithmic/market-data-decoders
|
|
4
|
+
*
|
|
5
|
+
* Manual decoders for LastTrade and BestBidOffer messages.
|
|
6
|
+
* Required because protobufjs can't handle field IDs > 100000
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { readVarint, readLengthDelimited, skipField } = require('./proto-decoders');
|
|
10
|
+
|
|
11
|
+
// Rithmic field IDs for LastTrade (from protobuf)
|
|
12
|
+
const LAST_TRADE_FIELDS = {
|
|
13
|
+
TEMPLATE_ID: 154467,
|
|
14
|
+
SYMBOL: 110100,
|
|
15
|
+
EXCHANGE: 110101,
|
|
16
|
+
TRADE_PRICE: 100006,
|
|
17
|
+
TRADE_SIZE: 100178,
|
|
18
|
+
AGGRESSOR: 112003, // 1=BUY, 2=SELL
|
|
19
|
+
SSBOE: 150100,
|
|
20
|
+
USECS: 150101,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Rithmic field IDs for BestBidOffer (from protobuf)
|
|
24
|
+
const BBO_FIELDS = {
|
|
25
|
+
TEMPLATE_ID: 154467,
|
|
26
|
+
SYMBOL: 110100,
|
|
27
|
+
EXCHANGE: 110101,
|
|
28
|
+
BID_PRICE: 100022,
|
|
29
|
+
BID_SIZE: 100030,
|
|
30
|
+
ASK_PRICE: 100025,
|
|
31
|
+
ASK_SIZE: 100031,
|
|
32
|
+
SSBOE: 150100,
|
|
33
|
+
USECS: 150101,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Manually decode LastTrade message from Rithmic
|
|
38
|
+
* @param {Buffer} buffer
|
|
39
|
+
* @returns {Object}
|
|
40
|
+
*/
|
|
41
|
+
function decodeLastTrade(buffer) {
|
|
42
|
+
const result = {};
|
|
43
|
+
let offset = 0;
|
|
44
|
+
|
|
45
|
+
while (offset < buffer.length) {
|
|
46
|
+
try {
|
|
47
|
+
const [tag, newOffset] = readVarint(buffer, offset);
|
|
48
|
+
const fieldNumber = tag >>> 3;
|
|
49
|
+
const wireType = tag & 0x7;
|
|
50
|
+
offset = newOffset;
|
|
51
|
+
|
|
52
|
+
switch (fieldNumber) {
|
|
53
|
+
case LAST_TRADE_FIELDS.SYMBOL:
|
|
54
|
+
if (wireType === 2) {
|
|
55
|
+
const [val, next] = readLengthDelimited(buffer, offset);
|
|
56
|
+
result.symbol = val;
|
|
57
|
+
offset = next;
|
|
58
|
+
} else {
|
|
59
|
+
offset = skipField(buffer, offset, wireType);
|
|
60
|
+
}
|
|
61
|
+
break;
|
|
62
|
+
case LAST_TRADE_FIELDS.EXCHANGE:
|
|
63
|
+
if (wireType === 2) {
|
|
64
|
+
const [val, next] = readLengthDelimited(buffer, offset);
|
|
65
|
+
result.exchange = val;
|
|
66
|
+
offset = next;
|
|
67
|
+
} else {
|
|
68
|
+
offset = skipField(buffer, offset, wireType);
|
|
69
|
+
}
|
|
70
|
+
break;
|
|
71
|
+
case LAST_TRADE_FIELDS.TRADE_PRICE:
|
|
72
|
+
if (wireType === 1) {
|
|
73
|
+
result.tradePrice = buffer.readDoubleLE(offset);
|
|
74
|
+
offset += 8;
|
|
75
|
+
} else {
|
|
76
|
+
offset = skipField(buffer, offset, wireType);
|
|
77
|
+
}
|
|
78
|
+
break;
|
|
79
|
+
case LAST_TRADE_FIELDS.TRADE_SIZE:
|
|
80
|
+
if (wireType === 0) {
|
|
81
|
+
const [val, next] = readVarint(buffer, offset);
|
|
82
|
+
result.tradeSize = val;
|
|
83
|
+
offset = next;
|
|
84
|
+
} else {
|
|
85
|
+
offset = skipField(buffer, offset, wireType);
|
|
86
|
+
}
|
|
87
|
+
break;
|
|
88
|
+
case LAST_TRADE_FIELDS.AGGRESSOR:
|
|
89
|
+
if (wireType === 0) {
|
|
90
|
+
const [val, next] = readVarint(buffer, offset);
|
|
91
|
+
result.aggressor = val;
|
|
92
|
+
offset = next;
|
|
93
|
+
} else {
|
|
94
|
+
offset = skipField(buffer, offset, wireType);
|
|
95
|
+
}
|
|
96
|
+
break;
|
|
97
|
+
case LAST_TRADE_FIELDS.SSBOE:
|
|
98
|
+
if (wireType === 0) {
|
|
99
|
+
const [val, next] = readVarint(buffer, offset);
|
|
100
|
+
result.ssboe = val;
|
|
101
|
+
offset = next;
|
|
102
|
+
} else {
|
|
103
|
+
offset = skipField(buffer, offset, wireType);
|
|
104
|
+
}
|
|
105
|
+
break;
|
|
106
|
+
case LAST_TRADE_FIELDS.USECS:
|
|
107
|
+
if (wireType === 0) {
|
|
108
|
+
const [val, next] = readVarint(buffer, offset);
|
|
109
|
+
result.usecs = val;
|
|
110
|
+
offset = next;
|
|
111
|
+
} else {
|
|
112
|
+
offset = skipField(buffer, offset, wireType);
|
|
113
|
+
}
|
|
114
|
+
break;
|
|
115
|
+
default:
|
|
116
|
+
offset = skipField(buffer, offset, wireType);
|
|
117
|
+
}
|
|
118
|
+
} catch {
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Manually decode BestBidOffer message from Rithmic
|
|
128
|
+
* @param {Buffer} buffer
|
|
129
|
+
* @returns {Object}
|
|
130
|
+
*/
|
|
131
|
+
function decodeBestBidOffer(buffer) {
|
|
132
|
+
const result = {};
|
|
133
|
+
let offset = 0;
|
|
134
|
+
|
|
135
|
+
while (offset < buffer.length) {
|
|
136
|
+
try {
|
|
137
|
+
const [tag, newOffset] = readVarint(buffer, offset);
|
|
138
|
+
const fieldNumber = tag >>> 3;
|
|
139
|
+
const wireType = tag & 0x7;
|
|
140
|
+
offset = newOffset;
|
|
141
|
+
|
|
142
|
+
switch (fieldNumber) {
|
|
143
|
+
case BBO_FIELDS.SYMBOL:
|
|
144
|
+
if (wireType === 2) {
|
|
145
|
+
const [val, next] = readLengthDelimited(buffer, offset);
|
|
146
|
+
result.symbol = val;
|
|
147
|
+
offset = next;
|
|
148
|
+
} else {
|
|
149
|
+
offset = skipField(buffer, offset, wireType);
|
|
150
|
+
}
|
|
151
|
+
break;
|
|
152
|
+
case BBO_FIELDS.EXCHANGE:
|
|
153
|
+
if (wireType === 2) {
|
|
154
|
+
const [val, next] = readLengthDelimited(buffer, offset);
|
|
155
|
+
result.exchange = val;
|
|
156
|
+
offset = next;
|
|
157
|
+
} else {
|
|
158
|
+
offset = skipField(buffer, offset, wireType);
|
|
159
|
+
}
|
|
160
|
+
break;
|
|
161
|
+
case BBO_FIELDS.BID_PRICE:
|
|
162
|
+
if (wireType === 1) {
|
|
163
|
+
result.bidPrice = buffer.readDoubleLE(offset);
|
|
164
|
+
offset += 8;
|
|
165
|
+
} else {
|
|
166
|
+
offset = skipField(buffer, offset, wireType);
|
|
167
|
+
}
|
|
168
|
+
break;
|
|
169
|
+
case BBO_FIELDS.BID_SIZE:
|
|
170
|
+
if (wireType === 0) {
|
|
171
|
+
const [val, next] = readVarint(buffer, offset);
|
|
172
|
+
result.bidSize = val;
|
|
173
|
+
offset = next;
|
|
174
|
+
} else {
|
|
175
|
+
offset = skipField(buffer, offset, wireType);
|
|
176
|
+
}
|
|
177
|
+
break;
|
|
178
|
+
case BBO_FIELDS.ASK_PRICE:
|
|
179
|
+
if (wireType === 1) {
|
|
180
|
+
result.askPrice = buffer.readDoubleLE(offset);
|
|
181
|
+
offset += 8;
|
|
182
|
+
} else {
|
|
183
|
+
offset = skipField(buffer, offset, wireType);
|
|
184
|
+
}
|
|
185
|
+
break;
|
|
186
|
+
case BBO_FIELDS.ASK_SIZE:
|
|
187
|
+
if (wireType === 0) {
|
|
188
|
+
const [val, next] = readVarint(buffer, offset);
|
|
189
|
+
result.askSize = val;
|
|
190
|
+
offset = next;
|
|
191
|
+
} else {
|
|
192
|
+
offset = skipField(buffer, offset, wireType);
|
|
193
|
+
}
|
|
194
|
+
break;
|
|
195
|
+
case BBO_FIELDS.SSBOE:
|
|
196
|
+
if (wireType === 0) {
|
|
197
|
+
const [val, next] = readVarint(buffer, offset);
|
|
198
|
+
result.ssboe = val;
|
|
199
|
+
offset = next;
|
|
200
|
+
} else {
|
|
201
|
+
offset = skipField(buffer, offset, wireType);
|
|
202
|
+
}
|
|
203
|
+
break;
|
|
204
|
+
case BBO_FIELDS.USECS:
|
|
205
|
+
if (wireType === 0) {
|
|
206
|
+
const [val, next] = readVarint(buffer, offset);
|
|
207
|
+
result.usecs = val;
|
|
208
|
+
offset = next;
|
|
209
|
+
} else {
|
|
210
|
+
offset = skipField(buffer, offset, wireType);
|
|
211
|
+
}
|
|
212
|
+
break;
|
|
213
|
+
default:
|
|
214
|
+
offset = skipField(buffer, offset, wireType);
|
|
215
|
+
}
|
|
216
|
+
} catch {
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return result;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
module.exports = {
|
|
225
|
+
LAST_TRADE_FIELDS,
|
|
226
|
+
BBO_FIELDS,
|
|
227
|
+
decodeLastTrade,
|
|
228
|
+
decodeBestBidOffer,
|
|
229
|
+
};
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
const EventEmitter = require('events');
|
|
15
15
|
const { logger } = require('../../utils/logger');
|
|
16
|
+
const { decodeLastTrade, decodeBestBidOffer } = require('./market-data-decoders');
|
|
16
17
|
|
|
17
18
|
const log = logger.scope('RithmicMD');
|
|
18
19
|
|
|
@@ -25,284 +26,6 @@ const TEMPLATE_IDS = {
|
|
|
25
26
|
BEST_BID_OFFER: 151,
|
|
26
27
|
};
|
|
27
28
|
|
|
28
|
-
// Rithmic field IDs for LastTrade (from protobuf)
|
|
29
|
-
const LAST_TRADE_FIELDS = {
|
|
30
|
-
TEMPLATE_ID: 154467,
|
|
31
|
-
SYMBOL: 110100,
|
|
32
|
-
EXCHANGE: 110101,
|
|
33
|
-
TRADE_PRICE: 100006,
|
|
34
|
-
TRADE_SIZE: 100178,
|
|
35
|
-
AGGRESSOR: 112003, // 1=BUY, 2=SELL
|
|
36
|
-
SSBOE: 150100,
|
|
37
|
-
USECS: 150101,
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
// Rithmic field IDs for BestBidOffer (from protobuf)
|
|
41
|
-
const BBO_FIELDS = {
|
|
42
|
-
TEMPLATE_ID: 154467,
|
|
43
|
-
SYMBOL: 110100,
|
|
44
|
-
EXCHANGE: 110101,
|
|
45
|
-
BID_PRICE: 100022,
|
|
46
|
-
BID_SIZE: 100030,
|
|
47
|
-
ASK_PRICE: 100025,
|
|
48
|
-
ASK_SIZE: 100031,
|
|
49
|
-
SSBOE: 150100,
|
|
50
|
-
USECS: 150101,
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Read a varint from buffer starting at offset
|
|
55
|
-
* Uses BigInt internally to handle large field IDs correctly
|
|
56
|
-
* @param {Buffer} buffer
|
|
57
|
-
* @param {number} offset
|
|
58
|
-
* @returns {[number, number]} [value, newOffset]
|
|
59
|
-
*/
|
|
60
|
-
function readVarint(buffer, offset) {
|
|
61
|
-
let result = BigInt(0);
|
|
62
|
-
let shift = BigInt(0);
|
|
63
|
-
let pos = offset;
|
|
64
|
-
|
|
65
|
-
while (pos < buffer.length) {
|
|
66
|
-
const byte = buffer[pos++];
|
|
67
|
-
result |= BigInt(byte & 0x7f) << shift;
|
|
68
|
-
if ((byte & 0x80) === 0) {
|
|
69
|
-
return [Number(result), pos];
|
|
70
|
-
}
|
|
71
|
-
shift += BigInt(7);
|
|
72
|
-
if (shift > BigInt(63)) {
|
|
73
|
-
throw new Error('Varint too large');
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
throw new Error('Incomplete varint');
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Read a length-delimited field (string/bytes)
|
|
82
|
-
* @param {Buffer} buffer
|
|
83
|
-
* @param {number} offset
|
|
84
|
-
* @returns {[string, number]} [value, newOffset]
|
|
85
|
-
*/
|
|
86
|
-
function readLengthDelimited(buffer, offset) {
|
|
87
|
-
const [length, newOffset] = readVarint(buffer, offset);
|
|
88
|
-
const value = buffer.slice(newOffset, newOffset + length).toString('utf8');
|
|
89
|
-
return [value, newOffset + length];
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Skip a field based on wire type
|
|
94
|
-
* @param {Buffer} buffer
|
|
95
|
-
* @param {number} offset
|
|
96
|
-
* @param {number} wireType
|
|
97
|
-
* @returns {number} newOffset
|
|
98
|
-
*/
|
|
99
|
-
function skipField(buffer, offset, wireType) {
|
|
100
|
-
switch (wireType) {
|
|
101
|
-
case 0: // Varint
|
|
102
|
-
const [, newOffset] = readVarint(buffer, offset);
|
|
103
|
-
return newOffset;
|
|
104
|
-
case 1: // 64-bit
|
|
105
|
-
return offset + 8;
|
|
106
|
-
case 2: // Length-delimited
|
|
107
|
-
const [length, lenOffset] = readVarint(buffer, offset);
|
|
108
|
-
return lenOffset + length;
|
|
109
|
-
case 5: // 32-bit
|
|
110
|
-
return offset + 4;
|
|
111
|
-
default:
|
|
112
|
-
throw new Error(`Unknown wire type: ${wireType}`);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Manually decode LastTrade message from Rithmic
|
|
118
|
-
* Required because protobufjs can't handle field IDs > 100000
|
|
119
|
-
* @param {Buffer} buffer
|
|
120
|
-
* @returns {Object}
|
|
121
|
-
*/
|
|
122
|
-
function decodeLastTrade(buffer) {
|
|
123
|
-
const result = {};
|
|
124
|
-
let offset = 0;
|
|
125
|
-
|
|
126
|
-
while (offset < buffer.length) {
|
|
127
|
-
try {
|
|
128
|
-
const [tag, newOffset] = readVarint(buffer, offset);
|
|
129
|
-
const fieldNumber = tag >>> 3;
|
|
130
|
-
const wireType = tag & 0x7;
|
|
131
|
-
offset = newOffset;
|
|
132
|
-
|
|
133
|
-
switch (fieldNumber) {
|
|
134
|
-
case LAST_TRADE_FIELDS.SYMBOL:
|
|
135
|
-
if (wireType === 2) {
|
|
136
|
-
const [val, next] = readLengthDelimited(buffer, offset);
|
|
137
|
-
result.symbol = val;
|
|
138
|
-
offset = next;
|
|
139
|
-
} else {
|
|
140
|
-
offset = skipField(buffer, offset, wireType);
|
|
141
|
-
}
|
|
142
|
-
break;
|
|
143
|
-
case LAST_TRADE_FIELDS.EXCHANGE:
|
|
144
|
-
if (wireType === 2) {
|
|
145
|
-
const [val, next] = readLengthDelimited(buffer, offset);
|
|
146
|
-
result.exchange = val;
|
|
147
|
-
offset = next;
|
|
148
|
-
} else {
|
|
149
|
-
offset = skipField(buffer, offset, wireType);
|
|
150
|
-
}
|
|
151
|
-
break;
|
|
152
|
-
case LAST_TRADE_FIELDS.TRADE_PRICE:
|
|
153
|
-
if (wireType === 1) {
|
|
154
|
-
result.tradePrice = buffer.readDoubleLE(offset);
|
|
155
|
-
offset += 8;
|
|
156
|
-
} else {
|
|
157
|
-
offset = skipField(buffer, offset, wireType);
|
|
158
|
-
}
|
|
159
|
-
break;
|
|
160
|
-
case LAST_TRADE_FIELDS.TRADE_SIZE:
|
|
161
|
-
if (wireType === 0) {
|
|
162
|
-
const [val, next] = readVarint(buffer, offset);
|
|
163
|
-
result.tradeSize = val;
|
|
164
|
-
offset = next;
|
|
165
|
-
} else {
|
|
166
|
-
offset = skipField(buffer, offset, wireType);
|
|
167
|
-
}
|
|
168
|
-
break;
|
|
169
|
-
case LAST_TRADE_FIELDS.AGGRESSOR:
|
|
170
|
-
if (wireType === 0) {
|
|
171
|
-
const [val, next] = readVarint(buffer, offset);
|
|
172
|
-
result.aggressor = val;
|
|
173
|
-
offset = next;
|
|
174
|
-
} else {
|
|
175
|
-
offset = skipField(buffer, offset, wireType);
|
|
176
|
-
}
|
|
177
|
-
break;
|
|
178
|
-
case LAST_TRADE_FIELDS.SSBOE:
|
|
179
|
-
if (wireType === 0) {
|
|
180
|
-
const [val, next] = readVarint(buffer, offset);
|
|
181
|
-
result.ssboe = val;
|
|
182
|
-
offset = next;
|
|
183
|
-
} else {
|
|
184
|
-
offset = skipField(buffer, offset, wireType);
|
|
185
|
-
}
|
|
186
|
-
break;
|
|
187
|
-
case LAST_TRADE_FIELDS.USECS:
|
|
188
|
-
if (wireType === 0) {
|
|
189
|
-
const [val, next] = readVarint(buffer, offset);
|
|
190
|
-
result.usecs = val;
|
|
191
|
-
offset = next;
|
|
192
|
-
} else {
|
|
193
|
-
offset = skipField(buffer, offset, wireType);
|
|
194
|
-
}
|
|
195
|
-
break;
|
|
196
|
-
default:
|
|
197
|
-
offset = skipField(buffer, offset, wireType);
|
|
198
|
-
}
|
|
199
|
-
} catch {
|
|
200
|
-
break;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
return result;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Manually decode BestBidOffer message from Rithmic
|
|
209
|
-
* Required because protobufjs can't handle field IDs > 100000
|
|
210
|
-
* @param {Buffer} buffer
|
|
211
|
-
* @returns {Object}
|
|
212
|
-
*/
|
|
213
|
-
function decodeBestBidOffer(buffer) {
|
|
214
|
-
const result = {};
|
|
215
|
-
let offset = 0;
|
|
216
|
-
|
|
217
|
-
while (offset < buffer.length) {
|
|
218
|
-
try {
|
|
219
|
-
const [tag, newOffset] = readVarint(buffer, offset);
|
|
220
|
-
const fieldNumber = tag >>> 3;
|
|
221
|
-
const wireType = tag & 0x7;
|
|
222
|
-
offset = newOffset;
|
|
223
|
-
|
|
224
|
-
switch (fieldNumber) {
|
|
225
|
-
case BBO_FIELDS.SYMBOL:
|
|
226
|
-
if (wireType === 2) {
|
|
227
|
-
const [val, next] = readLengthDelimited(buffer, offset);
|
|
228
|
-
result.symbol = val;
|
|
229
|
-
offset = next;
|
|
230
|
-
} else {
|
|
231
|
-
offset = skipField(buffer, offset, wireType);
|
|
232
|
-
}
|
|
233
|
-
break;
|
|
234
|
-
case BBO_FIELDS.EXCHANGE:
|
|
235
|
-
if (wireType === 2) {
|
|
236
|
-
const [val, next] = readLengthDelimited(buffer, offset);
|
|
237
|
-
result.exchange = val;
|
|
238
|
-
offset = next;
|
|
239
|
-
} else {
|
|
240
|
-
offset = skipField(buffer, offset, wireType);
|
|
241
|
-
}
|
|
242
|
-
break;
|
|
243
|
-
case BBO_FIELDS.BID_PRICE:
|
|
244
|
-
if (wireType === 1) {
|
|
245
|
-
result.bidPrice = buffer.readDoubleLE(offset);
|
|
246
|
-
offset += 8;
|
|
247
|
-
} else {
|
|
248
|
-
offset = skipField(buffer, offset, wireType);
|
|
249
|
-
}
|
|
250
|
-
break;
|
|
251
|
-
case BBO_FIELDS.BID_SIZE:
|
|
252
|
-
if (wireType === 0) {
|
|
253
|
-
const [val, next] = readVarint(buffer, offset);
|
|
254
|
-
result.bidSize = val;
|
|
255
|
-
offset = next;
|
|
256
|
-
} else {
|
|
257
|
-
offset = skipField(buffer, offset, wireType);
|
|
258
|
-
}
|
|
259
|
-
break;
|
|
260
|
-
case BBO_FIELDS.ASK_PRICE:
|
|
261
|
-
if (wireType === 1) {
|
|
262
|
-
result.askPrice = buffer.readDoubleLE(offset);
|
|
263
|
-
offset += 8;
|
|
264
|
-
} else {
|
|
265
|
-
offset = skipField(buffer, offset, wireType);
|
|
266
|
-
}
|
|
267
|
-
break;
|
|
268
|
-
case BBO_FIELDS.ASK_SIZE:
|
|
269
|
-
if (wireType === 0) {
|
|
270
|
-
const [val, next] = readVarint(buffer, offset);
|
|
271
|
-
result.askSize = val;
|
|
272
|
-
offset = next;
|
|
273
|
-
} else {
|
|
274
|
-
offset = skipField(buffer, offset, wireType);
|
|
275
|
-
}
|
|
276
|
-
break;
|
|
277
|
-
case BBO_FIELDS.SSBOE:
|
|
278
|
-
if (wireType === 0) {
|
|
279
|
-
const [val, next] = readVarint(buffer, offset);
|
|
280
|
-
result.ssboe = val;
|
|
281
|
-
offset = next;
|
|
282
|
-
} else {
|
|
283
|
-
offset = skipField(buffer, offset, wireType);
|
|
284
|
-
}
|
|
285
|
-
break;
|
|
286
|
-
case BBO_FIELDS.USECS:
|
|
287
|
-
if (wireType === 0) {
|
|
288
|
-
const [val, next] = readVarint(buffer, offset);
|
|
289
|
-
result.usecs = val;
|
|
290
|
-
offset = next;
|
|
291
|
-
} else {
|
|
292
|
-
offset = skipField(buffer, offset, wireType);
|
|
293
|
-
}
|
|
294
|
-
break;
|
|
295
|
-
default:
|
|
296
|
-
offset = skipField(buffer, offset, wireType);
|
|
297
|
-
}
|
|
298
|
-
} catch {
|
|
299
|
-
break;
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
return result;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
29
|
/**
|
|
307
30
|
* Rithmic Market Data Feed
|
|
308
31
|
* Provides real-time market data via Rithmic WebSocket connection
|