hedgequantx 2.7.14 → 2.7.16
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/lib/data.js +245 -471
- package/src/lib/m/s1-models.js +173 -0
- package/src/lib/m/s1.js +354 -735
- package/src/services/rithmic/accounts.js +22 -7
- 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,329 @@
|
|
|
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
|
-
const
|
|
13
|
-
topstep: {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
},
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
},
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
},
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
},
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
+
// RTC URL pattern: api.xxx.com -> rtc.xxx.com
|
|
26
|
+
const PROPFIRMS = {
|
|
27
|
+
topstep: { gatewayApi: 'api.topstepx.com' },
|
|
28
|
+
alpha_futures: { gatewayApi: 'api.alphafutures.projectx.com' },
|
|
29
|
+
tickticktrader: { gatewayApi: 'api.tickticktrader.projectx.com' },
|
|
30
|
+
bulenox: { gatewayApi: 'api.bulenox.projectx.com' },
|
|
31
|
+
tradeday: { gatewayApi: 'api.tradeday.projectx.com' },
|
|
32
|
+
blusky: { gatewayApi: 'api.blusky.projectx.com' },
|
|
33
|
+
goat_futures: { gatewayApi: 'api.goatfutures.projectx.com' },
|
|
34
|
+
futures_desk: { gatewayApi: 'api.thefuturesdesk.projectx.com' },
|
|
35
|
+
daytraders: { gatewayApi: 'api.daytraders.projectx.com' },
|
|
36
|
+
e8_futures: { gatewayApi: 'api.e8futures.projectx.com' },
|
|
37
|
+
blue_guardian: { gatewayApi: 'api.blueguardianfutures.projectx.com' },
|
|
38
|
+
futures_elite: { gatewayApi: 'api.futureselite.projectx.com' },
|
|
39
|
+
fxify: { gatewayApi: 'api.fxify.projectx.com' },
|
|
40
|
+
hola_prime: { gatewayApi: 'api.holaprime.projectx.com' },
|
|
41
|
+
top_one_futures: { gatewayApi: 'api.toponefutures.projectx.com' },
|
|
42
|
+
funding_futures: { gatewayApi: 'api.fundingfutures.projectx.com' },
|
|
43
|
+
tx3_funding: { gatewayApi: 'api.tx3funding.projectx.com' },
|
|
44
|
+
lucid_trading: { gatewayApi: 'api.lucidtrading.projectx.com' },
|
|
45
|
+
tradeify: { gatewayApi: 'api.tradeify.projectx.com' }
|
|
89
46
|
};
|
|
90
47
|
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
// NO STATIC CONTRACT DATA - All contract specs come from API
|
|
95
|
-
// Use contract.tickSize and contract.tickValue from API response
|
|
48
|
+
// =============================================================================
|
|
49
|
+
// MARKET DATA FEED CLASS
|
|
50
|
+
// =============================================================================
|
|
96
51
|
|
|
97
52
|
class MarketDataFeed extends EventEmitter {
|
|
98
|
-
constructor(
|
|
53
|
+
constructor(options = {}) {
|
|
99
54
|
super();
|
|
100
55
|
|
|
101
|
-
this.
|
|
102
|
-
this.
|
|
103
|
-
this.
|
|
104
|
-
this.
|
|
56
|
+
this.propfirmKey = (options.propfirm || 'topstep').toLowerCase().replace(/\s+/g, '_');
|
|
57
|
+
this.connection = null;
|
|
58
|
+
this.connected = false;
|
|
59
|
+
this.subscriptions = new Set();
|
|
105
60
|
this.reconnectAttempts = 0;
|
|
106
61
|
this.maxReconnectAttempts = 5;
|
|
107
|
-
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get market hub URL for propfirm
|
|
66
|
+
* Pattern: api.xxx.com -> rtc.xxx.com
|
|
67
|
+
*/
|
|
68
|
+
_getMarketHubUrl(propfirmKey) {
|
|
69
|
+
const propfirm = PROPFIRMS[propfirmKey] || PROPFIRMS.topstep;
|
|
108
70
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
71
|
+
if (propfirm.gatewayApi) {
|
|
72
|
+
// Convert api.xxx.com to rtc.xxx.com
|
|
73
|
+
const rtcHost = propfirm.gatewayApi.replace(/^api\./, 'rtc.');
|
|
74
|
+
return `https://${rtcHost}/hubs/market`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Fallback for topstep
|
|
78
|
+
return 'https://rtc.topstepx.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
|
+
this.token = token;
|
|
89
|
+
const hubUrl = this._getMarketHubUrl(this.propfirmKey);
|
|
90
|
+
|
|
91
|
+
// CRITICAL: Token must be in URL query string (same as HQX-TG and Python SDK)
|
|
92
|
+
const urlWithToken = `${hubUrl}?access_token=${encodeURIComponent(this.token)}`;
|
|
93
|
+
|
|
119
94
|
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()
|
|
95
|
+
// CRITICAL: skipNegotiation=true + WebSockets transport is REQUIRED for ProjectX/TopstepX
|
|
96
|
+
// This is how the Python SDK and HQX-TG connect successfully
|
|
97
|
+
this.connection = new HubConnectionBuilder()
|
|
128
98
|
.withUrl(urlWithToken, {
|
|
129
99
|
skipNegotiation: true,
|
|
130
|
-
transport:
|
|
100
|
+
transport: HttpTransportType.WebSockets
|
|
101
|
+
})
|
|
102
|
+
.withAutomaticReconnect({
|
|
103
|
+
nextRetryDelayInMilliseconds: (ctx) => {
|
|
104
|
+
if (ctx.previousRetryCount >= this.maxReconnectAttempts) return null;
|
|
105
|
+
return Math.min(1000 * Math.pow(2, ctx.previousRetryCount), 30000);
|
|
106
|
+
}
|
|
131
107
|
})
|
|
132
|
-
.
|
|
133
|
-
.configureLogging(signalR.LogLevel.None)
|
|
108
|
+
.configureLogging(LogLevel.Warning)
|
|
134
109
|
.build();
|
|
110
|
+
|
|
111
|
+
// Set server timeout and keepalive
|
|
112
|
+
this.connection.serverTimeoutInMilliseconds = 60000; // 60s
|
|
113
|
+
this.connection.keepAliveIntervalInMilliseconds = 15000; // 15s
|
|
114
|
+
|
|
115
|
+
this._setupEventHandlers();
|
|
116
|
+
await this.connection.start();
|
|
135
117
|
|
|
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;
|
|
118
|
+
this.connected = true;
|
|
165
119
|
this.reconnectAttempts = 0;
|
|
166
120
|
this.emit('connected');
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
121
|
+
|
|
122
|
+
if (contractId) {
|
|
123
|
+
await this.subscribe(null, contractId);
|
|
124
|
+
}
|
|
170
125
|
} catch (error) {
|
|
171
|
-
|
|
126
|
+
this.connected = false;
|
|
172
127
|
this.emit('error', error);
|
|
173
128
|
throw error;
|
|
174
129
|
}
|
|
175
130
|
}
|
|
176
131
|
|
|
177
132
|
/**
|
|
178
|
-
* Setup SignalR event handlers
|
|
133
|
+
* Setup SignalR event handlers
|
|
179
134
|
*/
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
135
|
+
_setupEventHandlers() {
|
|
136
|
+
if (!this.connection) return;
|
|
137
|
+
|
|
138
|
+
// Quote updates - GatewayQuote receives (contractId, data)
|
|
139
|
+
this.connection.on('GatewayQuote', (contractId, data) => {
|
|
140
|
+
// Handle both (contractId, data) and single object formats
|
|
141
|
+
const quote = data || contractId;
|
|
142
|
+
const cid = data ? contractId : (quote.symbol || quote.symbolId);
|
|
143
|
+
|
|
144
|
+
const tick = {
|
|
145
|
+
type: 'quote',
|
|
146
|
+
contractId: cid,
|
|
147
|
+
symbol: quote.symbolName || quote.symbol || cid,
|
|
148
|
+
price: quote.lastPrice,
|
|
149
|
+
bid: quote.bestBid,
|
|
150
|
+
ask: quote.bestAsk,
|
|
151
|
+
change: quote.change,
|
|
152
|
+
changePercent: quote.changePercent,
|
|
153
|
+
open: quote.open,
|
|
154
|
+
high: quote.high,
|
|
155
|
+
low: quote.low,
|
|
156
|
+
volume: quote.volume,
|
|
157
|
+
timestamp: quote.timestamp ? new Date(quote.timestamp).getTime() : Date.now()
|
|
158
|
+
};
|
|
159
|
+
this.emit('tick', tick);
|
|
160
|
+
this.emit('quote', tick);
|
|
194
161
|
});
|
|
195
|
-
|
|
196
|
-
//
|
|
197
|
-
this.
|
|
198
|
-
//
|
|
162
|
+
|
|
163
|
+
// Trade executions - GatewayTrade receives (contractId, data[])
|
|
164
|
+
this.connection.on('GatewayTrade', (contractId, trades) => {
|
|
165
|
+
// Handle both formats
|
|
166
|
+
const tradeList = Array.isArray(trades) ? trades : (Array.isArray(contractId) ? contractId : [trades || contractId]);
|
|
167
|
+
const cid = typeof contractId === 'string' ? contractId : null;
|
|
168
|
+
|
|
169
|
+
for (const trade of tradeList) {
|
|
170
|
+
if (!trade) continue;
|
|
171
|
+
const tick = {
|
|
172
|
+
type: 'trade',
|
|
173
|
+
contractId: cid || trade.symbolId,
|
|
174
|
+
price: trade.price,
|
|
175
|
+
volume: trade.volume,
|
|
176
|
+
side: trade.type === 0 ? 'buy' : 'sell',
|
|
177
|
+
lastTradeSide: trade.type === 0 ? 'buy' : 'sell',
|
|
178
|
+
timestamp: trade.timestamp ? new Date(trade.timestamp).getTime() : Date.now()
|
|
179
|
+
};
|
|
180
|
+
this.emit('tick', tick);
|
|
181
|
+
this.emit('trade', tick);
|
|
182
|
+
}
|
|
199
183
|
});
|
|
200
|
-
|
|
201
|
-
//
|
|
202
|
-
this.
|
|
203
|
-
//
|
|
184
|
+
|
|
185
|
+
// DOM updates - GatewayDepth receives (contractId, data[])
|
|
186
|
+
this.connection.on('GatewayDepth', (contractId, depthData) => {
|
|
187
|
+
// Handle both formats
|
|
188
|
+
const depthList = Array.isArray(depthData) ? depthData : (Array.isArray(contractId) ? contractId : [depthData || contractId]);
|
|
189
|
+
const cid = typeof contractId === 'string' ? contractId : null;
|
|
190
|
+
|
|
191
|
+
for (const depth of depthList) {
|
|
192
|
+
if (!depth) continue;
|
|
193
|
+
const domUpdate = {
|
|
194
|
+
type: 'depth',
|
|
195
|
+
contractId: cid,
|
|
196
|
+
price: depth.price,
|
|
197
|
+
volume: depth.volume || depth.totalVolume,
|
|
198
|
+
currentVolume: depth.currentVolume,
|
|
199
|
+
side: depth.type === 0 ? 'bid' : 'ask',
|
|
200
|
+
timestamp: depth.timestamp ? new Date(depth.timestamp).getTime() : Date.now()
|
|
201
|
+
};
|
|
202
|
+
this.emit('depth', domUpdate);
|
|
203
|
+
this.emit('dom', domUpdate);
|
|
204
|
+
}
|
|
204
205
|
});
|
|
205
|
-
|
|
206
|
-
// Connection state
|
|
207
|
-
this.
|
|
208
|
-
|
|
209
|
-
this.
|
|
210
|
-
this.emit('reconnecting', { error: error?.message });
|
|
206
|
+
|
|
207
|
+
// Connection state
|
|
208
|
+
this.connection.onreconnecting((error) => {
|
|
209
|
+
this.connected = false;
|
|
210
|
+
this.emit('reconnecting', error);
|
|
211
211
|
});
|
|
212
|
-
|
|
213
|
-
this.
|
|
214
|
-
|
|
215
|
-
this.
|
|
216
|
-
this.emit('reconnected',
|
|
217
|
-
// Resubscribe to all symbols
|
|
212
|
+
|
|
213
|
+
this.connection.onreconnected((connectionId) => {
|
|
214
|
+
this.connected = true;
|
|
215
|
+
this.reconnectAttempts = 0;
|
|
216
|
+
this.emit('reconnected', connectionId);
|
|
218
217
|
this._resubscribeAll();
|
|
219
218
|
});
|
|
220
|
-
|
|
221
|
-
this.
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
219
|
+
|
|
220
|
+
this.connection.onclose((error) => {
|
|
221
|
+
this.connected = false;
|
|
222
|
+
// Emit detailed error for debugging
|
|
223
|
+
let errMsg = 'unknown';
|
|
224
|
+
if (error) {
|
|
225
|
+
errMsg = error.message || error.toString();
|
|
226
|
+
if (error.stack) errMsg += ' | ' + error.stack.split('\n')[0];
|
|
227
|
+
}
|
|
228
|
+
this.emit('disconnected', new Error(errMsg));
|
|
225
229
|
});
|
|
226
230
|
}
|
|
227
231
|
|
|
228
232
|
/**
|
|
229
|
-
* Subscribe to
|
|
230
|
-
* Implements retry logic for connection race conditions
|
|
233
|
+
* Subscribe to contract market data
|
|
231
234
|
*/
|
|
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
|
-
}
|
|
235
|
+
async subscribe(symbol, contractId) {
|
|
236
|
+
if (!this.connection || !this.connected) {
|
|
237
|
+
throw new Error('Not connected');
|
|
255
238
|
}
|
|
239
|
+
|
|
240
|
+
const id = contractId || symbol;
|
|
256
241
|
|
|
257
242
|
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
|
-
|
|
243
|
+
await this.connection.invoke('SubscribeContractQuotes', id);
|
|
244
|
+
await this.connection.invoke('SubscribeContractTrades', id);
|
|
245
|
+
await this.connection.invoke('SubscribeContractMarketDepth', id);
|
|
246
|
+
this.subscriptions.add(id);
|
|
247
|
+
this.emit('subscribed', { symbol, contractId: id });
|
|
274
248
|
} 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;
|
|
249
|
+
this.emit('error', new Error(`Subscribe failed: ${error.message}`));
|
|
250
|
+
throw error;
|
|
285
251
|
}
|
|
286
252
|
}
|
|
287
253
|
|
|
288
254
|
/**
|
|
289
|
-
* Unsubscribe from
|
|
255
|
+
* Unsubscribe from contract
|
|
290
256
|
*/
|
|
291
|
-
async unsubscribe(
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
if (!this.subscriptions.has(subscriptionKey)) {
|
|
295
|
-
return true;
|
|
296
|
-
}
|
|
297
|
-
|
|
257
|
+
async unsubscribe(contractId) {
|
|
258
|
+
if (!this.connection || !this.connected) return;
|
|
259
|
+
|
|
298
260
|
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
|
-
|
|
261
|
+
await this.connection.invoke('UnsubscribeContractQuotes', contractId);
|
|
262
|
+
await this.connection.invoke('UnsubscribeContractTrades', contractId);
|
|
263
|
+
await this.connection.invoke('UnsubscribeContractMarketDepth', contractId);
|
|
264
|
+
this.subscriptions.delete(contractId);
|
|
265
|
+
this.emit('unsubscribed', { contractId });
|
|
309
266
|
} catch (error) {
|
|
310
|
-
|
|
311
|
-
return false;
|
|
267
|
+
// Silently handle
|
|
312
268
|
}
|
|
313
269
|
}
|
|
314
270
|
|
|
315
271
|
/**
|
|
316
|
-
* Resubscribe
|
|
272
|
+
* Resubscribe after reconnect
|
|
317
273
|
*/
|
|
318
274
|
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();
|
|
275
|
+
for (const contractId of this.subscriptions) {
|
|
276
|
+
try {
|
|
277
|
+
await this.connection.invoke('SubscribeContractQuotes', contractId);
|
|
278
|
+
await this.connection.invoke('SubscribeContractTrades', contractId);
|
|
279
|
+
await this.connection.invoke('SubscribeContractMarketDepth', contractId);
|
|
280
|
+
} catch (error) {
|
|
281
|
+
// Continue
|
|
463
282
|
}
|
|
464
|
-
this.dataBuffers.set(subscriptionKey, buffer);
|
|
465
|
-
|
|
466
|
-
this.emit('trade', tradeData);
|
|
467
|
-
this.emit('tick', this.lastTick.get(subscriptionKey));
|
|
468
283
|
}
|
|
469
284
|
}
|
|
470
285
|
|
|
471
286
|
/**
|
|
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
|
|
287
|
+
* Disconnect with timeout to prevent hanging
|
|
496
288
|
*/
|
|
497
289
|
async disconnect() {
|
|
498
|
-
this.
|
|
499
|
-
|
|
500
|
-
|
|
290
|
+
if (this.connection) {
|
|
291
|
+
const timeoutPromise = new Promise(r => setTimeout(r, 3000));
|
|
292
|
+
|
|
501
293
|
try {
|
|
502
|
-
|
|
503
|
-
|
|
294
|
+
// Race between disconnect and timeout
|
|
295
|
+
await Promise.race([
|
|
296
|
+
(async () => {
|
|
297
|
+
for (const contractId of this.subscriptions) {
|
|
298
|
+
try { await this.unsubscribe(contractId); } catch {}
|
|
299
|
+
}
|
|
300
|
+
await this.connection.stop();
|
|
301
|
+
})(),
|
|
302
|
+
timeoutPromise
|
|
303
|
+
]);
|
|
304
|
+
} catch (error) {
|
|
504
305
|
// Ignore disconnect errors
|
|
505
306
|
}
|
|
506
|
-
|
|
307
|
+
|
|
308
|
+
this.connection = null;
|
|
309
|
+
this.connected = false;
|
|
310
|
+
this.subscriptions.clear();
|
|
507
311
|
}
|
|
508
|
-
|
|
509
|
-
this.isConnected = false;
|
|
510
|
-
this.emit('disconnected', { code: 1000, reason: 'User requested disconnect' });
|
|
511
312
|
}
|
|
512
313
|
|
|
513
314
|
/**
|
|
514
|
-
*
|
|
315
|
+
* Check connection status
|
|
515
316
|
*/
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
return this.lastTick.get(subscriptionKey);
|
|
317
|
+
isConnected() {
|
|
318
|
+
return this.connected && this.connection?.state === 'Connected';
|
|
519
319
|
}
|
|
520
320
|
|
|
521
321
|
/**
|
|
522
|
-
* Get
|
|
322
|
+
* Get active subscriptions
|
|
523
323
|
*/
|
|
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;
|
|
324
|
+
getSubscriptions() {
|
|
325
|
+
return Array.from(this.subscriptions);
|
|
549
326
|
}
|
|
550
327
|
}
|
|
551
328
|
|
|
552
|
-
module.exports = {
|
|
553
|
-
MarketDataFeed,
|
|
554
|
-
PROPFIRM_RTC_ENDPOINTS
|
|
555
|
-
};
|
|
329
|
+
module.exports = { MarketDataFeed };
|