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.
@@ -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
+ };