hedgequantx 1.2.147 → 1.3.1

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.
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Rithmic Accounts Module
3
+ * Account fetching, PnL, and positions
4
+ */
5
+
6
+ const { REQ } = require('./constants');
7
+
8
+ /**
9
+ * Hash account ID to numeric (for compatibility)
10
+ */
11
+ const hashAccountId = (str) => {
12
+ let hash = 0;
13
+ for (let i = 0; i < str.length; i++) {
14
+ const char = str.charCodeAt(i);
15
+ hash = (hash << 5) - hash + char;
16
+ hash = hash & hash;
17
+ }
18
+ return Math.abs(hash);
19
+ };
20
+
21
+ /**
22
+ * Fetch accounts from ORDER_PLANT
23
+ * @param {RithmicService} service - The Rithmic service instance
24
+ */
25
+ const fetchAccounts = async (service) => {
26
+ if (!service.orderConn || !service.loginInfo) {
27
+ return [];
28
+ }
29
+
30
+ return new Promise((resolve) => {
31
+ const accounts = [];
32
+
33
+ const timeout = setTimeout(() => {
34
+ service.accounts = accounts;
35
+ resolve(accounts);
36
+ }, 2000);
37
+
38
+ service.once('accountReceived', (account) => {
39
+ accounts.push(account);
40
+ });
41
+
42
+ service.once('accountListComplete', () => {
43
+ clearTimeout(timeout);
44
+ service.accounts = accounts;
45
+ resolve(accounts);
46
+ });
47
+
48
+ try {
49
+ service.orderConn.send('RequestAccountList', {
50
+ templateId: REQ.ACCOUNT_LIST,
51
+ userMsg: ['HQX'],
52
+ fcmId: service.loginInfo.fcmId,
53
+ ibId: service.loginInfo.ibId,
54
+ });
55
+ } catch (e) {
56
+ clearTimeout(timeout);
57
+ resolve([]);
58
+ }
59
+ });
60
+ };
61
+
62
+ /**
63
+ * Get trading accounts formatted like ProjectX
64
+ * @param {RithmicService} service - The Rithmic service instance
65
+ */
66
+ const getTradingAccounts = async (service) => {
67
+ if (service.accounts.length === 0 && service.orderConn && service.loginInfo) {
68
+ try {
69
+ await fetchAccounts(service);
70
+ } catch (e) {
71
+ // Ignore fetch errors
72
+ }
73
+ }
74
+
75
+ let tradingAccounts = service.accounts.map((acc) => {
76
+ const pnl = service.accountPnL.get(acc.accountId) || {};
77
+ const balance = parseFloat(pnl.accountBalance || pnl.marginBalance || pnl.cashOnHand || 0) || service.propfirm.defaultBalance;
78
+ const startingBalance = service.propfirm.defaultBalance;
79
+ const profitAndLoss = balance - startingBalance;
80
+
81
+ return {
82
+ accountId: hashAccountId(acc.accountId),
83
+ rithmicAccountId: acc.accountId,
84
+ accountName: acc.accountName || acc.accountId,
85
+ name: acc.accountName || acc.accountId,
86
+ balance: balance,
87
+ startingBalance: startingBalance,
88
+ profitAndLoss: profitAndLoss,
89
+ status: 0,
90
+ platform: 'Rithmic',
91
+ propfirm: service.propfirm.name,
92
+ };
93
+ });
94
+
95
+ if (tradingAccounts.length === 0 && service.user) {
96
+ const userName = service.user.userName || 'Unknown';
97
+ tradingAccounts = [{
98
+ accountId: hashAccountId(userName),
99
+ rithmicAccountId: userName,
100
+ accountName: userName,
101
+ name: userName,
102
+ balance: service.propfirm.defaultBalance,
103
+ startingBalance: service.propfirm.defaultBalance,
104
+ profitAndLoss: 0,
105
+ status: 0,
106
+ platform: 'Rithmic',
107
+ propfirm: service.propfirm.name,
108
+ }];
109
+ }
110
+
111
+ return { success: true, accounts: tradingAccounts };
112
+ };
113
+
114
+ /**
115
+ * Request PnL snapshot for accounts
116
+ * @param {RithmicService} service - The Rithmic service instance
117
+ */
118
+ const requestPnLSnapshot = async (service) => {
119
+ if (!service.pnlConn || !service.loginInfo) return;
120
+
121
+ for (const acc of service.accounts) {
122
+ service.pnlConn.send('RequestPnLPositionSnapshot', {
123
+ templateId: REQ.PNL_POSITION_SNAPSHOT,
124
+ userMsg: ['HQX'],
125
+ fcmId: acc.fcmId || service.loginInfo.fcmId,
126
+ ibId: acc.ibId || service.loginInfo.ibId,
127
+ accountId: acc.accountId,
128
+ });
129
+ }
130
+
131
+ await new Promise(resolve => setTimeout(resolve, 2000));
132
+ };
133
+
134
+ /**
135
+ * Subscribe to PnL updates
136
+ * @param {RithmicService} service - The Rithmic service instance
137
+ */
138
+ const subscribePnLUpdates = (service) => {
139
+ if (!service.pnlConn || !service.loginInfo) return;
140
+
141
+ for (const acc of service.accounts) {
142
+ service.pnlConn.send('RequestPnLPositionUpdates', {
143
+ templateId: REQ.PNL_POSITION_UPDATES,
144
+ userMsg: ['HQX'],
145
+ request: 1,
146
+ fcmId: acc.fcmId || service.loginInfo.fcmId,
147
+ ibId: acc.ibId || service.loginInfo.ibId,
148
+ accountId: acc.accountId,
149
+ });
150
+ }
151
+ };
152
+
153
+ /**
154
+ * Get positions
155
+ * @param {RithmicService} service - The Rithmic service instance
156
+ */
157
+ const getPositions = async (service) => {
158
+ if (!service.pnlConn && service.credentials) {
159
+ await service.connectPnL(service.credentials.username, service.credentials.password);
160
+ await requestPnLSnapshot(service);
161
+ }
162
+
163
+ const positions = Array.from(service.positions.values()).map(pos => ({
164
+ symbol: pos.symbol,
165
+ exchange: pos.exchange,
166
+ quantity: pos.quantity,
167
+ averagePrice: pos.averagePrice,
168
+ unrealizedPnl: pos.openPnl,
169
+ realizedPnl: pos.closedPnl,
170
+ side: pos.quantity > 0 ? 'LONG' : 'SHORT',
171
+ }));
172
+
173
+ return { success: true, positions };
174
+ };
175
+
176
+ module.exports = {
177
+ hashAccountId,
178
+ fetchAccounts,
179
+ getTradingAccounts,
180
+ requestPnLSnapshot,
181
+ subscribePnLUpdates,
182
+ getPositions
183
+ };
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Rithmic Message Handlers
3
+ * Handles ORDER_PLANT and PNL_PLANT messages
4
+ */
5
+
6
+ const { proto, decodeAccountPnL, decodeInstrumentPnL } = require('./protobuf');
7
+ const { RES, STREAM } = require('./constants');
8
+
9
+ /**
10
+ * Create ORDER_PLANT message handler
11
+ * @param {RithmicService} service - The Rithmic service instance
12
+ */
13
+ const createOrderHandler = (service) => {
14
+ return (msg) => {
15
+ const { templateId, data } = msg;
16
+
17
+ switch (templateId) {
18
+ case RES.LOGIN_INFO:
19
+ handleLoginInfo(service, data);
20
+ break;
21
+ case RES.ACCOUNT_LIST:
22
+ handleAccountList(service, data);
23
+ break;
24
+ case RES.TRADE_ROUTES:
25
+ handleTradeRoutes(service, data);
26
+ break;
27
+ case RES.SHOW_ORDERS:
28
+ handleShowOrdersResponse(service, data);
29
+ break;
30
+ case STREAM.EXCHANGE_NOTIFICATION:
31
+ service.emit('exchangeNotification', data);
32
+ break;
33
+ case STREAM.ORDER_NOTIFICATION:
34
+ service.emit('orderNotification', data);
35
+ break;
36
+ }
37
+ };
38
+ };
39
+
40
+ /**
41
+ * Create PNL_PLANT message handler
42
+ * @param {RithmicService} service - The Rithmic service instance
43
+ */
44
+ const createPnLHandler = (service) => {
45
+ return (msg) => {
46
+ const { templateId, data } = msg;
47
+
48
+ switch (templateId) {
49
+ case RES.PNL_POSITION_SNAPSHOT:
50
+ case RES.PNL_POSITION_UPDATES:
51
+ // OK response, nothing to do
52
+ break;
53
+ case STREAM.ACCOUNT_PNL_UPDATE:
54
+ handleAccountPnLUpdate(service, data);
55
+ break;
56
+ case STREAM.INSTRUMENT_PNL_UPDATE:
57
+ handleInstrumentPnLUpdate(service, data);
58
+ break;
59
+ }
60
+ };
61
+ };
62
+
63
+ /**
64
+ * Handle login info response
65
+ */
66
+ const handleLoginInfo = (service, data) => {
67
+ try {
68
+ const res = proto.decode('ResponseLoginInfo', data);
69
+ service.emit('loginInfoReceived', {
70
+ fcmId: res.fcmId,
71
+ ibId: res.ibId,
72
+ firstName: res.firstName,
73
+ lastName: res.lastName,
74
+ userType: res.userType,
75
+ });
76
+ } catch (e) {
77
+ // Ignore decode errors
78
+ }
79
+ };
80
+
81
+ /**
82
+ * Handle account list response
83
+ */
84
+ const handleAccountList = (service, data) => {
85
+ try {
86
+ const res = proto.decode('ResponseAccountList', data);
87
+
88
+ if (res.rpCode?.[0] === '0') {
89
+ // End of list
90
+ service.emit('accountListComplete');
91
+ } else if (res.accountId) {
92
+ const account = {
93
+ fcmId: res.fcmId,
94
+ ibId: res.ibId,
95
+ accountId: res.accountId,
96
+ accountName: res.accountName,
97
+ accountCurrency: res.accountCurrency,
98
+ };
99
+ service.accounts.push(account);
100
+ service.emit('accountReceived', account);
101
+ }
102
+ } catch (e) {
103
+ // Ignore decode errors
104
+ }
105
+ };
106
+
107
+ /**
108
+ * Handle trade routes response
109
+ */
110
+ const handleTradeRoutes = (service, data) => {
111
+ try {
112
+ const res = proto.decode('ResponseTradeRoutes', data);
113
+ service.emit('tradeRoutes', res);
114
+ } catch (e) {
115
+ // Ignore decode errors
116
+ }
117
+ };
118
+
119
+ /**
120
+ * Handle show orders response
121
+ */
122
+ const handleShowOrdersResponse = (service, data) => {
123
+ try {
124
+ const res = proto.decode('ResponseShowOrders', data);
125
+ if (res.rpCode?.[0] === '0') {
126
+ service.emit('ordersReceived');
127
+ }
128
+ } catch (e) {
129
+ // Ignore decode errors
130
+ }
131
+ };
132
+
133
+ /**
134
+ * Handle account PnL update
135
+ */
136
+ const handleAccountPnLUpdate = (service, data) => {
137
+ try {
138
+ const pnl = decodeAccountPnL(data);
139
+ if (pnl.accountId) {
140
+ service.accountPnL.set(pnl.accountId, {
141
+ accountBalance: parseFloat(pnl.accountBalance || 0),
142
+ cashOnHand: parseFloat(pnl.cashOnHand || 0),
143
+ marginBalance: parseFloat(pnl.marginBalance || 0),
144
+ openPositionPnl: parseFloat(pnl.openPositionPnl || 0),
145
+ closedPositionPnl: parseFloat(pnl.closedPositionPnl || 0),
146
+ dayPnl: parseFloat(pnl.dayPnl || 0),
147
+ });
148
+ service.emit('pnlUpdate', pnl);
149
+ }
150
+ } catch (e) {
151
+ // Ignore decode errors
152
+ }
153
+ };
154
+
155
+ /**
156
+ * Handle instrument PnL update (positions)
157
+ */
158
+ const handleInstrumentPnLUpdate = (service, data) => {
159
+ try {
160
+ const pos = decodeInstrumentPnL(data);
161
+ if (pos.symbol && pos.accountId) {
162
+ const key = `${pos.accountId}:${pos.symbol}:${pos.exchange}`;
163
+ const netQty = pos.netQuantity || pos.openPositionQuantity || ((pos.buyQty || 0) - (pos.sellQty || 0));
164
+
165
+ if (netQty !== 0) {
166
+ service.positions.set(key, {
167
+ accountId: pos.accountId,
168
+ symbol: pos.symbol,
169
+ exchange: pos.exchange || 'CME',
170
+ quantity: netQty,
171
+ averagePrice: pos.avgOpenFillPrice || 0,
172
+ openPnl: parseFloat(pos.openPositionPnl || pos.dayOpenPnl || 0),
173
+ closedPnl: parseFloat(pos.closedPositionPnl || pos.dayClosedPnl || 0),
174
+ dayPnl: parseFloat(pos.dayPnl || 0),
175
+ isSnapshot: pos.isSnapshot || false,
176
+ });
177
+ } else {
178
+ service.positions.delete(key);
179
+ }
180
+
181
+ service.emit('positionUpdate', service.positions.get(key));
182
+ }
183
+ } catch (e) {
184
+ // Ignore decode errors
185
+ }
186
+ };
187
+
188
+ module.exports = {
189
+ createOrderHandler,
190
+ createPnLHandler
191
+ };