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.
- package/README.md +115 -71
- package/package.json +1 -1
- package/src/app.js +1 -22
- package/src/menus/connect.js +2 -1
- package/src/menus/dashboard.js +135 -31
- package/src/services/index.js +1 -1
- package/src/services/projectx/index.js +345 -0
- package/src/services/projectx/market.js +145 -0
- package/src/services/projectx/stats.js +110 -0
- package/src/services/rithmic/accounts.js +183 -0
- package/src/services/rithmic/handlers.js +191 -0
- package/src/services/rithmic/index.js +69 -673
- package/src/services/rithmic/orders.js +192 -0
- package/src/ui/index.js +23 -1
- package/src/services/projectx.js +0 -771
|
@@ -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
|
+
};
|