hedgequantx 2.6.161 → 2.6.163
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/providers/direct-providers.js +323 -0
- package/src/services/ai/providers/index.js +8 -472
- package/src/services/ai/providers/other-providers.js +104 -0
- 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/supervisor-optimize.js +215 -0
- package/src/services/ai/supervisor-sync.js +178 -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-exit-logic.js +174 -0
- package/src/services/position-manager.js +90 -629
- 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/contracts.js +218 -0
- package/src/services/rithmic/handlers.js +2 -208
- package/src/services/rithmic/index.js +28 -712
- package/src/services/rithmic/latency-tracker.js +182 -0
- package/src/services/rithmic/market-data-decoders.js +229 -0
- package/src/services/rithmic/market-data.js +1 -278
- package/src/services/rithmic/orders-fast.js +246 -0
- package/src/services/rithmic/orders.js +1 -251
- package/src/services/rithmic/proto-decoders.js +403 -0
- package/src/services/rithmic/protobuf.js +7 -443
- package/src/services/rithmic/specs.js +146 -0
- package/src/services/rithmic/trade-history.js +254 -0
- package/src/services/strategy/hft-signal-calc.js +147 -0
- package/src/services/strategy/hft-tick.js +33 -133
- package/src/services/tradovate/index.js +6 -119
- package/src/services/tradovate/orders.js +145 -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 { checkExitConditions, checkBreakevenActivation, calculatePnlTicks } = require('./position-exit-logic');
|
|
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,75 +292,10 @@ class PositionManager extends EventEmitter {
|
|
|
451
292
|
const holdDuration = now - position.fillTime;
|
|
452
293
|
const pnlTicks = this._calculatePnlTicks(position, currentPrice);
|
|
453
294
|
|
|
454
|
-
// Check
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
this._executeExit(orderTag, exitReason);
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
/**
|
|
464
|
-
* Check if position should be exited
|
|
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
|
-
}
|
|
481
|
-
|
|
482
|
-
const targetTicks = FAST_SCALPING.TARGET_TICKS;
|
|
483
|
-
const stopTicks = FAST_SCALPING.STOP_TICKS;
|
|
484
|
-
|
|
485
|
-
// 1. TARGET HIT - Always exit at target
|
|
486
|
-
if (pnlTicks >= targetTicks) {
|
|
487
|
-
return { type: 'target', reason: 'Target reached', pnlTicks };
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
// 2. BREAKEVEN CHECK - If BE is active, use BE price as stop
|
|
491
|
-
if (position.breakevenActive && position.breakevenPrice !== null) {
|
|
492
|
-
const tickSize = this._getTickSize(position);
|
|
493
|
-
if (tickSize) {
|
|
494
|
-
// Check if price hit breakeven stop
|
|
495
|
-
if (position.side === 0) { // Long
|
|
496
|
-
if (currentPrice <= position.breakevenPrice) {
|
|
497
|
-
return { type: 'breakeven', reason: 'Breakeven stop hit', pnlTicks };
|
|
498
|
-
}
|
|
499
|
-
} else { // Short
|
|
500
|
-
if (currentPrice >= position.breakevenPrice) {
|
|
501
|
-
return { type: 'breakeven', reason: 'Breakeven stop hit', pnlTicks };
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
// 3. STOP HIT - Only if BE not active (original stop)
|
|
508
|
-
if (!position.breakevenActive && pnlTicks <= -stopTicks) {
|
|
509
|
-
return { type: 'stop', reason: 'Stop loss hit', pnlTicks };
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
// 4. ACTIVATE BREAKEVEN - Move stop to entry after profit threshold
|
|
513
|
-
if (!position.breakevenActive && pnlTicks >= FAST_SCALPING.BREAKEVEN_ACTIVATION_TICKS) {
|
|
514
|
-
const tickSize = this._getTickSize(position);
|
|
515
|
-
if (tickSize && position.entryPrice) {
|
|
516
|
-
// Set BE price at entry + offset (small profit lock)
|
|
517
|
-
const offset = FAST_SCALPING.BREAKEVEN_OFFSET_TICKS * tickSize;
|
|
518
|
-
if (position.side === 0) { // Long
|
|
519
|
-
position.breakevenPrice = position.entryPrice + offset;
|
|
520
|
-
} else { // Short
|
|
521
|
-
position.breakevenPrice = position.entryPrice - offset;
|
|
522
|
-
}
|
|
295
|
+
// Check for breakeven activation
|
|
296
|
+
const beActivation = checkBreakevenActivation(position, pnlTicks, (p) => this._getTickSize(p));
|
|
297
|
+
if (beActivation) {
|
|
298
|
+
position.breakevenPrice = beActivation.breakevenPrice;
|
|
523
299
|
position.breakevenActive = true;
|
|
524
300
|
|
|
525
301
|
log.info('BREAKEVEN ACTIVATED', {
|
|
@@ -536,250 +312,24 @@ class PositionManager extends EventEmitter {
|
|
|
536
312
|
pnlTicks,
|
|
537
313
|
});
|
|
538
314
|
}
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
// 5. VPIN DANGER - Informed traders detected (from strategy)
|
|
542
|
-
const vpin = this._getVPIN(position);
|
|
543
|
-
if (vpin !== null && vpin > MOMENTUM.VPIN_DANGER) {
|
|
544
|
-
return { type: 'vpin', reason: `VPIN spike ${(vpin * 100).toFixed(0)}% - informed traders`, pnlTicks, vpin };
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
// 6. TRAILING STOP (only if in profit above threshold)
|
|
548
|
-
if (pnlTicks >= FAST_SCALPING.TRAILING_ACTIVATION_TICKS) {
|
|
549
|
-
const trailingPnl = this._calculateTrailingPnl(position, currentPrice);
|
|
550
|
-
if (trailingPnl !== null && trailingPnl <= -FAST_SCALPING.TRAILING_DISTANCE_TICKS) {
|
|
551
|
-
return { type: 'trailing', reason: 'Trailing stop triggered', pnlTicks, trailingPnl };
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
// 7. MOMENTUM-BASED EXIT (using strategy's math models)
|
|
556
|
-
const momentum = this._calculateMomentum(position);
|
|
557
|
-
|
|
558
|
-
if (momentum !== null) {
|
|
559
|
-
// Strong favorable momentum + profit → HOLD (let it run)
|
|
560
|
-
if (momentum > MOMENTUM.STRONG_FAVORABLE && pnlTicks > 4) {
|
|
561
|
-
// Don't exit - momentum is strong in our favor
|
|
562
|
-
return null;
|
|
563
|
-
}
|
|
564
315
|
|
|
565
|
-
//
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
316
|
+
// Check exit conditions
|
|
317
|
+
const exitReason = checkExitConditions({
|
|
318
|
+
position,
|
|
319
|
+
currentPrice,
|
|
320
|
+
pnlTicks,
|
|
321
|
+
holdDuration,
|
|
322
|
+
strategy: this.strategy,
|
|
323
|
+
getTickSize: (p) => this._getTickSize(p),
|
|
324
|
+
latestPrices: this.latestPrices,
|
|
325
|
+
});
|
|
569
326
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
return { type: 'momentum_adverse', reason: 'Adverse momentum detected', pnlTicks, momentum };
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
return null;
|
|
577
|
-
}
|
|
578
|
-
|
|
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
|
-
}
|
|
327
|
+
if (exitReason) {
|
|
328
|
+
this._executeExit(orderTag, exitReason);
|
|
766
329
|
}
|
|
767
330
|
}
|
|
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
331
|
}
|
|
778
332
|
|
|
779
|
-
/**
|
|
780
|
-
* Execute exit order
|
|
781
|
-
* @private
|
|
782
|
-
*/
|
|
783
333
|
_executeExit(orderTag, exitReason) {
|
|
784
334
|
const position = this.positions.get(orderTag);
|
|
785
335
|
if (!position || position.status === 'exiting' || position.status === 'closed') return;
|
|
@@ -795,8 +345,7 @@ class PositionManager extends EventEmitter {
|
|
|
795
345
|
momentum: exitReason.momentum,
|
|
796
346
|
});
|
|
797
347
|
|
|
798
|
-
|
|
799
|
-
const exitSide = position.side === 0 ? 1 : 0; // Reverse: Long->Sell, Short->Buy
|
|
348
|
+
const exitSide = position.side === 0 ? 1 : 0;
|
|
800
349
|
|
|
801
350
|
const exitResult = this.rithmic.fastExit({
|
|
802
351
|
accountId: position.accountId,
|
|
@@ -820,41 +369,26 @@ class PositionManager extends EventEmitter {
|
|
|
820
369
|
latencyMs: exitResult.latencyMs,
|
|
821
370
|
});
|
|
822
371
|
} else {
|
|
823
|
-
log.error('Exit order FAILED', {
|
|
824
|
-
orderTag,
|
|
825
|
-
error: exitResult.error,
|
|
826
|
-
});
|
|
827
|
-
// Reset status to try again next cycle
|
|
372
|
+
log.error('Exit order FAILED', { orderTag, error: exitResult.error });
|
|
828
373
|
position.status = 'active';
|
|
829
374
|
position.exitReason = null;
|
|
830
375
|
}
|
|
831
376
|
}
|
|
832
377
|
|
|
833
|
-
/**
|
|
834
|
-
* Get tick size for position (from API, not hardcoded)
|
|
835
|
-
* @private
|
|
836
|
-
*/
|
|
837
378
|
_getTickSize(position) {
|
|
838
|
-
// First try position's stored tickSize (from API)
|
|
839
379
|
if (position.tickSize !== null && position.tickSize !== undefined) {
|
|
840
380
|
return position.tickSize;
|
|
841
381
|
}
|
|
842
382
|
|
|
843
|
-
// Then try contract info cache
|
|
844
383
|
const info = this.contractInfo.get(position.symbol);
|
|
845
384
|
if (info && info.tickSize) {
|
|
846
385
|
return info.tickSize;
|
|
847
386
|
}
|
|
848
387
|
|
|
849
|
-
// Last resort: log warning and return null (will cause issues)
|
|
850
388
|
log.warn('No tick size available for symbol', { symbol: position.symbol });
|
|
851
389
|
return null;
|
|
852
390
|
}
|
|
853
391
|
|
|
854
|
-
/**
|
|
855
|
-
* Get tick value for position (from API, not hardcoded)
|
|
856
|
-
* @private
|
|
857
|
-
*/
|
|
858
392
|
_getTickValue(position) {
|
|
859
393
|
if (position.tickValue !== null && position.tickValue !== undefined) {
|
|
860
394
|
return position.tickValue;
|
|
@@ -869,85 +403,20 @@ class PositionManager extends EventEmitter {
|
|
|
869
403
|
return null;
|
|
870
404
|
}
|
|
871
405
|
|
|
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
406
|
_calculatePnlTicks(position, currentPrice) {
|
|
879
|
-
|
|
880
|
-
return null;
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
if (currentPrice === null || currentPrice === undefined) {
|
|
884
|
-
return null;
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
const tickSize = this._getTickSize(position);
|
|
888
|
-
if (tickSize === null || tickSize === undefined) {
|
|
889
|
-
log.error('Cannot calculate PnL - no tick size from API', { symbol: position.symbol });
|
|
890
|
-
return null;
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
const priceDiff = currentPrice - position.entryPrice;
|
|
894
|
-
const signedDiff = position.side === 0 ? priceDiff : -priceDiff;
|
|
895
|
-
|
|
896
|
-
return Math.round(signedDiff / tickSize);
|
|
897
|
-
}
|
|
898
|
-
|
|
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
|
-
_calculateTrailingPnl(position, currentPrice) {
|
|
906
|
-
const tickSize = this._getTickSize(position);
|
|
907
|
-
if (tickSize === null || tickSize === undefined) {
|
|
908
|
-
return null;
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
if (position.side === 0) { // Long
|
|
912
|
-
if (position.highWaterMark === null || position.highWaterMark === undefined) {
|
|
913
|
-
return null;
|
|
914
|
-
}
|
|
915
|
-
const dropFromHigh = position.highWaterMark - currentPrice;
|
|
916
|
-
return -Math.round(dropFromHigh / tickSize);
|
|
917
|
-
} else { // Short
|
|
918
|
-
if (position.lowWaterMark === null || position.lowWaterMark === undefined) {
|
|
919
|
-
return null;
|
|
920
|
-
}
|
|
921
|
-
const riseFromLow = currentPrice - position.lowWaterMark;
|
|
922
|
-
return -Math.round(riseFromLow / tickSize);
|
|
923
|
-
}
|
|
407
|
+
return calculatePnlTicks(position, currentPrice, (p) => this._getTickSize(p));
|
|
924
408
|
}
|
|
925
409
|
|
|
926
|
-
/**
|
|
927
|
-
* Get all active positions
|
|
928
|
-
* @returns {Array<ManagedPosition>}
|
|
929
|
-
*/
|
|
930
410
|
getActivePositions() {
|
|
931
411
|
return Array.from(this.positions.values()).filter(
|
|
932
412
|
p => p.status === 'holding' || p.status === 'active'
|
|
933
413
|
);
|
|
934
414
|
}
|
|
935
415
|
|
|
936
|
-
/**
|
|
937
|
-
* Get position by order tag
|
|
938
|
-
* @param {string} orderTag
|
|
939
|
-
* @returns {ManagedPosition|null}
|
|
940
|
-
*/
|
|
941
416
|
getPosition(orderTag) {
|
|
942
417
|
return this.positions.get(orderTag) || null;
|
|
943
418
|
}
|
|
944
419
|
|
|
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
420
|
canEnter(symbol) {
|
|
952
421
|
for (const position of this.positions.values()) {
|
|
953
422
|
if (position.symbol === symbol && position.status !== 'closed') {
|
|
@@ -957,18 +426,10 @@ class PositionManager extends EventEmitter {
|
|
|
957
426
|
return true;
|
|
958
427
|
}
|
|
959
428
|
|
|
960
|
-
/**
|
|
961
|
-
* Get momentum thresholds (for UI display)
|
|
962
|
-
* @returns {Object}
|
|
963
|
-
*/
|
|
964
429
|
getMomentumThresholds() {
|
|
965
430
|
return { ...MOMENTUM };
|
|
966
431
|
}
|
|
967
432
|
|
|
968
|
-
/**
|
|
969
|
-
* Get momentum weights (for UI display)
|
|
970
|
-
* @returns {Object}
|
|
971
|
-
*/
|
|
972
433
|
getMomentumWeights() {
|
|
973
434
|
return { ...WEIGHTS };
|
|
974
435
|
}
|