hedgequantx 2.9.48 → 2.9.50
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/app.js +1 -1
- package/src/config/propfirms.js +1 -8
- package/src/lib/data.js +185 -254
- package/src/pages/algo/algo-executor.js +88 -13
- package/src/pages/algo/copy-executor.js +9 -7
- package/src/pages/algo/copy-trading.js +1 -1
- package/src/pages/algo/custom-strategy.js +1 -1
- package/src/pages/algo/one-account.js +1 -1
package/package.json
CHANGED
package/src/app.js
CHANGED
|
@@ -77,7 +77,7 @@ const refreshStats = async () => {
|
|
|
77
77
|
|
|
78
78
|
try {
|
|
79
79
|
const allAccounts = await connections.getAllAccounts();
|
|
80
|
-
// Filter active accounts: status ===
|
|
80
|
+
// Filter active accounts: status === 'active' (Rithmic) OR status === 0 OR no status
|
|
81
81
|
const activeAccounts = allAccounts.filter(acc =>
|
|
82
82
|
acc.status === 0 || acc.status === 'active' || acc.status === undefined || acc.status === null
|
|
83
83
|
);
|
package/src/config/propfirms.js
CHANGED
|
@@ -15,14 +15,7 @@ const PROPFIRMS = {
|
|
|
15
15
|
rithmicSystem: 'Apex',
|
|
16
16
|
wsEndpoint: 'wss://ritpa11120.11.rithmic.com:443',
|
|
17
17
|
},
|
|
18
|
-
|
|
19
|
-
id: 'topsteptrader',
|
|
20
|
-
name: 'TopstepTrader',
|
|
21
|
-
displayName: 'TopstepTrader',
|
|
22
|
-
platform: 'Rithmic',
|
|
23
|
-
rithmicSystem: 'TopstepTrader',
|
|
24
|
-
wsEndpoint: 'wss://ritpa11120.11.rithmic.com:443'
|
|
25
|
-
},
|
|
18
|
+
|
|
26
19
|
mes_capital: {
|
|
27
20
|
id: 'mes-capital',
|
|
28
21
|
name: 'MES Capital',
|
package/src/lib/data.js
CHANGED
|
@@ -1,328 +1,259 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* =============================================================================
|
|
3
|
-
* MARKET DATA FEED -
|
|
3
|
+
* MARKET DATA FEED - Rithmic TICKER_PLANT Real-Time Data
|
|
4
4
|
* =============================================================================
|
|
5
|
-
* Connects to
|
|
5
|
+
* Connects to Rithmic TICKER_PLANT for real-time market data
|
|
6
6
|
*
|
|
7
7
|
* Events emitted:
|
|
8
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
9
|
* - connected: Connection established
|
|
13
10
|
* - disconnected: Connection lost
|
|
14
11
|
* - error: Connection error
|
|
15
|
-
*
|
|
16
|
-
* SOURCE: Based on ProjectX Gateway RTC API
|
|
17
12
|
*/
|
|
18
13
|
|
|
19
14
|
'use strict';
|
|
20
15
|
|
|
21
16
|
const EventEmitter = require('events');
|
|
22
|
-
const {
|
|
23
|
-
|
|
24
|
-
|
|
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' }
|
|
46
|
-
};
|
|
17
|
+
const { RithmicConnection } = require('../services/rithmic/connection');
|
|
18
|
+
const { proto } = require('../services/rithmic/protobuf');
|
|
19
|
+
const { REQ, RES, STREAM, RITHMIC_ENDPOINTS, RITHMIC_SYSTEMS } = require('../services/rithmic/constants');
|
|
47
20
|
|
|
48
21
|
// =============================================================================
|
|
49
|
-
// MARKET DATA FEED CLASS
|
|
22
|
+
// MARKET DATA FEED CLASS (Rithmic TICKER_PLANT)
|
|
50
23
|
// =============================================================================
|
|
51
24
|
|
|
52
25
|
class MarketDataFeed extends EventEmitter {
|
|
53
26
|
constructor(options = {}) {
|
|
54
27
|
super();
|
|
55
28
|
|
|
56
|
-
this.propfirmKey = (options.propfirm || 'topstep').toLowerCase().replace(/\s+/g, '_');
|
|
57
29
|
this.connection = null;
|
|
58
30
|
this.connected = false;
|
|
59
31
|
this.subscriptions = new Set();
|
|
60
|
-
this.
|
|
61
|
-
this.
|
|
32
|
+
this.credentials = null;
|
|
33
|
+
this.config = null;
|
|
62
34
|
}
|
|
63
35
|
|
|
64
36
|
/**
|
|
65
|
-
*
|
|
66
|
-
*
|
|
37
|
+
* Connect to Rithmic TICKER_PLANT
|
|
38
|
+
* @param {Object} rithmicCredentials - Credentials from RithmicService.getRithmicCredentials()
|
|
67
39
|
*/
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (
|
|
72
|
-
|
|
73
|
-
const rtcHost = propfirm.gatewayApi.replace(/^api\./, 'rtc.');
|
|
74
|
-
return `https://${rtcHost}/hubs/market`;
|
|
40
|
+
async connect(rithmicCredentials) {
|
|
41
|
+
if (this.connected) return;
|
|
42
|
+
|
|
43
|
+
if (!rithmicCredentials || !rithmicCredentials.userId || !rithmicCredentials.password) {
|
|
44
|
+
throw new Error('Rithmic credentials required (userId, password, systemName, gateway)');
|
|
75
45
|
}
|
|
76
|
-
|
|
77
|
-
// Fallback for topstep
|
|
78
|
-
return 'https://rtc.topstepx.com/hubs/market';
|
|
79
|
-
}
|
|
80
46
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
47
|
+
this.credentials = rithmicCredentials;
|
|
48
|
+
this.connection = new RithmicConnection();
|
|
49
|
+
|
|
50
|
+
this.config = {
|
|
51
|
+
uri: rithmicCredentials.gateway || RITHMIC_ENDPOINTS.CHICAGO,
|
|
52
|
+
systemName: rithmicCredentials.systemName || RITHMIC_SYSTEMS.PAPER,
|
|
53
|
+
userId: rithmicCredentials.userId,
|
|
54
|
+
password: rithmicCredentials.password,
|
|
55
|
+
appName: 'HQX-CLI',
|
|
56
|
+
appVersion: '2.0.0',
|
|
57
|
+
};
|
|
86
58
|
|
|
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
|
-
|
|
94
59
|
try {
|
|
95
|
-
|
|
96
|
-
// This is how the Python SDK and HQX-TG connect successfully
|
|
97
|
-
this.connection = new HubConnectionBuilder()
|
|
98
|
-
.withUrl(urlWithToken, {
|
|
99
|
-
skipNegotiation: true,
|
|
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
|
-
}
|
|
107
|
-
})
|
|
108
|
-
.configureLogging(LogLevel.Warning)
|
|
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();
|
|
117
|
-
|
|
118
|
-
this.connected = true;
|
|
119
|
-
this.reconnectAttempts = 0;
|
|
120
|
-
this.emit('connected');
|
|
60
|
+
await this.connection.connect(this.config);
|
|
121
61
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
} catch (error) {
|
|
126
|
-
this.connected = false;
|
|
127
|
-
this.emit('error', error);
|
|
128
|
-
throw error;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
62
|
+
// Setup message handler for market data
|
|
63
|
+
this.connection.on('message', (msg) => this._handleMessage(msg));
|
|
131
64
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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);
|
|
161
|
-
});
|
|
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
|
-
}
|
|
183
|
-
});
|
|
65
|
+
// Login to TICKER_PLANT
|
|
66
|
+
return new Promise((resolve, reject) => {
|
|
67
|
+
const timeout = setTimeout(() => {
|
|
68
|
+
reject(new Error('TICKER_PLANT login timeout'));
|
|
69
|
+
}, 15000);
|
|
184
70
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
this.emit('dom', domUpdate);
|
|
204
|
-
}
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
// Connection state
|
|
208
|
-
this.connection.onreconnecting((error) => {
|
|
209
|
-
this.connected = false;
|
|
210
|
-
this.emit('reconnecting', error);
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
this.connection.onreconnected((connectionId) => {
|
|
214
|
-
this.connected = true;
|
|
215
|
-
this.reconnectAttempts = 0;
|
|
216
|
-
this.emit('reconnected', connectionId);
|
|
217
|
-
this._resubscribeAll();
|
|
218
|
-
});
|
|
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));
|
|
229
|
-
});
|
|
71
|
+
this.connection.once('loggedIn', () => {
|
|
72
|
+
clearTimeout(timeout);
|
|
73
|
+
this.connected = true;
|
|
74
|
+
this.emit('connected');
|
|
75
|
+
resolve(true);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
this.connection.once('loginFailed', (data) => {
|
|
79
|
+
clearTimeout(timeout);
|
|
80
|
+
reject(new Error(data.message || 'TICKER_PLANT login failed'));
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
this.connection.login('TICKER_PLANT');
|
|
84
|
+
});
|
|
85
|
+
} catch (e) {
|
|
86
|
+
this.emit('error', e);
|
|
87
|
+
throw e;
|
|
88
|
+
}
|
|
230
89
|
}
|
|
231
90
|
|
|
232
91
|
/**
|
|
233
|
-
* Subscribe to
|
|
92
|
+
* Subscribe to market data for a symbol
|
|
93
|
+
* @param {string} symbol - Symbol name (e.g., 'ESH6')
|
|
94
|
+
* @param {string} exchange - Exchange (default: 'CME')
|
|
234
95
|
*/
|
|
235
|
-
async subscribe(symbol,
|
|
236
|
-
if (!this.
|
|
237
|
-
throw new Error('Not connected');
|
|
96
|
+
async subscribe(symbol, exchange = 'CME') {
|
|
97
|
+
if (!this.connected) {
|
|
98
|
+
throw new Error('Not connected to TICKER_PLANT');
|
|
238
99
|
}
|
|
239
100
|
|
|
240
|
-
const
|
|
241
|
-
|
|
101
|
+
const key = `${symbol}:${exchange}`;
|
|
102
|
+
if (this.subscriptions.has(key)) return;
|
|
103
|
+
|
|
242
104
|
try {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
105
|
+
// Subscribe to LAST_TRADE and BBO (Best Bid/Offer)
|
|
106
|
+
// UpdateBits: 1 = LAST_TRADE, 2 = BBO, 3 = BOTH
|
|
107
|
+
this.connection.send('RequestMarketDataUpdate', {
|
|
108
|
+
templateId: REQ.MARKET_DATA,
|
|
109
|
+
symbol: symbol,
|
|
110
|
+
exchange: exchange,
|
|
111
|
+
request: 1, // SUBSCRIBE
|
|
112
|
+
updateBits: 3, // LAST_TRADE + BBO
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
this.subscriptions.add(key);
|
|
116
|
+
this.emit('subscribed', `${symbol}:${exchange}`);
|
|
117
|
+
} catch (e) {
|
|
118
|
+
this.emit('error', e);
|
|
119
|
+
throw e;
|
|
251
120
|
}
|
|
252
121
|
}
|
|
253
122
|
|
|
254
123
|
/**
|
|
255
|
-
* Unsubscribe from
|
|
124
|
+
* Unsubscribe from market data for a symbol
|
|
256
125
|
*/
|
|
257
|
-
async unsubscribe(
|
|
258
|
-
if (!this.
|
|
126
|
+
async unsubscribe(symbol, exchange = 'CME') {
|
|
127
|
+
if (!this.connected) return;
|
|
128
|
+
|
|
129
|
+
const key = `${symbol}:${exchange}`;
|
|
130
|
+
if (!this.subscriptions.has(key)) return;
|
|
259
131
|
|
|
260
132
|
try {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
133
|
+
this.connection.send('RequestMarketDataUpdate', {
|
|
134
|
+
templateId: REQ.MARKET_DATA,
|
|
135
|
+
symbol: symbol,
|
|
136
|
+
exchange: exchange,
|
|
137
|
+
request: 2, // UNSUBSCRIBE
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
this.subscriptions.delete(key);
|
|
141
|
+
} catch (e) {
|
|
142
|
+
// Ignore unsubscribe errors
|
|
268
143
|
}
|
|
269
144
|
}
|
|
270
145
|
|
|
271
146
|
/**
|
|
272
|
-
*
|
|
147
|
+
* Disconnect from TICKER_PLANT
|
|
273
148
|
*/
|
|
274
|
-
async
|
|
275
|
-
|
|
149
|
+
async disconnect() {
|
|
150
|
+
// Unsubscribe from all
|
|
151
|
+
for (const key of this.subscriptions) {
|
|
152
|
+
const [symbol, exchange] = key.split(':');
|
|
153
|
+
await this.unsubscribe(symbol, exchange);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (this.connection) {
|
|
276
157
|
try {
|
|
277
|
-
await this.connection.
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
} catch (error) {
|
|
281
|
-
// Continue
|
|
158
|
+
await this.connection.disconnect();
|
|
159
|
+
} catch (e) {
|
|
160
|
+
// Ignore
|
|
282
161
|
}
|
|
283
162
|
}
|
|
163
|
+
|
|
164
|
+
this.connection = null;
|
|
165
|
+
this.connected = false;
|
|
166
|
+
this.subscriptions.clear();
|
|
167
|
+
this.emit('disconnected');
|
|
284
168
|
}
|
|
285
169
|
|
|
286
170
|
/**
|
|
287
|
-
*
|
|
171
|
+
* Handle incoming messages from TICKER_PLANT
|
|
288
172
|
*/
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
173
|
+
_handleMessage(msg) {
|
|
174
|
+
const { templateId, data } = msg;
|
|
175
|
+
|
|
176
|
+
// Debug: log all message template IDs
|
|
177
|
+
if (!this._seenTemplates) this._seenTemplates = new Set();
|
|
178
|
+
if (!this._seenTemplates.has(templateId)) {
|
|
179
|
+
this._seenTemplates.add(templateId);
|
|
180
|
+
this.emit('debug', `New message type: templateId=${templateId}`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
switch (templateId) {
|
|
184
|
+
case RES.MARKET_DATA:
|
|
185
|
+
// Subscription confirmed
|
|
186
|
+
this.emit('debug', 'Market data subscription confirmed');
|
|
187
|
+
break;
|
|
188
|
+
|
|
189
|
+
case STREAM.LAST_TRADE:
|
|
190
|
+
this._handleLastTrade(data);
|
|
191
|
+
break;
|
|
192
|
+
|
|
193
|
+
case STREAM.BBO:
|
|
194
|
+
this._handleBBO(data);
|
|
195
|
+
break;
|
|
196
|
+
|
|
197
|
+
default:
|
|
198
|
+
// Log unknown messages for debugging
|
|
199
|
+
if (templateId && !this._seenTemplates.has(`logged_${templateId}`)) {
|
|
200
|
+
this._seenTemplates.add(`logged_${templateId}`);
|
|
201
|
+
}
|
|
202
|
+
break;
|
|
311
203
|
}
|
|
312
204
|
}
|
|
313
205
|
|
|
314
206
|
/**
|
|
315
|
-
*
|
|
207
|
+
* Handle LastTrade message
|
|
316
208
|
*/
|
|
317
|
-
|
|
318
|
-
|
|
209
|
+
_handleLastTrade(data) {
|
|
210
|
+
try {
|
|
211
|
+
const trade = proto.decode('LastTrade', data);
|
|
212
|
+
|
|
213
|
+
const tick = {
|
|
214
|
+
type: 'trade',
|
|
215
|
+
symbol: trade.symbol,
|
|
216
|
+
exchange: trade.exchange,
|
|
217
|
+
price: trade.tradePrice,
|
|
218
|
+
size: trade.tradeSize,
|
|
219
|
+
volume: trade.volume,
|
|
220
|
+
side: trade.aggressor === 1 ? 'buy' : trade.aggressor === 2 ? 'sell' : 'unknown',
|
|
221
|
+
timestamp: Date.now(),
|
|
222
|
+
ssboe: trade.ssboe,
|
|
223
|
+
usecs: trade.usecs,
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
this.emit('tick', tick);
|
|
227
|
+
} catch (e) {
|
|
228
|
+
// Ignore decode errors
|
|
229
|
+
}
|
|
319
230
|
}
|
|
320
231
|
|
|
321
232
|
/**
|
|
322
|
-
*
|
|
233
|
+
* Handle BestBidOffer message
|
|
323
234
|
*/
|
|
324
|
-
|
|
325
|
-
|
|
235
|
+
_handleBBO(data) {
|
|
236
|
+
try {
|
|
237
|
+
const bbo = proto.decode('BestBidOffer', data);
|
|
238
|
+
|
|
239
|
+
const tick = {
|
|
240
|
+
type: 'quote',
|
|
241
|
+
symbol: bbo.symbol,
|
|
242
|
+
exchange: bbo.exchange,
|
|
243
|
+
bid: bbo.bidPrice,
|
|
244
|
+
bidSize: bbo.bidSize,
|
|
245
|
+
ask: bbo.askPrice,
|
|
246
|
+
askSize: bbo.askSize,
|
|
247
|
+
price: bbo.bidPrice && bbo.askPrice ? (bbo.bidPrice + bbo.askPrice) / 2 : null,
|
|
248
|
+
timestamp: Date.now(),
|
|
249
|
+
ssboe: bbo.ssboe,
|
|
250
|
+
usecs: bbo.usecs,
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
this.emit('tick', tick);
|
|
254
|
+
} catch (e) {
|
|
255
|
+
// Ignore decode errors
|
|
256
|
+
}
|
|
326
257
|
}
|
|
327
258
|
}
|
|
328
259
|
|
|
@@ -13,7 +13,7 @@ const { SupervisionEngine } = require('../../services/ai-supervision');
|
|
|
13
13
|
/**
|
|
14
14
|
* Execute algo strategy with market data
|
|
15
15
|
* @param {Object} params - Execution parameters
|
|
16
|
-
* @param {Object} params.service -
|
|
16
|
+
* @param {Object} params.service - Rithmic trading service
|
|
17
17
|
* @param {Object} params.account - Account object
|
|
18
18
|
* @param {Object} params.contract - Contract object
|
|
19
19
|
* @param {Object} params.config - Algo config (contracts, target, risk, showName)
|
|
@@ -77,8 +77,8 @@ const executeAlgo = async ({ service, account, contract, config, strategy: strat
|
|
|
77
77
|
const strategy = new StrategyClass({ tickSize });
|
|
78
78
|
strategy.initialize(contractId, tickSize);
|
|
79
79
|
|
|
80
|
-
// Initialize Market Data Feed
|
|
81
|
-
const marketFeed = new MarketDataFeed(
|
|
80
|
+
// Initialize Market Data Feed (Rithmic TICKER_PLANT)
|
|
81
|
+
const marketFeed = new MarketDataFeed();
|
|
82
82
|
|
|
83
83
|
// Log startup
|
|
84
84
|
ui.addLog('info', `Strategy: ${strategyName}${supervisionEnabled ? ' + AI' : ''}`);
|
|
@@ -93,7 +93,20 @@ const executeAlgo = async ({ service, account, contract, config, strategy: strat
|
|
|
93
93
|
|
|
94
94
|
// Handle strategy signals
|
|
95
95
|
strategy.on('signal', async (signal) => {
|
|
96
|
-
|
|
96
|
+
ui.addLog('info', `SIGNAL DETECTED: ${signal.direction?.toUpperCase()}`);
|
|
97
|
+
|
|
98
|
+
if (!running) {
|
|
99
|
+
ui.addLog('info', 'Signal ignored: not running');
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (pendingOrder) {
|
|
103
|
+
ui.addLog('info', 'Signal ignored: order pending');
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (currentPosition !== 0) {
|
|
107
|
+
ui.addLog('info', `Signal ignored: position open (${currentPosition})`);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
97
110
|
|
|
98
111
|
let { direction, entry, stopLoss, takeProfit, confidence } = signal;
|
|
99
112
|
let orderSize = contracts;
|
|
@@ -185,34 +198,96 @@ const executeAlgo = async ({ service, account, contract, config, strategy: strat
|
|
|
185
198
|
});
|
|
186
199
|
|
|
187
200
|
// Handle market data ticks
|
|
201
|
+
let lastPrice = null;
|
|
202
|
+
let lastBid = null;
|
|
203
|
+
let lastAsk = null;
|
|
204
|
+
let ticksPerSecond = 0;
|
|
205
|
+
let lastTickSecond = Math.floor(Date.now() / 1000);
|
|
206
|
+
|
|
188
207
|
marketFeed.on('tick', (tick) => {
|
|
189
208
|
tickCount++;
|
|
190
209
|
const latencyStart = Date.now();
|
|
210
|
+
const currentSecond = Math.floor(Date.now() / 1000);
|
|
211
|
+
|
|
212
|
+
// Count ticks per second
|
|
213
|
+
if (currentSecond === lastTickSecond) {
|
|
214
|
+
ticksPerSecond++;
|
|
215
|
+
} else {
|
|
216
|
+
ticksPerSecond = 1;
|
|
217
|
+
lastTickSecond = currentSecond;
|
|
218
|
+
}
|
|
191
219
|
|
|
192
220
|
aiContext.recentTicks.push(tick);
|
|
193
221
|
if (aiContext.recentTicks.length > aiContext.maxTicks) aiContext.recentTicks.shift();
|
|
194
222
|
|
|
223
|
+
// Smart logs for tick flow
|
|
224
|
+
const price = tick.price || tick.tradePrice;
|
|
225
|
+
const bid = tick.bid || tick.bidPrice;
|
|
226
|
+
const ask = tick.ask || tick.askPrice;
|
|
227
|
+
|
|
228
|
+
// Log first tick
|
|
229
|
+
if (tickCount === 1) {
|
|
230
|
+
ui.addLog('info', `First tick received @ ${price?.toFixed(2) || 'N/A'}`);
|
|
231
|
+
ui.addLog('info', `Tick type: ${tick.type || 'unknown'}`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Log price changes
|
|
235
|
+
if (price && lastPrice && price !== lastPrice) {
|
|
236
|
+
const direction = price > lastPrice ? 'UP' : 'DOWN';
|
|
237
|
+
const change = Math.abs(price - lastPrice).toFixed(2);
|
|
238
|
+
if (tickCount <= 10 || tickCount % 50 === 0) {
|
|
239
|
+
ui.addLog('info', `Price ${direction} ${change} -> ${price.toFixed(2)}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Log bid/ask spread
|
|
244
|
+
if (bid && ask && (bid !== lastBid || ask !== lastAsk)) {
|
|
245
|
+
const spread = (ask - bid).toFixed(2);
|
|
246
|
+
if (tickCount <= 5) {
|
|
247
|
+
ui.addLog('info', `Spread: ${spread} (Bid: ${bid.toFixed(2)} / Ask: ${ask.toFixed(2)})`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
lastPrice = price;
|
|
252
|
+
lastBid = bid;
|
|
253
|
+
lastAsk = ask;
|
|
254
|
+
|
|
195
255
|
strategy.processTick({
|
|
196
256
|
contractId: tick.contractId || contractId,
|
|
197
|
-
price:
|
|
198
|
-
volume: tick.volume ||
|
|
257
|
+
price: price, bid: bid, ask: ask,
|
|
258
|
+
volume: tick.volume || tick.size || 1,
|
|
259
|
+
side: tick.side || tick.lastTradeSide || 'unknown',
|
|
199
260
|
timestamp: tick.timestamp || Date.now()
|
|
200
261
|
});
|
|
201
262
|
|
|
202
263
|
stats.latency = Date.now() - latencyStart;
|
|
203
|
-
|
|
264
|
+
|
|
265
|
+
// Periodic status logs
|
|
266
|
+
if (tickCount === 10) ui.addLog('info', `Receiving ticks... (${ticksPerSecond}/sec)`);
|
|
267
|
+
if (tickCount === 50) ui.addLog('info', `50 ticks processed, strategy analyzing...`);
|
|
268
|
+
if (tickCount % 200 === 0) {
|
|
269
|
+
ui.addLog('info', `Tick #${tickCount} @ ${price?.toFixed(2) || 'N/A'} | ${ticksPerSecond}/sec`);
|
|
270
|
+
}
|
|
204
271
|
});
|
|
205
272
|
|
|
206
|
-
marketFeed.on('connected', () => {
|
|
273
|
+
marketFeed.on('connected', () => {
|
|
274
|
+
stats.connected = true;
|
|
275
|
+
ui.addLog('connected', 'Market data connected!');
|
|
276
|
+
ui.addLog('info', 'Subscribing to market data...');
|
|
277
|
+
});
|
|
278
|
+
marketFeed.on('subscribed', (symbol) => ui.addLog('info', `Subscribed to ${symbol}`));
|
|
279
|
+
marketFeed.on('debug', (msg) => ui.addLog('info', msg));
|
|
207
280
|
marketFeed.on('error', (err) => ui.addLog('error', `Market: ${err.message}`));
|
|
208
281
|
marketFeed.on('disconnected', () => { stats.connected = false; ui.addLog('error', 'Market disconnected'); });
|
|
209
282
|
|
|
210
|
-
// Connect to market data
|
|
283
|
+
// Connect to market data (Rithmic TICKER_PLANT)
|
|
211
284
|
try {
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
285
|
+
const rithmicCredentials = service.getRithmicCredentials?.();
|
|
286
|
+
if (!rithmicCredentials) {
|
|
287
|
+
throw new Error('Rithmic credentials not available');
|
|
288
|
+
}
|
|
289
|
+
await marketFeed.connect(rithmicCredentials);
|
|
290
|
+
await marketFeed.subscribe(symbolName, contract.exchange || 'CME');
|
|
216
291
|
} catch (e) {
|
|
217
292
|
ui.addLog('error', `Failed to connect: ${e.message}`);
|
|
218
293
|
}
|
|
@@ -77,8 +77,8 @@ const launchCopyTrading = async (config) => {
|
|
|
77
77
|
const strategy = new StrategyClass({ tickSize });
|
|
78
78
|
strategy.initialize(contractId, tickSize);
|
|
79
79
|
|
|
80
|
-
// Initialize Market Data Feed
|
|
81
|
-
const marketFeed = new MarketDataFeed(
|
|
80
|
+
// Initialize Market Data Feed (Rithmic TICKER_PLANT)
|
|
81
|
+
const marketFeed = new MarketDataFeed();
|
|
82
82
|
|
|
83
83
|
// Log startup
|
|
84
84
|
ui.addLog('info', `Lead: ${leadName} | Followers: ${followers.length}`);
|
|
@@ -186,12 +186,14 @@ const launchCopyTrading = async (config) => {
|
|
|
186
186
|
marketFeed.on('error', (err) => ui.addLog('error', `Market: ${err.message}`));
|
|
187
187
|
marketFeed.on('disconnected', () => { stats.connected = false; ui.addLog('error', 'Disconnected'); });
|
|
188
188
|
|
|
189
|
-
// Connect to market data
|
|
189
|
+
// Connect to market data (Rithmic TICKER_PLANT)
|
|
190
190
|
try {
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
191
|
+
const rithmicCredentials = leadService.getRithmicCredentials?.();
|
|
192
|
+
if (!rithmicCredentials) {
|
|
193
|
+
throw new Error('Rithmic credentials not available');
|
|
194
|
+
}
|
|
195
|
+
await marketFeed.connect(rithmicCredentials);
|
|
196
|
+
await marketFeed.subscribe(symbolName, contract.exchange || 'CME');
|
|
195
197
|
} catch (e) {
|
|
196
198
|
ui.addLog('error', `Connect failed: ${e.message}`);
|
|
197
199
|
}
|
|
@@ -40,7 +40,7 @@ const copyTradingMenu = async () => {
|
|
|
40
40
|
return;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
// Filter active accounts: status ===
|
|
43
|
+
// Filter active accounts: status === 'active' (Rithmic) OR status === 0 OR no status
|
|
44
44
|
const activeAccounts = allAccounts.filter(acc =>
|
|
45
45
|
acc.status === 0 || acc.status === 'active' || acc.status === undefined || acc.status === null
|
|
46
46
|
);
|
|
@@ -46,7 +46,7 @@ const customStrategyMenu = async (service) => {
|
|
|
46
46
|
|
|
47
47
|
if (!allAccounts?.length) { spinner.fail('No accounts found'); await prompts.waitForEnter(); return; }
|
|
48
48
|
|
|
49
|
-
// Filter active accounts: status ===
|
|
49
|
+
// Filter active accounts: status === 'active' (Rithmic) OR status === 0 OR no status
|
|
50
50
|
const activeAccounts = allAccounts.filter(acc =>
|
|
51
51
|
acc.status === 0 || acc.status === 'active' || acc.status === undefined || acc.status === null
|
|
52
52
|
);
|
|
@@ -41,7 +41,7 @@ const oneAccountMenu = async (service) => {
|
|
|
41
41
|
return;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
// Filter active accounts: status ===
|
|
44
|
+
// Filter active accounts: status === 'active' (Rithmic) OR status === 0 OR no status
|
|
45
45
|
const activeAccounts = allAccounts.filter(acc =>
|
|
46
46
|
acc.status === 0 || acc.status === 'active' || acc.status === undefined || acc.status === null
|
|
47
47
|
);
|