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