hedgequantx 1.8.31 → 1.8.33
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/config/propfirms.js +1 -1
- package/src/pages/algo/copy-trading.js +2 -2
- package/src/pages/algo/one-account.js +10 -19
- package/src/pages/algo/ui.js +7 -4
- package/src/pages/stats.js +10 -25
- package/src/services/rithmic/accounts.js +22 -45
- package/src/services/rithmic/constants.js +8 -0
- package/src/services/rithmic/handlers.js +11 -1
- package/src/services/rithmic/index.js +225 -71
- package/src/services/rithmic/proto/request_front_month_contract.proto +11 -0
- package/src/services/rithmic/proto/request_product_codes.proto +9 -0
- package/src/services/rithmic/proto/response_front_month_contract.proto +13 -0
- package/src/services/rithmic/proto/response_product_codes.proto +12 -0
- package/src/services/rithmic/protobuf.js +110 -0
- package/src/services/tradovate/index.js +4 -4
package/package.json
CHANGED
package/src/config/propfirms.js
CHANGED
|
@@ -72,7 +72,7 @@ const copyTradingMenu = async () => {
|
|
|
72
72
|
// Step 1: Select Lead Account
|
|
73
73
|
console.log(chalk.cyan(' Step 1: Select LEAD Account'));
|
|
74
74
|
const leadOptions = allAccounts.map((a, i) => ({
|
|
75
|
-
label: `${a.propfirm} - ${a.account.accountName || a.account.accountId} ($${a.account.balance.toLocaleString()})`,
|
|
75
|
+
label: `${a.propfirm} - ${a.account.accountName || a.account.accountId}${a.account.balance !== null ? ` ($${a.account.balance.toLocaleString()})` : ''}`,
|
|
76
76
|
value: i
|
|
77
77
|
}));
|
|
78
78
|
leadOptions.push({ label: '< Cancel', value: -1 });
|
|
@@ -88,7 +88,7 @@ const copyTradingMenu = async () => {
|
|
|
88
88
|
.map((a, i) => ({ a, i }))
|
|
89
89
|
.filter(x => x.i !== leadIdx)
|
|
90
90
|
.map(x => ({
|
|
91
|
-
label: `${x.a.propfirm} - ${x.a.account.accountName || x.a.account.accountId} ($${x.a.account.balance.toLocaleString()})`,
|
|
91
|
+
label: `${x.a.propfirm} - ${x.a.account.accountName || x.a.account.accountId}${x.a.account.balance !== null ? ` ($${x.a.account.balance.toLocaleString()})` : ''}`,
|
|
92
92
|
value: x.i
|
|
93
93
|
}));
|
|
94
94
|
followerOptions.push({ label: '< Cancel', value: -1 });
|
|
@@ -49,7 +49,7 @@ const oneAccountMenu = async (service) => {
|
|
|
49
49
|
|
|
50
50
|
// Select account
|
|
51
51
|
const options = activeAccounts.map(acc => ({
|
|
52
|
-
label: `${acc.accountName || acc.accountId} (${acc.propfirm || 'Unknown'}) - $${
|
|
52
|
+
label: `${acc.accountName || acc.accountId} (${acc.propfirm || 'Unknown'})${acc.balance !== null ? ` - $${acc.balance.toLocaleString()}` : ''}`,
|
|
53
53
|
value: acc
|
|
54
54
|
}));
|
|
55
55
|
options.push({ label: '< Back', value: 'back' });
|
|
@@ -183,26 +183,17 @@ const launchAlgo = async (service, account, contract, config) => {
|
|
|
183
183
|
if (!showName && account.accountName) msg = msg.replace(new RegExp(account.accountName, 'gi'), 'HQX *****');
|
|
184
184
|
ui.addLog(d.type || 'info', msg);
|
|
185
185
|
});
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
stats.trades++;
|
|
192
|
-
stats.pnl += d.pnl || 0;
|
|
193
|
-
if (d.pnl >= 0) { stats.wins++; ui.addLog('trade', `+$${d.pnl.toFixed(2)}`); }
|
|
194
|
-
else { stats.losses++; ui.addLog('loss', `-$${Math.abs(d.pnl).toFixed(2)}`); }
|
|
195
|
-
|
|
196
|
-
if (stats.pnl >= dailyTarget) {
|
|
197
|
-
stopReason = 'target'; running = false;
|
|
198
|
-
ui.addLog('success', `TARGET! +$${stats.pnl.toFixed(2)}`);
|
|
199
|
-
hqx.stopAlgo();
|
|
200
|
-
} else if (stats.pnl <= -maxRisk) {
|
|
201
|
-
stopReason = 'risk'; running = false;
|
|
202
|
-
ui.addLog('error', `MAX RISK! -$${Math.abs(stats.pnl).toFixed(2)}`);
|
|
203
|
-
hqx.stopAlgo();
|
|
186
|
+
|
|
187
|
+
// REAL P&L direct from Rithmic - no calculation
|
|
188
|
+
hqx.on('stats', (d) => {
|
|
189
|
+
if (d.realTimePnL) {
|
|
190
|
+
stats.pnl = d.realTimePnL.totalPnL;
|
|
204
191
|
}
|
|
192
|
+
stats.trades = d.trades;
|
|
193
|
+
stats.wins = d.wins;
|
|
194
|
+
stats.losses = d.losses;
|
|
205
195
|
});
|
|
196
|
+
|
|
206
197
|
hqx.on('error', (d) => { ui.addLog('error', d.message || 'Error'); });
|
|
207
198
|
hqx.on('disconnected', () => { stats.connected = false; ui.addLog('warning', 'Disconnected'); });
|
|
208
199
|
|
package/src/pages/algo/ui.js
CHANGED
|
@@ -128,8 +128,9 @@ class AlgoUI {
|
|
|
128
128
|
const { W } = this;
|
|
129
129
|
const isCopyTrading = this.config.mode === 'copy-trading';
|
|
130
130
|
|
|
131
|
-
const
|
|
132
|
-
const
|
|
131
|
+
const pnl = stats.pnl !== null && stats.pnl !== undefined ? stats.pnl : null;
|
|
132
|
+
const pnlColor = pnl === null ? chalk.gray : (pnl >= 0 ? chalk.green : chalk.red);
|
|
133
|
+
const pnlStr = pnl === null ? '--' : ((pnl >= 0 ? '+$' : '-$') + Math.abs(pnl).toFixed(2));
|
|
133
134
|
const latencyColor = stats.latency < 100 ? chalk.green : (stats.latency < 300 ? chalk.yellow : chalk.red);
|
|
134
135
|
const serverColor = stats.connected ? chalk.green : chalk.red;
|
|
135
136
|
|
|
@@ -172,8 +173,10 @@ class AlgoUI {
|
|
|
172
173
|
this._line(chalk.cyan(GM));
|
|
173
174
|
|
|
174
175
|
// Row 3: Target | Risk
|
|
175
|
-
const
|
|
176
|
-
const
|
|
176
|
+
const targetStr = stats.target !== null && stats.target !== undefined ? '$' + stats.target.toFixed(2) : '--';
|
|
177
|
+
const riskStr = stats.risk !== null && stats.risk !== undefined ? '$' + stats.risk.toFixed(2) : '--';
|
|
178
|
+
const r3c1 = buildCell('Target', targetStr, chalk.green, colL);
|
|
179
|
+
const r3c2 = buildCell('Risk', riskStr, chalk.red, colR);
|
|
177
180
|
row(r3c1.padded, r3c2.padded);
|
|
178
181
|
|
|
179
182
|
this._line(chalk.cyan(GM));
|
package/src/pages/stats.js
CHANGED
|
@@ -65,10 +65,9 @@ const showStats = async (service) => {
|
|
|
65
65
|
return;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
// Collect stats
|
|
69
|
-
let totalBalance =
|
|
70
|
-
let
|
|
71
|
-
let totalPnL = 0;
|
|
68
|
+
// Collect REAL stats only - no estimation
|
|
69
|
+
let totalBalance = null;
|
|
70
|
+
let totalPnL = null;
|
|
72
71
|
let allTrades = [];
|
|
73
72
|
let totalOpenPositions = 0;
|
|
74
73
|
let totalOpenOrders = 0;
|
|
@@ -78,27 +77,15 @@ const showStats = async (service) => {
|
|
|
78
77
|
|
|
79
78
|
for (const account of activeAccounts) {
|
|
80
79
|
const svc = account.service;
|
|
81
|
-
const currentBalance = account.balance || 0;
|
|
82
|
-
totalBalance += currentBalance;
|
|
83
80
|
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const name = (account.accountName || '').toUpperCase();
|
|
88
|
-
if (name.includes('150K') || name.includes('150')) startingBalance = 150000;
|
|
89
|
-
else if (name.includes('100K') || name.includes('100')) startingBalance = 100000;
|
|
90
|
-
else if (name.includes('50K') || name.includes('50')) startingBalance = 50000;
|
|
91
|
-
else if (currentBalance >= 140000) startingBalance = 150000;
|
|
92
|
-
else if (currentBalance >= 90000) startingBalance = 100000;
|
|
93
|
-
else if (currentBalance >= 45000) startingBalance = 50000;
|
|
94
|
-
else startingBalance = currentBalance;
|
|
81
|
+
// Only add REAL balance from API
|
|
82
|
+
if (account.balance !== null && account.balance !== undefined) {
|
|
83
|
+
totalBalance = (totalBalance || 0) + account.balance;
|
|
95
84
|
}
|
|
96
85
|
|
|
97
|
-
|
|
98
|
-
account.
|
|
99
|
-
|
|
100
|
-
if (account.profitAndLoss !== undefined) {
|
|
101
|
-
totalPnL += account.profitAndLoss;
|
|
86
|
+
// Only add REAL P&L from API
|
|
87
|
+
if (account.profitAndLoss !== null && account.profitAndLoss !== undefined) {
|
|
88
|
+
totalPnL = (totalPnL || 0) + account.profitAndLoss;
|
|
102
89
|
}
|
|
103
90
|
|
|
104
91
|
// Positions & Orders
|
|
@@ -138,9 +125,7 @@ const showStats = async (service) => {
|
|
|
138
125
|
}
|
|
139
126
|
}
|
|
140
127
|
|
|
141
|
-
|
|
142
|
-
totalPnL = totalBalance - totalStartingBalance;
|
|
143
|
-
}
|
|
128
|
+
// NO estimation - only real data from API
|
|
144
129
|
|
|
145
130
|
// Aggregate stats
|
|
146
131
|
let stats = {
|
|
@@ -34,6 +34,7 @@ const hashAccountId = (str) => {
|
|
|
34
34
|
*/
|
|
35
35
|
const fetchAccounts = async (service) => {
|
|
36
36
|
if (!service.orderConn || !service.loginInfo) {
|
|
37
|
+
debug('fetchAccounts: no connection or loginInfo');
|
|
37
38
|
return [];
|
|
38
39
|
}
|
|
39
40
|
|
|
@@ -41,29 +42,39 @@ const fetchAccounts = async (service) => {
|
|
|
41
42
|
const accounts = [];
|
|
42
43
|
|
|
43
44
|
const timeout = setTimeout(() => {
|
|
45
|
+
debug('fetchAccounts: timeout, found', accounts.length, 'accounts');
|
|
44
46
|
service.accounts = accounts;
|
|
45
47
|
resolve(accounts);
|
|
46
|
-
},
|
|
48
|
+
}, 5000);
|
|
47
49
|
|
|
48
|
-
|
|
50
|
+
// Listen for ALL accounts (not just once)
|
|
51
|
+
const onAccount = (account) => {
|
|
52
|
+
debug('fetchAccounts: received account', account.accountId);
|
|
49
53
|
accounts.push(account);
|
|
50
|
-
}
|
|
54
|
+
};
|
|
55
|
+
service.on('accountReceived', onAccount);
|
|
51
56
|
|
|
52
57
|
service.once('accountListComplete', () => {
|
|
58
|
+
debug('fetchAccounts: complete, found', accounts.length, 'accounts');
|
|
53
59
|
clearTimeout(timeout);
|
|
60
|
+
service.removeListener('accountReceived', onAccount);
|
|
54
61
|
service.accounts = accounts;
|
|
55
62
|
resolve(accounts);
|
|
56
63
|
});
|
|
57
64
|
|
|
58
65
|
try {
|
|
66
|
+
debug('fetchAccounts: sending RequestAccountList');
|
|
59
67
|
service.orderConn.send('RequestAccountList', {
|
|
60
68
|
templateId: REQ.ACCOUNT_LIST,
|
|
61
69
|
userMsg: ['HQX'],
|
|
62
70
|
fcmId: service.loginInfo.fcmId,
|
|
63
71
|
ibId: service.loginInfo.ibId,
|
|
72
|
+
userType: 3, // USER_TYPE_TRADER - required by Rithmic API
|
|
64
73
|
});
|
|
65
74
|
} catch (e) {
|
|
75
|
+
debug('fetchAccounts: error', e.message);
|
|
66
76
|
clearTimeout(timeout);
|
|
77
|
+
service.removeListener('accountReceived', onAccount);
|
|
67
78
|
resolve([]);
|
|
68
79
|
}
|
|
69
80
|
});
|
|
@@ -98,36 +109,19 @@ const getTradingAccounts = async (service) => {
|
|
|
98
109
|
debug(`Account ${acc.accountId} pnlData:`, JSON.stringify(pnlData));
|
|
99
110
|
debug(` accountPnL map size:`, service.accountPnL.size);
|
|
100
111
|
|
|
101
|
-
//
|
|
102
|
-
const accountBalance = parseFloat(pnlData.accountBalance
|
|
103
|
-
const openPnL = parseFloat(pnlData.openPositionPnl
|
|
104
|
-
const closedPnL = parseFloat(pnlData.closedPositionPnl
|
|
105
|
-
const dayPnL = parseFloat(pnlData.dayPnl
|
|
106
|
-
|
|
107
|
-
// Balance: use API value if > 0, otherwise default
|
|
108
|
-
// Most prop firms don't report balance via PnL stream, so we use default
|
|
109
|
-
const startingBalance = service.propfirm.defaultBalance;
|
|
110
|
-
const balance = accountBalance > 0 ? accountBalance : startingBalance;
|
|
111
|
-
|
|
112
|
-
// P&L: prefer dayPnl from API, otherwise calculate from open+closed
|
|
113
|
-
let profitAndLoss = 0;
|
|
114
|
-
if (dayPnL !== 0) {
|
|
115
|
-
profitAndLoss = dayPnL;
|
|
116
|
-
} else if (openPnL !== 0 || closedPnL !== 0) {
|
|
117
|
-
profitAndLoss = openPnL + closedPnL;
|
|
118
|
-
}
|
|
119
|
-
// Don't calculate P&L from balance difference - that's estimation
|
|
120
|
-
|
|
121
|
-
debug(` balance: ${balance}, startingBalance: ${startingBalance}, P&L: ${profitAndLoss}`);
|
|
112
|
+
// REAL DATA FROM RITHMIC ONLY - NO DEFAULTS
|
|
113
|
+
const accountBalance = pnlData.accountBalance ? parseFloat(pnlData.accountBalance) : null;
|
|
114
|
+
const openPnL = pnlData.openPositionPnl ? parseFloat(pnlData.openPositionPnl) : null;
|
|
115
|
+
const closedPnL = pnlData.closedPositionPnl ? parseFloat(pnlData.closedPositionPnl) : null;
|
|
116
|
+
const dayPnL = pnlData.dayPnl ? parseFloat(pnlData.dayPnl) : null;
|
|
122
117
|
|
|
123
118
|
return {
|
|
124
119
|
accountId: hashAccountId(acc.accountId),
|
|
125
120
|
rithmicAccountId: acc.accountId,
|
|
126
121
|
accountName: acc.accountName || acc.accountId,
|
|
127
122
|
name: acc.accountName || acc.accountId,
|
|
128
|
-
balance:
|
|
129
|
-
|
|
130
|
-
profitAndLoss: profitAndLoss,
|
|
123
|
+
balance: accountBalance,
|
|
124
|
+
profitAndLoss: dayPnL !== null ? dayPnL : (openPnL !== null || closedPnL !== null ? (openPnL || 0) + (closedPnL || 0) : null),
|
|
131
125
|
openPnL: openPnL,
|
|
132
126
|
todayPnL: closedPnL,
|
|
133
127
|
status: 0,
|
|
@@ -136,24 +130,7 @@ const getTradingAccounts = async (service) => {
|
|
|
136
130
|
};
|
|
137
131
|
});
|
|
138
132
|
|
|
139
|
-
//
|
|
140
|
-
if (tradingAccounts.length === 0 && service.user) {
|
|
141
|
-
const userName = service.user.userName || 'Unknown';
|
|
142
|
-
tradingAccounts = [{
|
|
143
|
-
accountId: hashAccountId(userName),
|
|
144
|
-
rithmicAccountId: userName,
|
|
145
|
-
accountName: userName,
|
|
146
|
-
name: userName,
|
|
147
|
-
balance: service.propfirm.defaultBalance,
|
|
148
|
-
startingBalance: service.propfirm.defaultBalance,
|
|
149
|
-
profitAndLoss: 0,
|
|
150
|
-
openPnL: 0,
|
|
151
|
-
todayPnL: 0,
|
|
152
|
-
status: 0,
|
|
153
|
-
platform: 'Rithmic',
|
|
154
|
-
propfirm: service.propfirm.name,
|
|
155
|
-
}];
|
|
156
|
-
}
|
|
133
|
+
// No fallback - only real accounts from Rithmic
|
|
157
134
|
|
|
158
135
|
return { success: true, accounts: tradingAccounts };
|
|
159
136
|
};
|
|
@@ -58,6 +58,8 @@ const REQ = {
|
|
|
58
58
|
SYSTEM_INFO: 16,
|
|
59
59
|
HEARTBEAT: 18,
|
|
60
60
|
MARKET_DATA: 100,
|
|
61
|
+
PRODUCT_CODES: 111,
|
|
62
|
+
FRONT_MONTH_CONTRACT: 113,
|
|
61
63
|
LOGIN_INFO: 300,
|
|
62
64
|
ACCOUNT_LIST: 302,
|
|
63
65
|
ACCOUNT_RMS: 304,
|
|
@@ -84,6 +86,8 @@ const RES = {
|
|
|
84
86
|
SYSTEM_INFO: 17,
|
|
85
87
|
HEARTBEAT: 19,
|
|
86
88
|
MARKET_DATA: 101,
|
|
89
|
+
PRODUCT_CODES: 112,
|
|
90
|
+
FRONT_MONTH_CONTRACT: 114,
|
|
87
91
|
LOGIN_INFO: 301,
|
|
88
92
|
ACCOUNT_LIST: 303,
|
|
89
93
|
ACCOUNT_RMS: 305,
|
|
@@ -155,6 +159,10 @@ const PROTO_FILES = [
|
|
|
155
159
|
'response_pnl_position_updates.proto',
|
|
156
160
|
'account_pnl_position_update.proto',
|
|
157
161
|
'instrument_pnl_position_update.proto',
|
|
162
|
+
'request_product_codes.proto',
|
|
163
|
+
'response_product_codes.proto',
|
|
164
|
+
'request_front_month_contract.proto',
|
|
165
|
+
'response_front_month_contract.proto',
|
|
158
166
|
];
|
|
159
167
|
|
|
160
168
|
module.exports = {
|
|
@@ -17,12 +17,16 @@ const debug = (...args) => DEBUG && console.log('[Rithmic:Handler]', ...args);
|
|
|
17
17
|
const createOrderHandler = (service) => {
|
|
18
18
|
return (msg) => {
|
|
19
19
|
const { templateId, data } = msg;
|
|
20
|
+
|
|
21
|
+
debug('ORDER_PLANT message received, templateId:', templateId);
|
|
20
22
|
|
|
21
23
|
switch (templateId) {
|
|
22
24
|
case RES.LOGIN_INFO:
|
|
25
|
+
debug('Handling LOGIN_INFO');
|
|
23
26
|
handleLoginInfo(service, data);
|
|
24
27
|
break;
|
|
25
28
|
case RES.ACCOUNT_LIST:
|
|
29
|
+
debug('Handling ACCOUNT_LIST (303)');
|
|
26
30
|
handleAccountList(service, data);
|
|
27
31
|
break;
|
|
28
32
|
case RES.TRADE_ROUTES:
|
|
@@ -93,10 +97,13 @@ const handleLoginInfo = (service, data) => {
|
|
|
93
97
|
*/
|
|
94
98
|
const handleAccountList = (service, data) => {
|
|
95
99
|
try {
|
|
100
|
+
debug('Decoding ResponseAccountList...');
|
|
96
101
|
const res = proto.decode('ResponseAccountList', data);
|
|
102
|
+
debug('Decoded account list response:', JSON.stringify(res));
|
|
97
103
|
|
|
98
104
|
if (res.rpCode?.[0] === '0') {
|
|
99
105
|
// End of list
|
|
106
|
+
debug('Account list complete signal received');
|
|
100
107
|
service.emit('accountListComplete');
|
|
101
108
|
} else if (res.accountId) {
|
|
102
109
|
const account = {
|
|
@@ -106,11 +113,14 @@ const handleAccountList = (service, data) => {
|
|
|
106
113
|
accountName: res.accountName,
|
|
107
114
|
accountCurrency: res.accountCurrency,
|
|
108
115
|
};
|
|
116
|
+
debug('Account received:', account.accountId);
|
|
109
117
|
service.accounts.push(account);
|
|
110
118
|
service.emit('accountReceived', account);
|
|
119
|
+
} else {
|
|
120
|
+
debug('No accountId and no rpCode[0]=0, raw response:', res);
|
|
111
121
|
}
|
|
112
122
|
} catch (e) {
|
|
113
|
-
|
|
123
|
+
debug('Error decoding account list:', e.message);
|
|
114
124
|
}
|
|
115
125
|
};
|
|
116
126
|
|
|
@@ -9,29 +9,67 @@ const { RITHMIC_ENDPOINTS, RITHMIC_SYSTEMS } = require('./constants');
|
|
|
9
9
|
const { createOrderHandler, createPnLHandler } = require('./handlers');
|
|
10
10
|
const { fetchAccounts, getTradingAccounts, requestPnLSnapshot, subscribePnLUpdates, getPositions, hashAccountId } = require('./accounts');
|
|
11
11
|
const { placeOrder, cancelOrder, getOrders, getOrderHistory, closePosition } = require('./orders');
|
|
12
|
+
const { decodeFrontMonthContract } = require('./protobuf');
|
|
12
13
|
|
|
13
14
|
// Debug mode
|
|
14
15
|
const DEBUG = process.env.HQX_DEBUG === '1';
|
|
15
16
|
const debug = (...args) => DEBUG && console.log('[Rithmic:Service]', ...args);
|
|
16
17
|
|
|
17
|
-
//
|
|
18
|
+
// Base symbols for futures contracts
|
|
19
|
+
const BASE_SYMBOLS = {
|
|
20
|
+
// Equity Index (Quarterly: H, M, U, Z)
|
|
21
|
+
quarterly: [
|
|
22
|
+
{ base: 'ES', name: 'E-mini S&P 500', exchange: 'CME', category: 'Index' },
|
|
23
|
+
{ base: 'NQ', name: 'E-mini NASDAQ-100', exchange: 'CME', category: 'Index' },
|
|
24
|
+
{ base: 'YM', name: 'E-mini Dow Jones', exchange: 'CBOT', category: 'Index' },
|
|
25
|
+
{ base: 'RTY', name: 'E-mini Russell 2000', exchange: 'CME', category: 'Index' },
|
|
26
|
+
{ base: 'MES', name: 'Micro E-mini S&P 500', exchange: 'CME', category: 'Micro Index' },
|
|
27
|
+
{ base: 'MNQ', name: 'Micro E-mini NASDAQ-100', exchange: 'CME', category: 'Micro Index' },
|
|
28
|
+
{ base: 'MYM', name: 'Micro E-mini Dow Jones', exchange: 'CBOT', category: 'Micro Index' },
|
|
29
|
+
{ base: 'M2K', name: 'Micro E-mini Russell 2000', exchange: 'CME', category: 'Micro Index' },
|
|
30
|
+
// Currencies (Quarterly)
|
|
31
|
+
{ base: '6E', name: 'Euro FX', exchange: 'CME', category: 'Currency' },
|
|
32
|
+
{ base: 'M6E', name: 'Micro Euro FX', exchange: 'CME', category: 'Currency' },
|
|
33
|
+
{ base: '6B', name: 'British Pound', exchange: 'CME', category: 'Currency' },
|
|
34
|
+
{ base: '6J', name: 'Japanese Yen', exchange: 'CME', category: 'Currency' },
|
|
35
|
+
{ base: '6A', name: 'Australian Dollar', exchange: 'CME', category: 'Currency' },
|
|
36
|
+
{ base: '6C', name: 'Canadian Dollar', exchange: 'CME', category: 'Currency' },
|
|
37
|
+
// Bonds (Quarterly)
|
|
38
|
+
{ base: 'ZB', name: '30-Year T-Bond', exchange: 'CBOT', category: 'Bonds' },
|
|
39
|
+
{ base: 'ZN', name: '10-Year T-Note', exchange: 'CBOT', category: 'Bonds' },
|
|
40
|
+
{ base: 'ZF', name: '5-Year T-Note', exchange: 'CBOT', category: 'Bonds' },
|
|
41
|
+
{ base: 'ZT', name: '2-Year T-Note', exchange: 'CBOT', category: 'Bonds' },
|
|
42
|
+
],
|
|
43
|
+
// Energy & Metals (Monthly)
|
|
44
|
+
monthly: [
|
|
45
|
+
{ base: 'CL', name: 'Crude Oil WTI', exchange: 'NYMEX', category: 'Energy' },
|
|
46
|
+
{ base: 'MCL', name: 'Micro Crude Oil', exchange: 'NYMEX', category: 'Energy' },
|
|
47
|
+
{ base: 'NG', name: 'Natural Gas', exchange: 'NYMEX', category: 'Energy' },
|
|
48
|
+
{ base: 'GC', name: 'Gold', exchange: 'COMEX', category: 'Metals' },
|
|
49
|
+
{ base: 'MGC', name: 'Micro Gold', exchange: 'COMEX', category: 'Metals' },
|
|
50
|
+
{ base: 'SI', name: 'Silver', exchange: 'COMEX', category: 'Metals' },
|
|
51
|
+
{ base: 'HG', name: 'Copper', exchange: 'COMEX', category: 'Metals' },
|
|
52
|
+
],
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// PropFirm configurations - NO FAKE DATA
|
|
18
56
|
const PROPFIRM_CONFIGS = {
|
|
19
|
-
'apex': { name: 'Apex Trader Funding', systemName: 'Apex',
|
|
20
|
-
'apex_rithmic': { name: 'Apex Trader Funding', systemName: 'Apex',
|
|
21
|
-
'topstep_r': { name: 'Topstep (Rithmic)', systemName: RITHMIC_SYSTEMS.TOPSTEP,
|
|
22
|
-
'bulenox_r': { name: 'Bulenox (Rithmic)', systemName: RITHMIC_SYSTEMS.BULENOX,
|
|
23
|
-
'earn2trade': { name: 'Earn2Trade', systemName: RITHMIC_SYSTEMS.EARN_2_TRADE,
|
|
24
|
-
'mescapital': { name: 'MES Capital', systemName: RITHMIC_SYSTEMS.MES_CAPITAL,
|
|
25
|
-
'tradefundrr': { name: 'TradeFundrr', systemName: RITHMIC_SYSTEMS.TRADEFUNDRR,
|
|
26
|
-
'thetradingpit': { name: 'The Trading Pit', systemName: RITHMIC_SYSTEMS.THE_TRADING_PIT,
|
|
27
|
-
'fundedfutures': { name: 'Funded Futures Network', systemName: RITHMIC_SYSTEMS.FUNDED_FUTURES_NETWORK,
|
|
28
|
-
'propshop': { name: 'PropShop Trader', systemName: RITHMIC_SYSTEMS.PROPSHOP_TRADER,
|
|
29
|
-
'4proptrader': { name: '4PropTrader', systemName: RITHMIC_SYSTEMS.FOUR_PROP_TRADER,
|
|
30
|
-
'daytraders': { name: 'DayTraders.com', systemName: RITHMIC_SYSTEMS.DAY_TRADERS,
|
|
31
|
-
'10xfutures': { name: '10X Futures', systemName: RITHMIC_SYSTEMS.TEN_X_FUTURES,
|
|
32
|
-
'lucidtrading': { name: 'Lucid Trading', systemName: RITHMIC_SYSTEMS.LUCID_TRADING,
|
|
33
|
-
'thrivetrading': { name: 'Thrive Trading', systemName: RITHMIC_SYSTEMS.THRIVE_TRADING,
|
|
34
|
-
'legendstrading': { name: 'Legends Trading', systemName: RITHMIC_SYSTEMS.LEGENDS_TRADING,
|
|
57
|
+
'apex': { name: 'Apex Trader Funding', systemName: 'Apex', gateway: RITHMIC_ENDPOINTS.CHICAGO },
|
|
58
|
+
'apex_rithmic': { name: 'Apex Trader Funding', systemName: 'Apex', gateway: RITHMIC_ENDPOINTS.CHICAGO },
|
|
59
|
+
'topstep_r': { name: 'Topstep (Rithmic)', systemName: RITHMIC_SYSTEMS.TOPSTEP, gateway: RITHMIC_ENDPOINTS.CHICAGO },
|
|
60
|
+
'bulenox_r': { name: 'Bulenox (Rithmic)', systemName: RITHMIC_SYSTEMS.BULENOX, gateway: RITHMIC_ENDPOINTS.CHICAGO },
|
|
61
|
+
'earn2trade': { name: 'Earn2Trade', systemName: RITHMIC_SYSTEMS.EARN_2_TRADE, gateway: RITHMIC_ENDPOINTS.CHICAGO },
|
|
62
|
+
'mescapital': { name: 'MES Capital', systemName: RITHMIC_SYSTEMS.MES_CAPITAL, gateway: RITHMIC_ENDPOINTS.CHICAGO },
|
|
63
|
+
'tradefundrr': { name: 'TradeFundrr', systemName: RITHMIC_SYSTEMS.TRADEFUNDRR, gateway: RITHMIC_ENDPOINTS.CHICAGO },
|
|
64
|
+
'thetradingpit': { name: 'The Trading Pit', systemName: RITHMIC_SYSTEMS.THE_TRADING_PIT, gateway: RITHMIC_ENDPOINTS.CHICAGO },
|
|
65
|
+
'fundedfutures': { name: 'Funded Futures Network', systemName: RITHMIC_SYSTEMS.FUNDED_FUTURES_NETWORK, gateway: RITHMIC_ENDPOINTS.CHICAGO },
|
|
66
|
+
'propshop': { name: 'PropShop Trader', systemName: RITHMIC_SYSTEMS.PROPSHOP_TRADER, gateway: RITHMIC_ENDPOINTS.CHICAGO },
|
|
67
|
+
'4proptrader': { name: '4PropTrader', systemName: RITHMIC_SYSTEMS.FOUR_PROP_TRADER, gateway: RITHMIC_ENDPOINTS.CHICAGO },
|
|
68
|
+
'daytraders': { name: 'DayTraders.com', systemName: RITHMIC_SYSTEMS.DAY_TRADERS, gateway: RITHMIC_ENDPOINTS.CHICAGO },
|
|
69
|
+
'10xfutures': { name: '10X Futures', systemName: RITHMIC_SYSTEMS.TEN_X_FUTURES, gateway: RITHMIC_ENDPOINTS.CHICAGO },
|
|
70
|
+
'lucidtrading': { name: 'Lucid Trading', systemName: RITHMIC_SYSTEMS.LUCID_TRADING, gateway: RITHMIC_ENDPOINTS.CHICAGO },
|
|
71
|
+
'thrivetrading': { name: 'Thrive Trading', systemName: RITHMIC_SYSTEMS.THRIVE_TRADING, gateway: RITHMIC_ENDPOINTS.CHICAGO },
|
|
72
|
+
'legendstrading': { name: 'Legends Trading', systemName: RITHMIC_SYSTEMS.LEGENDS_TRADING, gateway: RITHMIC_ENDPOINTS.CHICAGO },
|
|
35
73
|
};
|
|
36
74
|
|
|
37
75
|
class RithmicService extends EventEmitter {
|
|
@@ -41,11 +79,11 @@ class RithmicService extends EventEmitter {
|
|
|
41
79
|
this.propfirm = PROPFIRM_CONFIGS[propfirmKey] || {
|
|
42
80
|
name: propfirmKey,
|
|
43
81
|
systemName: 'Rithmic Paper Trading',
|
|
44
|
-
defaultBalance: 150000,
|
|
45
82
|
gateway: RITHMIC_ENDPOINTS.PAPER
|
|
46
83
|
};
|
|
47
84
|
this.orderConn = null;
|
|
48
85
|
this.pnlConn = null;
|
|
86
|
+
this.tickerConn = null; // TICKER_PLANT for symbol lookup
|
|
49
87
|
this.loginInfo = null;
|
|
50
88
|
this.accounts = [];
|
|
51
89
|
this.accountPnL = new Map();
|
|
@@ -53,6 +91,7 @@ class RithmicService extends EventEmitter {
|
|
|
53
91
|
this.orders = [];
|
|
54
92
|
this.user = null;
|
|
55
93
|
this.credentials = null;
|
|
94
|
+
this.cachedContracts = null; // Cache contracts to avoid repeated API calls
|
|
56
95
|
}
|
|
57
96
|
|
|
58
97
|
/**
|
|
@@ -85,17 +124,15 @@ class RithmicService extends EventEmitter {
|
|
|
85
124
|
this.loginInfo = data;
|
|
86
125
|
this.user = { userName: username, fcmId: data.fcmId, ibId: data.ibId };
|
|
87
126
|
|
|
88
|
-
try {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
accountName: username,
|
|
94
|
-
fcmId: data.fcmId,
|
|
95
|
-
ibId: data.ibId,
|
|
96
|
-
}];
|
|
127
|
+
try {
|
|
128
|
+
await fetchAccounts(this);
|
|
129
|
+
debug('Fetched accounts:', this.accounts.map(a => a.accountId));
|
|
130
|
+
} catch (e) {
|
|
131
|
+
debug('fetchAccounts error:', e.message);
|
|
97
132
|
}
|
|
98
133
|
|
|
134
|
+
// NO FAKE ACCOUNTS - only real from Rithmic API
|
|
135
|
+
|
|
99
136
|
this.credentials = { username, password };
|
|
100
137
|
|
|
101
138
|
debug('Accounts found:', this.accounts.length);
|
|
@@ -211,53 +248,165 @@ class RithmicService extends EventEmitter {
|
|
|
211
248
|
}
|
|
212
249
|
|
|
213
250
|
/**
|
|
214
|
-
*
|
|
215
|
-
|
|
216
|
-
|
|
251
|
+
* Connect to TICKER_PLANT for symbol lookup
|
|
252
|
+
*/
|
|
253
|
+
async connectTicker(username, password) {
|
|
254
|
+
try {
|
|
255
|
+
this.tickerConn = new RithmicConnection();
|
|
256
|
+
const gateway = this.propfirm.gateway || RITHMIC_ENDPOINTS.CHICAGO;
|
|
257
|
+
|
|
258
|
+
const config = {
|
|
259
|
+
uri: gateway,
|
|
260
|
+
systemName: this.propfirm.systemName,
|
|
261
|
+
userId: username,
|
|
262
|
+
password: password,
|
|
263
|
+
appName: 'HQX-CLI',
|
|
264
|
+
appVersion: '1.0.0',
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
await this.tickerConn.connect(config);
|
|
268
|
+
|
|
269
|
+
return new Promise((resolve) => {
|
|
270
|
+
const timeout = setTimeout(() => {
|
|
271
|
+
debug('TICKER_PLANT login timeout');
|
|
272
|
+
resolve(false);
|
|
273
|
+
}, 10000);
|
|
274
|
+
|
|
275
|
+
this.tickerConn.once('loggedIn', () => {
|
|
276
|
+
clearTimeout(timeout);
|
|
277
|
+
debug('TICKER_PLANT connected');
|
|
278
|
+
resolve(true);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
this.tickerConn.once('loginFailed', () => {
|
|
282
|
+
clearTimeout(timeout);
|
|
283
|
+
debug('TICKER_PLANT login failed');
|
|
284
|
+
resolve(false);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
this.tickerConn.login('TICKER_PLANT');
|
|
288
|
+
});
|
|
289
|
+
} catch (e) {
|
|
290
|
+
debug('TICKER_PLANT connection error:', e.message);
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Get front month contract from Rithmic API
|
|
297
|
+
*/
|
|
298
|
+
async getFrontMonth(baseSymbol, exchange) {
|
|
299
|
+
if (!this.tickerConn) {
|
|
300
|
+
if (!this.credentials) {
|
|
301
|
+
throw new Error('Not logged in - cannot fetch front month');
|
|
302
|
+
}
|
|
303
|
+
const connected = await this.connectTicker(this.credentials.username, this.credentials.password);
|
|
304
|
+
if (!connected) {
|
|
305
|
+
throw new Error('Failed to connect to TICKER_PLANT');
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return new Promise((resolve, reject) => {
|
|
310
|
+
const timeout = setTimeout(() => {
|
|
311
|
+
reject(new Error(`Timeout getting front month for ${baseSymbol}`));
|
|
312
|
+
}, 10000);
|
|
313
|
+
|
|
314
|
+
const handler = (msg) => {
|
|
315
|
+
if (msg.templateId === 114) { // ResponseFrontMonthContract
|
|
316
|
+
const decoded = decodeFrontMonthContract(msg.data);
|
|
317
|
+
if (decoded.userMsg === baseSymbol) {
|
|
318
|
+
clearTimeout(timeout);
|
|
319
|
+
this.tickerConn.removeListener('message', handler);
|
|
320
|
+
|
|
321
|
+
if (decoded.rpCode[0] === '0') {
|
|
322
|
+
resolve({
|
|
323
|
+
baseSymbol: baseSymbol,
|
|
324
|
+
symbol: decoded.tradingSymbol || decoded.symbol,
|
|
325
|
+
exchange: decoded.exchange || exchange,
|
|
326
|
+
});
|
|
327
|
+
} else {
|
|
328
|
+
reject(new Error(`API error for ${baseSymbol}: ${decoded.rpCode.join(' ')}`));
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
this.tickerConn.on('message', handler);
|
|
335
|
+
|
|
336
|
+
// Send RequestFrontMonthContract (template 113)
|
|
337
|
+
this.tickerConn.send('RequestFrontMonthContract', {
|
|
338
|
+
templateId: 113,
|
|
339
|
+
userMsg: [baseSymbol],
|
|
340
|
+
symbol: baseSymbol,
|
|
341
|
+
exchange: exchange,
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Get all available contracts - REAL DATA from Rithmic API
|
|
217
348
|
*/
|
|
218
349
|
async getContracts() {
|
|
219
|
-
//
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
//
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
350
|
+
// Return cached if available and fresh (5 min cache)
|
|
351
|
+
if (this.cachedContracts && this.cachedContracts.timestamp > Date.now() - 300000) {
|
|
352
|
+
return { success: true, contracts: this.cachedContracts.data, source: 'cache' };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Need credentials to fetch real data
|
|
356
|
+
if (!this.credentials) {
|
|
357
|
+
return { success: false, error: 'Not logged in - cannot fetch contracts from API' };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
try {
|
|
361
|
+
// Connect to TICKER_PLANT if not connected
|
|
362
|
+
if (!this.tickerConn) {
|
|
363
|
+
const connected = await this.connectTicker(this.credentials.username, this.credentials.password);
|
|
364
|
+
if (!connected) {
|
|
365
|
+
return { success: false, error: 'Failed to connect to TICKER_PLANT' };
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const contracts = [];
|
|
370
|
+
const allSymbols = [...BASE_SYMBOLS.quarterly, ...BASE_SYMBOLS.monthly];
|
|
371
|
+
|
|
372
|
+
debug(`Fetching front months for ${allSymbols.length} symbols...`);
|
|
373
|
+
|
|
374
|
+
// Fetch front months in parallel batches
|
|
375
|
+
const batchSize = 10;
|
|
376
|
+
for (let i = 0; i < allSymbols.length; i += batchSize) {
|
|
377
|
+
const batch = allSymbols.slice(i, i + batchSize);
|
|
378
|
+
const promises = batch.map(async (sym) => {
|
|
379
|
+
try {
|
|
380
|
+
const result = await this.getFrontMonth(sym.base, sym.exchange);
|
|
381
|
+
return {
|
|
382
|
+
symbol: result.symbol,
|
|
383
|
+
name: `${sym.name} (${result.symbol})`,
|
|
384
|
+
exchange: result.exchange,
|
|
385
|
+
category: sym.category,
|
|
386
|
+
baseSymbol: sym.base,
|
|
387
|
+
};
|
|
388
|
+
} catch (e) {
|
|
389
|
+
debug(`Failed to get front month for ${sym.base}: ${e.message}`);
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
const results = await Promise.all(promises);
|
|
395
|
+
contracts.push(...results.filter(r => r !== null));
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (contracts.length === 0) {
|
|
399
|
+
return { success: false, error: 'No contracts returned from API' };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Cache the results
|
|
403
|
+
this.cachedContracts = { data: contracts, timestamp: Date.now() };
|
|
404
|
+
return { success: true, contracts, source: 'api' };
|
|
405
|
+
|
|
406
|
+
} catch (e) {
|
|
407
|
+
debug('getContracts error:', e.message);
|
|
408
|
+
return { success: false, error: e.message };
|
|
239
409
|
}
|
|
240
|
-
|
|
241
|
-
const y = frontYear; // e.g., 26 for 2026
|
|
242
|
-
const fy = `${frontYear}`; // full year string
|
|
243
|
-
|
|
244
|
-
const contracts = [
|
|
245
|
-
{ symbol: `ES${frontMonth}${y}`, name: `E-mini S&P 500 (${frontMonthName} ${fy})`, exchange: 'CME' },
|
|
246
|
-
{ symbol: `NQ${frontMonth}${y}`, name: `E-mini NASDAQ-100 (${frontMonthName} ${fy})`, exchange: 'CME' },
|
|
247
|
-
{ symbol: `MES${frontMonth}${y}`, name: `Micro E-mini S&P 500 (${frontMonthName} ${fy})`, exchange: 'CME' },
|
|
248
|
-
{ symbol: `MNQ${frontMonth}${y}`, name: `Micro E-mini NASDAQ-100 (${frontMonthName} ${fy})`, exchange: 'CME' },
|
|
249
|
-
{ symbol: `RTY${frontMonth}${y}`, name: `E-mini Russell 2000 (${frontMonthName} ${fy})`, exchange: 'CME' },
|
|
250
|
-
{ symbol: `M2K${frontMonth}${y}`, name: `Micro E-mini Russell 2000 (${frontMonthName} ${fy})`, exchange: 'CME' },
|
|
251
|
-
{ symbol: `YM${frontMonth}${y}`, name: `E-mini Dow Jones (${frontMonthName} ${fy})`, exchange: 'CBOT' },
|
|
252
|
-
{ symbol: `MYM${frontMonth}${y}`, name: `Micro E-mini Dow Jones (${frontMonthName} ${fy})`, exchange: 'CBOT' },
|
|
253
|
-
{ symbol: `CL${frontMonth}${y}`, name: `Crude Oil (${frontMonthName} ${fy})`, exchange: 'NYMEX' },
|
|
254
|
-
{ symbol: `MCL${frontMonth}${y}`, name: `Micro Crude Oil (${frontMonthName} ${fy})`, exchange: 'NYMEX' },
|
|
255
|
-
{ symbol: `GC${frontMonth}${y}`, name: `Gold (${frontMonthName} ${fy})`, exchange: 'COMEX' },
|
|
256
|
-
{ symbol: `MGC${frontMonth}${y}`, name: `Micro Gold (${frontMonthName} ${fy})`, exchange: 'COMEX' },
|
|
257
|
-
{ symbol: `SI${frontMonth}${y}`, name: `Silver (${frontMonthName} ${fy})`, exchange: 'COMEX' },
|
|
258
|
-
{ symbol: `NG${frontMonth}${y}`, name: `Natural Gas (${frontMonthName} ${fy})`, exchange: 'NYMEX' },
|
|
259
|
-
];
|
|
260
|
-
return { success: true, contracts };
|
|
261
410
|
}
|
|
262
411
|
|
|
263
412
|
async searchContracts(searchText) {
|
|
@@ -299,6 +448,10 @@ class RithmicService extends EventEmitter {
|
|
|
299
448
|
await this.pnlConn.disconnect();
|
|
300
449
|
this.pnlConn = null;
|
|
301
450
|
}
|
|
451
|
+
if (this.tickerConn) {
|
|
452
|
+
await this.tickerConn.disconnect();
|
|
453
|
+
this.tickerConn = null;
|
|
454
|
+
}
|
|
302
455
|
this.accounts = [];
|
|
303
456
|
this.accountPnL.clear();
|
|
304
457
|
this.positions.clear();
|
|
@@ -306,6 +459,7 @@ class RithmicService extends EventEmitter {
|
|
|
306
459
|
this.loginInfo = null;
|
|
307
460
|
this.user = null;
|
|
308
461
|
this.credentials = null;
|
|
462
|
+
this.cachedContracts = null;
|
|
309
463
|
}
|
|
310
464
|
}
|
|
311
465
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
|
|
2
|
+
package rti;
|
|
3
|
+
|
|
4
|
+
message RequestFrontMonthContract
|
|
5
|
+
{
|
|
6
|
+
required int32 template_id = 154467; // Template ID = 113
|
|
7
|
+
repeated string user_msg = 132760; // User message for tracking
|
|
8
|
+
optional string symbol = 110100; // Base symbol (e.g., "MNQ", "ES")
|
|
9
|
+
optional string exchange = 110101; // Exchange (e.g., "CME")
|
|
10
|
+
optional bool need_updates = 154352; // Request updates
|
|
11
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
|
|
2
|
+
package rti;
|
|
3
|
+
|
|
4
|
+
message ResponseFrontMonthContract
|
|
5
|
+
{
|
|
6
|
+
required int32 template_id = 154467; // Template ID = 114
|
|
7
|
+
repeated string rp_code = 132766; // Response code
|
|
8
|
+
repeated string user_msg = 132760; // Echo of user message
|
|
9
|
+
optional string symbol = 110100; // Full contract symbol (e.g., "MNQH5")
|
|
10
|
+
optional string exchange = 110101; // Exchange
|
|
11
|
+
optional string trading_symbol = 157095; // Trading symbol
|
|
12
|
+
optional string description = 110114; // Contract description
|
|
13
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
|
|
2
|
+
package rti;
|
|
3
|
+
|
|
4
|
+
message ResponseProductCodes
|
|
5
|
+
{
|
|
6
|
+
required int32 template_id = 154467; // Template ID = 112
|
|
7
|
+
repeated string rp_code = 132766; // Response code
|
|
8
|
+
repeated string user_msg = 132760; // Echo of user message
|
|
9
|
+
optional string exchange = 110101; // Exchange
|
|
10
|
+
optional string product_code = 110102; // Product code (e.g., "MNQ", "ES")
|
|
11
|
+
optional string product_name = 110103; // Product name
|
|
12
|
+
}
|
|
@@ -28,6 +28,19 @@ const PNL_FIELDS = {
|
|
|
28
28
|
USECS: 150101,
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
+
// Symbol/Contract field IDs (ResponseProductCodes, ResponseFrontMonthContract)
|
|
32
|
+
const SYMBOL_FIELDS = {
|
|
33
|
+
TEMPLATE_ID: 154467,
|
|
34
|
+
RP_CODE: 132766,
|
|
35
|
+
EXCHANGE: 110101,
|
|
36
|
+
PRODUCT_CODE: 110102, // Base symbol (ES, NQ, MNQ)
|
|
37
|
+
PRODUCT_NAME: 110103, // Product name
|
|
38
|
+
SYMBOL: 110100, // Full contract symbol (ESH26)
|
|
39
|
+
TRADING_SYMBOL: 157095, // Trading symbol
|
|
40
|
+
DESCRIPTION: 110114, // Contract description
|
|
41
|
+
USER_MSG: 132760,
|
|
42
|
+
};
|
|
43
|
+
|
|
31
44
|
// Instrument PnL Position Update field IDs
|
|
32
45
|
const INSTRUMENT_PNL_FIELDS = {
|
|
33
46
|
TEMPLATE_ID: 154467,
|
|
@@ -377,6 +390,101 @@ class ProtobufHandler {
|
|
|
377
390
|
}
|
|
378
391
|
}
|
|
379
392
|
|
|
393
|
+
/**
|
|
394
|
+
* Decode ResponseProductCodes (template 112) - list of available symbols
|
|
395
|
+
*/
|
|
396
|
+
function decodeProductCodes(buffer) {
|
|
397
|
+
const result = { rpCode: [] };
|
|
398
|
+
let offset = 0;
|
|
399
|
+
|
|
400
|
+
while (offset < buffer.length) {
|
|
401
|
+
try {
|
|
402
|
+
const [tag, tagOffset] = readVarint(buffer, offset);
|
|
403
|
+
const wireType = tag & 0x7;
|
|
404
|
+
const fieldNumber = tag >>> 3;
|
|
405
|
+
offset = tagOffset;
|
|
406
|
+
|
|
407
|
+
switch (fieldNumber) {
|
|
408
|
+
case SYMBOL_FIELDS.TEMPLATE_ID:
|
|
409
|
+
[result.templateId, offset] = readVarint(buffer, offset);
|
|
410
|
+
break;
|
|
411
|
+
case SYMBOL_FIELDS.RP_CODE:
|
|
412
|
+
let rpCode;
|
|
413
|
+
[rpCode, offset] = readLengthDelimited(buffer, offset);
|
|
414
|
+
result.rpCode.push(rpCode);
|
|
415
|
+
break;
|
|
416
|
+
case SYMBOL_FIELDS.EXCHANGE:
|
|
417
|
+
[result.exchange, offset] = readLengthDelimited(buffer, offset);
|
|
418
|
+
break;
|
|
419
|
+
case SYMBOL_FIELDS.PRODUCT_CODE:
|
|
420
|
+
[result.productCode, offset] = readLengthDelimited(buffer, offset);
|
|
421
|
+
break;
|
|
422
|
+
case SYMBOL_FIELDS.PRODUCT_NAME:
|
|
423
|
+
[result.productName, offset] = readLengthDelimited(buffer, offset);
|
|
424
|
+
break;
|
|
425
|
+
case SYMBOL_FIELDS.USER_MSG:
|
|
426
|
+
[result.userMsg, offset] = readLengthDelimited(buffer, offset);
|
|
427
|
+
break;
|
|
428
|
+
default:
|
|
429
|
+
offset = skipField(buffer, offset, wireType);
|
|
430
|
+
}
|
|
431
|
+
} catch (error) {
|
|
432
|
+
break;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return result;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Decode ResponseFrontMonthContract (template 114) - current tradeable contract
|
|
441
|
+
*/
|
|
442
|
+
function decodeFrontMonthContract(buffer) {
|
|
443
|
+
const result = { rpCode: [] };
|
|
444
|
+
let offset = 0;
|
|
445
|
+
|
|
446
|
+
while (offset < buffer.length) {
|
|
447
|
+
try {
|
|
448
|
+
const [tag, tagOffset] = readVarint(buffer, offset);
|
|
449
|
+
const wireType = tag & 0x7;
|
|
450
|
+
const fieldNumber = tag >>> 3;
|
|
451
|
+
offset = tagOffset;
|
|
452
|
+
|
|
453
|
+
switch (fieldNumber) {
|
|
454
|
+
case SYMBOL_FIELDS.TEMPLATE_ID:
|
|
455
|
+
[result.templateId, offset] = readVarint(buffer, offset);
|
|
456
|
+
break;
|
|
457
|
+
case SYMBOL_FIELDS.RP_CODE:
|
|
458
|
+
let rpCode;
|
|
459
|
+
[rpCode, offset] = readLengthDelimited(buffer, offset);
|
|
460
|
+
result.rpCode.push(rpCode);
|
|
461
|
+
break;
|
|
462
|
+
case SYMBOL_FIELDS.SYMBOL:
|
|
463
|
+
[result.symbol, offset] = readLengthDelimited(buffer, offset);
|
|
464
|
+
break;
|
|
465
|
+
case SYMBOL_FIELDS.EXCHANGE:
|
|
466
|
+
[result.exchange, offset] = readLengthDelimited(buffer, offset);
|
|
467
|
+
break;
|
|
468
|
+
case SYMBOL_FIELDS.TRADING_SYMBOL:
|
|
469
|
+
[result.tradingSymbol, offset] = readLengthDelimited(buffer, offset);
|
|
470
|
+
break;
|
|
471
|
+
case SYMBOL_FIELDS.DESCRIPTION:
|
|
472
|
+
[result.description, offset] = readLengthDelimited(buffer, offset);
|
|
473
|
+
break;
|
|
474
|
+
case SYMBOL_FIELDS.USER_MSG:
|
|
475
|
+
[result.userMsg, offset] = readLengthDelimited(buffer, offset);
|
|
476
|
+
break;
|
|
477
|
+
default:
|
|
478
|
+
offset = skipField(buffer, offset, wireType);
|
|
479
|
+
}
|
|
480
|
+
} catch (error) {
|
|
481
|
+
break;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
return result;
|
|
486
|
+
}
|
|
487
|
+
|
|
380
488
|
// Singleton
|
|
381
489
|
const proto = new ProtobufHandler();
|
|
382
490
|
|
|
@@ -384,6 +492,8 @@ module.exports = {
|
|
|
384
492
|
proto,
|
|
385
493
|
decodeAccountPnL,
|
|
386
494
|
decodeInstrumentPnL,
|
|
495
|
+
decodeProductCodes,
|
|
496
|
+
decodeFrontMonthContract,
|
|
387
497
|
readVarint,
|
|
388
498
|
readLengthDelimited,
|
|
389
499
|
skipField,
|
|
@@ -34,11 +34,11 @@ class TradovateService extends EventEmitter {
|
|
|
34
34
|
*/
|
|
35
35
|
getPropFirmConfig(key) {
|
|
36
36
|
const propfirms = {
|
|
37
|
-
'apex_tradovate': { name: 'Apex (Tradovate)', isDemo: false
|
|
38
|
-
'takeprofittrader': { name: 'TakeProfitTrader', isDemo: false
|
|
39
|
-
'myfundedfutures': { name: 'MyFundedFutures', isDemo: false
|
|
37
|
+
'apex_tradovate': { name: 'Apex (Tradovate)', isDemo: false },
|
|
38
|
+
'takeprofittrader': { name: 'TakeProfitTrader', isDemo: false },
|
|
39
|
+
'myfundedfutures': { name: 'MyFundedFutures', isDemo: false },
|
|
40
40
|
};
|
|
41
|
-
return propfirms[key] || { name: key, isDemo: false
|
|
41
|
+
return propfirms[key] || { name: key, isDemo: false };
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
/**
|