hedgequantx 2.5.44 → 2.6.0
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/bin/cli.js +11 -0
- package/package.json +1 -1
- package/src/app.js +11 -0
- package/src/config/settings.js +29 -0
- package/src/pages/algo/one-account.js +241 -47
- package/src/services/position-manager.js +927 -0
- package/src/services/rithmic/connection.js +201 -1
- package/src/services/rithmic/handlers.js +341 -5
- package/src/services/rithmic/index.js +75 -3
- package/src/services/rithmic/orders.js +193 -1
- package/src/services/rithmic/protobuf.js +171 -2
|
@@ -0,0 +1,927 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Position Manager for Fast Scalping
|
|
3
|
+
* @module services/position-manager
|
|
4
|
+
*
|
|
5
|
+
* Manages position lifecycle with:
|
|
6
|
+
* - 10 second minimum hold (prop firm rule - NON-NEGOTIABLE)
|
|
7
|
+
* - Intelligent exit based on momentum (OFI 50%, Kalman 25%, Z-Score 25%)
|
|
8
|
+
* - Trailing stop after profit threshold
|
|
9
|
+
* - 60 second failsafe exit (NON-NEGOTIABLE)
|
|
10
|
+
* - VPIN protection filter
|
|
11
|
+
*
|
|
12
|
+
* USES EXISTING MATH MODELS from HQX Ultra Scalping Strategy:
|
|
13
|
+
* - OFI (Order Flow Imbalance)
|
|
14
|
+
* - Kalman Filter with velocity tracking
|
|
15
|
+
* - Z-Score Mean Reversion
|
|
16
|
+
* - VPIN for toxicity detection
|
|
17
|
+
*
|
|
18
|
+
* Data source: Rithmic ORDER_PLANT (fills), PNL_PLANT (positions), TICKER_PLANT (prices)
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const EventEmitter = require('events');
|
|
22
|
+
const { performance } = require('perf_hooks');
|
|
23
|
+
const { FAST_SCALPING } = require('../config/settings');
|
|
24
|
+
const { logger } = require('../utils/logger');
|
|
25
|
+
|
|
26
|
+
const log = logger.scope('PositionMgr');
|
|
27
|
+
|
|
28
|
+
// =============================================================================
|
|
29
|
+
// MOMENTUM THRESHOLDS (from analysis)
|
|
30
|
+
// =============================================================================
|
|
31
|
+
const MOMENTUM = {
|
|
32
|
+
STRONG_FAVORABLE: 0.5, // momentum > 0.5 + profit → HOLD
|
|
33
|
+
WEAK_THRESHOLD: 0.2, // momentum < 0.2 + profit → EXIT with profit
|
|
34
|
+
ADVERSE_THRESHOLD: -0.3, // momentum < -0.3 → EXIT immediately
|
|
35
|
+
VPIN_DANGER: 0.7, // VPIN > 0.7 → informed traders = EXIT
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Momentum weights
|
|
39
|
+
const WEIGHTS = {
|
|
40
|
+
OFI: 0.50, // Order Flow Imbalance - 50%
|
|
41
|
+
KALMAN: 0.25, // Kalman Velocity - 25%
|
|
42
|
+
ZSCORE: 0.25, // Z-Score progression - 25%
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Position state for tracking
|
|
47
|
+
* @typedef {Object} ManagedPosition
|
|
48
|
+
* @property {string} orderTag - Entry order correlation ID
|
|
49
|
+
* @property {string} accountId - Account ID
|
|
50
|
+
* @property {string} symbol - Trading symbol (e.g., NQH5)
|
|
51
|
+
* @property {string} exchange - Exchange (e.g., CME)
|
|
52
|
+
* @property {number} side - 0=Long, 1=Short
|
|
53
|
+
* @property {number} size - Position size
|
|
54
|
+
* @property {number} entryPrice - Average fill price
|
|
55
|
+
* @property {number} entryTime - Entry timestamp (ms)
|
|
56
|
+
* @property {number} fillTime - When fill was confirmed (ms)
|
|
57
|
+
* @property {number} highWaterMark - Highest price since entry (for trailing)
|
|
58
|
+
* @property {number} lowWaterMark - Lowest price since entry (for trailing)
|
|
59
|
+
* @property {string} status - 'pending' | 'active' | 'holding' | 'exiting' | 'closed'
|
|
60
|
+
* @property {boolean} holdComplete - True after MIN_HOLD_MS elapsed
|
|
61
|
+
* @property {Object|null} exitReason - Why position was exited
|
|
62
|
+
* @property {number} tickSize - Tick size from API
|
|
63
|
+
* @property {number} tickValue - Tick value from API
|
|
64
|
+
* @property {string} contractId - Contract ID for strategy lookups
|
|
65
|
+
*/
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Position Manager Service
|
|
69
|
+
* Handles position lifecycle for fast scalping strategy
|
|
70
|
+
*/
|
|
71
|
+
class PositionManager extends EventEmitter {
|
|
72
|
+
/**
|
|
73
|
+
* @param {RithmicService} rithmicService - Connected Rithmic service
|
|
74
|
+
* @param {Object} strategy - HQX Ultra Scalping strategy instance (M1)
|
|
75
|
+
*/
|
|
76
|
+
constructor(rithmicService, strategy = null) {
|
|
77
|
+
super();
|
|
78
|
+
this.rithmic = rithmicService;
|
|
79
|
+
this.strategy = strategy; // Reference to HQX Ultra Scalping strategy
|
|
80
|
+
|
|
81
|
+
/** @type {Map<string, ManagedPosition>} orderTag -> position */
|
|
82
|
+
this.positions = new Map();
|
|
83
|
+
|
|
84
|
+
/** @type {Map<string, number>} symbol -> latest price */
|
|
85
|
+
this.latestPrices = new Map();
|
|
86
|
+
|
|
87
|
+
/** @type {Map<string, Object>} symbol -> contract info (tickSize, tickValue) */
|
|
88
|
+
this.contractInfo = new Map();
|
|
89
|
+
|
|
90
|
+
/** @type {NodeJS.Timer|null} */
|
|
91
|
+
this._monitorInterval = null;
|
|
92
|
+
|
|
93
|
+
/** @type {boolean} */
|
|
94
|
+
this._isRunning = false;
|
|
95
|
+
|
|
96
|
+
// Bind event handlers
|
|
97
|
+
this._onOrderFilled = this._onOrderFilled.bind(this);
|
|
98
|
+
this._onPriceUpdate = this._onPriceUpdate.bind(this);
|
|
99
|
+
this._onPositionUpdate = this._onPositionUpdate.bind(this);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Set the strategy reference (for accessing math models)
|
|
104
|
+
* @param {Object} strategy - HQX Ultra Scalping strategy instance
|
|
105
|
+
*/
|
|
106
|
+
setStrategy(strategy) {
|
|
107
|
+
this.strategy = strategy;
|
|
108
|
+
log.debug('Strategy reference set');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Set contract info from API (tick size, tick value)
|
|
113
|
+
* @param {string} symbol - Trading symbol
|
|
114
|
+
* @param {Object} info - { tickSize, tickValue, contractId }
|
|
115
|
+
*/
|
|
116
|
+
setContractInfo(symbol, info) {
|
|
117
|
+
this.contractInfo.set(symbol, {
|
|
118
|
+
tickSize: info.tickSize,
|
|
119
|
+
tickValue: info.tickValue,
|
|
120
|
+
contractId: info.contractId,
|
|
121
|
+
});
|
|
122
|
+
log.debug('Contract info set', { symbol, tickSize: info.tickSize, tickValue: info.tickValue });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Start the position manager
|
|
127
|
+
* Attaches to Rithmic service events
|
|
128
|
+
*/
|
|
129
|
+
start() {
|
|
130
|
+
if (this._isRunning) return;
|
|
131
|
+
|
|
132
|
+
log.info('Starting position manager', {
|
|
133
|
+
minHoldMs: FAST_SCALPING.MIN_HOLD_MS,
|
|
134
|
+
maxHoldMs: FAST_SCALPING.MAX_HOLD_MS,
|
|
135
|
+
targetTicks: FAST_SCALPING.TARGET_TICKS,
|
|
136
|
+
stopTicks: FAST_SCALPING.STOP_TICKS,
|
|
137
|
+
momentumWeights: WEIGHTS,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Subscribe to Rithmic events
|
|
141
|
+
this.rithmic.on('orderFilled', this._onOrderFilled);
|
|
142
|
+
this.rithmic.on('priceUpdate', this._onPriceUpdate);
|
|
143
|
+
this.rithmic.on('positionUpdate', this._onPositionUpdate);
|
|
144
|
+
|
|
145
|
+
// Start monitoring loop
|
|
146
|
+
this._monitorInterval = setInterval(() => {
|
|
147
|
+
this._monitorPositions();
|
|
148
|
+
}, FAST_SCALPING.MONITOR_INTERVAL_MS);
|
|
149
|
+
|
|
150
|
+
this._isRunning = true;
|
|
151
|
+
log.debug('Position manager started');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Stop the position manager
|
|
156
|
+
* Removes event listeners and stops monitoring
|
|
157
|
+
*/
|
|
158
|
+
stop() {
|
|
159
|
+
if (!this._isRunning) return;
|
|
160
|
+
|
|
161
|
+
log.info('Stopping position manager');
|
|
162
|
+
|
|
163
|
+
// Remove event listeners
|
|
164
|
+
this.rithmic.off('orderFilled', this._onOrderFilled);
|
|
165
|
+
this.rithmic.off('priceUpdate', this._onPriceUpdate);
|
|
166
|
+
this.rithmic.off('positionUpdate', this._onPositionUpdate);
|
|
167
|
+
|
|
168
|
+
// Stop monitoring
|
|
169
|
+
if (this._monitorInterval) {
|
|
170
|
+
clearInterval(this._monitorInterval);
|
|
171
|
+
this._monitorInterval = null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
this._isRunning = false;
|
|
175
|
+
log.debug('Position manager stopped');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Register a new entry order
|
|
180
|
+
* Called immediately after fastEntry() to track the position
|
|
181
|
+
*
|
|
182
|
+
* @param {Object} entryResult - Result from fastEntry()
|
|
183
|
+
* @param {Object} orderData - Original order data
|
|
184
|
+
* @param {Object} contractInfo - { tickSize, tickValue, contractId } from API
|
|
185
|
+
* @returns {string} orderTag for tracking
|
|
186
|
+
*/
|
|
187
|
+
registerEntry(entryResult, orderData, contractInfo = null) {
|
|
188
|
+
if (!entryResult.success) {
|
|
189
|
+
log.warn('Cannot register failed entry', { error: entryResult.error });
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const { orderTag, entryTime, latencyMs } = entryResult;
|
|
194
|
+
|
|
195
|
+
// Get contract info from cache or parameter
|
|
196
|
+
const info = contractInfo || this.contractInfo.get(orderData.symbol) || {
|
|
197
|
+
tickSize: null,
|
|
198
|
+
tickValue: null,
|
|
199
|
+
contractId: orderData.contractId || orderData.symbol,
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
/** @type {ManagedPosition} */
|
|
203
|
+
const position = {
|
|
204
|
+
orderTag,
|
|
205
|
+
accountId: orderData.accountId,
|
|
206
|
+
symbol: orderData.symbol,
|
|
207
|
+
exchange: orderData.exchange || 'CME',
|
|
208
|
+
side: orderData.side, // 0=Long, 1=Short
|
|
209
|
+
size: orderData.size,
|
|
210
|
+
entryPrice: null, // Will be filled from order notification (async)
|
|
211
|
+
entryTime,
|
|
212
|
+
fillTime: null,
|
|
213
|
+
highWaterMark: null,
|
|
214
|
+
lowWaterMark: null,
|
|
215
|
+
status: 'pending', // Waiting for fill confirmation
|
|
216
|
+
holdComplete: false,
|
|
217
|
+
exitReason: null,
|
|
218
|
+
latencyMs,
|
|
219
|
+
// Contract info from API (NOT hardcoded)
|
|
220
|
+
tickSize: info.tickSize,
|
|
221
|
+
tickValue: info.tickValue,
|
|
222
|
+
contractId: info.contractId,
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
this.positions.set(orderTag, position);
|
|
226
|
+
|
|
227
|
+
log.debug('Registered entry', {
|
|
228
|
+
orderTag,
|
|
229
|
+
symbol: orderData.symbol,
|
|
230
|
+
side: orderData.side === 0 ? 'LONG' : 'SHORT',
|
|
231
|
+
size: orderData.size,
|
|
232
|
+
latencyMs,
|
|
233
|
+
tickSize: info.tickSize,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
return orderTag;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Handle order fill notification from Rithmic (templateId: 351)
|
|
241
|
+
* This is ASYNC - does not block fastEntry()
|
|
242
|
+
* @private
|
|
243
|
+
*/
|
|
244
|
+
_onOrderFilled(fillInfo) {
|
|
245
|
+
const { orderTag, avgFillPrice, totalFillQuantity, symbol, transactionType, localTimestamp } = fillInfo;
|
|
246
|
+
|
|
247
|
+
if (!orderTag) return;
|
|
248
|
+
|
|
249
|
+
const position = this.positions.get(orderTag);
|
|
250
|
+
if (!position) {
|
|
251
|
+
// Could be an exit order fill - check if any position is exiting
|
|
252
|
+
for (const [tag, pos] of this.positions) {
|
|
253
|
+
if (pos.status === 'exiting' && pos.symbol === symbol) {
|
|
254
|
+
// This is likely our exit fill
|
|
255
|
+
pos.status = 'closed';
|
|
256
|
+
const holdDuration = Date.now() - pos.fillTime;
|
|
257
|
+
const pnlTicks = this._calculatePnlTicks(pos, avgFillPrice);
|
|
258
|
+
|
|
259
|
+
log.info('EXIT FILLED', {
|
|
260
|
+
orderTag: tag,
|
|
261
|
+
symbol,
|
|
262
|
+
exitPrice: avgFillPrice,
|
|
263
|
+
entryPrice: pos.entryPrice,
|
|
264
|
+
pnlTicks,
|
|
265
|
+
holdDurationMs: holdDuration,
|
|
266
|
+
reason: pos.exitReason,
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
this.emit('exitFilled', {
|
|
270
|
+
orderTag: tag,
|
|
271
|
+
position: pos,
|
|
272
|
+
exitPrice: avgFillPrice,
|
|
273
|
+
pnlTicks,
|
|
274
|
+
holdDurationMs: holdDuration,
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
this.positions.delete(tag);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
log.debug('Fill for untracked order', { orderTag });
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (position.status === 'pending') {
|
|
286
|
+
// Entry fill confirmed - UPDATE with real fill price
|
|
287
|
+
position.entryPrice = avgFillPrice;
|
|
288
|
+
position.fillTime = Date.now();
|
|
289
|
+
position.highWaterMark = avgFillPrice;
|
|
290
|
+
position.lowWaterMark = avgFillPrice;
|
|
291
|
+
position.status = 'holding'; // Now in holding period
|
|
292
|
+
|
|
293
|
+
const fillLatency = position.fillTime - position.entryTime;
|
|
294
|
+
|
|
295
|
+
log.info('ENTRY FILLED', {
|
|
296
|
+
orderTag,
|
|
297
|
+
symbol,
|
|
298
|
+
side: position.side === 0 ? 'LONG' : 'SHORT',
|
|
299
|
+
size: position.size,
|
|
300
|
+
price: avgFillPrice,
|
|
301
|
+
entryLatencyMs: position.latencyMs,
|
|
302
|
+
fillLatencyMs: fillLatency,
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
this.emit('entryFilled', {
|
|
306
|
+
orderTag,
|
|
307
|
+
position,
|
|
308
|
+
fillLatencyMs: fillLatency,
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// Schedule hold completion check
|
|
312
|
+
setTimeout(() => {
|
|
313
|
+
this._onHoldComplete(orderTag);
|
|
314
|
+
}, FAST_SCALPING.MIN_HOLD_MS);
|
|
315
|
+
|
|
316
|
+
// Schedule 60s failsafe (NON-NEGOTIABLE)
|
|
317
|
+
setTimeout(() => {
|
|
318
|
+
this._failsafeExit(orderTag);
|
|
319
|
+
}, FAST_SCALPING.MAX_HOLD_MS);
|
|
320
|
+
|
|
321
|
+
} else if (position.status === 'exiting') {
|
|
322
|
+
// Exit fill confirmed
|
|
323
|
+
position.status = 'closed';
|
|
324
|
+
|
|
325
|
+
const holdDuration = Date.now() - position.fillTime;
|
|
326
|
+
const pnlTicks = this._calculatePnlTicks(position, avgFillPrice);
|
|
327
|
+
|
|
328
|
+
log.info('EXIT FILLED', {
|
|
329
|
+
orderTag,
|
|
330
|
+
symbol,
|
|
331
|
+
exitPrice: avgFillPrice,
|
|
332
|
+
entryPrice: position.entryPrice,
|
|
333
|
+
pnlTicks,
|
|
334
|
+
holdDurationMs: holdDuration,
|
|
335
|
+
reason: position.exitReason,
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
this.emit('exitFilled', {
|
|
339
|
+
orderTag,
|
|
340
|
+
position,
|
|
341
|
+
exitPrice: avgFillPrice,
|
|
342
|
+
pnlTicks,
|
|
343
|
+
holdDurationMs: holdDuration,
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// Clean up
|
|
347
|
+
this.positions.delete(orderTag);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* 60 second failsafe exit (NON-NEGOTIABLE)
|
|
353
|
+
* Forces market exit if position still open
|
|
354
|
+
* @private
|
|
355
|
+
*/
|
|
356
|
+
_failsafeExit(orderTag) {
|
|
357
|
+
const position = this.positions.get(orderTag);
|
|
358
|
+
if (!position) return;
|
|
359
|
+
|
|
360
|
+
// Only force exit if still active (not already exiting/closed)
|
|
361
|
+
if (position.status === 'holding' || position.status === 'active') {
|
|
362
|
+
const currentPrice = this.latestPrices.get(position.symbol);
|
|
363
|
+
const pnlTicks = currentPrice ? this._calculatePnlTicks(position, currentPrice) : 0;
|
|
364
|
+
|
|
365
|
+
log.warn('FAILSAFE EXIT - 60s max hold exceeded', {
|
|
366
|
+
orderTag,
|
|
367
|
+
symbol: position.symbol,
|
|
368
|
+
pnlTicks,
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
this._executeExit(orderTag, {
|
|
372
|
+
type: 'failsafe',
|
|
373
|
+
reason: '60s max hold exceeded (NON-NEGOTIABLE)',
|
|
374
|
+
pnlTicks,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Handle price update from market data
|
|
381
|
+
* @private
|
|
382
|
+
*/
|
|
383
|
+
_onPriceUpdate(priceData) {
|
|
384
|
+
const { symbol, price, timestamp } = priceData;
|
|
385
|
+
|
|
386
|
+
this.latestPrices.set(symbol, price);
|
|
387
|
+
|
|
388
|
+
// Update high/low water marks for active positions
|
|
389
|
+
for (const [orderTag, position] of this.positions) {
|
|
390
|
+
if (position.symbol === symbol && (position.status === 'holding' || position.status === 'active') && position.entryPrice) {
|
|
391
|
+
if (position.side === 0) { // Long
|
|
392
|
+
position.highWaterMark = Math.max(position.highWaterMark, price);
|
|
393
|
+
} else { // Short
|
|
394
|
+
position.lowWaterMark = Math.min(position.lowWaterMark, price);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Handle position update from PNL_PLANT
|
|
402
|
+
* @private
|
|
403
|
+
*/
|
|
404
|
+
_onPositionUpdate(posData) {
|
|
405
|
+
log.debug('Position update from PNL_PLANT', {
|
|
406
|
+
symbol: posData?.symbol,
|
|
407
|
+
qty: posData?.quantity,
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Called when minimum hold period is complete
|
|
413
|
+
* @private
|
|
414
|
+
*/
|
|
415
|
+
_onHoldComplete(orderTag) {
|
|
416
|
+
const position = this.positions.get(orderTag);
|
|
417
|
+
if (!position) return;
|
|
418
|
+
|
|
419
|
+
if (position.status === 'holding') {
|
|
420
|
+
position.holdComplete = true;
|
|
421
|
+
position.status = 'active'; // Now eligible for exit
|
|
422
|
+
|
|
423
|
+
log.info('Hold complete - now monitoring for exit', {
|
|
424
|
+
orderTag,
|
|
425
|
+
symbol: position.symbol,
|
|
426
|
+
entryPrice: position.entryPrice,
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
this.emit('holdComplete', { orderTag, position });
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Main monitoring loop - runs every MONITOR_INTERVAL_MS
|
|
435
|
+
* @private
|
|
436
|
+
*/
|
|
437
|
+
_monitorPositions() {
|
|
438
|
+
const now = Date.now();
|
|
439
|
+
|
|
440
|
+
for (const [orderTag, position] of this.positions) {
|
|
441
|
+
// Skip if not ready to exit
|
|
442
|
+
if (position.status !== 'active') continue;
|
|
443
|
+
|
|
444
|
+
const currentPrice = this.latestPrices.get(position.symbol);
|
|
445
|
+
if (!currentPrice) continue;
|
|
446
|
+
|
|
447
|
+
const holdDuration = now - position.fillTime;
|
|
448
|
+
const pnlTicks = this._calculatePnlTicks(position, currentPrice);
|
|
449
|
+
|
|
450
|
+
// Check exit conditions
|
|
451
|
+
const exitReason = this._checkExitConditions(position, currentPrice, pnlTicks, holdDuration);
|
|
452
|
+
|
|
453
|
+
if (exitReason) {
|
|
454
|
+
this._executeExit(orderTag, exitReason);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Check if position should be exited
|
|
461
|
+
* Uses momentum calculation with OFI/Kalman/Z-Score from strategy
|
|
462
|
+
*
|
|
463
|
+
* Data sources:
|
|
464
|
+
* - pnlTicks: Calculated from entryPrice (Rithmic fill) and currentPrice (market data)
|
|
465
|
+
* - VPIN: Strategy's computeVPIN()
|
|
466
|
+
* - Momentum: Strategy's OFI, Kalman, Z-Score
|
|
467
|
+
*
|
|
468
|
+
* @private
|
|
469
|
+
* @returns {Object|null} Exit reason or null
|
|
470
|
+
*/
|
|
471
|
+
_checkExitConditions(position, currentPrice, pnlTicks, holdDuration) {
|
|
472
|
+
// Cannot evaluate exit conditions without PnL data
|
|
473
|
+
if (pnlTicks === null) {
|
|
474
|
+
log.debug('Cannot check exit - no PnL data', { symbol: position.symbol });
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const targetTicks = FAST_SCALPING.TARGET_TICKS;
|
|
479
|
+
const stopTicks = FAST_SCALPING.STOP_TICKS;
|
|
480
|
+
|
|
481
|
+
// 1. TARGET HIT - Always exit at target
|
|
482
|
+
if (pnlTicks >= targetTicks) {
|
|
483
|
+
return { type: 'target', reason: 'Target reached', pnlTicks };
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// 2. STOP HIT - Always exit at stop
|
|
487
|
+
if (pnlTicks <= -stopTicks) {
|
|
488
|
+
return { type: 'stop', reason: 'Stop loss hit', pnlTicks };
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// 3. VPIN DANGER - Informed traders detected (from strategy)
|
|
492
|
+
const vpin = this._getVPIN(position);
|
|
493
|
+
if (vpin !== null && vpin > MOMENTUM.VPIN_DANGER) {
|
|
494
|
+
return { type: 'vpin', reason: `VPIN spike ${(vpin * 100).toFixed(0)}% - informed traders`, pnlTicks, vpin };
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// 4. TRAILING STOP (only if in profit above threshold)
|
|
498
|
+
if (pnlTicks >= FAST_SCALPING.TRAILING_ACTIVATION_TICKS) {
|
|
499
|
+
const trailingPnl = this._calculateTrailingPnl(position, currentPrice);
|
|
500
|
+
if (trailingPnl !== null && trailingPnl <= -FAST_SCALPING.TRAILING_DISTANCE_TICKS) {
|
|
501
|
+
return { type: 'trailing', reason: 'Trailing stop triggered', pnlTicks, trailingPnl };
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// 5. MOMENTUM-BASED EXIT (using strategy's math models)
|
|
506
|
+
const momentum = this._calculateMomentum(position);
|
|
507
|
+
|
|
508
|
+
if (momentum !== null) {
|
|
509
|
+
// Strong favorable momentum + profit → HOLD (let it run)
|
|
510
|
+
if (momentum > MOMENTUM.STRONG_FAVORABLE && pnlTicks > 4) {
|
|
511
|
+
// Don't exit - momentum is strong in our favor
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Weak momentum + profit → EXIT (secure profit)
|
|
516
|
+
if (momentum < MOMENTUM.WEAK_THRESHOLD && pnlTicks > 0) {
|
|
517
|
+
return { type: 'momentum_weak', reason: 'Weak momentum - securing profit', pnlTicks, momentum };
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Adverse momentum → EXIT immediately
|
|
521
|
+
if (momentum < MOMENTUM.ADVERSE_THRESHOLD) {
|
|
522
|
+
return { type: 'momentum_adverse', reason: 'Adverse momentum detected', pnlTicks, momentum };
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Calculate momentum score using strategy's existing math models
|
|
531
|
+
* Weighted: OFI (50%) + Kalman Velocity (25%) + Z-Score (25%)
|
|
532
|
+
*
|
|
533
|
+
* Data sources:
|
|
534
|
+
* - OFI: Strategy's computeOrderFlowImbalance()
|
|
535
|
+
* - Kalman: Strategy's kalmanStates
|
|
536
|
+
* - Z-Score: Strategy's computeZScore()
|
|
537
|
+
*
|
|
538
|
+
* @private
|
|
539
|
+
* @param {ManagedPosition} position
|
|
540
|
+
* @returns {number|null} Momentum score [-1 to 1], positive = favorable, null if insufficient data
|
|
541
|
+
*/
|
|
542
|
+
_calculateMomentum(position) {
|
|
543
|
+
if (!this.strategy) {
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Get individual model values (all from API/strategy, not invented)
|
|
548
|
+
const ofi = this._getOFI(position);
|
|
549
|
+
const velocity = this._getKalmanVelocity(position);
|
|
550
|
+
const zscore = this._getZScore(position);
|
|
551
|
+
|
|
552
|
+
// Count how many models have data
|
|
553
|
+
let availableModels = 0;
|
|
554
|
+
let totalWeight = 0;
|
|
555
|
+
let weightedSum = 0;
|
|
556
|
+
|
|
557
|
+
// 1. OFI (50%) - Order Flow Imbalance
|
|
558
|
+
if (ofi !== null) {
|
|
559
|
+
// For long: positive OFI = favorable, For short: negative OFI = favorable
|
|
560
|
+
const favorableOfi = position.side === 0 ? ofi : -ofi;
|
|
561
|
+
const ofiScore = Math.min(1, Math.max(-1, favorableOfi));
|
|
562
|
+
weightedSum += ofiScore * WEIGHTS.OFI;
|
|
563
|
+
totalWeight += WEIGHTS.OFI;
|
|
564
|
+
availableModels++;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// 2. Kalman Velocity (25%)
|
|
568
|
+
if (velocity !== null) {
|
|
569
|
+
const tickSize = this._getTickSize(position);
|
|
570
|
+
if (tickSize !== null) {
|
|
571
|
+
// Normalize velocity: favorable direction = positive
|
|
572
|
+
const favorableVelocity = position.side === 0 ? velocity : -velocity;
|
|
573
|
+
// Normalize to [-1, 1]: 4 ticks of velocity = 1.0 score
|
|
574
|
+
const normalizedVelocity = favorableVelocity / (tickSize * 4);
|
|
575
|
+
const velocityScore = Math.min(1, Math.max(-1, normalizedVelocity));
|
|
576
|
+
weightedSum += velocityScore * WEIGHTS.KALMAN;
|
|
577
|
+
totalWeight += WEIGHTS.KALMAN;
|
|
578
|
+
availableModels++;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// 3. Z-Score (25%) - Progression toward mean
|
|
583
|
+
if (zscore !== null) {
|
|
584
|
+
let zscoreScore;
|
|
585
|
+
if (position.side === 0) {
|
|
586
|
+
// Long: entered when Z < -threshold, improving = Z moving toward 0
|
|
587
|
+
// Z > -0.5 means close to mean = favorable
|
|
588
|
+
zscoreScore = zscore > -0.5 ? 0.5 : -0.5;
|
|
589
|
+
} else {
|
|
590
|
+
// Short: entered when Z > threshold, improving = Z moving toward 0
|
|
591
|
+
// Z < 0.5 means close to mean = favorable
|
|
592
|
+
zscoreScore = zscore < 0.5 ? 0.5 : -0.5;
|
|
593
|
+
}
|
|
594
|
+
weightedSum += zscoreScore * WEIGHTS.ZSCORE;
|
|
595
|
+
totalWeight += WEIGHTS.ZSCORE;
|
|
596
|
+
availableModels++;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Need at least 1 model with data to calculate momentum
|
|
600
|
+
if (availableModels === 0 || totalWeight === 0) {
|
|
601
|
+
return null;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Normalize by actual total weight (in case some models unavailable)
|
|
605
|
+
const momentum = weightedSum / totalWeight;
|
|
606
|
+
|
|
607
|
+
// Clamp to [-1, 1]
|
|
608
|
+
return Math.min(1, Math.max(-1, momentum));
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Get OFI (Order Flow Imbalance) from strategy
|
|
613
|
+
* Data source: Strategy's computeOrderFlowImbalance() or getModelValues()
|
|
614
|
+
* @private
|
|
615
|
+
* @returns {number|null} OFI value [-1, 1] or null if unavailable
|
|
616
|
+
*/
|
|
617
|
+
_getOFI(position) {
|
|
618
|
+
if (!this.strategy) return null;
|
|
619
|
+
|
|
620
|
+
const contractId = position.contractId || position.symbol;
|
|
621
|
+
|
|
622
|
+
// Try strategy's computeOrderFlowImbalance (direct calculation from bars)
|
|
623
|
+
if (typeof this.strategy.computeOrderFlowImbalance === 'function') {
|
|
624
|
+
const bars = this.strategy.getBarHistory?.(contractId);
|
|
625
|
+
if (bars && bars.length >= 20) {
|
|
626
|
+
try {
|
|
627
|
+
return this.strategy.computeOrderFlowImbalance(bars);
|
|
628
|
+
} catch (error) {
|
|
629
|
+
log.debug('OFI calculation failed', { error: error.message });
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Try getModelValues (pre-calculated values)
|
|
635
|
+
const modelValues = this.strategy.getModelValues?.(contractId);
|
|
636
|
+
if (modelValues && modelValues.rawOfi !== undefined) {
|
|
637
|
+
return modelValues.rawOfi;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
return null;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Get Kalman velocity from strategy's Kalman filter
|
|
645
|
+
* Data source: Strategy's kalmanStates or kalmanFilterManager
|
|
646
|
+
* @private
|
|
647
|
+
* @returns {number|null} Velocity value or null if unavailable
|
|
648
|
+
*/
|
|
649
|
+
_getKalmanVelocity(position) {
|
|
650
|
+
if (!this.strategy) return null;
|
|
651
|
+
|
|
652
|
+
const contractId = position.contractId || position.symbol;
|
|
653
|
+
|
|
654
|
+
// Try to access kalmanStates from strategy
|
|
655
|
+
if (this.strategy.kalmanStates) {
|
|
656
|
+
const state = this.strategy.kalmanStates.get(contractId);
|
|
657
|
+
if (state && typeof state.estimate === 'number') {
|
|
658
|
+
const currentPrice = this.latestPrices.get(position.symbol);
|
|
659
|
+
if (currentPrice !== undefined && currentPrice !== null) {
|
|
660
|
+
// Velocity = price difference from Kalman estimate
|
|
661
|
+
// Positive = price above estimate (upward momentum)
|
|
662
|
+
return currentPrice - state.estimate;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
return null;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Get Z-Score from strategy
|
|
672
|
+
* Data source: Strategy's computeZScore() or priceBuffer
|
|
673
|
+
* @private
|
|
674
|
+
* @returns {number|null} Z-Score value or null if unavailable
|
|
675
|
+
*/
|
|
676
|
+
_getZScore(position) {
|
|
677
|
+
if (!this.strategy) return null;
|
|
678
|
+
|
|
679
|
+
const contractId = position.contractId || position.symbol;
|
|
680
|
+
|
|
681
|
+
// Try strategy's computeZScore (direct calculation from price buffer)
|
|
682
|
+
if (typeof this.strategy.computeZScore === 'function') {
|
|
683
|
+
const prices = this.strategy.priceBuffer?.get(contractId);
|
|
684
|
+
if (prices && prices.length >= 50) {
|
|
685
|
+
try {
|
|
686
|
+
return this.strategy.computeZScore(prices);
|
|
687
|
+
} catch (error) {
|
|
688
|
+
log.debug('Z-Score calculation failed', { error: error.message });
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
return null;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Get VPIN from strategy
|
|
698
|
+
* Data source: Strategy's computeVPIN() or volumeBuffer
|
|
699
|
+
* @private
|
|
700
|
+
* @returns {number|null} VPIN value [0, 1] or null if unavailable
|
|
701
|
+
*/
|
|
702
|
+
_getVPIN(position) {
|
|
703
|
+
if (!this.strategy) return null;
|
|
704
|
+
|
|
705
|
+
const contractId = position.contractId || position.symbol;
|
|
706
|
+
|
|
707
|
+
// Try strategy's computeVPIN (direct calculation from volume buffer)
|
|
708
|
+
if (typeof this.strategy.computeVPIN === 'function') {
|
|
709
|
+
const volumes = this.strategy.volumeBuffer?.get(contractId);
|
|
710
|
+
if (volumes && volumes.length >= 50) {
|
|
711
|
+
try {
|
|
712
|
+
return this.strategy.computeVPIN(volumes);
|
|
713
|
+
} catch (error) {
|
|
714
|
+
log.debug('VPIN calculation failed', { error: error.message });
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// Try getModelValues (pre-calculated, stored as 1 - vpin for scoring)
|
|
720
|
+
const modelValues = this.strategy.getModelValues?.(contractId);
|
|
721
|
+
if (modelValues && typeof modelValues.vpin === 'number') {
|
|
722
|
+
// modelValues.vpin is normalized score (1 - vpin), convert back
|
|
723
|
+
return 1 - modelValues.vpin;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
return null;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Execute exit order
|
|
731
|
+
* @private
|
|
732
|
+
*/
|
|
733
|
+
_executeExit(orderTag, exitReason) {
|
|
734
|
+
const position = this.positions.get(orderTag);
|
|
735
|
+
if (!position || position.status === 'exiting' || position.status === 'closed') return;
|
|
736
|
+
|
|
737
|
+
position.status = 'exiting';
|
|
738
|
+
position.exitReason = exitReason;
|
|
739
|
+
|
|
740
|
+
log.info('Executing EXIT', {
|
|
741
|
+
orderTag,
|
|
742
|
+
symbol: position.symbol,
|
|
743
|
+
reason: exitReason.reason,
|
|
744
|
+
pnlTicks: exitReason.pnlTicks,
|
|
745
|
+
momentum: exitReason.momentum,
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
// Fire exit order (opposite side)
|
|
749
|
+
const exitSide = position.side === 0 ? 1 : 0; // Reverse: Long->Sell, Short->Buy
|
|
750
|
+
|
|
751
|
+
const exitResult = this.rithmic.fastExit({
|
|
752
|
+
accountId: position.accountId,
|
|
753
|
+
symbol: position.symbol,
|
|
754
|
+
exchange: position.exchange,
|
|
755
|
+
size: position.size,
|
|
756
|
+
side: exitSide,
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
if (exitResult.success) {
|
|
760
|
+
log.debug('Exit order fired', {
|
|
761
|
+
orderTag: position.orderTag,
|
|
762
|
+
exitOrderTag: exitResult.orderTag,
|
|
763
|
+
latencyMs: exitResult.latencyMs,
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
this.emit('exitOrderFired', {
|
|
767
|
+
orderTag,
|
|
768
|
+
exitOrderTag: exitResult.orderTag,
|
|
769
|
+
exitReason,
|
|
770
|
+
latencyMs: exitResult.latencyMs,
|
|
771
|
+
});
|
|
772
|
+
} else {
|
|
773
|
+
log.error('Exit order FAILED', {
|
|
774
|
+
orderTag,
|
|
775
|
+
error: exitResult.error,
|
|
776
|
+
});
|
|
777
|
+
// Reset status to try again next cycle
|
|
778
|
+
position.status = 'active';
|
|
779
|
+
position.exitReason = null;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* Get tick size for position (from API, not hardcoded)
|
|
785
|
+
* @private
|
|
786
|
+
*/
|
|
787
|
+
_getTickSize(position) {
|
|
788
|
+
// First try position's stored tickSize (from API)
|
|
789
|
+
if (position.tickSize !== null && position.tickSize !== undefined) {
|
|
790
|
+
return position.tickSize;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// Then try contract info cache
|
|
794
|
+
const info = this.contractInfo.get(position.symbol);
|
|
795
|
+
if (info && info.tickSize) {
|
|
796
|
+
return info.tickSize;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// Last resort: log warning and return null (will cause issues)
|
|
800
|
+
log.warn('No tick size available for symbol', { symbol: position.symbol });
|
|
801
|
+
return null;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* Get tick value for position (from API, not hardcoded)
|
|
806
|
+
* @private
|
|
807
|
+
*/
|
|
808
|
+
_getTickValue(position) {
|
|
809
|
+
if (position.tickValue !== null && position.tickValue !== undefined) {
|
|
810
|
+
return position.tickValue;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
const info = this.contractInfo.get(position.symbol);
|
|
814
|
+
if (info && info.tickValue) {
|
|
815
|
+
return info.tickValue;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
log.warn('No tick value available for symbol', { symbol: position.symbol });
|
|
819
|
+
return null;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* Calculate P&L in ticks from entry
|
|
824
|
+
* Data source: position.entryPrice (from Rithmic fill), tickSize (from API)
|
|
825
|
+
* @private
|
|
826
|
+
* @returns {number|null} PnL in ticks, null if cannot calculate
|
|
827
|
+
*/
|
|
828
|
+
_calculatePnlTicks(position, currentPrice) {
|
|
829
|
+
if (position.entryPrice === null || position.entryPrice === undefined) {
|
|
830
|
+
return null;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
if (currentPrice === null || currentPrice === undefined) {
|
|
834
|
+
return null;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
const tickSize = this._getTickSize(position);
|
|
838
|
+
if (tickSize === null || tickSize === undefined) {
|
|
839
|
+
log.error('Cannot calculate PnL - no tick size from API', { symbol: position.symbol });
|
|
840
|
+
return null;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
const priceDiff = currentPrice - position.entryPrice;
|
|
844
|
+
const signedDiff = position.side === 0 ? priceDiff : -priceDiff;
|
|
845
|
+
|
|
846
|
+
return Math.round(signedDiff / tickSize);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* Calculate trailing P&L from high/low water mark
|
|
851
|
+
* Data source: position water marks (from price updates), tickSize (from API)
|
|
852
|
+
* @private
|
|
853
|
+
* @returns {number|null} Trailing PnL in ticks, null if cannot calculate
|
|
854
|
+
*/
|
|
855
|
+
_calculateTrailingPnl(position, currentPrice) {
|
|
856
|
+
const tickSize = this._getTickSize(position);
|
|
857
|
+
if (tickSize === null || tickSize === undefined) {
|
|
858
|
+
return null;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
if (position.side === 0) { // Long
|
|
862
|
+
if (position.highWaterMark === null || position.highWaterMark === undefined) {
|
|
863
|
+
return null;
|
|
864
|
+
}
|
|
865
|
+
const dropFromHigh = position.highWaterMark - currentPrice;
|
|
866
|
+
return -Math.round(dropFromHigh / tickSize);
|
|
867
|
+
} else { // Short
|
|
868
|
+
if (position.lowWaterMark === null || position.lowWaterMark === undefined) {
|
|
869
|
+
return null;
|
|
870
|
+
}
|
|
871
|
+
const riseFromLow = currentPrice - position.lowWaterMark;
|
|
872
|
+
return -Math.round(riseFromLow / tickSize);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/**
|
|
877
|
+
* Get all active positions
|
|
878
|
+
* @returns {Array<ManagedPosition>}
|
|
879
|
+
*/
|
|
880
|
+
getActivePositions() {
|
|
881
|
+
return Array.from(this.positions.values()).filter(
|
|
882
|
+
p => p.status === 'holding' || p.status === 'active'
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* Get position by order tag
|
|
888
|
+
* @param {string} orderTag
|
|
889
|
+
* @returns {ManagedPosition|null}
|
|
890
|
+
*/
|
|
891
|
+
getPosition(orderTag) {
|
|
892
|
+
return this.positions.get(orderTag) || null;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/**
|
|
896
|
+
* Check if we can enter a new position
|
|
897
|
+
* (no existing position in same symbol - 1 position at a time)
|
|
898
|
+
* @param {string} symbol
|
|
899
|
+
* @returns {boolean}
|
|
900
|
+
*/
|
|
901
|
+
canEnter(symbol) {
|
|
902
|
+
for (const position of this.positions.values()) {
|
|
903
|
+
if (position.symbol === symbol && position.status !== 'closed') {
|
|
904
|
+
return false;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
return true;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/**
|
|
911
|
+
* Get momentum thresholds (for UI display)
|
|
912
|
+
* @returns {Object}
|
|
913
|
+
*/
|
|
914
|
+
getMomentumThresholds() {
|
|
915
|
+
return { ...MOMENTUM };
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/**
|
|
919
|
+
* Get momentum weights (for UI display)
|
|
920
|
+
* @returns {Object}
|
|
921
|
+
*/
|
|
922
|
+
getMomentumWeights() {
|
|
923
|
+
return { ...WEIGHTS };
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
module.exports = { PositionManager };
|