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
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Rithmic Trade History
|
|
3
|
+
* Handles trade history fetching and P&L calculation
|
|
4
|
+
*
|
|
5
|
+
* NO FAKE DATA - Only real values from Rithmic API
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { logger } = require('../../utils/logger');
|
|
9
|
+
const { getTickMultiplier } = require('./specs');
|
|
10
|
+
|
|
11
|
+
const log = logger.scope('RithmicTradeHistory');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Get trade history from Rithmic API
|
|
15
|
+
* Uses RequestShowOrderHistory to fetch historical fills
|
|
16
|
+
* @param {Object} service - RithmicService instance
|
|
17
|
+
* @param {string} accountId - Optional account filter
|
|
18
|
+
* @param {number} days - Number of days to fetch (default: 30)
|
|
19
|
+
* @returns {Promise<{success: boolean, trades: Array}>}
|
|
20
|
+
*/
|
|
21
|
+
const getTradeHistory = async (service, accountId, days = 30) => {
|
|
22
|
+
if (!service.orderConn || !service.loginInfo) {
|
|
23
|
+
return { success: true, trades: service.completedTrades || [] };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return new Promise((resolve) => {
|
|
27
|
+
const historyOrders = [];
|
|
28
|
+
let resolved = false;
|
|
29
|
+
|
|
30
|
+
// Timeout after 5 seconds
|
|
31
|
+
const timeout = setTimeout(() => {
|
|
32
|
+
if (!resolved) {
|
|
33
|
+
resolved = true;
|
|
34
|
+
cleanup();
|
|
35
|
+
// Combine API history with session trades
|
|
36
|
+
const allTrades = [...processHistoryToTrades(historyOrders, service), ...service.completedTrades];
|
|
37
|
+
resolve({ success: true, trades: allTrades });
|
|
38
|
+
}
|
|
39
|
+
}, 5000);
|
|
40
|
+
|
|
41
|
+
// Listen for order history snapshots
|
|
42
|
+
const onOrderNotification = (data) => {
|
|
43
|
+
try {
|
|
44
|
+
if (data.isSnapshot || data.status === 'complete' || data.status === 'Complete') {
|
|
45
|
+
const order = {
|
|
46
|
+
orderId: data.orderId || data.orderTag,
|
|
47
|
+
accountId: data.accountId,
|
|
48
|
+
symbol: data.symbol,
|
|
49
|
+
exchange: data.exchange,
|
|
50
|
+
side: data.transactionType === 1 ? 0 : 1,
|
|
51
|
+
quantity: data.quantity || data.totalFillQuantity || 0,
|
|
52
|
+
fillPrice: data.avgFillPrice || data.lastFillPrice || 0,
|
|
53
|
+
timestamp: data.ssboe ? data.ssboe * 1000 : Date.now(),
|
|
54
|
+
status: data.status,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
if (order.quantity > 0 && order.fillPrice > 0) {
|
|
58
|
+
historyOrders.push(order);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
} catch (e) {
|
|
62
|
+
// Ignore parse errors
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const onHistoryComplete = () => {
|
|
67
|
+
if (!resolved) {
|
|
68
|
+
resolved = true;
|
|
69
|
+
cleanup();
|
|
70
|
+
const allTrades = [...processHistoryToTrades(historyOrders, service), ...service.completedTrades];
|
|
71
|
+
resolve({ success: true, trades: allTrades });
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const cleanup = () => {
|
|
76
|
+
clearTimeout(timeout);
|
|
77
|
+
service.removeListener('orderNotification', onOrderNotification);
|
|
78
|
+
service.removeListener('orderHistoryComplete', onHistoryComplete);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
service.on('orderNotification', onOrderNotification);
|
|
82
|
+
service.on('orderHistoryComplete', onHistoryComplete);
|
|
83
|
+
|
|
84
|
+
// Request order history for each account
|
|
85
|
+
try {
|
|
86
|
+
const accounts = accountId
|
|
87
|
+
? service.accounts.filter(a => a.accountId === accountId)
|
|
88
|
+
: service.accounts;
|
|
89
|
+
|
|
90
|
+
for (const acc of accounts) {
|
|
91
|
+
service.orderConn.send('RequestShowOrderHistory', {
|
|
92
|
+
templateId: 324,
|
|
93
|
+
userMsg: ['HQX-HISTORY'],
|
|
94
|
+
fcmId: acc.fcmId || service.loginInfo.fcmId,
|
|
95
|
+
ibId: acc.ibId || service.loginInfo.ibId,
|
|
96
|
+
accountId: acc.accountId,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
} catch (e) {
|
|
100
|
+
if (!resolved) {
|
|
101
|
+
resolved = true;
|
|
102
|
+
cleanup();
|
|
103
|
+
resolve({ success: false, error: e.message, trades: service.completedTrades || [] });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Process historical orders into trades with P&L
|
|
111
|
+
* Matches entries and exits to calculate P&L
|
|
112
|
+
* @param {Array} orders - Historical orders
|
|
113
|
+
* @param {Object} service - RithmicService instance (for _getTickMultiplier)
|
|
114
|
+
* @returns {Array} Processed trades
|
|
115
|
+
*/
|
|
116
|
+
const processHistoryToTrades = (orders, service) => {
|
|
117
|
+
const trades = [];
|
|
118
|
+
const openPositions = new Map();
|
|
119
|
+
|
|
120
|
+
// Sort by timestamp (oldest first)
|
|
121
|
+
const sorted = [...orders].sort((a, b) => a.timestamp - b.timestamp);
|
|
122
|
+
|
|
123
|
+
for (const order of sorted) {
|
|
124
|
+
const key = `${order.accountId}:${order.symbol}`;
|
|
125
|
+
const open = openPositions.get(key);
|
|
126
|
+
|
|
127
|
+
if (open && open.side !== order.side) {
|
|
128
|
+
// Closing trade - calculate P&L
|
|
129
|
+
const closeQty = Math.min(order.quantity, open.quantity);
|
|
130
|
+
let pnl;
|
|
131
|
+
|
|
132
|
+
if (open.side === 0) {
|
|
133
|
+
pnl = (order.fillPrice - open.price) * closeQty;
|
|
134
|
+
} else {
|
|
135
|
+
pnl = (open.price - order.fillPrice) * closeQty;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const tickMultiplier = getTickMultiplier(order.symbol);
|
|
139
|
+
pnl = pnl * tickMultiplier;
|
|
140
|
+
|
|
141
|
+
trades.push({
|
|
142
|
+
id: order.orderId,
|
|
143
|
+
accountId: order.accountId,
|
|
144
|
+
symbol: order.symbol,
|
|
145
|
+
exchange: order.exchange,
|
|
146
|
+
side: open.side,
|
|
147
|
+
size: closeQty,
|
|
148
|
+
entryPrice: open.price,
|
|
149
|
+
exitPrice: order.fillPrice,
|
|
150
|
+
price: order.fillPrice,
|
|
151
|
+
timestamp: order.timestamp,
|
|
152
|
+
creationTimestamp: new Date(order.timestamp).toISOString(),
|
|
153
|
+
status: 'CLOSED',
|
|
154
|
+
profitAndLoss: pnl,
|
|
155
|
+
pnl: pnl,
|
|
156
|
+
fees: 0,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const remaining = open.quantity - closeQty;
|
|
160
|
+
if (remaining <= 0) {
|
|
161
|
+
openPositions.delete(key);
|
|
162
|
+
} else {
|
|
163
|
+
open.quantity = remaining;
|
|
164
|
+
}
|
|
165
|
+
} else if (open && open.side === order.side) {
|
|
166
|
+
// Adding to position
|
|
167
|
+
const totalQty = open.quantity + order.quantity;
|
|
168
|
+
open.price = ((open.price * open.quantity) + (order.fillPrice * order.quantity)) / totalQty;
|
|
169
|
+
open.quantity = totalQty;
|
|
170
|
+
} else {
|
|
171
|
+
// Opening new position
|
|
172
|
+
openPositions.set(key, {
|
|
173
|
+
side: order.side,
|
|
174
|
+
quantity: order.quantity,
|
|
175
|
+
price: order.fillPrice,
|
|
176
|
+
timestamp: order.timestamp,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return trades;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Setup order fill listener for real-time P&L tracking
|
|
186
|
+
* @param {Object} service - RithmicService instance
|
|
187
|
+
*/
|
|
188
|
+
const setupOrderFillListener = (service) => {
|
|
189
|
+
service._openEntries = new Map();
|
|
190
|
+
|
|
191
|
+
service.on('orderFilled', (fillInfo) => {
|
|
192
|
+
const key = `${fillInfo.accountId}:${fillInfo.symbol}`;
|
|
193
|
+
const side = fillInfo.transactionType === 1 ? 0 : 1;
|
|
194
|
+
const qty = fillInfo.fillQuantity || fillInfo.totalFillQuantity || 0;
|
|
195
|
+
const price = fillInfo.avgFillPrice || fillInfo.lastFillPrice || 0;
|
|
196
|
+
|
|
197
|
+
const openEntry = service._openEntries.get(key);
|
|
198
|
+
let pnl = null;
|
|
199
|
+
|
|
200
|
+
if (openEntry && openEntry.side !== side) {
|
|
201
|
+
// Closing trade
|
|
202
|
+
const closeQty = Math.min(qty, openEntry.qty);
|
|
203
|
+
|
|
204
|
+
if (openEntry.side === 0) {
|
|
205
|
+
pnl = (price - openEntry.price) * closeQty;
|
|
206
|
+
} else {
|
|
207
|
+
pnl = (openEntry.price - price) * closeQty;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const remainingQty = openEntry.qty - closeQty;
|
|
211
|
+
if (remainingQty <= 0) {
|
|
212
|
+
service._openEntries.delete(key);
|
|
213
|
+
} else {
|
|
214
|
+
openEntry.qty = remainingQty;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
service.completedTrades.push({
|
|
218
|
+
id: fillInfo.orderId || fillInfo.orderTag,
|
|
219
|
+
orderTag: fillInfo.orderTag,
|
|
220
|
+
accountId: fillInfo.accountId,
|
|
221
|
+
symbol: fillInfo.symbol,
|
|
222
|
+
exchange: fillInfo.exchange,
|
|
223
|
+
side: openEntry.side,
|
|
224
|
+
size: closeQty,
|
|
225
|
+
entryPrice: openEntry.price,
|
|
226
|
+
exitPrice: price,
|
|
227
|
+
price: price,
|
|
228
|
+
timestamp: fillInfo.localTimestamp || Date.now(),
|
|
229
|
+
creationTimestamp: new Date().toISOString(),
|
|
230
|
+
status: 'CLOSED',
|
|
231
|
+
profitAndLoss: pnl,
|
|
232
|
+
pnl: pnl,
|
|
233
|
+
fees: 0,
|
|
234
|
+
});
|
|
235
|
+
log.debug('Trade closed', { symbol: fillInfo.symbol, pnl, trades: service.completedTrades.length });
|
|
236
|
+
} else {
|
|
237
|
+
// Opening or adding to position
|
|
238
|
+
if (openEntry && openEntry.side === side) {
|
|
239
|
+
const totalQty = openEntry.qty + qty;
|
|
240
|
+
openEntry.price = ((openEntry.price * openEntry.qty) + (price * qty)) / totalQty;
|
|
241
|
+
openEntry.qty = totalQty;
|
|
242
|
+
} else {
|
|
243
|
+
service._openEntries.set(key, { side, qty, price, timestamp: Date.now() });
|
|
244
|
+
}
|
|
245
|
+
log.debug('Position opened/added', { symbol: fillInfo.symbol, side, qty, price });
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
module.exports = {
|
|
251
|
+
getTradeHistory,
|
|
252
|
+
processHistoryToTrades,
|
|
253
|
+
setupOrderFillListener,
|
|
254
|
+
};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HFT Signal Calculation
|
|
3
|
+
* @module services/strategy/hft-signal-calc
|
|
4
|
+
*
|
|
5
|
+
* Signal generation and building logic for HFT strategy
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Calculate trading signal from all models
|
|
10
|
+
* @param {Object} state - Strategy state
|
|
11
|
+
* @param {Object} config - Strategy config
|
|
12
|
+
* @returns {Object} {direction, confidence, scores}
|
|
13
|
+
*/
|
|
14
|
+
function calculateSignal(state, config) {
|
|
15
|
+
let direction = 'none';
|
|
16
|
+
let confidence = 0;
|
|
17
|
+
|
|
18
|
+
const { ofiValue, zscore, momentum, buyVolume, sellVolume, cumulativeDelta } = state;
|
|
19
|
+
const { ofiThreshold, zscoreThreshold, momentumThreshold, minConfidence } = config;
|
|
20
|
+
|
|
21
|
+
const scores = {
|
|
22
|
+
ofi: 0,
|
|
23
|
+
zscore: 0,
|
|
24
|
+
momentum: 0,
|
|
25
|
+
delta: 0,
|
|
26
|
+
composite: 0,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// === MODEL 1: OFI ===
|
|
30
|
+
const absOfi = Math.abs(ofiValue);
|
|
31
|
+
if (absOfi > ofiThreshold) {
|
|
32
|
+
scores.ofi = Math.min(1.0, absOfi / 0.6);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// === MODEL 2: Z-Score Mean Reversion ===
|
|
36
|
+
const absZ = Math.abs(zscore);
|
|
37
|
+
if (absZ > zscoreThreshold) {
|
|
38
|
+
scores.zscore = Math.min(1.0, absZ / 3.0);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// === MODEL 3: Momentum ===
|
|
42
|
+
const absMom = Math.abs(momentum);
|
|
43
|
+
if (absMom > momentumThreshold) {
|
|
44
|
+
scores.momentum = Math.min(1.0, absMom / 3.0);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// === MODEL 4: Delta ===
|
|
48
|
+
const totalVol = buyVolume + sellVolume;
|
|
49
|
+
if (totalVol > 0) {
|
|
50
|
+
const deltaRatio = cumulativeDelta / totalVol;
|
|
51
|
+
scores.delta = Math.min(1.0, Math.abs(deltaRatio) * 2);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// === COMPOSITE SCORE ===
|
|
55
|
+
scores.composite =
|
|
56
|
+
scores.ofi * 0.35 + // OFI: 35%
|
|
57
|
+
scores.zscore * 0.25 + // Z-Score: 25%
|
|
58
|
+
scores.momentum * 0.20 + // Momentum: 20%
|
|
59
|
+
scores.delta * 0.20; // Delta: 20%
|
|
60
|
+
|
|
61
|
+
confidence = scores.composite;
|
|
62
|
+
|
|
63
|
+
// === DETERMINE DIRECTION ===
|
|
64
|
+
if (scores.composite >= minConfidence) {
|
|
65
|
+
// Primary: Mean reversion
|
|
66
|
+
if (absZ > zscoreThreshold) {
|
|
67
|
+
direction = zscore > 0 ? 'short' : 'long';
|
|
68
|
+
|
|
69
|
+
// Confirm with OFI
|
|
70
|
+
const ofiConfirms =
|
|
71
|
+
(direction === 'long' && ofiValue > 0) ||
|
|
72
|
+
(direction === 'short' && ofiValue < 0);
|
|
73
|
+
|
|
74
|
+
if (ofiConfirms) {
|
|
75
|
+
confidence += 0.1;
|
|
76
|
+
} else if (Math.abs(ofiValue) > 0.2) {
|
|
77
|
+
confidence -= 0.15;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Fallback: Momentum breakout
|
|
81
|
+
else if (absMom > momentumThreshold * 2 && absOfi > ofiThreshold) {
|
|
82
|
+
direction = momentum > 0 ? 'long' : 'short';
|
|
83
|
+
if ((direction === 'long' && ofiValue < 0) ||
|
|
84
|
+
(direction === 'short' && ofiValue > 0)) {
|
|
85
|
+
direction = 'none';
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
confidence = Math.min(1.0, Math.max(0, confidence));
|
|
91
|
+
|
|
92
|
+
return { direction, confidence, scores };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Build signal object
|
|
97
|
+
* @param {string} direction
|
|
98
|
+
* @param {number} confidence
|
|
99
|
+
* @param {Object} scores
|
|
100
|
+
* @param {Object} tick
|
|
101
|
+
* @param {Object} state
|
|
102
|
+
* @param {Object} config
|
|
103
|
+
* @returns {Object} Signal object
|
|
104
|
+
*/
|
|
105
|
+
function buildSignal(direction, confidence, scores, tick, state, config) {
|
|
106
|
+
const entry = tick.price;
|
|
107
|
+
const isLong = direction === 'long';
|
|
108
|
+
|
|
109
|
+
const { std, ofiValue, zscore, momentum, cumulativeDelta, tickCount, signalCount, contractId, tickSize } = state;
|
|
110
|
+
const { baseStopTicks, baseTargetTicks } = config;
|
|
111
|
+
|
|
112
|
+
// Adaptive stops based on volatility
|
|
113
|
+
const volMult = Math.max(0.5, Math.min(2.0, std / tickSize / 4));
|
|
114
|
+
const stopTicks = Math.round(baseStopTicks * volMult);
|
|
115
|
+
const targetTicks = Math.round(baseTargetTicks * volMult);
|
|
116
|
+
|
|
117
|
+
const stopLoss = isLong
|
|
118
|
+
? entry - stopTicks * tickSize
|
|
119
|
+
: entry + stopTicks * tickSize;
|
|
120
|
+
|
|
121
|
+
const takeProfit = isLong
|
|
122
|
+
? entry + targetTicks * tickSize
|
|
123
|
+
: entry - targetTicks * tickSize;
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
id: `hft-${Date.now()}-${signalCount}`,
|
|
127
|
+
timestamp: Date.now(),
|
|
128
|
+
contractId,
|
|
129
|
+
direction,
|
|
130
|
+
side: isLong ? 0 : 1,
|
|
131
|
+
entry,
|
|
132
|
+
stopLoss,
|
|
133
|
+
takeProfit,
|
|
134
|
+
confidence,
|
|
135
|
+
scores,
|
|
136
|
+
ofi: ofiValue,
|
|
137
|
+
zscore,
|
|
138
|
+
momentum,
|
|
139
|
+
delta: cumulativeDelta,
|
|
140
|
+
tickCount,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
module.exports = {
|
|
145
|
+
calculateSignal,
|
|
146
|
+
buildSignal,
|
|
147
|
+
};
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
const EventEmitter = require('events');
|
|
22
22
|
const { logger } = require('../../utils/logger');
|
|
23
|
+
const { calculateSignal, buildSignal } = require('./hft-signal-calc');
|
|
23
24
|
|
|
24
25
|
const log = logger.scope('HFT');
|
|
25
26
|
|
|
@@ -306,23 +307,49 @@ class HFTTickStrategy extends EventEmitter {
|
|
|
306
307
|
* Check for trading signal
|
|
307
308
|
*/
|
|
308
309
|
_checkSignal(tick) {
|
|
309
|
-
// Need minimum data
|
|
310
310
|
if (this.tickCount < 50) return;
|
|
311
311
|
|
|
312
|
-
// Cooldown check
|
|
313
312
|
const now = Date.now();
|
|
314
313
|
if (now - this.lastSignalTime < this.cooldownMs) return;
|
|
315
314
|
|
|
316
|
-
|
|
317
|
-
|
|
315
|
+
const state = {
|
|
316
|
+
ofiValue: this.ofiValue,
|
|
317
|
+
zscore: this.zscore,
|
|
318
|
+
momentum: this.momentum,
|
|
319
|
+
buyVolume: this.buyVolume,
|
|
320
|
+
sellVolume: this.sellVolume,
|
|
321
|
+
cumulativeDelta: this.cumulativeDelta,
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const config = {
|
|
325
|
+
ofiThreshold: this.ofiThreshold,
|
|
326
|
+
zscoreThreshold: this.zscoreThreshold,
|
|
327
|
+
momentumThreshold: this.momentumThreshold,
|
|
328
|
+
minConfidence: this.minConfidence,
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
const { direction, confidence, scores } = calculateSignal(state, config);
|
|
318
332
|
|
|
319
333
|
if (direction === 'none' || confidence < this.minConfidence) return;
|
|
320
334
|
|
|
321
|
-
// Generate signal
|
|
322
335
|
this.signalCount++;
|
|
323
336
|
this.lastSignalTime = now;
|
|
324
337
|
|
|
325
|
-
const
|
|
338
|
+
const signalState = {
|
|
339
|
+
...state,
|
|
340
|
+
std: this.std,
|
|
341
|
+
tickCount: this.tickCount,
|
|
342
|
+
signalCount: this.signalCount,
|
|
343
|
+
contractId: this.contractId,
|
|
344
|
+
tickSize: this.tickSize,
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const signalConfig = {
|
|
348
|
+
baseStopTicks: this.baseStopTicks,
|
|
349
|
+
baseTargetTicks: this.baseTargetTicks,
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
const signal = buildSignal(direction, confidence, scores, tick, signalState, signalConfig);
|
|
326
353
|
|
|
327
354
|
log.info(`SIGNAL #${this.signalCount}: ${direction.toUpperCase()} @ ${tick.price} | ` +
|
|
328
355
|
`OFI=${this.ofiValue.toFixed(2)} Z=${this.zscore.toFixed(2)} Mom=${this.momentum.toFixed(1)} | ` +
|
|
@@ -331,133 +358,6 @@ class HFTTickStrategy extends EventEmitter {
|
|
|
331
358
|
this.emit('signal', signal);
|
|
332
359
|
}
|
|
333
360
|
|
|
334
|
-
/**
|
|
335
|
-
* Calculate trading signal from all models
|
|
336
|
-
*/
|
|
337
|
-
_calculateSignal() {
|
|
338
|
-
let direction = 'none';
|
|
339
|
-
let confidence = 0;
|
|
340
|
-
|
|
341
|
-
const scores = {
|
|
342
|
-
ofi: 0,
|
|
343
|
-
zscore: 0,
|
|
344
|
-
momentum: 0,
|
|
345
|
-
delta: 0,
|
|
346
|
-
composite: 0,
|
|
347
|
-
};
|
|
348
|
-
|
|
349
|
-
// === MODEL 1: OFI ===
|
|
350
|
-
const absOfi = Math.abs(this.ofiValue);
|
|
351
|
-
if (absOfi > this.ofiThreshold) {
|
|
352
|
-
scores.ofi = Math.min(1.0, absOfi / 0.6);
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
// === MODEL 2: Z-Score Mean Reversion ===
|
|
356
|
-
const absZ = Math.abs(this.zscore);
|
|
357
|
-
if (absZ > this.zscoreThreshold) {
|
|
358
|
-
scores.zscore = Math.min(1.0, absZ / 3.0);
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// === MODEL 3: Momentum ===
|
|
362
|
-
const absMom = Math.abs(this.momentum);
|
|
363
|
-
if (absMom > this.momentumThreshold) {
|
|
364
|
-
scores.momentum = Math.min(1.0, absMom / 3.0);
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
// === MODEL 4: Delta ===
|
|
368
|
-
const totalVol = this.buyVolume + this.sellVolume;
|
|
369
|
-
if (totalVol > 0) {
|
|
370
|
-
const deltaRatio = this.cumulativeDelta / totalVol;
|
|
371
|
-
scores.delta = Math.min(1.0, Math.abs(deltaRatio) * 2);
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// === COMPOSITE SCORE ===
|
|
375
|
-
scores.composite =
|
|
376
|
-
scores.ofi * 0.35 + // OFI: 35%
|
|
377
|
-
scores.zscore * 0.25 + // Z-Score: 25%
|
|
378
|
-
scores.momentum * 0.20 + // Momentum: 20%
|
|
379
|
-
scores.delta * 0.20; // Delta: 20%
|
|
380
|
-
|
|
381
|
-
confidence = scores.composite;
|
|
382
|
-
|
|
383
|
-
// === DETERMINE DIRECTION ===
|
|
384
|
-
// Mean reversion: go opposite to z-score
|
|
385
|
-
// Momentum: confirm with OFI and delta
|
|
386
|
-
|
|
387
|
-
if (scores.composite >= this.minConfidence) {
|
|
388
|
-
// Primary: Mean reversion
|
|
389
|
-
if (absZ > this.zscoreThreshold) {
|
|
390
|
-
direction = this.zscore > 0 ? 'short' : 'long';
|
|
391
|
-
|
|
392
|
-
// Confirm with OFI
|
|
393
|
-
const ofiConfirms =
|
|
394
|
-
(direction === 'long' && this.ofiValue > 0) ||
|
|
395
|
-
(direction === 'short' && this.ofiValue < 0);
|
|
396
|
-
|
|
397
|
-
if (ofiConfirms) {
|
|
398
|
-
confidence += 0.1;
|
|
399
|
-
} else if (Math.abs(this.ofiValue) > 0.2) {
|
|
400
|
-
// OFI contradicts - reduce confidence
|
|
401
|
-
confidence -= 0.15;
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
// Fallback: Momentum breakout
|
|
405
|
-
else if (absMom > this.momentumThreshold * 2 && absOfi > this.ofiThreshold) {
|
|
406
|
-
direction = this.momentum > 0 ? 'long' : 'short';
|
|
407
|
-
// Must be confirmed by OFI
|
|
408
|
-
if ((direction === 'long' && this.ofiValue < 0) ||
|
|
409
|
-
(direction === 'short' && this.ofiValue > 0)) {
|
|
410
|
-
direction = 'none';
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
confidence = Math.min(1.0, Math.max(0, confidence));
|
|
416
|
-
|
|
417
|
-
return { direction, confidence, scores };
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
/**
|
|
421
|
-
* Build signal object
|
|
422
|
-
*/
|
|
423
|
-
_buildSignal(direction, confidence, scores, tick) {
|
|
424
|
-
const entry = tick.price;
|
|
425
|
-
const isLong = direction === 'long';
|
|
426
|
-
|
|
427
|
-
// Adaptive stops based on volatility (std dev)
|
|
428
|
-
const volMult = Math.max(0.5, Math.min(2.0, this.std / this.tickSize / 4));
|
|
429
|
-
const stopTicks = Math.round(this.baseStopTicks * volMult);
|
|
430
|
-
const targetTicks = Math.round(this.baseTargetTicks * volMult);
|
|
431
|
-
|
|
432
|
-
const stopLoss = isLong
|
|
433
|
-
? entry - stopTicks * this.tickSize
|
|
434
|
-
: entry + stopTicks * this.tickSize;
|
|
435
|
-
|
|
436
|
-
const takeProfit = isLong
|
|
437
|
-
? entry + targetTicks * this.tickSize
|
|
438
|
-
: entry - targetTicks * this.tickSize;
|
|
439
|
-
|
|
440
|
-
return {
|
|
441
|
-
id: `hft-${Date.now()}-${this.signalCount}`,
|
|
442
|
-
timestamp: Date.now(),
|
|
443
|
-
contractId: this.contractId,
|
|
444
|
-
direction,
|
|
445
|
-
side: isLong ? 0 : 1,
|
|
446
|
-
entry,
|
|
447
|
-
stopLoss,
|
|
448
|
-
takeProfit,
|
|
449
|
-
confidence,
|
|
450
|
-
scores,
|
|
451
|
-
// Model values for logging
|
|
452
|
-
ofi: this.ofiValue,
|
|
453
|
-
zscore: this.zscore,
|
|
454
|
-
momentum: this.momentum,
|
|
455
|
-
delta: this.cumulativeDelta,
|
|
456
|
-
// Tick count
|
|
457
|
-
tickCount: this.tickCount,
|
|
458
|
-
};
|
|
459
|
-
}
|
|
460
|
-
|
|
461
361
|
/**
|
|
462
362
|
* Get current model values for monitoring
|
|
463
363
|
*/
|