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
package/src/lib/data.js
ADDED
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Market Data Feed
|
|
3
|
+
* Connects to PropFirm market data via SignalR (ProjectX Gateway API)
|
|
4
|
+
* Feeds real-time data to the algo engine
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const EventEmitter = require('events');
|
|
8
|
+
const signalR = require('@microsoft/signalr');
|
|
9
|
+
|
|
10
|
+
// ProjectX Gateway SignalR endpoints
|
|
11
|
+
// Pattern: replace 'api.' with 'rtc.' in gatewayApi URL
|
|
12
|
+
const PROPFIRM_RTC_ENDPOINTS = {
|
|
13
|
+
topstep: {
|
|
14
|
+
market: 'https://rtc.topstepx.com/hubs/market',
|
|
15
|
+
user: 'https://rtc.topstepx.com/hubs/user'
|
|
16
|
+
},
|
|
17
|
+
alpha_futures: {
|
|
18
|
+
market: 'https://rtc.alphafutures.projectx.com/hubs/market',
|
|
19
|
+
user: 'https://rtc.alphafutures.projectx.com/hubs/user'
|
|
20
|
+
},
|
|
21
|
+
tickticktrader: {
|
|
22
|
+
market: 'https://rtc.tickticktrader.projectx.com/hubs/market',
|
|
23
|
+
user: 'https://rtc.tickticktrader.projectx.com/hubs/user'
|
|
24
|
+
},
|
|
25
|
+
bulenox: {
|
|
26
|
+
market: 'https://rtc.bulenox.projectx.com/hubs/market',
|
|
27
|
+
user: 'https://rtc.bulenox.projectx.com/hubs/user'
|
|
28
|
+
},
|
|
29
|
+
tradeday: {
|
|
30
|
+
market: 'https://rtc.tradeday.projectx.com/hubs/market',
|
|
31
|
+
user: 'https://rtc.tradeday.projectx.com/hubs/user'
|
|
32
|
+
},
|
|
33
|
+
blusky: {
|
|
34
|
+
market: 'https://rtc.blusky.projectx.com/hubs/market',
|
|
35
|
+
user: 'https://rtc.blusky.projectx.com/hubs/user'
|
|
36
|
+
},
|
|
37
|
+
goat_futures: {
|
|
38
|
+
market: 'https://rtc.goatfutures.projectx.com/hubs/market',
|
|
39
|
+
user: 'https://rtc.goatfutures.projectx.com/hubs/user'
|
|
40
|
+
},
|
|
41
|
+
futures_desk: {
|
|
42
|
+
market: 'https://rtc.thefuturesdesk.projectx.com/hubs/market',
|
|
43
|
+
user: 'https://rtc.thefuturesdesk.projectx.com/hubs/user'
|
|
44
|
+
},
|
|
45
|
+
daytraders: {
|
|
46
|
+
market: 'https://rtc.daytraders.projectx.com/hubs/market',
|
|
47
|
+
user: 'https://rtc.daytraders.projectx.com/hubs/user'
|
|
48
|
+
},
|
|
49
|
+
e8_futures: {
|
|
50
|
+
market: 'https://rtc.e8futures.projectx.com/hubs/market',
|
|
51
|
+
user: 'https://rtc.e8futures.projectx.com/hubs/user'
|
|
52
|
+
},
|
|
53
|
+
blue_guardian: {
|
|
54
|
+
market: 'https://rtc.blueguardianfutures.projectx.com/hubs/market',
|
|
55
|
+
user: 'https://rtc.blueguardianfutures.projectx.com/hubs/user'
|
|
56
|
+
},
|
|
57
|
+
futures_elite: {
|
|
58
|
+
market: 'https://rtc.futureselite.projectx.com/hubs/market',
|
|
59
|
+
user: 'https://rtc.futureselite.projectx.com/hubs/user'
|
|
60
|
+
},
|
|
61
|
+
fxify: {
|
|
62
|
+
market: 'https://rtc.fxify.projectx.com/hubs/market',
|
|
63
|
+
user: 'https://rtc.fxify.projectx.com/hubs/user'
|
|
64
|
+
},
|
|
65
|
+
hola_prime: {
|
|
66
|
+
market: 'https://rtc.holaprime.projectx.com/hubs/market',
|
|
67
|
+
user: 'https://rtc.holaprime.projectx.com/hubs/user'
|
|
68
|
+
},
|
|
69
|
+
top_one_futures: {
|
|
70
|
+
market: 'https://rtc.toponefutures.projectx.com/hubs/market',
|
|
71
|
+
user: 'https://rtc.toponefutures.projectx.com/hubs/user'
|
|
72
|
+
},
|
|
73
|
+
funding_futures: {
|
|
74
|
+
market: 'https://rtc.fundingfutures.projectx.com/hubs/market',
|
|
75
|
+
user: 'https://rtc.fundingfutures.projectx.com/hubs/user'
|
|
76
|
+
},
|
|
77
|
+
tx3_funding: {
|
|
78
|
+
market: 'https://rtc.tx3funding.projectx.com/hubs/market',
|
|
79
|
+
user: 'https://rtc.tx3funding.projectx.com/hubs/user'
|
|
80
|
+
},
|
|
81
|
+
lucid_trading: {
|
|
82
|
+
market: 'https://rtc.lucidtrading.projectx.com/hubs/market',
|
|
83
|
+
user: 'https://rtc.lucidtrading.projectx.com/hubs/user'
|
|
84
|
+
},
|
|
85
|
+
tradeify: {
|
|
86
|
+
market: 'https://rtc.tradeify.projectx.com/hubs/market',
|
|
87
|
+
user: 'https://rtc.tradeify.projectx.com/hubs/user'
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// Default fallback
|
|
92
|
+
const DEFAULT_ENDPOINTS = PROPFIRM_RTC_ENDPOINTS.topstep;
|
|
93
|
+
|
|
94
|
+
// NO STATIC CONTRACT DATA - All contract specs come from API
|
|
95
|
+
// Use contract.tickSize and contract.tickValue from API response
|
|
96
|
+
|
|
97
|
+
class MarketDataFeed extends EventEmitter {
|
|
98
|
+
constructor(config) {
|
|
99
|
+
super();
|
|
100
|
+
|
|
101
|
+
this.config = config;
|
|
102
|
+
this.marketConnection = null;
|
|
103
|
+
this.userConnection = null;
|
|
104
|
+
this.isConnected = false;
|
|
105
|
+
this.reconnectAttempts = 0;
|
|
106
|
+
this.maxReconnectAttempts = 5;
|
|
107
|
+
this.subscriptions = new Set();
|
|
108
|
+
|
|
109
|
+
// Data buffers
|
|
110
|
+
this.dataBuffers = new Map();
|
|
111
|
+
this.lastTick = new Map();
|
|
112
|
+
this.orderBook = new Map(); // DOM data
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Connect to market data feed via SignalR
|
|
117
|
+
*/
|
|
118
|
+
async connect(userToken, propfirm, contractId = null) {
|
|
119
|
+
try {
|
|
120
|
+
const endpoints = PROPFIRM_RTC_ENDPOINTS[propfirm] || DEFAULT_ENDPOINTS;
|
|
121
|
+
// // console.log(`[MARKET] Connecting to ${propfirm} RTC: ${endpoints.market}`);
|
|
122
|
+
|
|
123
|
+
// Build URL with access_token query parameter
|
|
124
|
+
const urlWithToken = `${endpoints.market}?access_token=${encodeURIComponent(userToken)}`;
|
|
125
|
+
|
|
126
|
+
// Build SignalR connection - NO LOGGING to prevent stdout pollution
|
|
127
|
+
this.marketConnection = new signalR.HubConnectionBuilder()
|
|
128
|
+
.withUrl(urlWithToken, {
|
|
129
|
+
skipNegotiation: true,
|
|
130
|
+
transport: signalR.HttpTransportType.WebSockets
|
|
131
|
+
})
|
|
132
|
+
.withAutomaticReconnect([0, 2000, 5000, 10000, 30000])
|
|
133
|
+
.configureLogging(signalR.LogLevel.None)
|
|
134
|
+
.build();
|
|
135
|
+
|
|
136
|
+
// Store contractId for immediate subscription
|
|
137
|
+
this._pendingContractId = contractId;
|
|
138
|
+
|
|
139
|
+
// Setup event handlers BEFORE starting
|
|
140
|
+
this._setupMarketEventHandlers();
|
|
141
|
+
|
|
142
|
+
// Start connection
|
|
143
|
+
// // console.log(`[MARKET] Starting SignalR connection...`);
|
|
144
|
+
await this.marketConnection.start();
|
|
145
|
+
// // console.log(`[MARKET] Connected! State: ${this.marketConnection.state}`);
|
|
146
|
+
|
|
147
|
+
// IMMEDIATELY subscribe if contractId provided - don't wait!
|
|
148
|
+
if (contractId && this.marketConnection.state === 'Connected') {
|
|
149
|
+
// // console.log(`[MARKET] Immediate subscribe to ${contractId}`);
|
|
150
|
+
try {
|
|
151
|
+
await this.marketConnection.invoke('SubscribeContractQuotes', contractId);
|
|
152
|
+
// // console.log(`[MARKET] Quotes OK`);
|
|
153
|
+
await this.marketConnection.invoke('SubscribeContractTrades', contractId);
|
|
154
|
+
// // console.log(`[MARKET] Trades OK`);
|
|
155
|
+
|
|
156
|
+
const subscriptionKey = `${contractId}:${contractId}`;
|
|
157
|
+
this.subscriptions.add(subscriptionKey);
|
|
158
|
+
this.dataBuffers.set(subscriptionKey, []);
|
|
159
|
+
} catch (subError) {
|
|
160
|
+
// // console.log(`[MARKET] Immediate subscribe failed: ${subError.message}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
this.isConnected = true;
|
|
165
|
+
this.reconnectAttempts = 0;
|
|
166
|
+
this.emit('connected');
|
|
167
|
+
|
|
168
|
+
return true;
|
|
169
|
+
|
|
170
|
+
} catch (error) {
|
|
171
|
+
// // console.log(`[MARKET] Connection error: ${error.message}`);
|
|
172
|
+
this.emit('error', error);
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Setup SignalR event handlers for market data
|
|
179
|
+
*/
|
|
180
|
+
_setupMarketEventHandlers() {
|
|
181
|
+
// Quote updates (bid/ask) - args: [contractId, quoteObject]
|
|
182
|
+
this.marketConnection.on('GatewayQuote', (...args) => {
|
|
183
|
+
this._handleQuote(args);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Trade updates - args: [contractId, tradesArray]
|
|
187
|
+
this.marketConnection.on('GatewayTrade', (...args) => {
|
|
188
|
+
this._handleTrade(args);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// DOM/Depth updates - args: [contractId, depthObject]
|
|
192
|
+
this.marketConnection.on('GatewayDepth', (...args) => {
|
|
193
|
+
this._handleDepth(args);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Gateway logout - server is kicking us out
|
|
197
|
+
this.marketConnection.on('GatewayLogout', (...args) => {
|
|
198
|
+
// // console.log(`[MARKET] GatewayLogout received:`, args);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Also handle lowercase version
|
|
202
|
+
this.marketConnection.on('gatewaylogout', (...args) => {
|
|
203
|
+
// // console.log(`[MARKET] gatewaylogout received:`, args);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Connection state changes
|
|
207
|
+
this.marketConnection.onreconnecting((error) => {
|
|
208
|
+
// // console.log(`[MARKET] Reconnecting... Error: ${error?.message || 'none'}`);
|
|
209
|
+
this.isConnected = false;
|
|
210
|
+
this.emit('reconnecting', { error: error?.message });
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
this.marketConnection.onreconnected((connectionId) => {
|
|
214
|
+
// // console.log(`[MARKET] Reconnected! ConnectionId: ${connectionId}`);
|
|
215
|
+
this.isConnected = true;
|
|
216
|
+
this.emit('reconnected', { connectionId });
|
|
217
|
+
// Resubscribe to all symbols
|
|
218
|
+
this._resubscribeAll();
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
this.marketConnection.onclose((error) => {
|
|
222
|
+
// // console.log(`[MARKET] Connection CLOSED! Error: ${error?.message || 'none'}`);
|
|
223
|
+
this.isConnected = false;
|
|
224
|
+
this.emit('disconnected', { error: error?.message });
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Subscribe to symbol market data
|
|
230
|
+
* Implements retry logic for connection race conditions
|
|
231
|
+
*/
|
|
232
|
+
async subscribe(symbol, contractId, retryCount = 0) {
|
|
233
|
+
// // console.log(`[MARKET] Subscribe called - symbol: ${symbol}, contractId: ${contractId}`);
|
|
234
|
+
|
|
235
|
+
const subscriptionKey = `${symbol}:${contractId}`;
|
|
236
|
+
|
|
237
|
+
if (this.subscriptions.has(subscriptionKey)) {
|
|
238
|
+
return true; // Already subscribed
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Check connection state
|
|
242
|
+
const state = this.marketConnection?.state;
|
|
243
|
+
// // console.log(`[MARKET] Connection state: ${state}`);
|
|
244
|
+
|
|
245
|
+
if (state !== 'Connected') {
|
|
246
|
+
if (retryCount < 5) {
|
|
247
|
+
// // console.log(`[MARKET] Not connected, waiting 500ms and retrying... (${retryCount + 1}/5)`);
|
|
248
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
249
|
+
return this.subscribe(symbol, contractId, retryCount + 1);
|
|
250
|
+
} else {
|
|
251
|
+
// // console.log(`[MARKET] ERROR: Connection not available after 5 retries`);
|
|
252
|
+
this.emit('error', new Error('Connection not available'));
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
// Subscribe to quotes - do it immediately, no delay
|
|
259
|
+
// // console.log(`[MARKET] Subscribing to ${contractId}...`);
|
|
260
|
+
await this.marketConnection.invoke('SubscribeContractQuotes', contractId);
|
|
261
|
+
// // console.log(`[MARKET] Quotes subscribed`);
|
|
262
|
+
|
|
263
|
+
// Subscribe to trades
|
|
264
|
+
await this.marketConnection.invoke('SubscribeContractTrades', contractId);
|
|
265
|
+
// // console.log(`[MARKET] Trades subscribed`);
|
|
266
|
+
|
|
267
|
+
this.subscriptions.add(subscriptionKey);
|
|
268
|
+
this.dataBuffers.set(subscriptionKey, []);
|
|
269
|
+
this.isConnected = true;
|
|
270
|
+
|
|
271
|
+
this.emit('subscribed', { symbol, contractId });
|
|
272
|
+
return true;
|
|
273
|
+
|
|
274
|
+
} catch (error) {
|
|
275
|
+
// // console.log(`[MARKET] Subscribe ERROR: ${error.message}`);
|
|
276
|
+
|
|
277
|
+
// If connection was closed, try to reconnect
|
|
278
|
+
if (error.message.includes('not in the') && retryCount < 3) {
|
|
279
|
+
// // console.log(`[MARKET] Connection lost, will retry on reconnect`);
|
|
280
|
+
this.isConnected = false;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
this.emit('error', error);
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Unsubscribe from symbol market data
|
|
290
|
+
*/
|
|
291
|
+
async unsubscribe(symbol, contractId) {
|
|
292
|
+
const subscriptionKey = `${symbol}:${contractId}`;
|
|
293
|
+
|
|
294
|
+
if (!this.subscriptions.has(subscriptionKey)) {
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
await this.marketConnection.invoke('UnsubscribeContractQuotes', contractId);
|
|
300
|
+
await this.marketConnection.invoke('UnsubscribeContractTrades', contractId);
|
|
301
|
+
await this.marketConnection.invoke('UnsubscribeContractMarketDepth', contractId);
|
|
302
|
+
|
|
303
|
+
this.subscriptions.delete(subscriptionKey);
|
|
304
|
+
this.dataBuffers.delete(subscriptionKey);
|
|
305
|
+
|
|
306
|
+
this.emit('unsubscribed', { symbol, contractId });
|
|
307
|
+
return true;
|
|
308
|
+
|
|
309
|
+
} catch (error) {
|
|
310
|
+
this.emit('error', error);
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Resubscribe to all symbols after reconnection
|
|
317
|
+
*/
|
|
318
|
+
async _resubscribeAll() {
|
|
319
|
+
for (const subscriptionKey of this.subscriptions) {
|
|
320
|
+
const [symbol, contractId] = subscriptionKey.split(':');
|
|
321
|
+
this.subscriptions.delete(subscriptionKey); // Remove so subscribe works
|
|
322
|
+
await this.subscribe(symbol, contractId);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Handle quote update (bid/ask)
|
|
328
|
+
* Format: args[0] = contractId, args[1] = quote object
|
|
329
|
+
*/
|
|
330
|
+
_handleQuote(args) {
|
|
331
|
+
// Debug log every 100th quote
|
|
332
|
+
if (!this._quoteCount) this._quoteCount = 0;
|
|
333
|
+
this._quoteCount++;
|
|
334
|
+
if (this._quoteCount % 100 === 1) {
|
|
335
|
+
// // console.log(`[MARKET] Quote #${this._quoteCount}:`, JSON.stringify(args).substring(0, 200));
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const contractId = args[0];
|
|
339
|
+
const quote = args[1] || {};
|
|
340
|
+
|
|
341
|
+
const { symbol, lastPrice, bestBid, bestAsk, change, changePercent, volume, timestamp } = quote;
|
|
342
|
+
|
|
343
|
+
// Find matching subscription
|
|
344
|
+
let subscriptionKey = null;
|
|
345
|
+
for (const key of this.subscriptions) {
|
|
346
|
+
if (key.includes(contractId)) {
|
|
347
|
+
subscriptionKey = key;
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (!subscriptionKey) {
|
|
353
|
+
subscriptionKey = `${contractId}:${contractId}`;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Calculate price - use lastPrice, or mid of bid/ask
|
|
357
|
+
const bid = parseFloat(bestBid) || 0;
|
|
358
|
+
const ask = parseFloat(bestAsk) || 0;
|
|
359
|
+
const last = parseFloat(lastPrice) || 0;
|
|
360
|
+
const price = last > 0 ? last : (bid > 0 && ask > 0 ? (bid + ask) / 2 : bid || ask);
|
|
361
|
+
|
|
362
|
+
const quoteData = {
|
|
363
|
+
type: 'quote',
|
|
364
|
+
symbol: symbol || contractId,
|
|
365
|
+
contractId,
|
|
366
|
+
price,
|
|
367
|
+
bid,
|
|
368
|
+
ask,
|
|
369
|
+
spread: ask > 0 && bid > 0 ? ask - bid : 0,
|
|
370
|
+
mid: bid > 0 && ask > 0 ? (bid + ask) / 2 : price,
|
|
371
|
+
change: parseFloat(change) || 0,
|
|
372
|
+
changePercent: parseFloat(changePercent) || 0,
|
|
373
|
+
volume: parseInt(volume) || 0,
|
|
374
|
+
timestamp: timestamp || Date.now()
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
// Update last tick - merge with existing data
|
|
378
|
+
const lastTick = this.lastTick.get(subscriptionKey) || {};
|
|
379
|
+
const updatedTick = { ...lastTick };
|
|
380
|
+
|
|
381
|
+
// Only update fields that have valid values
|
|
382
|
+
if (price > 0) updatedTick.price = price;
|
|
383
|
+
if (bid > 0) updatedTick.bid = bid;
|
|
384
|
+
if (ask > 0) updatedTick.ask = ask;
|
|
385
|
+
if (quoteData.spread > 0) updatedTick.spread = quoteData.spread;
|
|
386
|
+
if (quoteData.mid > 0) updatedTick.mid = quoteData.mid;
|
|
387
|
+
if (quoteData.change !== 0) updatedTick.change = quoteData.change;
|
|
388
|
+
if (quoteData.changePercent !== 0) updatedTick.changePercent = quoteData.changePercent;
|
|
389
|
+
if (quoteData.volume > 0) updatedTick.volume = quoteData.volume;
|
|
390
|
+
updatedTick.timestamp = quoteData.timestamp;
|
|
391
|
+
updatedTick.type = 'quote';
|
|
392
|
+
updatedTick.symbol = quoteData.symbol;
|
|
393
|
+
updatedTick.contractId = contractId;
|
|
394
|
+
|
|
395
|
+
this.lastTick.set(subscriptionKey, updatedTick);
|
|
396
|
+
|
|
397
|
+
this.emit('quote', updatedTick);
|
|
398
|
+
this.emit('tick', updatedTick);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Handle trade update
|
|
403
|
+
* Format: args[0] = contractId, args[1] = array of trades
|
|
404
|
+
*/
|
|
405
|
+
_handleTrade(args) {
|
|
406
|
+
// Debug log every 50th trade
|
|
407
|
+
if (!this._tradeCount) this._tradeCount = 0;
|
|
408
|
+
this._tradeCount++;
|
|
409
|
+
if (this._tradeCount % 50 === 1) {
|
|
410
|
+
// // console.log(`[MARKET] Trade #${this._tradeCount}:`, JSON.stringify(args).substring(0, 200));
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const contractId = args[0];
|
|
414
|
+
const trades = args[1] || [];
|
|
415
|
+
|
|
416
|
+
// Find matching subscription
|
|
417
|
+
let subscriptionKey = null;
|
|
418
|
+
for (const key of this.subscriptions) {
|
|
419
|
+
if (key.includes(contractId)) {
|
|
420
|
+
subscriptionKey = key;
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (!subscriptionKey) {
|
|
426
|
+
subscriptionKey = `${contractId}:${contractId}`;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Process each trade in the array
|
|
430
|
+
for (const trade of trades) {
|
|
431
|
+
const { symbolId, price, timestamp, type, volume } = trade;
|
|
432
|
+
|
|
433
|
+
const tradeData = {
|
|
434
|
+
type: 'trade',
|
|
435
|
+
symbol: symbolId || contractId,
|
|
436
|
+
contractId,
|
|
437
|
+
price: parseFloat(price),
|
|
438
|
+
size: parseInt(volume) || 1,
|
|
439
|
+
side: type === 0 ? 'buy' : type === 1 ? 'sell' : 'unknown',
|
|
440
|
+
volume: parseInt(volume) || 1,
|
|
441
|
+
timestamp: timestamp || Date.now()
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
// Update last tick with trade price
|
|
445
|
+
const lastTick = this.lastTick.get(subscriptionKey) || {};
|
|
446
|
+
if (tradeData.price > 0) {
|
|
447
|
+
this.lastTick.set(subscriptionKey, {
|
|
448
|
+
...lastTick,
|
|
449
|
+
price: tradeData.price,
|
|
450
|
+
lastTradeVolume: tradeData.volume,
|
|
451
|
+
lastTradeTime: tradeData.timestamp,
|
|
452
|
+
lastTradeSide: tradeData.side
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Add to buffer
|
|
457
|
+
const buffer = this.dataBuffers.get(subscriptionKey) || [];
|
|
458
|
+
buffer.push(tradeData);
|
|
459
|
+
|
|
460
|
+
// Keep buffer size limited (last 1000 trades)
|
|
461
|
+
if (buffer.length > 1000) {
|
|
462
|
+
buffer.shift();
|
|
463
|
+
}
|
|
464
|
+
this.dataBuffers.set(subscriptionKey, buffer);
|
|
465
|
+
|
|
466
|
+
this.emit('trade', tradeData);
|
|
467
|
+
this.emit('tick', this.lastTick.get(subscriptionKey));
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Handle depth/DOM update
|
|
473
|
+
* Format: args[0] = contractId, args[1] = depth data
|
|
474
|
+
*/
|
|
475
|
+
_handleDepth(args) {
|
|
476
|
+
const contractId = args[0];
|
|
477
|
+
const depth = args[1] || {};
|
|
478
|
+
const { timestamp, type, price, volume, currentVolume } = depth;
|
|
479
|
+
|
|
480
|
+
// type: 0 = bid, 1 = ask
|
|
481
|
+
const depthData = {
|
|
482
|
+
type: 'depth',
|
|
483
|
+
contractId,
|
|
484
|
+
side: type === 0 ? 'bid' : 'ask',
|
|
485
|
+
price: parseFloat(price) || 0,
|
|
486
|
+
volume: parseInt(volume) || 0,
|
|
487
|
+
currentVolume: parseInt(currentVolume) || 0,
|
|
488
|
+
timestamp: timestamp || Date.now()
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
this.emit('depth', depthData);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Disconnect from market data feed
|
|
496
|
+
*/
|
|
497
|
+
async disconnect() {
|
|
498
|
+
this.subscriptions.clear();
|
|
499
|
+
|
|
500
|
+
if (this.marketConnection) {
|
|
501
|
+
try {
|
|
502
|
+
await this.marketConnection.stop();
|
|
503
|
+
} catch (e) {
|
|
504
|
+
// Ignore disconnect errors
|
|
505
|
+
}
|
|
506
|
+
this.marketConnection = null;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
this.isConnected = false;
|
|
510
|
+
this.emit('disconnected', { code: 1000, reason: 'User requested disconnect' });
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Get last tick for symbol
|
|
515
|
+
*/
|
|
516
|
+
getLastTick(symbol, contractId) {
|
|
517
|
+
const subscriptionKey = `${symbol}:${contractId}`;
|
|
518
|
+
return this.lastTick.get(subscriptionKey);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Get trade buffer for symbol
|
|
523
|
+
*/
|
|
524
|
+
getTradeBuffer(symbol, contractId) {
|
|
525
|
+
const subscriptionKey = `${symbol}:${contractId}`;
|
|
526
|
+
return this.dataBuffers.get(subscriptionKey) || [];
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// NO STATIC CONTRACT DATA - All specs come from API
|
|
530
|
+
// ProjectX: GET /api/Contract/available
|
|
531
|
+
// Rithmic: TICKER_PLANT API
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Check if market is open
|
|
535
|
+
*/
|
|
536
|
+
static isMarketOpen() {
|
|
537
|
+
const now = new Date();
|
|
538
|
+
const day = now.getUTCDay();
|
|
539
|
+
const hour = now.getUTCHours();
|
|
540
|
+
|
|
541
|
+
// Futures trade Sunday 6pm - Friday 5pm ET (with daily break 5pm-6pm ET)
|
|
542
|
+
// Weekend check
|
|
543
|
+
if (day === 6) return false; // Saturday
|
|
544
|
+
if (day === 0 && hour < 23) return false; // Sunday before open
|
|
545
|
+
if (day === 5 && hour >= 22) return false; // Friday after close
|
|
546
|
+
if (hour === 22) return false; // Daily maintenance
|
|
547
|
+
|
|
548
|
+
return true;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
module.exports = {
|
|
553
|
+
MarketDataFeed,
|
|
554
|
+
PROPFIRM_RTC_ENDPOINTS
|
|
555
|
+
};
|