hedgequantx 1.3.0 → 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/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/services/projectx.js +0 -771
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProjectX Market Hours & Holidays
|
|
3
|
+
* CME Futures trading hours and US market holidays
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Get nth weekday of a month
|
|
8
|
+
*/
|
|
9
|
+
const getNthWeekday = (year, month, weekday, n) => {
|
|
10
|
+
let count = 0;
|
|
11
|
+
for (let day = 1; day <= 31; day++) {
|
|
12
|
+
const d = new Date(year, month, day);
|
|
13
|
+
if (d.getMonth() !== month) break;
|
|
14
|
+
if (d.getDay() === weekday) {
|
|
15
|
+
count++;
|
|
16
|
+
if (count === n) return d.toISOString().split('T')[0];
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Get last weekday of a month
|
|
24
|
+
*/
|
|
25
|
+
const getLastWeekday = (year, month, weekday) => {
|
|
26
|
+
const lastDay = new Date(year, month + 1, 0);
|
|
27
|
+
for (let day = lastDay.getDate(); day >= 1; day--) {
|
|
28
|
+
const d = new Date(year, month, day);
|
|
29
|
+
if (d.getDay() === weekday) return d.toISOString().split('T')[0];
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get Good Friday (Friday before Easter)
|
|
36
|
+
*/
|
|
37
|
+
const getGoodFriday = (year) => {
|
|
38
|
+
const a = year % 19;
|
|
39
|
+
const b = Math.floor(year / 100);
|
|
40
|
+
const c = year % 100;
|
|
41
|
+
const d = Math.floor(b / 4);
|
|
42
|
+
const e = b % 4;
|
|
43
|
+
const f = Math.floor((b + 8) / 25);
|
|
44
|
+
const g = Math.floor((b - f + 1) / 3);
|
|
45
|
+
const h = (19 * a + b - d - g + 15) % 30;
|
|
46
|
+
const i = Math.floor(c / 4);
|
|
47
|
+
const k = c % 4;
|
|
48
|
+
const l = (32 + 2 * e + 2 * i - h - k) % 7;
|
|
49
|
+
const m = Math.floor((a + 11 * h + 22 * l) / 451);
|
|
50
|
+
const month = Math.floor((h + l - 7 * m + 114) / 31) - 1;
|
|
51
|
+
const day = ((h + l - 7 * m + 114) % 31) + 1;
|
|
52
|
+
|
|
53
|
+
const easter = new Date(year, month, day);
|
|
54
|
+
const goodFriday = new Date(easter);
|
|
55
|
+
goodFriday.setDate(easter.getDate() - 2);
|
|
56
|
+
return goodFriday.toISOString().split('T')[0];
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get day after a date
|
|
61
|
+
*/
|
|
62
|
+
const getDayAfter = (dateStr) => {
|
|
63
|
+
const d = new Date(dateStr);
|
|
64
|
+
d.setDate(d.getDate() + 1);
|
|
65
|
+
return d.toISOString().split('T')[0];
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get US market holidays for the current year
|
|
70
|
+
*/
|
|
71
|
+
const getMarketHolidays = () => {
|
|
72
|
+
const year = new Date().getFullYear();
|
|
73
|
+
|
|
74
|
+
return [
|
|
75
|
+
{ date: `${year}-01-01`, name: "New Year's Day", earlyClose: false },
|
|
76
|
+
{ date: getNthWeekday(year, 0, 1, 3), name: 'MLK Day', earlyClose: false },
|
|
77
|
+
{ date: getNthWeekday(year, 1, 1, 3), name: "Presidents' Day", earlyClose: false },
|
|
78
|
+
{ date: getGoodFriday(year), name: 'Good Friday', earlyClose: false },
|
|
79
|
+
{ date: getLastWeekday(year, 4, 1), name: 'Memorial Day', earlyClose: false },
|
|
80
|
+
{ date: `${year}-06-19`, name: 'Juneteenth', earlyClose: false },
|
|
81
|
+
{ date: `${year}-07-04`, name: 'Independence Day', earlyClose: false },
|
|
82
|
+
{ date: `${year}-07-03`, name: 'Independence Day Eve', earlyClose: true },
|
|
83
|
+
{ date: getNthWeekday(year, 8, 1, 1), name: 'Labor Day', earlyClose: false },
|
|
84
|
+
{ date: getNthWeekday(year, 10, 4, 4), name: 'Thanksgiving', earlyClose: false },
|
|
85
|
+
{ date: getDayAfter(getNthWeekday(year, 10, 4, 4)), name: 'Black Friday', earlyClose: true },
|
|
86
|
+
{ date: `${year}-12-25`, name: 'Christmas Day', earlyClose: false },
|
|
87
|
+
{ date: `${year}-12-24`, name: 'Christmas Eve', earlyClose: true },
|
|
88
|
+
{ date: `${year}-12-31`, name: "New Year's Eve", earlyClose: true },
|
|
89
|
+
];
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Check if today is a market holiday
|
|
94
|
+
*/
|
|
95
|
+
const checkHoliday = () => {
|
|
96
|
+
const today = new Date().toISOString().split('T')[0];
|
|
97
|
+
const holidays = getMarketHolidays();
|
|
98
|
+
const holiday = holidays.find(h => h.date === today);
|
|
99
|
+
|
|
100
|
+
if (holiday) {
|
|
101
|
+
return { isHoliday: !holiday.earlyClose, holiday };
|
|
102
|
+
}
|
|
103
|
+
return { isHoliday: false };
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Check if futures market is open based on CME hours and holidays
|
|
108
|
+
*/
|
|
109
|
+
const checkMarketHours = () => {
|
|
110
|
+
const now = new Date();
|
|
111
|
+
const utcDay = now.getUTCDay();
|
|
112
|
+
const utcHour = now.getUTCHours();
|
|
113
|
+
|
|
114
|
+
const holidayCheck = checkHoliday();
|
|
115
|
+
if (holidayCheck.isHoliday) {
|
|
116
|
+
return { isOpen: false, message: `Market closed - ${holidayCheck.holiday.name}` };
|
|
117
|
+
}
|
|
118
|
+
if (holidayCheck.holiday && holidayCheck.holiday.earlyClose && utcHour >= 18) {
|
|
119
|
+
return { isOpen: false, message: `Market closed early - ${holidayCheck.holiday.name}` };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (utcDay === 6) {
|
|
123
|
+
return { isOpen: false, message: 'Market closed - Weekend (Saturday)' };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (utcDay === 0 && utcHour < 23) {
|
|
127
|
+
return { isOpen: false, message: 'Market closed - Opens Sunday 6:00 PM ET' };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (utcDay === 5 && utcHour >= 22) {
|
|
131
|
+
return { isOpen: false, message: 'Market closed - Weekend' };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (utcHour === 22 && utcDay !== 5) {
|
|
135
|
+
return { isOpen: false, message: 'Market closed - Daily maintenance (5:00-6:00 PM ET)' };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return { isOpen: true, message: 'Market is open' };
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
module.exports = {
|
|
142
|
+
getMarketHolidays,
|
|
143
|
+
checkHoliday,
|
|
144
|
+
checkMarketHours
|
|
145
|
+
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProjectX Statistics Module
|
|
3
|
+
* Trade statistics calculations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Calculate lifetime statistics from trades
|
|
8
|
+
* @param {Array} trades - Array of trade objects
|
|
9
|
+
* @returns {Object} Calculated statistics
|
|
10
|
+
*/
|
|
11
|
+
const calculateLifetimeStats = (trades) => {
|
|
12
|
+
if (!trades || trades.length === 0) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const stats = {
|
|
17
|
+
totalTrades: trades.length,
|
|
18
|
+
winningTrades: 0,
|
|
19
|
+
losingTrades: 0,
|
|
20
|
+
totalWinAmount: 0,
|
|
21
|
+
totalLossAmount: 0,
|
|
22
|
+
bestTrade: 0,
|
|
23
|
+
worstTrade: 0,
|
|
24
|
+
totalVolume: 0,
|
|
25
|
+
maxConsecutiveWins: 0,
|
|
26
|
+
maxConsecutiveLosses: 0,
|
|
27
|
+
longTrades: 0,
|
|
28
|
+
shortTrades: 0
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
let consecutiveWins = 0;
|
|
32
|
+
let consecutiveLosses = 0;
|
|
33
|
+
|
|
34
|
+
trades.forEach(t => {
|
|
35
|
+
const pnl = t.profitAndLoss || t.pnl || 0;
|
|
36
|
+
const size = t.size || t.quantity || 1;
|
|
37
|
+
|
|
38
|
+
stats.totalVolume += Math.abs(size);
|
|
39
|
+
|
|
40
|
+
if (t.side === 0) stats.longTrades++;
|
|
41
|
+
else if (t.side === 1) stats.shortTrades++;
|
|
42
|
+
|
|
43
|
+
if (pnl > 0) {
|
|
44
|
+
stats.winningTrades++;
|
|
45
|
+
stats.totalWinAmount += pnl;
|
|
46
|
+
if (pnl > stats.bestTrade) stats.bestTrade = pnl;
|
|
47
|
+
consecutiveWins++;
|
|
48
|
+
consecutiveLosses = 0;
|
|
49
|
+
if (consecutiveWins > stats.maxConsecutiveWins) stats.maxConsecutiveWins = consecutiveWins;
|
|
50
|
+
} else if (pnl < 0) {
|
|
51
|
+
stats.losingTrades++;
|
|
52
|
+
stats.totalLossAmount += Math.abs(pnl);
|
|
53
|
+
if (pnl < stats.worstTrade) stats.worstTrade = pnl;
|
|
54
|
+
consecutiveLosses++;
|
|
55
|
+
consecutiveWins = 0;
|
|
56
|
+
if (consecutiveLosses > stats.maxConsecutiveLosses) stats.maxConsecutiveLosses = consecutiveLosses;
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
stats.profitFactor = stats.totalLossAmount > 0 ? stats.totalWinAmount / stats.totalLossAmount : 0;
|
|
61
|
+
stats.avgWin = stats.winningTrades > 0 ? stats.totalWinAmount / stats.winningTrades : 0;
|
|
62
|
+
stats.avgLoss = stats.losingTrades > 0 ? stats.totalLossAmount / stats.losingTrades : 0;
|
|
63
|
+
|
|
64
|
+
return stats;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Calculate daily P&L from trades
|
|
69
|
+
* @param {Array} trades - Array of trade objects
|
|
70
|
+
* @returns {Array} Daily P&L array
|
|
71
|
+
*/
|
|
72
|
+
const calculateDailyPnL = (trades) => {
|
|
73
|
+
if (!trades || trades.length === 0) {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const dailyPnL = {};
|
|
78
|
+
|
|
79
|
+
trades.forEach(t => {
|
|
80
|
+
const ts = t.creationTimestamp || t.timestamp;
|
|
81
|
+
if (ts) {
|
|
82
|
+
const d = new Date(ts);
|
|
83
|
+
const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
84
|
+
dailyPnL[key] = (dailyPnL[key] || 0) + (t.profitAndLoss || t.pnl || 0);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
return Object.entries(dailyPnL).map(([date, pnl]) => ({ date, profitAndLoss: pnl }));
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Format trades for consistent output
|
|
93
|
+
* @param {Array} trades - Raw trades from API
|
|
94
|
+
* @returns {Array} Formatted trades
|
|
95
|
+
*/
|
|
96
|
+
const formatTrades = (trades) => {
|
|
97
|
+
if (!Array.isArray(trades)) return [];
|
|
98
|
+
|
|
99
|
+
return trades.map(t => ({
|
|
100
|
+
...t,
|
|
101
|
+
timestamp: t.creationTimestamp || t.timestamp,
|
|
102
|
+
pnl: t.profitAndLoss || t.pnl || 0
|
|
103
|
+
}));
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
module.exports = {
|
|
107
|
+
calculateLifetimeStats,
|
|
108
|
+
calculateDailyPnL,
|
|
109
|
+
formatTrades
|
|
110
|
+
};
|
|
@@ -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
|
+
};
|