hedgequantx 2.7.15 → 2.7.17

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/src/lib/core.js DELETED
@@ -1,539 +0,0 @@
1
- /**
2
- * Core Module
3
- */
4
-
5
- const { M1 } = require('./m/s1');
6
- const { L2 } = require('./o/l2');
7
- const L1 = require('./o/l1');
8
- const { getB, closeB } = require('./python-bridge');
9
- const {
10
- RithmicAdapter: N1,
11
- getCurrentFrontMonth: getFM
12
- } = require('./n/r1');
13
-
14
- class C1 {
15
- constructor(sessionId, config, callbacks) {
16
- this.sessionId = sessionId;
17
- this.config = config;
18
- this.callbacks = callbacks;
19
- this.running = false;
20
- this.rithmicAdapter = null;
21
- this.strategy = null;
22
- this.pythonBridge = null; // For Python/Cython strategy mode
23
- this.usePythonStrategy = config.usePythonStrategy || false;
24
- this.symbol = null;
25
- this.exchange = 'CME';
26
-
27
- // Account info
28
- this.rithmicAccountId = null;
29
- this.accountDisplayName = null;
30
-
31
- // Position & P&L from Rithmic
32
- this.rithmicData = {
33
- pnl: { open: 0, closed: 0, total: 0, dayPnl: 0 },
34
- position: null,
35
- lastPrice: 0,
36
- lastBid: 0,
37
- lastAsk: 0
38
- };
39
-
40
- // Stats
41
- this.stats = {
42
- trades: 0,
43
- wins: 0,
44
- losses: 0,
45
- startTime: null,
46
- lastClosedPnl: 0 // Track P&L at last position close
47
- };
48
-
49
- // Order management - CRITICAL
50
- this.lastOrderTime = 0;
51
- this.orderCooldownMs = 30000; // 30 seconds minimum between entries
52
- this.pendingOrder = false;
53
- this.hasOpenPosition = false;
54
- this.lastEntryPrice = 0;
55
- this.lastEntrySide = null;
56
- this.entryTime = 0;
57
- this.minHoldTimeMs = 10000; // MINIMUM 10 seconds hold time per trade
58
-
59
- // Smart logs state tracking
60
- this.lastMarketCondition = null;
61
- this.lastLoggedPrice = 0;
62
- this.lastDelta = 0;
63
- this.logThrottleMs = 5000; // Min 5s between market flow logs
64
- this.lastFlowLogTime = 0;
65
- }
66
-
67
- async start() {
68
- this.running = true;
69
- this.stats.startTime = Date.now();
70
-
71
- const propfirm = this.config.propfirm || 'apex';
72
- const rithmicCredentials = this.config.rithmicCredentials || {};
73
-
74
- if (!rithmicCredentials.userId || !rithmicCredentials.password) {
75
- this._log('error', 'Rithmic credentials required');
76
- throw new Error('Rithmic credentials required');
77
- }
78
-
79
- // Initialize smart logger
80
- L2.reset();
81
- L2.algoStarting('Rithmic', this.config.symbol || 'MNQ');
82
-
83
- this._log('info', 'Connecting to Rithmic...');
84
-
85
- try {
86
- // Connect to Rithmic
87
- this.rithmicAdapter = new N1({ debug: false, usePool: false });
88
-
89
- await this.rithmicAdapter.connect({
90
- userId: rithmicCredentials.userId,
91
- password: rithmicCredentials.password,
92
- propfirm: propfirm
93
- }, {
94
- marketData: true,
95
- trading: true,
96
- pnl: true
97
- });
98
-
99
- // Get account
100
- const accounts = this.rithmicAdapter.getAccounts();
101
- if (accounts.length === 0) throw new Error('No Rithmic accounts found');
102
-
103
- this.rithmicAccountId = accounts[0].accountId;
104
- this.accountDisplayName = this.rithmicAccountId; // Real Rithmic ID like APEX-130042-62
105
- this._log('info', `Account: ${this.accountDisplayName}`);
106
-
107
- // Resolve symbol
108
- this.symbol = this._getSymbol(this.config.symbol);
109
- this.exchange = this._getExchange(this.symbol);
110
-
111
- // Subscribe market data
112
- await this.rithmicAdapter.subscribe(this.symbol, this.exchange);
113
- this._log('info', `Symbol: ${this.symbol}`);
114
-
115
- // Subscribe P&L
116
- await this.rithmicAdapter.subscribePnL(this.rithmicAccountId);
117
-
118
- // Event handlers
119
- this.rithmicAdapter.on('quote', (q) => this._onQuote(q));
120
- this.rithmicAdapter.on('trade', (t) => this._onTrade(t));
121
- this.rithmicAdapter.on('fill', (f) => this._onFill(f));
122
- this.rithmicAdapter.on('reject', (r) => this._onReject(r));
123
- this.rithmicAdapter.on('accountPnL', (p) => this._onAccountPnL(p));
124
- this.rithmicAdapter.on('instrumentPnL', (p) => this._onPositionPnL(p));
125
-
126
- // Initialize strategy
127
- // Contract specs MUST come from API via config - NO defaults
128
- // P&L comes from PNL_PLANT API - never calculated locally
129
- if (!this.config.tickSize) {
130
- this._log('warn', 'tickSize not provided - using API data only for P&L');
131
- }
132
- const tickSize = this.config.tickSize || 0.25; // Only for stop/target calculations in ticks
133
- const tickValue = 0; // NOT USED - P&L comes from API
134
-
135
- if (this.usePythonStrategy) {
136
- // Use Python/Cython compiled strategy
137
- this._log('info', 'Initializing Python strategy bridge...');
138
- try {
139
- this.pythonBridge = await getB();
140
- await this.pythonBridge.initialize(this.symbol, tickSize);
141
-
142
- // Listen for signals from Python bridge
143
- this.pythonBridge.on('signal', (signal) => {
144
- if (signal && signal.confidence >= 0.5) {
145
- this._executeSignal(signal);
146
- }
147
- });
148
-
149
- // Listen for smart logs from Python
150
- this.pythonBridge.on('log', (logData) => {
151
- if (logData && logData.message) {
152
- this._log('info', `[PY] ${logData.message}${logData.details ? ` | ${logData.details}` : ''}`);
153
- }
154
- });
155
-
156
- this._log('success', 'Python strategy bridge connected');
157
- } catch (error) {
158
- this._log('error', `Python bridge failed: ${error.message}, falling back to JS strategy`);
159
- this.usePythonStrategy = false;
160
- }
161
- }
162
-
163
- if (!this.usePythonStrategy) {
164
- // Use JavaScript strategy - P&L from API, not calculated
165
- this.strategy = new M1({
166
- tickSize: tickSize
167
- });
168
- this.strategy.initialize(this.symbol, tickSize);
169
-
170
- // Forward strategy signals to smart logger
171
- this.strategy.on('signal', (signal) => {
172
- L2.strategySignal(
173
- signal.direction,
174
- signal.zScore || 0,
175
- signal.confidence,
176
- signal.stopTicks,
177
- signal.targetTicks
178
- );
179
- });
180
- }
181
-
182
- L2.algoRunning();
183
- const strategyMode = this.usePythonStrategy ? 'Python/Cython' : 'JavaScript';
184
- this._log('success', `Engine started [${strategyMode}]`);
185
-
186
- // Stats interval
187
- this.statsInterval = setInterval(() => this._sendStats(), 2000);
188
-
189
- } catch (error) {
190
- this._log('error', `Connection failed: ${error.message}`);
191
- throw error;
192
- }
193
- }
194
-
195
- async stop() {
196
- L2.algoStopping();
197
- this._log('info', 'Stopping algo...');
198
- this.running = false;
199
-
200
- if (this.statsInterval) {
201
- clearInterval(this.statsInterval);
202
- this.statsInterval = null;
203
- }
204
-
205
- // CRITICAL: Cancel all orders and flatten positions
206
- if (this.rithmicAdapter && this.rithmicAccountId) {
207
- try {
208
- // 1. Cancel all open orders
209
- this._log('info', 'Cancelling orders...');
210
- await this.rithmicAdapter.cancelAllOrders(this.rithmicAccountId);
211
- await new Promise(r => setTimeout(r, 500));
212
-
213
- // 2. Flatten position if any
214
- if (this.hasOpenPosition && this.rithmicData.position) {
215
- const pos = this.rithmicData.position;
216
- const qty = Math.abs(pos.qty || 0);
217
- if (qty > 0) {
218
- const side = pos.qty > 0 ? 'sell' : 'buy';
219
- this._log('info', `Flatten: ${side.toUpperCase()} ${qty}x @MKT`);
220
- await this.rithmicAdapter.placeMarketOrder(
221
- this.rithmicAccountId,
222
- this.symbol,
223
- side,
224
- qty,
225
- this.exchange
226
- );
227
- await new Promise(r => setTimeout(r, 2000));
228
- }
229
- }
230
-
231
- this._log('success', 'Positions closed');
232
- } catch (error) {
233
- this._log('error', `Cleanup error: ${error.message}`);
234
- }
235
- }
236
-
237
- if (this.rithmicAdapter) {
238
- this.rithmicAdapter.disconnect();
239
- this.rithmicAdapter = null;
240
- }
241
-
242
- // Close Python bridge if using it
243
- if (this.pythonBridge) {
244
- closeB();
245
- this.pythonBridge = null;
246
- }
247
-
248
- this._log('info', 'ALGO STOPPED');
249
- }
250
-
251
- // Quote handler - ONLY process valid signals
252
- async _onQuote(quote) {
253
- if (!this.running) return;
254
-
255
- if (quote.bid > 0) this.rithmicData.lastBid = quote.bid;
256
- if (quote.ask > 0) this.rithmicData.lastAsk = quote.ask;
257
- if (quote.price > 0) this.rithmicData.lastPrice = quote.price;
258
-
259
- const tick = {
260
- contract_id: this.symbol,
261
- price: this.rithmicData.lastPrice,
262
- bid: this.rithmicData.lastBid,
263
- ask: this.rithmicData.lastAsk,
264
- volume: 1,
265
- timestamp: Date.now()
266
- };
267
-
268
- // Process tick with appropriate strategy
269
- if (this.usePythonStrategy && this.pythonBridge && this.rithmicData.lastPrice > 0) {
270
- // Python/Cython strategy path
271
- try {
272
- const result = await this.pythonBridge.processTick(tick);
273
- if (result && result.signal) {
274
- // ONLY execute if: no position, no pending order, cooldown passed
275
- if (!this.hasOpenPosition && !this.pendingOrder && this._canOrder()) {
276
- if (result.signal.confidence >= 0.5) {
277
- this._executeSignal(result.signal);
278
- }
279
- }
280
- }
281
- } catch (error) {
282
- // Silent fail - don't spam logs
283
- }
284
- } else if (this.strategy && this.rithmicData.lastPrice > 0) {
285
- // JavaScript strategy path
286
- this.strategy.processTick(tick);
287
-
288
- // ONLY generate signal if:
289
- // 1. No open position
290
- // 2. No pending order
291
- // 3. Cooldown passed
292
- if (!this.hasOpenPosition && !this.pendingOrder && this._canOrder()) {
293
- const signal = this.strategy.generateSignal(this.rithmicData.lastPrice);
294
- if (signal && signal.confidence >= 0.5) {
295
- this._executeSignal(signal);
296
- }
297
- }
298
- }
299
-
300
- // Check risk limits
301
- this._checkRiskLimits();
302
- }
303
-
304
- // Trade handler - pass to strategy for order flow
305
- async _onTrade(trade) {
306
- if (!this.running) return;
307
-
308
- this.rithmicData.lastPrice = trade.price;
309
-
310
- const tradeData = {
311
- contract_id: this.symbol,
312
- price: trade.price,
313
- size: trade.size || trade.volume || 1,
314
- volume: trade.size || trade.volume || 1,
315
- side: trade.side,
316
- timestamp: trade.timestamp || Date.now()
317
- };
318
-
319
- if (this.usePythonStrategy && this.pythonBridge) {
320
- // Python strategy processes trades as bars
321
- try {
322
- await this.pythonBridge.processTick(tradeData);
323
- } catch (error) {
324
- // Silent fail
325
- }
326
- } else if (this.strategy) {
327
- this.strategy.onTrade(tradeData);
328
- }
329
- }
330
-
331
- // Fill handler
332
- async _onFill(fill) {
333
- if (!this.running) return;
334
-
335
- const side = fill.side === 'buy' ? 'BUY' : 'SELL';
336
- const price = fill.fillPrice || fill.avgFillPrice || fill.price;
337
- const qty = fill.fillSize || fill.totalFillSize || 1;
338
-
339
- // Determine if this is entry or exit
340
- const wasFlat = !this.hasOpenPosition;
341
-
342
- if (wasFlat) {
343
- // Entry fill
344
- this.hasOpenPosition = true;
345
- this.lastEntryPrice = price;
346
- this.lastEntrySide = side.toLowerCase();
347
- this.entryTime = Date.now(); // Track entry time for minimum hold
348
- this.pendingOrder = false;
349
- this._log('trade', `ENTRY ${side} ${qty}x @${price}`);
350
- } else {
351
- // Exit fill - P&L comes from API (PNL_PLANT), not calculated locally
352
- // Use closedPnL delta from rithmicData which is updated by _onAccountPnL
353
- const currentClosedPnl = this.rithmicData.pnl.closed || 0;
354
- const tradePnl = currentClosedPnl - this.stats.lastClosedPnl;
355
- this.stats.lastClosedPnl = currentClosedPnl;
356
-
357
- // Calculate hold time
358
- const holdTimeMs = Date.now() - this.entryTime;
359
- const holdTimeSec = Math.round(holdTimeMs / 1000);
360
-
361
- if (tradePnl > 0) {
362
- this.stats.wins++;
363
- this._log('fill_win', `EXIT ${side} ${qty}x @${price} | +$${tradePnl.toFixed(2)} | ${holdTimeSec}s`);
364
- } else {
365
- this.stats.losses++;
366
- this._log('fill_loss', `EXIT ${side} ${qty}x @${price} | -$${Math.abs(tradePnl).toFixed(2)} | ${holdTimeSec}s`);
367
- }
368
-
369
- // Record trade result in strategy for performance tracking
370
- if (this.usePythonStrategy && this.pythonBridge) {
371
- try {
372
- await this.pythonBridge.recordTradeResult(tradePnl);
373
- } catch (error) {
374
- // Silent fail
375
- }
376
- } else if (this.strategy && this.strategy.recordTradeResult) {
377
- this.strategy.recordTradeResult(tradePnl);
378
- }
379
-
380
- this.hasOpenPosition = false;
381
- this.pendingOrder = false;
382
- this.lastEntryPrice = 0;
383
- this.lastEntrySide = null;
384
- this.entryTime = 0;
385
- }
386
-
387
- this.stats.trades++;
388
- this._sendStats();
389
- }
390
-
391
- // Reject handler
392
- _onReject(reject) {
393
- this.pendingOrder = false;
394
- this._log('error', `REJECTED: ${reject.reason || reject.text || 'Unknown'}`);
395
- }
396
-
397
- // Account P&L from Rithmic
398
- _onAccountPnL(pnl) {
399
- if (!this.running) return;
400
-
401
- this.rithmicData.pnl.open = parseFloat(pnl.openPositionPnl || pnl.openPositionPnL || 0);
402
- this.rithmicData.pnl.closed = parseFloat(pnl.closedPositionPnl || pnl.closedPositionPnL || 0);
403
- this.rithmicData.pnl.dayPnl = parseFloat(pnl.dayPnl || 0);
404
- this.rithmicData.pnl.total = this.rithmicData.pnl.open + this.rithmicData.pnl.closed;
405
- }
406
-
407
- // Position P&L from Rithmic
408
- _onPositionPnL(pnl) {
409
- if (!this.running) return;
410
-
411
- const netQty = pnl.netQuantity || 0;
412
-
413
- if (netQty !== 0) {
414
- this.rithmicData.position = {
415
- symbol: pnl.symbol,
416
- qty: netQty,
417
- side: netQty > 0 ? 'long' : 'short',
418
- avgPrice: pnl.avgOpenFillPrice || 0,
419
- pnl: parseFloat(pnl.openPositionPnl || pnl.openPositionPnL || 0)
420
- };
421
- this.hasOpenPosition = true;
422
- } else {
423
- // Position closed
424
- if (this.hasOpenPosition && this.rithmicData.position) {
425
- const closedPnl = this.rithmicData.pnl.closed - this.stats.lastClosedPnl;
426
- this.stats.lastClosedPnl = this.rithmicData.pnl.closed;
427
- }
428
- this.rithmicData.position = null;
429
- this.hasOpenPosition = false;
430
- }
431
- }
432
-
433
- // Execute signal
434
- async _executeSignal(signal) {
435
- if (!this.rithmicAdapter || !this.running) return;
436
- if (this.hasOpenPosition || this.pendingOrder) return;
437
-
438
- const side = signal.direction === 'long' ? 'buy' : 'sell';
439
- const qty = this.config.contracts || 1;
440
-
441
- this.pendingOrder = true;
442
- this.lastOrderTime = Date.now();
443
-
444
- const confidence = (signal.confidence * 100).toFixed(0);
445
- this._log('info', `SIGNAL: ${side.toUpperCase()} (${confidence}% conf)`);
446
-
447
- try {
448
- await this.rithmicAdapter.placeMarketOrder(
449
- this.rithmicAccountId,
450
- this.symbol,
451
- side,
452
- qty,
453
- this.exchange
454
- );
455
- } catch (error) {
456
- this.pendingOrder = false;
457
- this._log('error', `ORDER FAILED: ${error.message}`);
458
- }
459
- }
460
-
461
- // Check daily risk limits
462
- _checkRiskLimits() {
463
- const target = this.config.dailyTarget || 200;
464
- const maxRisk = this.config.maxRisk || 100;
465
- const totalPnl = this.rithmicData.pnl.total;
466
-
467
- if (totalPnl >= target) {
468
- this._log('success', `TARGET REACHED: +$${totalPnl.toFixed(2)}`);
469
- this.stop();
470
- }
471
-
472
- if (totalPnl <= -maxRisk) {
473
- this._log('error', `MAX LOSS: -$${Math.abs(totalPnl).toFixed(2)}`);
474
- this.stop();
475
- }
476
- }
477
-
478
- _canOrder() {
479
- return Date.now() - this.lastOrderTime >= this.orderCooldownMs;
480
- }
481
-
482
- _canExit() {
483
- // Minimum 10 seconds hold time before allowing exit
484
- if (!this.hasOpenPosition || this.entryTime === 0) return true;
485
- const holdTimeMs = Date.now() - this.entryTime;
486
- return holdTimeMs >= this.minHoldTimeMs;
487
- }
488
-
489
- _getHoldTime() {
490
- if (!this.entryTime) return 0;
491
- return Math.round((Date.now() - this.entryTime) / 1000);
492
- }
493
-
494
- _sendStats() {
495
- const totalTrades = this.stats.wins + this.stats.losses;
496
- const winRate = totalTrades > 0 ? (this.stats.wins / totalTrades * 100) : 0;
497
-
498
- this.callbacks.onStats({
499
- accountName: this.accountDisplayName,
500
- realTimePnL: {
501
- openPnL: this.rithmicData.pnl.open,
502
- closedPnL: this.rithmicData.pnl.closed,
503
- totalPnL: this.rithmicData.pnl.total,
504
- dayPnL: this.rithmicData.pnl.dayPnl
505
- },
506
- position: this.rithmicData.position,
507
- hasPosition: this.hasOpenPosition,
508
- holdTime: this._getHoldTime(),
509
- canExit: this._canExit(),
510
- trades: this.stats.trades,
511
- wins: this.stats.wins,
512
- losses: this.stats.losses,
513
- winRate: winRate.toFixed(1),
514
- lastPrice: this.rithmicData.lastPrice
515
- });
516
- }
517
-
518
- _log(type, message) {
519
- this.callbacks.onLog({ type, message, timestamp: Date.now() });
520
- }
521
-
522
- _getSymbol(symbol) {
523
- if (/^[A-Z]{2,4}[FGHJKMNQUVXZ]\d{1,2}$/.test(symbol?.toUpperCase())) {
524
- return symbol.toUpperCase();
525
- }
526
- const base = symbol?.replace(/[^A-Z]/gi, '').toUpperCase() || 'MNQ';
527
- return getFM(base);
528
- }
529
-
530
- _getExchange(symbol) {
531
- const base = symbol?.replace(/[FGHJKMNQUVXZ]\d{1,2}$/, '').toUpperCase();
532
- if (['ZB', 'ZN', 'ZF', 'ZT', 'ZC', 'ZS', 'ZW', 'ZL', 'ZM', 'ZO', 'YM', 'MYM', 'TN', 'ZQ'].includes(base)) return 'CBOT';
533
- if (['CL', 'MCL', 'NG', 'HO', 'RB', 'PA', 'PL', 'BZ'].includes(base)) return 'NYMEX';
534
- if (['GC', 'MGC', 'SI', 'SIL', 'HG', 'MHG', '1OZ'].includes(base)) return 'COMEX';
535
- return 'CME';
536
- }
537
- }
538
-
539
- module.exports = { C1 };