hedgequantx 2.7.11 → 2.7.13
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 +77 -11
- package/src/config/propfirms.js +16 -4
- package/src/menus/connect.js +2 -2
- package/src/pages/accounts.js +33 -6
- package/src/pages/stats/display.js +41 -28
- package/src/pages/stats/metrics.js +11 -3
- package/src/services/rithmic/accounts.js +120 -1
- package/src/services/rithmic/constants.js +4 -0
- package/src/services/rithmic/handlers.js +43 -0
- package/src/services/rithmic/index.js +27 -71
- package/src/services/rithmic/orders.js +97 -67
- package/src/services/rithmic/proto/request_account_rms_info.proto +20 -0
- package/src/services/rithmic/proto/response_account_rms_info.proto +61 -0
- package/src/services/rithmic/proto/response_show_order_history_summary.proto +5 -5
- package/src/services/rithmic/trades.js +334 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rithmic Trades Module
|
|
3
|
+
* Convert individual fills to round-trip trades with P&L
|
|
4
|
+
*
|
|
5
|
+
* NO MOCK DATA - Only real fills from Rithmic API
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Tick values for common futures contracts
|
|
9
|
+
const TICK_VALUES = {
|
|
10
|
+
// E-mini contracts
|
|
11
|
+
'ES': { tickSize: 0.25, tickValue: 12.50 },
|
|
12
|
+
'NQ': { tickSize: 0.25, tickValue: 5.00 },
|
|
13
|
+
'YM': { tickSize: 1.00, tickValue: 5.00 },
|
|
14
|
+
'RTY': { tickSize: 0.10, tickValue: 5.00 },
|
|
15
|
+
// Micro contracts
|
|
16
|
+
'MES': { tickSize: 0.25, tickValue: 1.25 },
|
|
17
|
+
'MNQ': { tickSize: 0.25, tickValue: 0.50 },
|
|
18
|
+
'MYM': { tickSize: 1.00, tickValue: 0.50 },
|
|
19
|
+
'M2K': { tickSize: 0.10, tickValue: 0.50 },
|
|
20
|
+
// Commodities
|
|
21
|
+
'CL': { tickSize: 0.01, tickValue: 10.00 },
|
|
22
|
+
'GC': { tickSize: 0.10, tickValue: 10.00 },
|
|
23
|
+
'SI': { tickSize: 0.005, tickValue: 25.00 },
|
|
24
|
+
'NG': { tickSize: 0.001, tickValue: 10.00 },
|
|
25
|
+
// Bonds
|
|
26
|
+
'ZB': { tickSize: 0.03125, tickValue: 31.25 },
|
|
27
|
+
'ZN': { tickSize: 0.015625, tickValue: 15.625 },
|
|
28
|
+
'ZF': { tickSize: 0.0078125, tickValue: 7.8125 },
|
|
29
|
+
// Default
|
|
30
|
+
'DEFAULT': { tickSize: 0.25, tickValue: 1.25 },
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get base symbol from contract (e.g., "MNQH6" -> "MNQ")
|
|
35
|
+
* @param {string} symbol - Full contract symbol
|
|
36
|
+
* @returns {string} Base symbol
|
|
37
|
+
*/
|
|
38
|
+
const getBaseSymbol = (symbol) => {
|
|
39
|
+
if (!symbol) return 'DEFAULT';
|
|
40
|
+
// Remove month/year suffix (e.g., H6, M5, Z4)
|
|
41
|
+
const match = symbol.match(/^([A-Z0-9]+?)([FGHJKMNQUVXZ]\d{1,2})?$/i);
|
|
42
|
+
return match ? match[1].toUpperCase() : symbol.toUpperCase();
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get tick value for a symbol
|
|
47
|
+
* @param {string} symbol - Contract symbol
|
|
48
|
+
* @returns {Object} { tickSize, tickValue }
|
|
49
|
+
*/
|
|
50
|
+
const getTickInfo = (symbol) => {
|
|
51
|
+
const base = getBaseSymbol(symbol);
|
|
52
|
+
return TICK_VALUES[base] || TICK_VALUES['DEFAULT'];
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Calculate P&L for a round-trip trade
|
|
57
|
+
* @param {number} entryPrice - Entry price
|
|
58
|
+
* @param {number} exitPrice - Exit price
|
|
59
|
+
* @param {number} quantity - Number of contracts
|
|
60
|
+
* @param {number} side - 1=Long (BUY first), 2=Short (SELL first)
|
|
61
|
+
* @param {string} symbol - Contract symbol
|
|
62
|
+
* @returns {number} P&L in dollars
|
|
63
|
+
*/
|
|
64
|
+
const calculatePnL = (entryPrice, exitPrice, quantity, side, symbol) => {
|
|
65
|
+
const { tickSize, tickValue } = getTickInfo(symbol);
|
|
66
|
+
const priceDiff = side === 1 ? (exitPrice - entryPrice) : (entryPrice - exitPrice);
|
|
67
|
+
const ticks = priceDiff / tickSize;
|
|
68
|
+
return ticks * tickValue * quantity;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Convert fills to round-trip trades using FIFO matching
|
|
73
|
+
* @param {Array} fills - Array of fill objects from Rithmic API
|
|
74
|
+
* @returns {Array} Array of round-trip trade objects
|
|
75
|
+
*/
|
|
76
|
+
const fillsToRoundTrips = (fills) => {
|
|
77
|
+
if (!fills || fills.length === 0) return [];
|
|
78
|
+
|
|
79
|
+
// Group fills by account and symbol
|
|
80
|
+
const groups = new Map();
|
|
81
|
+
|
|
82
|
+
for (const fill of fills) {
|
|
83
|
+
const key = `${fill.accountId}:${fill.symbol}`;
|
|
84
|
+
if (!groups.has(key)) {
|
|
85
|
+
groups.set(key, []);
|
|
86
|
+
}
|
|
87
|
+
groups.get(key).push({
|
|
88
|
+
...fill,
|
|
89
|
+
size: fill.fillSize || fill.quantity || 1,
|
|
90
|
+
price: fill.fillPrice || fill.price || 0,
|
|
91
|
+
side: fill.side || fill.transactionType, // 1=BUY, 2=SELL
|
|
92
|
+
timestamp: fill.timestamp || parseDateTime(fill.fillDate, fill.fillTime),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const roundTrips = [];
|
|
97
|
+
|
|
98
|
+
// Process each symbol group
|
|
99
|
+
for (const [key, symbolFills] of groups) {
|
|
100
|
+
// Sort by timestamp ascending (oldest first for FIFO)
|
|
101
|
+
symbolFills.sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0));
|
|
102
|
+
|
|
103
|
+
// Position tracking: positive = long, negative = short
|
|
104
|
+
let position = 0;
|
|
105
|
+
let openTrades = []; // Stack of open fills for matching
|
|
106
|
+
|
|
107
|
+
for (const fill of symbolFills) {
|
|
108
|
+
const fillSide = fill.side; // 1=BUY, 2=SELL
|
|
109
|
+
const fillQty = fill.size;
|
|
110
|
+
const fillPrice = fill.price;
|
|
111
|
+
|
|
112
|
+
if (fillSide === 1) {
|
|
113
|
+
// BUY
|
|
114
|
+
if (position >= 0) {
|
|
115
|
+
// Opening or adding to long position
|
|
116
|
+
openTrades.push({ ...fill, remainingQty: fillQty });
|
|
117
|
+
position += fillQty;
|
|
118
|
+
} else {
|
|
119
|
+
// Closing short position (BUY to cover)
|
|
120
|
+
let qtyToClose = fillQty;
|
|
121
|
+
|
|
122
|
+
while (qtyToClose > 0 && openTrades.length > 0) {
|
|
123
|
+
const openTrade = openTrades[0];
|
|
124
|
+
const closeQty = Math.min(qtyToClose, openTrade.remainingQty);
|
|
125
|
+
|
|
126
|
+
// Create round-trip (short trade)
|
|
127
|
+
const pnl = calculatePnL(openTrade.price, fillPrice, closeQty, 2, fill.symbol);
|
|
128
|
+
roundTrips.push({
|
|
129
|
+
id: `${openTrade.id || openTrade.fillId}-${fill.id || fill.fillId}`,
|
|
130
|
+
accountId: fill.accountId,
|
|
131
|
+
symbol: fill.symbol,
|
|
132
|
+
exchange: fill.exchange,
|
|
133
|
+
side: 2, // Short
|
|
134
|
+
quantity: closeQty,
|
|
135
|
+
entryPrice: openTrade.price,
|
|
136
|
+
exitPrice: fillPrice,
|
|
137
|
+
entryTime: openTrade.timestamp,
|
|
138
|
+
exitTime: fill.timestamp,
|
|
139
|
+
entryDate: openTrade.fillDate,
|
|
140
|
+
exitDate: fill.fillDate,
|
|
141
|
+
pnl: pnl,
|
|
142
|
+
profitAndLoss: pnl,
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
openTrade.remainingQty -= closeQty;
|
|
146
|
+
qtyToClose -= closeQty;
|
|
147
|
+
position += closeQty;
|
|
148
|
+
|
|
149
|
+
if (openTrade.remainingQty === 0) {
|
|
150
|
+
openTrades.shift();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// If still have qty, it's opening a new long
|
|
155
|
+
if (qtyToClose > 0) {
|
|
156
|
+
openTrades.push({ ...fill, remainingQty: qtyToClose });
|
|
157
|
+
position += qtyToClose;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
} else if (fillSide === 2) {
|
|
161
|
+
// SELL
|
|
162
|
+
if (position <= 0) {
|
|
163
|
+
// Opening or adding to short position
|
|
164
|
+
openTrades.push({ ...fill, remainingQty: fillQty });
|
|
165
|
+
position -= fillQty;
|
|
166
|
+
} else {
|
|
167
|
+
// Closing long position (SELL to close)
|
|
168
|
+
let qtyToClose = fillQty;
|
|
169
|
+
|
|
170
|
+
while (qtyToClose > 0 && openTrades.length > 0) {
|
|
171
|
+
const openTrade = openTrades[0];
|
|
172
|
+
const closeQty = Math.min(qtyToClose, openTrade.remainingQty);
|
|
173
|
+
|
|
174
|
+
// Create round-trip (long trade)
|
|
175
|
+
const pnl = calculatePnL(openTrade.price, fillPrice, closeQty, 1, fill.symbol);
|
|
176
|
+
roundTrips.push({
|
|
177
|
+
id: `${openTrade.id || openTrade.fillId}-${fill.id || fill.fillId}`,
|
|
178
|
+
accountId: fill.accountId,
|
|
179
|
+
symbol: fill.symbol,
|
|
180
|
+
exchange: fill.exchange,
|
|
181
|
+
side: 1, // Long
|
|
182
|
+
quantity: closeQty,
|
|
183
|
+
entryPrice: openTrade.price,
|
|
184
|
+
exitPrice: fillPrice,
|
|
185
|
+
entryTime: openTrade.timestamp,
|
|
186
|
+
exitTime: fill.timestamp,
|
|
187
|
+
entryDate: openTrade.fillDate,
|
|
188
|
+
exitDate: fill.fillDate,
|
|
189
|
+
pnl: pnl,
|
|
190
|
+
profitAndLoss: pnl,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
openTrade.remainingQty -= closeQty;
|
|
194
|
+
qtyToClose -= closeQty;
|
|
195
|
+
position -= closeQty;
|
|
196
|
+
|
|
197
|
+
if (openTrade.remainingQty === 0) {
|
|
198
|
+
openTrades.shift();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// If still have qty, it's opening a new short
|
|
203
|
+
if (qtyToClose > 0) {
|
|
204
|
+
openTrades.push({ ...fill, remainingQty: qtyToClose });
|
|
205
|
+
position -= qtyToClose;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Sort round-trips by exit time descending (newest first)
|
|
213
|
+
roundTrips.sort((a, b) => (b.exitTime || 0) - (a.exitTime || 0));
|
|
214
|
+
|
|
215
|
+
return roundTrips;
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Parse Rithmic date/time to timestamp
|
|
220
|
+
* @param {string} dateStr - Date in YYYYMMDD format
|
|
221
|
+
* @param {string} timeStr - Time in HH:MM:SS format
|
|
222
|
+
* @returns {number} Unix timestamp in milliseconds
|
|
223
|
+
*/
|
|
224
|
+
const parseDateTime = (dateStr, timeStr) => {
|
|
225
|
+
if (!dateStr) return Date.now();
|
|
226
|
+
try {
|
|
227
|
+
const year = dateStr.slice(0, 4);
|
|
228
|
+
const month = dateStr.slice(4, 6);
|
|
229
|
+
const day = dateStr.slice(6, 8);
|
|
230
|
+
const time = timeStr || '00:00:00';
|
|
231
|
+
return new Date(`${year}-${month}-${day}T${time}Z`).getTime();
|
|
232
|
+
} catch (e) {
|
|
233
|
+
return Date.now();
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Calculate summary statistics from round-trips
|
|
239
|
+
* @param {Array} roundTrips - Array of round-trip trades
|
|
240
|
+
* @returns {Object} Summary statistics
|
|
241
|
+
*/
|
|
242
|
+
const calculateTradeStats = (roundTrips) => {
|
|
243
|
+
if (!roundTrips || roundTrips.length === 0) {
|
|
244
|
+
return {
|
|
245
|
+
totalTrades: 0,
|
|
246
|
+
winningTrades: 0,
|
|
247
|
+
losingTrades: 0,
|
|
248
|
+
breakEvenTrades: 0,
|
|
249
|
+
totalPnL: 0,
|
|
250
|
+
totalProfit: 0,
|
|
251
|
+
totalLoss: 0,
|
|
252
|
+
winRate: 0,
|
|
253
|
+
avgWin: 0,
|
|
254
|
+
avgLoss: 0,
|
|
255
|
+
largestWin: 0,
|
|
256
|
+
largestLoss: 0,
|
|
257
|
+
profitFactor: 0,
|
|
258
|
+
longTrades: 0,
|
|
259
|
+
shortTrades: 0,
|
|
260
|
+
longWins: 0,
|
|
261
|
+
shortWins: 0,
|
|
262
|
+
totalVolume: 0,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
let stats = {
|
|
267
|
+
totalTrades: roundTrips.length,
|
|
268
|
+
winningTrades: 0,
|
|
269
|
+
losingTrades: 0,
|
|
270
|
+
breakEvenTrades: 0,
|
|
271
|
+
totalPnL: 0,
|
|
272
|
+
totalProfit: 0,
|
|
273
|
+
totalLoss: 0,
|
|
274
|
+
largestWin: 0,
|
|
275
|
+
largestLoss: 0,
|
|
276
|
+
longTrades: 0,
|
|
277
|
+
shortTrades: 0,
|
|
278
|
+
longWins: 0,
|
|
279
|
+
shortWins: 0,
|
|
280
|
+
totalVolume: 0,
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
for (const trade of roundTrips) {
|
|
284
|
+
const pnl = trade.pnl || 0;
|
|
285
|
+
stats.totalPnL += pnl;
|
|
286
|
+
stats.totalVolume += trade.quantity || 1;
|
|
287
|
+
|
|
288
|
+
if (trade.side === 1) {
|
|
289
|
+
stats.longTrades++;
|
|
290
|
+
if (pnl > 0) stats.longWins++;
|
|
291
|
+
} else {
|
|
292
|
+
stats.shortTrades++;
|
|
293
|
+
if (pnl > 0) stats.shortWins++;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (pnl > 0) {
|
|
297
|
+
stats.winningTrades++;
|
|
298
|
+
stats.totalProfit += pnl;
|
|
299
|
+
if (pnl > stats.largestWin) stats.largestWin = pnl;
|
|
300
|
+
} else if (pnl < 0) {
|
|
301
|
+
stats.losingTrades++;
|
|
302
|
+
stats.totalLoss += Math.abs(pnl);
|
|
303
|
+
if (pnl < stats.largestLoss) stats.largestLoss = pnl;
|
|
304
|
+
} else {
|
|
305
|
+
stats.breakEvenTrades++;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Calculate derived metrics
|
|
310
|
+
stats.winRate = stats.totalTrades > 0
|
|
311
|
+
? (stats.winningTrades / stats.totalTrades) * 100
|
|
312
|
+
: 0;
|
|
313
|
+
stats.avgWin = stats.winningTrades > 0
|
|
314
|
+
? stats.totalProfit / stats.winningTrades
|
|
315
|
+
: 0;
|
|
316
|
+
stats.avgLoss = stats.losingTrades > 0
|
|
317
|
+
? stats.totalLoss / stats.losingTrades
|
|
318
|
+
: 0;
|
|
319
|
+
stats.profitFactor = stats.totalLoss > 0
|
|
320
|
+
? stats.totalProfit / stats.totalLoss
|
|
321
|
+
: (stats.totalProfit > 0 ? Infinity : 0);
|
|
322
|
+
|
|
323
|
+
return stats;
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
module.exports = {
|
|
327
|
+
fillsToRoundTrips,
|
|
328
|
+
calculateTradeStats,
|
|
329
|
+
calculatePnL,
|
|
330
|
+
getTickInfo,
|
|
331
|
+
getBaseSymbol,
|
|
332
|
+
parseDateTime,
|
|
333
|
+
TICK_VALUES,
|
|
334
|
+
};
|