hedgequantx 2.4.6 → 2.4.8
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 +2 -6
- package/src/lib/data.js +204 -473
- package/src/lib/m/s1.js +450 -301
- package/dist/lib/api.js +0 -1
- package/dist/lib/api.jsc +0 -0
- package/dist/lib/api2.js +0 -1
- package/dist/lib/api2.jsc +0 -0
- package/dist/lib/core.js +0 -1
- package/dist/lib/core.jsc +0 -0
- package/dist/lib/core2.js +0 -1
- package/dist/lib/core2.jsc +0 -0
- package/dist/lib/data.js +0 -1
- package/dist/lib/data.jsc +0 -0
- package/dist/lib/data2.js +0 -1
- package/dist/lib/data2.jsc +0 -0
- package/dist/lib/decoder.js +0 -1
- package/dist/lib/decoder.jsc +0 -0
- package/dist/lib/m/mod1.js +0 -1
- package/dist/lib/m/mod1.jsc +0 -0
- package/dist/lib/m/mod2.js +0 -1
- package/dist/lib/m/mod2.jsc +0 -0
- package/dist/lib/n/r1.js +0 -1
- package/dist/lib/n/r1.jsc +0 -0
- package/dist/lib/n/r2.js +0 -1
- package/dist/lib/n/r2.jsc +0 -0
- package/dist/lib/n/r3.js +0 -1
- package/dist/lib/n/r3.jsc +0 -0
- package/dist/lib/n/r4.js +0 -1
- package/dist/lib/n/r4.jsc +0 -0
- package/dist/lib/n/r5.js +0 -1
- package/dist/lib/n/r5.jsc +0 -0
- package/dist/lib/n/r6.js +0 -1
- package/dist/lib/n/r6.jsc +0 -0
- package/dist/lib/n/r7.js +0 -1
- package/dist/lib/n/r7.jsc +0 -0
- package/dist/lib/o/util1.js +0 -1
- package/dist/lib/o/util1.jsc +0 -0
- package/dist/lib/o/util2.js +0 -1
- package/dist/lib/o/util2.jsc +0 -0
- package/src/lib/api.js +0 -198
- package/src/lib/api2.js +0 -353
- package/src/lib/core.js +0 -539
- package/src/lib/core2.js +0 -341
- package/src/lib/data2.js +0 -492
- package/src/lib/decoder.js +0 -599
- package/src/lib/m/s2.js +0 -34
- package/src/lib/n/r1.js +0 -454
- package/src/lib/n/r2.js +0 -514
- package/src/lib/n/r3.js +0 -631
- package/src/lib/n/r4.js +0 -401
- package/src/lib/n/r5.js +0 -335
- package/src/lib/n/r6.js +0 -425
- package/src/lib/n/r7.js +0 -530
- package/src/lib/o/l1.js +0 -44
- package/src/lib/o/l2.js +0 -427
- package/src/lib/python-bridge.js +0 -206
package/src/lib/data.js
CHANGED
|
@@ -1,555 +1,286 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* =============================================================================
|
|
3
|
+
* MARKET DATA FEED - SignalR Real-Time Data
|
|
4
|
+
* =============================================================================
|
|
5
|
+
* Connects to ProjectX Gateway RTC for real-time market data
|
|
6
|
+
*
|
|
7
|
+
* Events emitted:
|
|
8
|
+
* - tick: Quote/trade updates (price, bid, ask, volume)
|
|
9
|
+
* - quote: Quote updates only
|
|
10
|
+
* - trade: Trade executions only
|
|
11
|
+
* - depth: DOM/Level 2 updates
|
|
12
|
+
* - connected: Connection established
|
|
13
|
+
* - disconnected: Connection lost
|
|
14
|
+
* - error: Connection error
|
|
15
|
+
*
|
|
16
|
+
* SOURCE: Based on ProjectX Gateway RTC API
|
|
5
17
|
*/
|
|
6
18
|
|
|
19
|
+
'use strict';
|
|
20
|
+
|
|
7
21
|
const EventEmitter = require('events');
|
|
8
|
-
const
|
|
22
|
+
const { HubConnectionBuilder, HttpTransportType, LogLevel } = require('@microsoft/signalr');
|
|
9
23
|
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
},
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
},
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
},
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
},
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
}
|
|
24
|
+
// Inline PROPFIRMS config for RTC URLs (standalone module)
|
|
25
|
+
const PROPFIRMS = {
|
|
26
|
+
topstep: { gatewayApi: 'api.topstepx.com' },
|
|
27
|
+
alpha_futures: { gatewayApi: 'api.alphafutures.projectx.com' },
|
|
28
|
+
tickticktrader: { gatewayApi: 'api.tickticktrader.projectx.com' },
|
|
29
|
+
bulenox: { gatewayApi: 'api.bulenox.projectx.com' },
|
|
30
|
+
tradeday: { gatewayApi: 'api.tradeday.projectx.com' },
|
|
31
|
+
blusky: { gatewayApi: 'api.blusky.projectx.com' },
|
|
32
|
+
goat_futures: { gatewayApi: 'api.goatfutures.projectx.com' },
|
|
33
|
+
futures_desk: { gatewayApi: 'api.thefuturesdesk.projectx.com' },
|
|
34
|
+
daytraders: { gatewayApi: 'api.daytraders.projectx.com' },
|
|
35
|
+
e8_futures: { gatewayApi: 'api.e8futures.projectx.com' },
|
|
36
|
+
blue_guardian: { gatewayApi: 'api.blueguardianfutures.projectx.com' },
|
|
37
|
+
futures_elite: { gatewayApi: 'api.futureselite.projectx.com' },
|
|
38
|
+
fxify: { gatewayApi: 'api.fxify.projectx.com' },
|
|
39
|
+
hola_prime: { gatewayApi: 'api.holaprime.projectx.com' },
|
|
40
|
+
top_one_futures: { gatewayApi: 'api.toponefutures.projectx.com' },
|
|
41
|
+
funding_futures: { gatewayApi: 'api.fundingfutures.projectx.com' },
|
|
42
|
+
tx3_funding: { gatewayApi: 'api.tx3funding.projectx.com' },
|
|
43
|
+
lucid_trading: { gatewayApi: 'api.lucidtrading.projectx.com' },
|
|
44
|
+
tradeify: { gatewayApi: 'api.tradeify.projectx.com' }
|
|
89
45
|
};
|
|
90
46
|
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
// NO STATIC CONTRACT DATA - All contract specs come from API
|
|
95
|
-
// Use contract.tickSize and contract.tickValue from API response
|
|
47
|
+
// =============================================================================
|
|
48
|
+
// MARKET DATA FEED CLASS
|
|
49
|
+
// =============================================================================
|
|
96
50
|
|
|
97
51
|
class MarketDataFeed extends EventEmitter {
|
|
98
|
-
constructor(
|
|
52
|
+
constructor(options = {}) {
|
|
99
53
|
super();
|
|
100
54
|
|
|
101
|
-
this.
|
|
102
|
-
this.
|
|
103
|
-
this.
|
|
104
|
-
this.
|
|
55
|
+
this.propfirmKey = (options.propfirm || 'topstep').toLowerCase().replace(/\s+/g, '_');
|
|
56
|
+
this.connection = null;
|
|
57
|
+
this.connected = false;
|
|
58
|
+
this.subscriptions = new Set();
|
|
105
59
|
this.reconnectAttempts = 0;
|
|
106
60
|
this.maxReconnectAttempts = 5;
|
|
107
|
-
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get market hub URL for propfirm
|
|
65
|
+
*/
|
|
66
|
+
_getMarketHubUrl(propfirmKey) {
|
|
67
|
+
const propfirm = PROPFIRMS[propfirmKey] || PROPFIRMS.topstep;
|
|
108
68
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
69
|
+
if (propfirm.rtcApi) {
|
|
70
|
+
return `https://${propfirm.rtcApi}/hubs/market`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (propfirm.gatewayApi) {
|
|
74
|
+
const rtcHost = propfirm.gatewayApi.replace('gateway-api', 'gateway-rtc');
|
|
75
|
+
return `https://${rtcHost}/hubs/market`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return 'https://gateway-rtc-demo.s2f.projectx.com/hubs/market';
|
|
113
79
|
}
|
|
114
80
|
|
|
115
81
|
/**
|
|
116
|
-
* Connect to market data
|
|
82
|
+
* Connect to market data hub
|
|
117
83
|
*/
|
|
118
|
-
async connect(
|
|
84
|
+
async connect(token, propfirmKey, contractId = null) {
|
|
85
|
+
if (this.connected) return;
|
|
86
|
+
|
|
87
|
+
this.propfirmKey = propfirmKey || this.propfirmKey;
|
|
88
|
+
const hubUrl = this._getMarketHubUrl(this.propfirmKey);
|
|
89
|
+
|
|
119
90
|
try {
|
|
120
|
-
|
|
121
|
-
|
|
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, {
|
|
91
|
+
this.connection = new HubConnectionBuilder()
|
|
92
|
+
.withUrl(hubUrl, {
|
|
129
93
|
skipNegotiation: true,
|
|
130
|
-
transport:
|
|
94
|
+
transport: HttpTransportType.WebSockets,
|
|
95
|
+
accessTokenFactory: () => token,
|
|
96
|
+
timeout: 30000,
|
|
131
97
|
})
|
|
132
|
-
.withAutomaticReconnect(
|
|
133
|
-
|
|
98
|
+
.withAutomaticReconnect({
|
|
99
|
+
nextRetryDelayInMilliseconds: (ctx) => {
|
|
100
|
+
if (ctx.previousRetryCount >= this.maxReconnectAttempts) return null;
|
|
101
|
+
return Math.min(1000 * Math.pow(2, ctx.previousRetryCount), 30000);
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
.configureLogging(LogLevel.Warning)
|
|
134
105
|
.build();
|
|
106
|
+
|
|
107
|
+
this._setupEventHandlers();
|
|
108
|
+
await this.connection.start();
|
|
135
109
|
|
|
136
|
-
|
|
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;
|
|
110
|
+
this.connected = true;
|
|
165
111
|
this.reconnectAttempts = 0;
|
|
166
112
|
this.emit('connected');
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
113
|
+
|
|
114
|
+
if (contractId) {
|
|
115
|
+
await this.subscribe(null, contractId);
|
|
116
|
+
}
|
|
170
117
|
} catch (error) {
|
|
171
|
-
// // console.log(`[MARKET] Connection error: ${error.message}`);
|
|
172
118
|
this.emit('error', error);
|
|
173
119
|
throw error;
|
|
174
120
|
}
|
|
175
121
|
}
|
|
176
122
|
|
|
177
123
|
/**
|
|
178
|
-
* Setup SignalR event handlers
|
|
124
|
+
* Setup SignalR event handlers
|
|
179
125
|
*/
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
126
|
+
_setupEventHandlers() {
|
|
127
|
+
if (!this.connection) return;
|
|
128
|
+
|
|
129
|
+
// Quote updates
|
|
130
|
+
this.connection.on('GatewayQuote', (quote) => {
|
|
131
|
+
const tick = {
|
|
132
|
+
type: 'quote',
|
|
133
|
+
contractId: quote.symbol || quote.symbolId,
|
|
134
|
+
symbol: quote.symbolName || quote.symbol,
|
|
135
|
+
price: quote.lastPrice,
|
|
136
|
+
bid: quote.bestBid,
|
|
137
|
+
ask: quote.bestAsk,
|
|
138
|
+
change: quote.change,
|
|
139
|
+
changePercent: quote.changePercent,
|
|
140
|
+
open: quote.open,
|
|
141
|
+
high: quote.high,
|
|
142
|
+
low: quote.low,
|
|
143
|
+
volume: quote.volume,
|
|
144
|
+
timestamp: quote.timestamp ? new Date(quote.timestamp).getTime() : Date.now()
|
|
145
|
+
};
|
|
146
|
+
this.emit('tick', tick);
|
|
147
|
+
this.emit('quote', tick);
|
|
194
148
|
});
|
|
195
|
-
|
|
196
|
-
//
|
|
197
|
-
this.
|
|
198
|
-
|
|
149
|
+
|
|
150
|
+
// Trade executions
|
|
151
|
+
this.connection.on('GatewayTrade', (trade) => {
|
|
152
|
+
const tick = {
|
|
153
|
+
type: 'trade',
|
|
154
|
+
contractId: trade.symbolId,
|
|
155
|
+
price: trade.price,
|
|
156
|
+
volume: trade.volume,
|
|
157
|
+
side: trade.type === 0 ? 'buy' : 'sell',
|
|
158
|
+
lastTradeSide: trade.type === 0 ? 'buy' : 'sell',
|
|
159
|
+
timestamp: trade.timestamp ? new Date(trade.timestamp).getTime() : Date.now()
|
|
160
|
+
};
|
|
161
|
+
this.emit('tick', tick);
|
|
162
|
+
this.emit('trade', tick);
|
|
199
163
|
});
|
|
200
|
-
|
|
201
|
-
//
|
|
202
|
-
this.
|
|
203
|
-
|
|
164
|
+
|
|
165
|
+
// DOM updates
|
|
166
|
+
this.connection.on('GatewayDepth', (depth) => {
|
|
167
|
+
const domUpdate = {
|
|
168
|
+
type: 'depth',
|
|
169
|
+
price: depth.price,
|
|
170
|
+
volume: depth.volume,
|
|
171
|
+
currentVolume: depth.currentVolume,
|
|
172
|
+
side: depth.type === 0 ? 'bid' : 'ask',
|
|
173
|
+
timestamp: depth.timestamp ? new Date(depth.timestamp).getTime() : Date.now()
|
|
174
|
+
};
|
|
175
|
+
this.emit('depth', domUpdate);
|
|
176
|
+
this.emit('dom', domUpdate);
|
|
204
177
|
});
|
|
205
|
-
|
|
206
|
-
// Connection state
|
|
207
|
-
this.
|
|
208
|
-
|
|
209
|
-
this.
|
|
210
|
-
this.emit('reconnecting', { error: error?.message });
|
|
178
|
+
|
|
179
|
+
// Connection state
|
|
180
|
+
this.connection.onreconnecting((error) => {
|
|
181
|
+
this.connected = false;
|
|
182
|
+
this.emit('reconnecting', error);
|
|
211
183
|
});
|
|
212
|
-
|
|
213
|
-
this.
|
|
214
|
-
|
|
215
|
-
this.
|
|
216
|
-
this.emit('reconnected',
|
|
217
|
-
// Resubscribe to all symbols
|
|
184
|
+
|
|
185
|
+
this.connection.onreconnected((connectionId) => {
|
|
186
|
+
this.connected = true;
|
|
187
|
+
this.reconnectAttempts = 0;
|
|
188
|
+
this.emit('reconnected', connectionId);
|
|
218
189
|
this._resubscribeAll();
|
|
219
190
|
});
|
|
220
|
-
|
|
221
|
-
this.
|
|
222
|
-
|
|
223
|
-
this.
|
|
224
|
-
this.emit('disconnected', { error: error?.message });
|
|
191
|
+
|
|
192
|
+
this.connection.onclose((error) => {
|
|
193
|
+
this.connected = false;
|
|
194
|
+
this.emit('disconnected', error);
|
|
225
195
|
});
|
|
226
196
|
}
|
|
227
197
|
|
|
228
198
|
/**
|
|
229
|
-
* Subscribe to
|
|
230
|
-
* Implements retry logic for connection race conditions
|
|
199
|
+
* Subscribe to contract market data
|
|
231
200
|
*/
|
|
232
|
-
async subscribe(symbol, contractId
|
|
233
|
-
|
|
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
|
-
}
|
|
201
|
+
async subscribe(symbol, contractId) {
|
|
202
|
+
if (!this.connection || !this.connected) {
|
|
203
|
+
throw new Error('Not connected');
|
|
255
204
|
}
|
|
205
|
+
|
|
206
|
+
const id = contractId || symbol;
|
|
256
207
|
|
|
257
208
|
try {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
await this.
|
|
261
|
-
|
|
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
|
-
|
|
209
|
+
await this.connection.invoke('SubscribeContractQuotes', id);
|
|
210
|
+
await this.connection.invoke('SubscribeContractTrades', id);
|
|
211
|
+
await this.connection.invoke('SubscribeContractMarketDepth', id);
|
|
212
|
+
this.subscriptions.add(id);
|
|
213
|
+
this.emit('subscribed', { symbol, contractId: id });
|
|
274
214
|
} catch (error) {
|
|
275
|
-
|
|
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;
|
|
215
|
+
this.emit('error', new Error(`Subscribe failed: ${error.message}`));
|
|
216
|
+
throw error;
|
|
285
217
|
}
|
|
286
218
|
}
|
|
287
219
|
|
|
288
220
|
/**
|
|
289
|
-
* Unsubscribe from
|
|
221
|
+
* Unsubscribe from contract
|
|
290
222
|
*/
|
|
291
|
-
async unsubscribe(
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
if (!this.subscriptions.has(subscriptionKey)) {
|
|
295
|
-
return true;
|
|
296
|
-
}
|
|
297
|
-
|
|
223
|
+
async unsubscribe(contractId) {
|
|
224
|
+
if (!this.connection || !this.connected) return;
|
|
225
|
+
|
|
298
226
|
try {
|
|
299
|
-
await this.
|
|
300
|
-
await this.
|
|
301
|
-
await this.
|
|
302
|
-
|
|
303
|
-
this.
|
|
304
|
-
this.dataBuffers.delete(subscriptionKey);
|
|
305
|
-
|
|
306
|
-
this.emit('unsubscribed', { symbol, contractId });
|
|
307
|
-
return true;
|
|
308
|
-
|
|
227
|
+
await this.connection.invoke('UnsubscribeContractQuotes', contractId);
|
|
228
|
+
await this.connection.invoke('UnsubscribeContractTrades', contractId);
|
|
229
|
+
await this.connection.invoke('UnsubscribeContractMarketDepth', contractId);
|
|
230
|
+
this.subscriptions.delete(contractId);
|
|
231
|
+
this.emit('unsubscribed', { contractId });
|
|
309
232
|
} catch (error) {
|
|
310
|
-
|
|
311
|
-
return false;
|
|
233
|
+
// Silently handle
|
|
312
234
|
}
|
|
313
235
|
}
|
|
314
236
|
|
|
315
237
|
/**
|
|
316
|
-
* Resubscribe
|
|
238
|
+
* Resubscribe after reconnect
|
|
317
239
|
*/
|
|
318
240
|
async _resubscribeAll() {
|
|
319
|
-
for (const
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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();
|
|
241
|
+
for (const contractId of this.subscriptions) {
|
|
242
|
+
try {
|
|
243
|
+
await this.connection.invoke('SubscribeContractQuotes', contractId);
|
|
244
|
+
await this.connection.invoke('SubscribeContractTrades', contractId);
|
|
245
|
+
await this.connection.invoke('SubscribeContractMarketDepth', contractId);
|
|
246
|
+
} catch (error) {
|
|
247
|
+
// Continue
|
|
463
248
|
}
|
|
464
|
-
this.dataBuffers.set(subscriptionKey, buffer);
|
|
465
|
-
|
|
466
|
-
this.emit('trade', tradeData);
|
|
467
|
-
this.emit('tick', this.lastTick.get(subscriptionKey));
|
|
468
249
|
}
|
|
469
250
|
}
|
|
470
251
|
|
|
471
252
|
/**
|
|
472
|
-
*
|
|
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
|
|
253
|
+
* Disconnect
|
|
496
254
|
*/
|
|
497
255
|
async disconnect() {
|
|
498
|
-
this.
|
|
499
|
-
|
|
500
|
-
if (this.marketConnection) {
|
|
256
|
+
if (this.connection) {
|
|
501
257
|
try {
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
258
|
+
for (const contractId of this.subscriptions) {
|
|
259
|
+
await this.unsubscribe(contractId);
|
|
260
|
+
}
|
|
261
|
+
await this.connection.stop();
|
|
262
|
+
} catch (error) {
|
|
263
|
+
// Ignore
|
|
505
264
|
}
|
|
506
|
-
this.
|
|
265
|
+
this.connection = null;
|
|
266
|
+
this.connected = false;
|
|
267
|
+
this.subscriptions.clear();
|
|
507
268
|
}
|
|
508
|
-
|
|
509
|
-
this.isConnected = false;
|
|
510
|
-
this.emit('disconnected', { code: 1000, reason: 'User requested disconnect' });
|
|
511
269
|
}
|
|
512
270
|
|
|
513
271
|
/**
|
|
514
|
-
*
|
|
272
|
+
* Check connection status
|
|
515
273
|
*/
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
return this.lastTick.get(subscriptionKey);
|
|
274
|
+
isConnected() {
|
|
275
|
+
return this.connected && this.connection?.state === 'Connected';
|
|
519
276
|
}
|
|
520
277
|
|
|
521
278
|
/**
|
|
522
|
-
* Get
|
|
279
|
+
* Get active subscriptions
|
|
523
280
|
*/
|
|
524
|
-
|
|
525
|
-
|
|
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;
|
|
281
|
+
getSubscriptions() {
|
|
282
|
+
return Array.from(this.subscriptions);
|
|
549
283
|
}
|
|
550
284
|
}
|
|
551
285
|
|
|
552
|
-
module.exports = {
|
|
553
|
-
MarketDataFeed,
|
|
554
|
-
PROPFIRM_RTC_ENDPOINTS
|
|
555
|
-
};
|
|
286
|
+
module.exports = { MarketDataFeed };
|