hedgequantx 2.9.19 → 2.9.20

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.
Files changed (39) hide show
  1. package/package.json +1 -1
  2. package/src/app.js +42 -64
  3. package/src/lib/m/hqx-2b.js +7 -0
  4. package/src/lib/m/index.js +138 -0
  5. package/src/lib/m/ultra-scalping.js +7 -0
  6. package/src/menus/connect.js +14 -17
  7. package/src/menus/dashboard.js +58 -76
  8. package/src/pages/accounts.js +38 -49
  9. package/src/pages/algo/copy-trading.js +546 -178
  10. package/src/pages/algo/index.js +18 -75
  11. package/src/pages/algo/one-account.js +322 -57
  12. package/src/pages/algo/ui.js +15 -15
  13. package/src/pages/orders.js +19 -22
  14. package/src/pages/positions.js +19 -22
  15. package/src/pages/stats/index.js +15 -16
  16. package/src/pages/user.js +7 -11
  17. package/src/services/ai-supervision/health.js +35 -47
  18. package/src/services/index.js +1 -9
  19. package/src/services/rithmic/accounts.js +8 -6
  20. package/src/ui/box.js +9 -5
  21. package/src/ui/index.js +5 -18
  22. package/src/ui/menu.js +4 -4
  23. package/src/pages/ai-agents-ui.js +0 -388
  24. package/src/pages/ai-agents.js +0 -494
  25. package/src/pages/ai-models.js +0 -389
  26. package/src/pages/algo/algo-executor.js +0 -307
  27. package/src/pages/algo/copy-executor.js +0 -331
  28. package/src/pages/algo/custom-strategy.js +0 -313
  29. package/src/services/ai-supervision/consensus.js +0 -284
  30. package/src/services/ai-supervision/context.js +0 -275
  31. package/src/services/ai-supervision/directive.js +0 -167
  32. package/src/services/ai-supervision/index.js +0 -359
  33. package/src/services/ai-supervision/parser.js +0 -278
  34. package/src/services/ai-supervision/symbols.js +0 -259
  35. package/src/services/cliproxy/index.js +0 -256
  36. package/src/services/cliproxy/installer.js +0 -111
  37. package/src/services/cliproxy/manager.js +0 -387
  38. package/src/services/llmproxy/index.js +0 -166
  39. package/src/services/llmproxy/manager.js +0 -411
@@ -1,389 +0,0 @@
1
- /**
2
- * AI Models - Fetch from provider APIs
3
- *
4
- * Models are fetched dynamically from each provider's API.
5
- * Exception: MiniMax (no /models API) - see RULES.md for details.
6
- */
7
-
8
- const https = require('https');
9
-
10
- /**
11
- * API endpoints for fetching models
12
- * null = provider doesn't have /models endpoint
13
- */
14
- const API_ENDPOINTS = {
15
- anthropic: 'https://api.anthropic.com/v1/models',
16
- openai: 'https://api.openai.com/v1/models',
17
- google: 'https://generativelanguage.googleapis.com/v1beta/models',
18
- minimax: null, // No /models API - uses MINIMAX_MODELS (see RULES.md exception)
19
- deepseek: 'https://api.deepseek.com/v1/models',
20
- mistral: 'https://api.mistral.ai/v1/models',
21
- xai: 'https://api.x.ai/v1/models',
22
- openrouter: 'https://openrouter.ai/api/v1/models',
23
- };
24
-
25
- /**
26
- * MiniMax Models - EXCEPTION to no-hardcode rule (see RULES.md)
27
- *
28
- * MiniMax does not provide /models API endpoint.
29
- * Confirmed by: OpenCode, Cursor, LiteLLM - all use hardcoded models.
30
- * Source: https://platform.minimax.io/docs/api-reference/text-intro
31
- */
32
- const MINIMAX_MODELS = [
33
- { id: 'MiniMax-M2.1', name: 'MiniMax-M2.1' },
34
- ];
35
-
36
- /**
37
- * Make HTTPS request
38
- * @param {string} url - API URL
39
- * @param {Object} headers - Request headers
40
- * @param {number} timeout - Timeout in ms (default 60000 per RULES.md #15)
41
- * @returns {Promise<Object>} Response data
42
- */
43
- const fetchApi = (url, headers = {}, timeout = 60000) => {
44
- return new Promise((resolve, reject) => {
45
- const urlObj = new URL(url);
46
- const options = {
47
- hostname: urlObj.hostname,
48
- path: urlObj.pathname + urlObj.search,
49
- method: 'GET',
50
- headers: {
51
- 'Content-Type': 'application/json',
52
- ...headers
53
- },
54
- timeout
55
- };
56
-
57
- const req = https.request(options, (res) => {
58
- let data = '';
59
- res.on('data', chunk => data += chunk);
60
- res.on('end', () => {
61
- try {
62
- if (res.statusCode >= 200 && res.statusCode < 300) {
63
- resolve({ success: true, data: JSON.parse(data) });
64
- } else {
65
- resolve({ success: false, error: `HTTP ${res.statusCode}` });
66
- }
67
- } catch (error) {
68
- resolve({ success: false, error: 'Invalid JSON response' });
69
- }
70
- });
71
- });
72
-
73
- req.on('error', (error) => {
74
- resolve({ success: false, error: error.message });
75
- });
76
-
77
- req.on('timeout', () => {
78
- req.destroy();
79
- resolve({ success: false, error: 'Request timeout' });
80
- });
81
-
82
- req.end();
83
- });
84
- };
85
-
86
- /**
87
- * Get auth headers for provider
88
- * @param {string} providerId - Provider ID
89
- * @param {string} apiKey - API key
90
- * @returns {Object} Headers object
91
- */
92
- const getAuthHeaders = (providerId, apiKey) => {
93
- if (!apiKey) return {};
94
-
95
- switch (providerId) {
96
- case 'anthropic':
97
- return { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01' };
98
- case 'openai':
99
- case 'deepseek':
100
- case 'minimax':
101
- case 'xai':
102
- case 'openrouter':
103
- case 'mistral':
104
- return { 'Authorization': `Bearer ${apiKey}` };
105
- case 'google':
106
- return {}; // Google uses query param
107
- default:
108
- return { 'Authorization': `Bearer ${apiKey}` };
109
- }
110
- };
111
-
112
- /**
113
- * Excluded patterns - models NOT suitable for algo trading
114
- * These are image, audio, embedding, moderation models
115
- */
116
- const EXCLUDED_PATTERNS = [
117
- 'whisper', 'tts', 'dall-e', 'embedding', 'embed', 'moderation',
118
- 'image', 'vision', 'audio', 'speech', 'realtime', 'transcription',
119
- 'aqa', 'gecko', 'bison', 'learnlm'
120
- ];
121
-
122
- /**
123
- * Check if model should be excluded (not for algo trading)
124
- * @param {string} modelId - Model ID
125
- * @returns {boolean} True if should be excluded
126
- */
127
- const shouldExcludeModel = (modelId) => {
128
- const id = modelId.toLowerCase();
129
- return EXCLUDED_PATTERNS.some(pattern => id.includes(pattern));
130
- };
131
-
132
- /**
133
- * Extract version number from model ID for sorting
134
- * @param {string} modelId - Model ID
135
- * @returns {number} Version number (higher = newer)
136
- */
137
- const extractVersion = (modelId) => {
138
- const id = modelId.toLowerCase();
139
-
140
- // Gemini: gemini-3 > gemini-2.5 > gemini-2.0
141
- const geminiMatch = id.match(/gemini-(\d+\.?\d*)/);
142
- if (geminiMatch) return parseFloat(geminiMatch[1]) * 100;
143
-
144
- // Claude: opus-4.5 > opus-4 > sonnet-4 > haiku
145
- if (id.includes('opus-4.5') || id.includes('opus-4-5')) return 450;
146
- if (id.includes('opus-4.1') || id.includes('opus-4-1')) return 410;
147
- if (id.includes('opus-4')) return 400;
148
- if (id.includes('sonnet-4.5') || id.includes('sonnet-4-5')) return 350;
149
- if (id.includes('sonnet-4')) return 340;
150
- if (id.includes('haiku-4.5') || id.includes('haiku-4-5')) return 250;
151
- if (id.includes('sonnet-3.7') || id.includes('3-7-sonnet')) return 237;
152
- if (id.includes('sonnet-3.5') || id.includes('3-5-sonnet')) return 235;
153
- if (id.includes('haiku-3.5') || id.includes('3-5-haiku')) return 135;
154
- if (id.includes('opus')) return 300;
155
- if (id.includes('sonnet')) return 200;
156
- if (id.includes('haiku')) return 100;
157
-
158
- // GPT: gpt-4o > gpt-4-turbo > gpt-4 > gpt-3.5
159
- if (id.includes('gpt-4o')) return 450;
160
- if (id.includes('gpt-4-turbo')) return 420;
161
- if (id.includes('gpt-4')) return 400;
162
- if (id.includes('gpt-3.5')) return 350;
163
- if (id.includes('o1')) return 500; // o1 reasoning models
164
- if (id.includes('o3')) return 530; // o3 reasoning models
165
-
166
- // Mistral: large > medium > small
167
- if (id.includes('large')) return 300;
168
- if (id.includes('medium')) return 200;
169
- if (id.includes('small') || id.includes('tiny')) return 100;
170
-
171
- // Default
172
- return 50;
173
- };
174
-
175
- /**
176
- * Get model tier for display (Pro/Flash/Lite)
177
- * @param {string} modelId - Model ID
178
- * @returns {number} Tier weight (higher = more powerful)
179
- */
180
- const getModelTier = (modelId) => {
181
- const id = modelId.toLowerCase();
182
- if (id.includes('pro') || id.includes('opus') || id.includes('large')) return 30;
183
- if (id.includes('flash') || id.includes('sonnet') || id.includes('medium')) return 20;
184
- if (id.includes('lite') || id.includes('haiku') || id.includes('small')) return 10;
185
- return 15;
186
- };
187
-
188
- /**
189
- * Parse models response based on provider - filtered for algo trading
190
- * @param {string} providerId - Provider ID
191
- * @param {Object} data - API response data
192
- * @returns {Array} Parsed and filtered models list
193
- */
194
- const parseModelsResponse = (providerId, data) => {
195
- if (!data) return [];
196
-
197
- try {
198
- let models = [];
199
-
200
- switch (providerId) {
201
- case 'anthropic':
202
- // Anthropic returns { data: [{ id, display_name, ... }] }
203
- models = (data.data || [])
204
- .filter(m => m.id && !shouldExcludeModel(m.id))
205
- .map(m => ({
206
- id: m.id,
207
- name: m.display_name || m.id
208
- }));
209
- break;
210
-
211
- case 'openai':
212
- // OpenAI format: { data: [{ id, ... }] }
213
- models = (data.data || [])
214
- .filter(m => m.id && !shouldExcludeModel(m.id))
215
- .filter(m => m.id.startsWith('gpt-') || m.id.startsWith('o1') || m.id.startsWith('o3'))
216
- .map(m => ({
217
- id: m.id,
218
- name: m.id
219
- }));
220
- break;
221
-
222
- case 'google':
223
- // Google format: { models: [{ name, displayName, supportedGenerationMethods }] }
224
- models = (data.models || [])
225
- .filter(m => {
226
- const id = m.name?.replace('models/', '') || '';
227
- // Only Gemini chat models
228
- return id.startsWith('gemini-') &&
229
- !shouldExcludeModel(id) &&
230
- m.supportedGenerationMethods?.includes('generateContent');
231
- })
232
- .map(m => ({
233
- id: m.name?.replace('models/', '') || m.name,
234
- name: m.displayName || m.name
235
- }));
236
- break;
237
-
238
- case 'deepseek':
239
- // DeepSeek format: { data: [{ id, ... }] } - OpenAI compatible
240
- models = (data.data || [])
241
- .filter(m => m.id && !shouldExcludeModel(m.id))
242
- .filter(m => m.id.includes('deepseek'))
243
- .map(m => ({
244
- id: m.id,
245
- name: m.id
246
- }));
247
- break;
248
-
249
- case 'minimax':
250
- // MiniMax format: { data: [{ id, ... }] } or { models: [...] }
251
- models = (data.data || data.models || [])
252
- .filter(m => (m.id || m.model) && !shouldExcludeModel(m.id || m.model))
253
- .map(m => ({
254
- id: m.id || m.model,
255
- name: m.id || m.model
256
- }));
257
- break;
258
-
259
- case 'xai':
260
- // xAI format: { data: [{ id, ... }] }
261
- models = (data.data || [])
262
- .filter(m => m.id && !shouldExcludeModel(m.id))
263
- .filter(m => m.id.includes('grok'))
264
- .map(m => ({
265
- id: m.id,
266
- name: m.id
267
- }));
268
- break;
269
-
270
- case 'mistral':
271
- // Mistral format: { data: [{ id, ... }] }
272
- models = (data.data || [])
273
- .filter(m => m.id && !shouldExcludeModel(m.id))
274
- .map(m => ({
275
- id: m.id,
276
- name: m.id
277
- }));
278
- break;
279
-
280
- case 'perplexity':
281
- // Perplexity format varies
282
- models = (data.models || data.data || [])
283
- .filter(m => (m.id || m.model) && !shouldExcludeModel(m.id || m.model))
284
- .map(m => ({
285
- id: m.id || m.model,
286
- name: m.id || m.model
287
- }));
288
- break;
289
-
290
- case 'openrouter':
291
- // OpenRouter format: { data: [{ id, name, ... }] }
292
- // Filter to show only main providers' chat models
293
- models = (data.data || [])
294
- .filter(m => {
295
- if (!m.id || shouldExcludeModel(m.id)) return false;
296
- // Only keep major providers for trading
297
- const validPrefixes = [
298
- 'anthropic/claude', 'openai/gpt', 'openai/o1', 'openai/o3',
299
- 'google/gemini', 'mistralai/', 'meta-llama/', 'x-ai/grok'
300
- ];
301
- return validPrefixes.some(p => m.id.startsWith(p));
302
- })
303
- .map(m => ({
304
- id: m.id,
305
- name: m.name || m.id
306
- }));
307
- break;
308
-
309
- default:
310
- return [];
311
- }
312
-
313
- // Sort by version (newest first), then by tier (most powerful first)
314
- return models.sort((a, b) => {
315
- const versionDiff = extractVersion(b.id) - extractVersion(a.id);
316
- if (versionDiff !== 0) return versionDiff;
317
- return getModelTier(b.id) - getModelTier(a.id);
318
- });
319
-
320
- } catch (error) {
321
- return [];
322
- }
323
- };
324
-
325
- /**
326
- * Fetch models from provider API
327
- * @param {string} providerId - Provider ID
328
- * @param {string} apiKey - API key (required for most providers)
329
- * @returns {Promise<Object>} { success, models, error }
330
- */
331
- const fetchModelsFromApi = async (providerId, apiKey) => {
332
- // MiniMax: no /models API, use hardcoded list (see RULES.md exception)
333
- if (providerId === 'minimax') {
334
- return { success: true, models: MINIMAX_MODELS, error: null };
335
- }
336
-
337
- const endpoint = API_ENDPOINTS[providerId];
338
-
339
- if (!endpoint) {
340
- return { success: false, models: [], error: 'Unknown provider' };
341
- }
342
-
343
- // Build URL (Google needs API key in query)
344
- let url = endpoint;
345
- if (providerId === 'google' && apiKey) {
346
- url += `?key=${apiKey}`;
347
- }
348
-
349
- const headers = getAuthHeaders(providerId, apiKey);
350
- const result = await fetchApi(url, headers);
351
-
352
- if (!result.success) {
353
- return { success: false, models: [], error: result.error };
354
- }
355
-
356
- const models = parseModelsResponse(providerId, result.data);
357
-
358
- if (models.length === 0) {
359
- return { success: false, models: [], error: 'No models returned' };
360
- }
361
-
362
- return { success: true, models, error: null };
363
- };
364
-
365
- /**
366
- * Get models for a provider - returns empty, use fetchModelsFromApi
367
- * @param {string} providerId - Provider ID
368
- * @returns {Array} Empty array
369
- */
370
- const getModelsForProvider = (providerId) => {
371
- return [];
372
- };
373
-
374
- /**
375
- * Get model by ID - returns null, use API data
376
- * @param {string} providerId - Provider ID
377
- * @param {string} modelId - Model ID
378
- * @returns {null} Always null
379
- */
380
- const getModelById = (providerId, modelId) => {
381
- return null;
382
- };
383
-
384
- module.exports = {
385
- fetchModelsFromApi,
386
- getModelsForProvider,
387
- getModelById,
388
- API_ENDPOINTS
389
- };
@@ -1,307 +0,0 @@
1
- /**
2
- * Algo Executor - Shared execution engine for all algo modes
3
- * Handles market data, signals, orders, and P&L tracking
4
- * Supports multi-agent AI supervision for signal optimization
5
- */
6
-
7
- const readline = require('readline');
8
- const { AlgoUI, renderSessionSummary } = require('./ui');
9
- const { M1 } = require('../../lib/m/s1');
10
- const { MarketDataFeed } = require('../../lib/data');
11
- const { SupervisionEngine } = require('../../services/ai-supervision');
12
-
13
- /**
14
- * Execute algo strategy with market data
15
- * @param {Object} params - Execution parameters
16
- * @param {Object} params.service - Trading service (Rithmic/ProjectX)
17
- * @param {Object} params.account - Account object
18
- * @param {Object} params.contract - Contract object
19
- * @param {Object} params.config - Algo config (contracts, target, risk, showName)
20
- * @param {Object} params.options - Optional: supervisionConfig for multi-agent AI
21
- */
22
- const executeAlgo = async ({ service, account, contract, config, options = {} }) => {
23
- const { contracts, dailyTarget, maxRisk, showName } = config;
24
- const { supervisionConfig, subtitle } = options;
25
-
26
- // Initialize AI Supervision Engine if configured
27
- const supervisionEnabled = supervisionConfig?.supervisionEnabled && supervisionConfig?.agents?.length > 0;
28
- const supervisionEngine = supervisionEnabled ? new SupervisionEngine(supervisionConfig) : null;
29
-
30
- const accountName = showName
31
- ? (account.accountName || account.rithmicAccountId || account.accountId)
32
- : 'HQX *****';
33
- const symbolName = contract.name;
34
- const contractId = contract.id;
35
- const tickSize = contract.tickSize || 0.25;
36
-
37
- const ui = new AlgoUI({
38
- subtitle: subtitle || (supervisionEnabled ? 'HQX + AI SUPERVISION' : 'HQX Ultra Scalping'),
39
- mode: 'one-account'
40
- });
41
-
42
- const stats = {
43
- accountName,
44
- symbol: symbolName,
45
- qty: contracts,
46
- target: dailyTarget,
47
- risk: maxRisk,
48
- propfirm: account.propfirm || 'Unknown',
49
- platform: account.platform || 'Rithmic',
50
- pnl: 0,
51
- trades: 0,
52
- wins: 0,
53
- losses: 0,
54
- latency: 0,
55
- connected: false,
56
- startTime: Date.now()
57
- };
58
-
59
- let running = true;
60
- let stopReason = null;
61
- let startingPnL = null;
62
- let currentPosition = 0;
63
- let pendingOrder = false;
64
- let tickCount = 0;
65
-
66
- // Context for AI supervision
67
- const aiContext = { recentTicks: [], recentSignals: [], recentTrades: [], maxTicks: 100 };
68
-
69
- // Initialize Strategy
70
- const strategy = new M1({ tickSize });
71
- strategy.initialize(contractId, tickSize);
72
-
73
- // Initialize Market Data Feed
74
- const marketFeed = new MarketDataFeed({ propfirm: account.propfirm });
75
-
76
- // Log startup
77
- ui.addLog('info', `Strategy: ${supervisionEnabled ? 'HQX + AI Supervision' : 'HQX Ultra Scalping'}`);
78
- ui.addLog('info', `Account: ${accountName}`);
79
- ui.addLog('info', `Symbol: ${symbolName} | Qty: ${contracts}`);
80
- ui.addLog('info', `Target: $${dailyTarget} | Risk: $${maxRisk}`);
81
- if (supervisionEnabled) {
82
- const agentCount = supervisionEngine.getActiveCount();
83
- ui.addLog('info', `AI Agents: ${agentCount} active`);
84
- }
85
- ui.addLog('info', 'Connecting to market data...');
86
-
87
- // Handle strategy signals
88
- strategy.on('signal', async (signal) => {
89
- if (!running || pendingOrder || currentPosition !== 0) return;
90
-
91
- let { direction, entry, stopLoss, takeProfit, confidence } = signal;
92
- let orderSize = contracts;
93
-
94
- aiContext.recentSignals.push({ ...signal, timestamp: Date.now() });
95
- if (aiContext.recentSignals.length > 10) aiContext.recentSignals.shift();
96
-
97
- ui.addLog('info', `Signal: ${direction.toUpperCase()} @ ${entry.toFixed(2)} (${(confidence * 100).toFixed(0)}%)`);
98
-
99
- // Multi-Agent AI Supervision
100
- if (supervisionEnabled && supervisionEngine) {
101
- ui.addLog('info', 'AI analyzing signal...');
102
-
103
- const supervisionResult = await supervisionEngine.supervise({
104
- symbolId: symbolName,
105
- signal: { direction, entry, stopLoss, takeProfit, confidence, size: contracts },
106
- recentTicks: aiContext.recentTicks,
107
- recentSignals: aiContext.recentSignals,
108
- recentTrades: aiContext.recentTrades,
109
- stats,
110
- config: { dailyTarget, maxRisk }
111
- });
112
-
113
- if (!supervisionResult.success) {
114
- ui.addLog('info', `AI: ${supervisionResult.reason || 'Error'}`);
115
- } else if (supervisionResult.decision === 'reject') {
116
- ui.addLog('info', `AI rejected (${supervisionResult.confidence}%): ${supervisionResult.reason}`);
117
- return;
118
- } else {
119
- // Apply optimizations
120
- const opt = supervisionResult.optimizedSignal;
121
- if (opt.aiOptimized) {
122
- if (opt.entry !== entry) entry = opt.entry;
123
- if (opt.stopLoss !== stopLoss) stopLoss = opt.stopLoss;
124
- if (opt.takeProfit !== takeProfit) takeProfit = opt.takeProfit;
125
- if (opt.size && opt.size !== contracts) orderSize = opt.size;
126
- }
127
- const action = supervisionResult.decision === 'modify' ? 'optimized' : 'approved';
128
- ui.addLog('info', `AI ${action} (${supervisionResult.confidence}%): ${supervisionResult.reason}`);
129
-
130
- // Check timing
131
- if (opt.aiTiming === 'wait') {
132
- ui.addLog('info', 'AI: Wait for better entry');
133
- return;
134
- } else if (opt.aiTiming === 'cancel') {
135
- ui.addLog('info', 'AI: Signal cancelled');
136
- return;
137
- }
138
- }
139
- }
140
-
141
- // Place order
142
- pendingOrder = true;
143
- try {
144
- const orderSide = direction === 'long' ? 0 : 1;
145
- const orderResult = await service.placeOrder({
146
- accountId: account.accountId,
147
- contractId: contractId,
148
- type: 2,
149
- side: orderSide,
150
- size: orderSize
151
- });
152
-
153
- if (orderResult.success) {
154
- currentPosition = direction === 'long' ? orderSize : -orderSize;
155
- stats.trades++;
156
- ui.addLog('fill_' + (direction === 'long' ? 'buy' : 'sell'),
157
- `OPENED ${direction.toUpperCase()} ${orderSize}x @ market`);
158
-
159
- // Bracket orders
160
- if (stopLoss && takeProfit) {
161
- await service.placeOrder({
162
- accountId: account.accountId, contractId, type: 4,
163
- side: direction === 'long' ? 1 : 0, size: orderSize, stopPrice: stopLoss
164
- });
165
- await service.placeOrder({
166
- accountId: account.accountId, contractId, type: 1,
167
- side: direction === 'long' ? 1 : 0, size: orderSize, limitPrice: takeProfit
168
- });
169
- ui.addLog('info', `SL: ${stopLoss.toFixed(2)} | TP: ${takeProfit.toFixed(2)}`);
170
- }
171
- } else {
172
- ui.addLog('error', `Order failed: ${orderResult.error}`);
173
- }
174
- } catch (e) {
175
- ui.addLog('error', `Order error: ${e.message}`);
176
- }
177
- pendingOrder = false;
178
- });
179
-
180
- // Handle market data ticks
181
- marketFeed.on('tick', (tick) => {
182
- tickCount++;
183
- const latencyStart = Date.now();
184
-
185
- aiContext.recentTicks.push(tick);
186
- if (aiContext.recentTicks.length > aiContext.maxTicks) aiContext.recentTicks.shift();
187
-
188
- strategy.processTick({
189
- contractId: tick.contractId || contractId,
190
- price: tick.price, bid: tick.bid, ask: tick.ask,
191
- volume: tick.volume || 1, side: tick.lastTradeSide || 'unknown',
192
- timestamp: tick.timestamp || Date.now()
193
- });
194
-
195
- stats.latency = Date.now() - latencyStart;
196
- if (tickCount % 100 === 0) ui.addLog('info', `Tick #${tickCount} @ ${tick.price?.toFixed(2) || 'N/A'}`);
197
- });
198
-
199
- marketFeed.on('connected', () => { stats.connected = true; ui.addLog('connected', 'Market data connected!'); });
200
- marketFeed.on('error', (err) => ui.addLog('error', `Market: ${err.message}`));
201
- marketFeed.on('disconnected', () => { stats.connected = false; ui.addLog('error', 'Market disconnected'); });
202
-
203
- // Connect to market data
204
- try {
205
- const token = service.token || service.getToken?.();
206
- const propfirmKey = (account.propfirm || 'topstep').toLowerCase().replace(/\s+/g, '_');
207
- await marketFeed.connect(token, propfirmKey, contractId);
208
- await marketFeed.subscribe(symbolName, contractId);
209
- } catch (e) {
210
- ui.addLog('error', `Failed to connect: ${e.message}`);
211
- }
212
-
213
- // Poll P&L
214
- const pollPnL = async () => {
215
- try {
216
- const accountResult = await service.getTradingAccounts();
217
- if (accountResult.success && accountResult.accounts) {
218
- const acc = accountResult.accounts.find(a => a.accountId === account.accountId);
219
- if (acc && acc.profitAndLoss !== undefined) {
220
- if (startingPnL === null) startingPnL = acc.profitAndLoss;
221
- stats.pnl = acc.profitAndLoss - startingPnL;
222
- if (stats.pnl !== 0) strategy.recordTradeResult(stats.pnl);
223
- }
224
- }
225
-
226
- const posResult = await service.getPositions(account.accountId);
227
- if (posResult.success && posResult.positions) {
228
- const pos = posResult.positions.find(p => {
229
- const sym = p.contractId || p.symbol || '';
230
- return sym.includes(contract.name) || sym.includes(contractId);
231
- });
232
-
233
- if (pos && pos.quantity !== 0) {
234
- currentPosition = pos.quantity;
235
- const pnl = pos.profitAndLoss || 0;
236
- if (pnl > 0) stats.wins = Math.max(stats.wins, 1);
237
- else if (pnl < 0) stats.losses = Math.max(stats.losses, 1);
238
- } else {
239
- currentPosition = 0;
240
- }
241
- }
242
-
243
- if (stats.pnl >= dailyTarget) {
244
- stopReason = 'target'; running = false;
245
- ui.addLog('fill_win', `TARGET REACHED! +$${stats.pnl.toFixed(2)}`);
246
- } else if (stats.pnl <= -maxRisk) {
247
- stopReason = 'risk'; running = false;
248
- ui.addLog('fill_loss', `MAX RISK! -$${Math.abs(stats.pnl).toFixed(2)}`);
249
- }
250
- } catch (e) { /* silent */ }
251
- };
252
-
253
- // Start loops
254
- const refreshInterval = setInterval(() => { if (running) ui.render(stats); }, 250);
255
- const pnlInterval = setInterval(() => { if (running) pollPnL(); }, 2000);
256
- pollPnL();
257
-
258
- // Keyboard handler
259
- const setupKeyHandler = () => {
260
- if (!process.stdin.isTTY) return;
261
- readline.emitKeypressEvents(process.stdin);
262
- process.stdin.setRawMode(true);
263
- process.stdin.resume();
264
-
265
- const onKey = (str, key) => {
266
- if (key && (key.name === 'x' || key.name === 'X' || (key.ctrl && key.name === 'c'))) {
267
- running = false; stopReason = 'manual';
268
- }
269
- };
270
- process.stdin.on('keypress', onKey);
271
- return () => {
272
- process.stdin.removeListener('keypress', onKey);
273
- if (process.stdin.isTTY) process.stdin.setRawMode(false);
274
- };
275
- };
276
-
277
- const cleanupKeys = setupKeyHandler();
278
-
279
- // Wait for stop
280
- await new Promise(resolve => {
281
- const check = setInterval(() => { if (!running) { clearInterval(check); resolve(); } }, 100);
282
- });
283
-
284
- // Cleanup
285
- clearInterval(refreshInterval);
286
- clearInterval(pnlInterval);
287
- await marketFeed.disconnect();
288
- if (cleanupKeys) cleanupKeys();
289
- ui.cleanup();
290
-
291
- if (process.stdin.isTTY) process.stdin.setRawMode(false);
292
- process.stdin.resume();
293
-
294
- // Duration
295
- const durationMs = Date.now() - stats.startTime;
296
- const hours = Math.floor(durationMs / 3600000);
297
- const minutes = Math.floor((durationMs % 3600000) / 60000);
298
- const seconds = Math.floor((durationMs % 60000) / 1000);
299
- stats.duration = hours > 0 ? `${hours}h ${minutes}m ${seconds}s` : minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
300
-
301
- renderSessionSummary(stats, stopReason);
302
-
303
- console.log('\n Returning to menu in 3 seconds...');
304
- await new Promise(resolve => setTimeout(resolve, 3000));
305
- };
306
-
307
- module.exports = { executeAlgo };