hedgequantx 2.6.149 → 2.6.151

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hedgequantx",
3
- "version": "2.6.149",
3
+ "version": "2.6.151",
4
4
  "description": "HedgeQuantX - Prop Futures Trading CLI",
5
5
  "main": "src/app.js",
6
6
  "bin": {
@@ -0,0 +1,493 @@
1
+ /**
2
+ * Custom Strategy - AI-Generated Trading Strategy
3
+ *
4
+ * Flow:
5
+ * 1. User describes strategy in natural language
6
+ * 2. Connected AI agent generates the code
7
+ * 3. Agent tests and validates the code
8
+ * 4. User confirms, enters TARGET/RISK
9
+ * 5. Execution same as one-account
10
+ */
11
+
12
+ 'use strict';
13
+
14
+ const chalk = require('chalk');
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+ const os = require('os');
18
+ const readline = require('readline');
19
+
20
+ const { connections } = require('../../services');
21
+ const { AlgoUI, renderSessionSummary, renderMultiSymbolSummary } = require('./ui');
22
+ const { prompts } = require('../../utils');
23
+ const { checkMarketHours } = require('../../services/projectx/market');
24
+ const { FAST_SCALPING } = require('../../config/settings');
25
+ const { PositionManager } = require('../../services/position-manager');
26
+ const { RithmicMarketDataFeed } = require('../../services/rithmic/market-data');
27
+ const { algoLogger } = require('./logger');
28
+ const { recoveryMath } = require('../../services/strategy/recovery-math');
29
+ const aiService = require('../../services/ai');
30
+ const { launchMultiSymbolRithmic } = require('./one-account');
31
+
32
+ // Strategy template that the AI will fill
33
+ const STRATEGY_TEMPLATE = `/**
34
+ * Custom Strategy: {{STRATEGY_NAME}}
35
+ * Generated by AI Agent
36
+ *
37
+ * Description: {{STRATEGY_DESCRIPTION}}
38
+ */
39
+
40
+ const EventEmitter = require('events');
41
+
42
+ class CustomStrategy extends EventEmitter {
43
+ constructor() {
44
+ super();
45
+ this.tickSize = null;
46
+ this.tickValue = null;
47
+ this.contractId = null;
48
+ this.initialized = false;
49
+ this.tickCount = 0;
50
+ this.lastSignalTime = 0;
51
+ this.cooldownMs = 5000;
52
+
53
+ // Strategy-specific state
54
+ {{STRATEGY_STATE}}
55
+ }
56
+
57
+ initialize(contractId, tickSize, tickValue) {
58
+ if (!contractId || tickSize === undefined || tickValue === undefined) {
59
+ throw new Error('Strategy requires contractId, tickSize, and tickValue from API');
60
+ }
61
+ this.contractId = contractId;
62
+ this.tickSize = tickSize;
63
+ this.tickValue = tickValue;
64
+ this.initialized = true;
65
+ }
66
+
67
+ processTick(tick) {
68
+ if (!this.initialized) return;
69
+ this.tickCount++;
70
+
71
+ const { price, bid, ask, volume, side, timestamp } = tick;
72
+
73
+ // Strategy logic
74
+ {{STRATEGY_LOGIC}}
75
+ }
76
+
77
+ _emitSignal(direction, confidence, entry) {
78
+ const now = Date.now();
79
+ if (now - this.lastSignalTime < this.cooldownMs) return;
80
+ this.lastSignalTime = now;
81
+
82
+ const signal = {
83
+ id: \`custom-\${now}\`,
84
+ timestamp: now,
85
+ contractId: this.contractId,
86
+ direction,
87
+ confidence,
88
+ entry,
89
+ };
90
+
91
+ this.emit('signal', signal);
92
+ }
93
+
94
+ getState() {
95
+ return {
96
+ tickCount: this.tickCount,
97
+ {{STRATEGY_GET_STATE}}
98
+ };
99
+ }
100
+
101
+ getModelValues() {
102
+ return this.getState();
103
+ }
104
+
105
+ reset() {
106
+ this.tickCount = 0;
107
+ this.lastSignalTime = 0;
108
+ {{STRATEGY_RESET}}
109
+ }
110
+ }
111
+
112
+ module.exports = { CustomStrategy };
113
+ `;
114
+
115
+ /**
116
+ * Get strategy directory
117
+ */
118
+ function getStrategyDir() {
119
+ const dir = path.join(os.homedir(), '.hqx', 'strategies');
120
+ if (!fs.existsSync(dir)) {
121
+ fs.mkdirSync(dir, { recursive: true });
122
+ }
123
+ return dir;
124
+ }
125
+
126
+ /**
127
+ * Generate strategy code using AI agent
128
+ */
129
+ async function generateStrategyCode(description, agentName) {
130
+ const agent = aiService.getAgents().find(a => a.name === agentName || a.provider === agentName);
131
+ if (!agent) {
132
+ return { success: false, error: 'No AI agent available' };
133
+ }
134
+
135
+ const prompt = `You are a trading strategy code generator. Generate a trading strategy based on this description:
136
+
137
+ "${description}"
138
+
139
+ You must output ONLY valid JavaScript code that fills in these template sections:
140
+
141
+ 1. STRATEGY_NAME: A short name for the strategy (string)
142
+ 2. STRATEGY_DESCRIPTION: One line description (string)
143
+ 3. STRATEGY_STATE: Variable declarations for strategy state (e.g., "this.ema = 0; this.prices = [];")
144
+ 4. STRATEGY_LOGIC: The main logic that processes each tick and calls this._emitSignal(direction, confidence, price) when conditions are met
145
+ - direction: 'long' or 'short'
146
+ - confidence: number between 0 and 1
147
+ - price: the entry price
148
+ 5. STRATEGY_GET_STATE: Return additional state variables (e.g., "ema: this.ema,")
149
+ 6. STRATEGY_RESET: Reset strategy-specific state (e.g., "this.ema = 0; this.prices = [];")
150
+
151
+ Available in processTick:
152
+ - tick.price, tick.bid, tick.ask, tick.volume, tick.side, tick.timestamp
153
+ - this.tickSize, this.tickValue (contract specs)
154
+ - this.tickCount (number of ticks processed)
155
+
156
+ Output format (JSON):
157
+ {
158
+ "STRATEGY_NAME": "...",
159
+ "STRATEGY_DESCRIPTION": "...",
160
+ "STRATEGY_STATE": "...",
161
+ "STRATEGY_LOGIC": "...",
162
+ "STRATEGY_GET_STATE": "...",
163
+ "STRATEGY_RESET": "..."
164
+ }
165
+
166
+ IMPORTANT: Output ONLY the JSON, no markdown, no explanation.`;
167
+
168
+ try {
169
+ const response = await aiService.chat(agentName, [
170
+ { role: 'user', content: prompt }
171
+ ]);
172
+
173
+ if (!response.success) {
174
+ return { success: false, error: response.error || 'AI request failed' };
175
+ }
176
+
177
+ // Parse JSON response
178
+ let strategyParts;
179
+ try {
180
+ // Extract JSON from response (might have markdown)
181
+ let jsonStr = response.content || response.message || '';
182
+ const jsonMatch = jsonStr.match(/\{[\s\S]*\}/);
183
+ if (jsonMatch) {
184
+ jsonStr = jsonMatch[0];
185
+ }
186
+ strategyParts = JSON.parse(jsonStr);
187
+ } catch (parseError) {
188
+ return { success: false, error: `Failed to parse AI response: ${parseError.message}` };
189
+ }
190
+
191
+ // Build the code
192
+ let code = STRATEGY_TEMPLATE;
193
+ code = code.replace('{{STRATEGY_NAME}}', strategyParts.STRATEGY_NAME || 'Custom Strategy');
194
+ code = code.replace('{{STRATEGY_DESCRIPTION}}', strategyParts.STRATEGY_DESCRIPTION || description);
195
+ code = code.replace('{{STRATEGY_STATE}}', strategyParts.STRATEGY_STATE || '');
196
+ code = code.replace('{{STRATEGY_LOGIC}}', strategyParts.STRATEGY_LOGIC || '// No logic generated');
197
+ code = code.replace('{{STRATEGY_GET_STATE}}', strategyParts.STRATEGY_GET_STATE || '');
198
+ code = code.replace('{{STRATEGY_RESET}}', strategyParts.STRATEGY_RESET || '');
199
+
200
+ return {
201
+ success: true,
202
+ code,
203
+ name: strategyParts.STRATEGY_NAME || 'Custom Strategy',
204
+ };
205
+ } catch (error) {
206
+ return { success: false, error: error.message };
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Validate generated strategy code
212
+ */
213
+ async function validateStrategyCode(code, filepath) {
214
+ const errors = [];
215
+
216
+ // 1. Write to file
217
+ try {
218
+ fs.writeFileSync(filepath, code, 'utf8');
219
+ } catch (e) {
220
+ return { success: false, errors: [`Failed to write file: ${e.message}`] };
221
+ }
222
+
223
+ // 2. Syntax check - try to require it
224
+ try {
225
+ // Clear require cache
226
+ delete require.cache[require.resolve(filepath)];
227
+ const mod = require(filepath);
228
+
229
+ if (!mod.CustomStrategy) {
230
+ errors.push('Module does not export CustomStrategy class');
231
+ } else {
232
+ // 3. Instantiation test
233
+ const instance = new mod.CustomStrategy();
234
+
235
+ // 4. Check required methods
236
+ if (typeof instance.initialize !== 'function') {
237
+ errors.push('Missing initialize() method');
238
+ }
239
+ if (typeof instance.processTick !== 'function') {
240
+ errors.push('Missing processTick() method');
241
+ }
242
+ if (typeof instance.getState !== 'function') {
243
+ errors.push('Missing getState() method');
244
+ }
245
+ if (typeof instance.on !== 'function') {
246
+ errors.push('Strategy must extend EventEmitter');
247
+ }
248
+
249
+ // 5. Dry-run with fake tick
250
+ try {
251
+ instance.initialize('TEST', 0.25, 12.50);
252
+ instance.processTick({
253
+ price: 5000,
254
+ bid: 4999.75,
255
+ ask: 5000.25,
256
+ volume: 1,
257
+ side: 'BUY',
258
+ timestamp: Date.now(),
259
+ });
260
+ // Process a few more ticks
261
+ for (let i = 0; i < 10; i++) {
262
+ instance.processTick({
263
+ price: 5000 + (Math.random() - 0.5) * 2,
264
+ bid: 4999.75,
265
+ ask: 5000.25,
266
+ volume: 1,
267
+ side: Math.random() > 0.5 ? 'BUY' : 'SELL',
268
+ timestamp: Date.now(),
269
+ });
270
+ }
271
+ } catch (e) {
272
+ errors.push(`Runtime error in processTick: ${e.message}`);
273
+ }
274
+ }
275
+ } catch (e) {
276
+ errors.push(`Syntax/Import error: ${e.message}`);
277
+ }
278
+
279
+ if (errors.length > 0) {
280
+ return { success: false, errors };
281
+ }
282
+
283
+ return { success: true, errors: [] };
284
+ }
285
+
286
+ /**
287
+ * Custom Strategy Menu
288
+ */
289
+ const customStrategyMenu = async (service) => {
290
+ console.clear();
291
+
292
+ // Check if AI agent is connected
293
+ const agents = aiService.getAgents();
294
+ if (!agents || agents.length === 0) {
295
+ console.log(chalk.red('\n No AI agent connected.'));
296
+ console.log(chalk.gray(' Connect an AI agent in AI SETTINGS first.\n'));
297
+ await prompts.waitForEnter();
298
+ return;
299
+ }
300
+
301
+ const agentName = agents[0].name || agents[0].provider;
302
+ console.log(chalk.cyan('\n ╔════════════════════════════════════════════════════════════╗'));
303
+ console.log(chalk.cyan(' ║') + chalk.yellow.bold(' CUSTOM STRATEGY - AI GENERATED ') + chalk.cyan('║'));
304
+ console.log(chalk.cyan(' ╠════════════════════════════════════════════════════════════╣'));
305
+ console.log(chalk.cyan(' ║') + chalk.gray(` Agent: ${agentName.padEnd(51)}`) + chalk.cyan('║'));
306
+ console.log(chalk.cyan(' ╚════════════════════════════════════════════════════════════╝\n'));
307
+
308
+ // Step 1: Get strategy description from user
309
+ console.log(chalk.yellow(' Describe your trading strategy in natural language:'));
310
+ console.log(chalk.gray(' (Example: "Buy when price crosses above 20-period EMA and RSI < 30")'));
311
+ console.log(chalk.gray(' (Example: "Scalp long when 3 consecutive green ticks with increasing volume")\n'));
312
+
313
+ const description = await prompts.textInput(chalk.cyan(' STRATEGY: '));
314
+ if (!description || description.trim().length < 10) {
315
+ console.log(chalk.red('\n Strategy description too short. Minimum 10 characters.\n'));
316
+ await prompts.waitForEnter();
317
+ return;
318
+ }
319
+
320
+ // Step 2: Generate strategy code
321
+ console.log(chalk.cyan('\n Generating strategy code...'));
322
+ const spinner = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
323
+ let spinnerIdx = 0;
324
+ const spinnerInterval = setInterval(() => {
325
+ process.stdout.write(`\r ${spinner[spinnerIdx++ % spinner.length]} Generating...`);
326
+ }, 100);
327
+
328
+ const genResult = await generateStrategyCode(description, agentName);
329
+ clearInterval(spinnerInterval);
330
+ process.stdout.write('\r \r');
331
+
332
+ if (!genResult.success) {
333
+ console.log(chalk.red(`\n Failed to generate strategy: ${genResult.error}\n`));
334
+ await prompts.waitForEnter();
335
+ return;
336
+ }
337
+
338
+ console.log(chalk.green(` ✓ Strategy "${genResult.name}" generated`));
339
+
340
+ // Step 3: Save and validate
341
+ const timestamp = Date.now();
342
+ const filename = `custom_${timestamp}.js`;
343
+ const filepath = path.join(getStrategyDir(), filename);
344
+
345
+ console.log(chalk.cyan(' Validating strategy...'));
346
+
347
+ const validation = await validateStrategyCode(genResult.code, filepath);
348
+
349
+ if (!validation.success) {
350
+ console.log(chalk.red('\n Strategy validation FAILED:'));
351
+ for (const err of validation.errors) {
352
+ console.log(chalk.red(` - ${err}`));
353
+ }
354
+ console.log(chalk.gray(`\n Code saved to: ${filepath}`));
355
+ console.log(chalk.gray(' You can manually fix and retry.\n'));
356
+ await prompts.waitForEnter();
357
+ return;
358
+ }
359
+
360
+ console.log(chalk.green(' ✓ Strategy validated successfully'));
361
+ console.log(chalk.gray(` Saved to: ${filepath}\n`));
362
+
363
+ // Step 4: Show strategy summary and confirm
364
+ console.log(chalk.cyan(' ╔════════════════════════════════════════════════════════════╗'));
365
+ console.log(chalk.cyan(' ║') + chalk.green.bold(' STRATEGY READY ') + chalk.cyan('║'));
366
+ console.log(chalk.cyan(' ╠════════════════════════════════════════════════════════════╣'));
367
+ console.log(chalk.cyan(' ║') + chalk.white(` Name: ${genResult.name.substring(0, 51).padEnd(51)}`) + chalk.cyan('║'));
368
+ console.log(chalk.cyan(' ║') + chalk.gray(` File: ${filename.padEnd(51)}`) + chalk.cyan('║'));
369
+ console.log(chalk.cyan(' ╚════════════════════════════════════════════════════════════╝\n'));
370
+
371
+ const confirm = await prompts.textInput(chalk.cyan(' Continue with this strategy? (Y/n): '));
372
+ if (confirm.toLowerCase() === 'n') {
373
+ console.log(chalk.yellow('\n Cancelled.\n'));
374
+ await prompts.waitForEnter();
375
+ return;
376
+ }
377
+
378
+ // Step 5: Continue with normal one-account flow
379
+ // Load the custom strategy
380
+ const CustomStrategyModule = require(filepath);
381
+
382
+ // Pass to execution with the custom strategy
383
+ await executeWithCustomStrategy(service, CustomStrategyModule.CustomStrategy, genResult.name);
384
+ };
385
+
386
+ /**
387
+ * Execute trading with custom strategy
388
+ * Same as one-account but uses the custom strategy class
389
+ */
390
+ async function executeWithCustomStrategy(service, StrategyClass, strategyName) {
391
+ // Import one-account's configuration prompts
392
+ const { getLogoWidth, drawBoxHeaderContinue, drawBoxFooter, displayBanner } = require('../../ui');
393
+
394
+ // Get accounts
395
+ const accountsResult = await service.getTradingAccounts();
396
+ if (!accountsResult.success || !accountsResult.accounts?.length) {
397
+ console.log(chalk.red('\n No trading accounts available.\n'));
398
+ await prompts.waitForEnter();
399
+ return;
400
+ }
401
+
402
+ // Account selection
403
+ console.log(chalk.cyan('\n SELECT ACCOUNT:\n'));
404
+ accountsResult.accounts.forEach((acc, i) => {
405
+ const balance = acc.balance !== null ? `$${acc.balance.toLocaleString()}` : 'N/A';
406
+ console.log(chalk.cyan(` [${i + 1}]`) + chalk.white(` ${acc.name || acc.accountId}`) + chalk.gray(` - ${balance}`));
407
+ });
408
+
409
+ const accChoice = await prompts.numberInput('\n ACCOUNT #:', 1, 1, accountsResult.accounts.length);
410
+ if (accChoice === null) return;
411
+ const account = accountsResult.accounts[accChoice - 1];
412
+
413
+ // Symbol selection
414
+ const symbolInput = await prompts.textInput(chalk.cyan(' SYMBOL (e.g., MES, MNQ): '));
415
+ if (!symbolInput) return;
416
+ const symbols = symbolInput.toUpperCase().split(',').map(s => s.trim()).filter(Boolean);
417
+
418
+ // Target and Risk
419
+ const dailyTarget = await prompts.numberInput(' TARGET ($):', 1000, 100, 50000);
420
+ if (dailyTarget === null) return;
421
+
422
+ const maxRisk = await prompts.numberInput(' MAX RISK ($):', 500, 50, 10000);
423
+ if (maxRisk === null) return;
424
+
425
+ // Confirm
426
+ console.log(chalk.cyan('\n ╔════════════════════════════════════════════════════════════╗'));
427
+ console.log(chalk.cyan(' ║') + chalk.yellow.bold(' CONFIRM SETTINGS ') + chalk.cyan('║'));
428
+ console.log(chalk.cyan(' ╠════════════════════════════════════════════════════════════╣'));
429
+ console.log(chalk.cyan(' ║') + chalk.white(` Strategy: ${strategyName.substring(0, 47).padEnd(47)}`) + chalk.cyan('║'));
430
+ console.log(chalk.cyan(' ║') + chalk.white(` Account: ${(account.name || account.accountId).substring(0, 47).padEnd(47)}`) + chalk.cyan('║'));
431
+ console.log(chalk.cyan(' ║') + chalk.white(` Symbols: ${symbols.join(', ').substring(0, 47).padEnd(47)}`) + chalk.cyan('║'));
432
+ console.log(chalk.cyan(' ║') + chalk.green(` Target: $${dailyTarget.toLocaleString().padEnd(46)}`) + chalk.cyan('║'));
433
+ console.log(chalk.cyan(' ║') + chalk.red(` Risk: $${maxRisk.toLocaleString().padEnd(46)}`) + chalk.cyan('║'));
434
+ console.log(chalk.cyan(' ╚════════════════════════════════════════════════════════════╝\n'));
435
+
436
+ const startConfirm = await prompts.textInput(chalk.cyan(' START TRADING? (Y/n): '));
437
+ if (startConfirm.toLowerCase() === 'n') {
438
+ console.log(chalk.yellow('\n Cancelled.\n'));
439
+ await prompts.waitForEnter();
440
+ return;
441
+ }
442
+
443
+ // Launch with custom strategy
444
+ console.log(chalk.green('\n Starting custom strategy trading...\n'));
445
+
446
+ // Get front month contracts for each symbol
447
+ const contracts = [];
448
+ for (const symbol of symbols) {
449
+ // Get front month contract from Rithmic API
450
+ const frontMonth = await service.getFrontMonthContract(symbol);
451
+ if (frontMonth && frontMonth.success) {
452
+ contracts.push({
453
+ name: frontMonth.symbol || symbol,
454
+ symbol: frontMonth.symbol || symbol,
455
+ exchange: frontMonth.exchange || 'CME',
456
+ id: frontMonth.contractId || symbol,
457
+ tickSize: frontMonth.tickSize,
458
+ tickValue: frontMonth.tickValue,
459
+ qty: 1, // Default 1 contract per symbol
460
+ });
461
+ } else {
462
+ // Fallback - use symbol directly
463
+ contracts.push({
464
+ name: symbol,
465
+ symbol: symbol,
466
+ exchange: 'CME',
467
+ id: symbol,
468
+ tickSize: null,
469
+ tickValue: null,
470
+ qty: 1,
471
+ });
472
+ }
473
+ }
474
+
475
+ if (contracts.length === 0) {
476
+ console.log(chalk.red('\n No valid contracts found.\n'));
477
+ await prompts.waitForEnter();
478
+ return;
479
+ }
480
+
481
+ // Config for launchMultiSymbolRithmic
482
+ const config = {
483
+ dailyTarget,
484
+ maxRisk,
485
+ showName: true,
486
+ enableAI: false, // Custom strategy doesn't need AI supervisor (it IS the AI strategy)
487
+ };
488
+
489
+ // Launch with custom strategy class
490
+ await launchMultiSymbolRithmic(service, account, contracts, config, StrategyClass);
491
+ }
492
+
493
+ module.exports = { customStrategyMenu, generateStrategyCode, validateStrategyCode };
@@ -9,6 +9,8 @@ const { checkMarketHours } = require('../../services/projectx/market');
9
9
 
10
10
  const { oneAccountMenu } = require('./one-account');
11
11
  const { copyTradingMenu } = require('./copy-trading');
12
+ const { customStrategyMenu } = require('./custom-strategy');
13
+ const aiService = require('../../services/ai');
12
14
 
13
15
  /**
14
16
  * Algo Trading Menu - Simplified
@@ -59,13 +61,23 @@ const algoTradingMenu = async (service) => {
59
61
  return chalk.cyan('║') + ' ' + content + ' '.repeat(Math.max(0, padding - 1)) + chalk.cyan('║');
60
62
  };
61
63
 
64
+ // Check if AI agent is connected
65
+ const agents = aiService.getAgents();
66
+ const hasAgent = agents && agents.length > 0;
67
+
62
68
  console.clear();
63
69
  displayBanner();
64
70
  drawBoxHeaderContinue('ALGO TRADING', boxWidth);
65
71
 
66
- // Centered menu line: numbers in cyan, text in yellow
67
- const menuText = chalk.cyan('[1]') + chalk.yellow(' ONE ACCOUNT ') + chalk.cyan('[2]') + chalk.yellow(' COPY TRADING');
68
- const plainLen = '[1] ONE ACCOUNT [2] COPY TRADING'.length;
72
+ // Menu options - show CUSTOM STRATEGY only if agent connected
73
+ let menuText, plainLen;
74
+ if (hasAgent) {
75
+ menuText = chalk.cyan('[1]') + chalk.yellow(' ONE ACCOUNT ') + chalk.cyan('[2]') + chalk.yellow(' COPY TRADING ') + chalk.cyan('[3]') + chalk.green(' CUSTOM STRATEGY');
76
+ plainLen = '[1] ONE ACCOUNT [2] COPY TRADING [3] CUSTOM STRATEGY'.length;
77
+ } else {
78
+ menuText = chalk.cyan('[1]') + chalk.yellow(' ONE ACCOUNT ') + chalk.cyan('[2]') + chalk.yellow(' COPY TRADING');
79
+ plainLen = '[1] ONE ACCOUNT [2] COPY TRADING'.length;
80
+ }
69
81
  const leftPad = Math.floor((W - plainLen) / 2);
70
82
  const rightPad = W - plainLen - leftPad;
71
83
  console.log(chalk.cyan('║') + ' '.repeat(leftPad) + menuText + ' '.repeat(rightPad) + chalk.cyan('║'));
@@ -92,6 +104,15 @@ const algoTradingMenu = async (service) => {
92
104
  await prompts.waitForEnter();
93
105
  }
94
106
  return algoTradingMenu(service);
107
+ } else if (choice === '3' && hasAgent) {
108
+ try {
109
+ await customStrategyMenu(service);
110
+ } catch (err) {
111
+ console.log(chalk.red(`\n ERROR: ${err.message}`));
112
+ console.log(chalk.gray(` ${err.stack}`));
113
+ await prompts.waitForEnter();
114
+ }
115
+ return algoTradingMenu(service);
95
116
  }
96
117
 
97
118
  // Empty or other = back
@@ -1428,8 +1428,9 @@ const launchAlgo = async (service, account, contract, config) => {
1428
1428
  * @param {Object} account - Trading account
1429
1429
  * @param {Array} contracts - Array of contracts to trade
1430
1430
  * @param {Object} config - Algo configuration
1431
+ * @param {Function} [CustomStrategyClass] - Optional custom strategy class (if not provided, uses hftStrategy)
1431
1432
  */
1432
- const launchMultiSymbolRithmic = async (service, account, contracts, config) => {
1433
+ const launchMultiSymbolRithmic = async (service, account, contracts, config, CustomStrategyClass = null) => {
1433
1434
  const { dailyTarget, maxRisk, showName, enableAI } = config;
1434
1435
 
1435
1436
  const accountName = showName
@@ -1553,7 +1554,13 @@ const launchMultiSymbolRithmic = async (service, account, contracts, config) =>
1553
1554
  const { tickSize, tickValue, contractId } = contractInfoMap[symbolName];
1554
1555
 
1555
1556
  // Create strategy instance for this symbol
1556
- const strategy = Object.create(hftStrategy);
1557
+ // Use custom strategy class if provided, otherwise use default hftStrategy
1558
+ let strategy;
1559
+ if (CustomStrategyClass) {
1560
+ strategy = new CustomStrategyClass();
1561
+ } else {
1562
+ strategy = Object.create(hftStrategy);
1563
+ }
1557
1564
  if (tickSize !== null && tickValue !== null) {
1558
1565
  strategy.initialize(contractId, tickSize, tickValue);
1559
1566
  }
@@ -2022,78 +2029,58 @@ const launchMultiSymbolRithmic = async (service, account, contracts, config) =>
2022
2029
  ]);
2023
2030
 
2024
2031
  try {
2025
- // Step 1: Cancel all pending orders
2032
+ // CRITICAL: Use ONLY Rithmic API to get REAL positions from exchange
2033
+ // DO NOT use local stats which can be corrupted
2034
+
2035
+ // Step 1: Cancel all pending orders first
2026
2036
  ui.addLog('info', 'Cancelling all orders...');
2027
2037
  ui.render(stats);
2028
2038
  try {
2029
2039
  if (service && typeof service.cancelAllOrders === 'function') {
2030
2040
  await withTimeout(service.cancelAllOrders(rithmicAccountId), TIMEOUT_MS);
2041
+ ui.addLog('success', 'All orders cancelled');
2031
2042
  }
2032
2043
  } catch (e) {
2033
2044
  ui.addLog('warning', `Cancel orders: ${e.message}`);
2034
2045
  }
2035
2046
 
2036
- // Step 2: Force close each symbol's position via fastExit
2037
- ui.addLog('info', 'Closing all positions...');
2047
+ // Step 2: Use Rithmic flattenAll - reads REAL positions from exchange
2048
+ ui.addLog('info', 'Flattening all positions via Rithmic API...');
2038
2049
  ui.render(stats);
2039
2050
 
2040
- for (const [symbolName, symStats] of Object.entries(stats.symbolStats)) {
2041
- // Get position from positionManager (more reliable than stats)
2042
- const pm = positionManagers[symbolName];
2043
- const pmPosition = pm?.currentPosition;
2044
-
2045
- // Use PM position if available, otherwise fallback to stats
2046
- let posQty = 0;
2047
- let closeSide = 0;
2048
-
2049
- if (pmPosition && pmPosition.size > 0) {
2050
- posQty = pmPosition.size;
2051
- closeSide = pmPosition.side === 0 ? 1 : 0; // Sell if long (0), Buy if short (1)
2052
- } else if (symStats.position && symStats.position !== 0) {
2053
- // Fallback: use stats but sanitize the value
2054
- posQty = Math.min(Math.abs(Number(symStats.position) || 0), 10); // Max 10 contracts safety
2055
- if (posQty === 0) posQty = 1; // Default to 1 if we think there's a position
2056
- closeSide = symStats.position > 0 ? 1 : 0;
2057
- }
2058
-
2059
- if (posQty === 0) continue;
2060
-
2061
- const sideStr = closeSide === 1 ? 'SELL' : 'BUY';
2062
-
2063
- ui.addLog('info', `Closing [${symbolName}] ${sideStr} ${posQty}x...`);
2064
- ui.render(stats);
2065
-
2066
- try {
2067
- // Try fastExit first (fastest)
2068
- if (service && typeof service.fastExit === 'function') {
2069
- const result = await withTimeout(service.fastExit({
2070
- accountId: rithmicAccountId,
2071
- symbol: symbolName,
2072
- exchange: contractInfoMap[symbolName]?.exchange || 'CME',
2073
- size: posQty,
2074
- side: closeSide,
2075
- }), TIMEOUT_MS);
2076
-
2077
- if (result.success) {
2078
- ui.addLog('success', `[${symbolName}] FLATTENED`);
2079
- symStats.position = 0;
2080
- } else {
2081
- ui.addLog('error', `[${symbolName}] Exit failed: ${result.error}`);
2051
+ try {
2052
+ if (service && typeof service.flattenAll === 'function') {
2053
+ const result = await withTimeout(service.flattenAll(rithmicAccountId), TIMEOUT_MS);
2054
+ if (result && result.results) {
2055
+ for (const r of result.results) {
2056
+ if (r.success) {
2057
+ ui.addLog('success', `[${r.symbol}] FLATTENED`);
2058
+ } else {
2059
+ ui.addLog('error', `[${r.symbol}] ${r.error || 'Failed'}`);
2060
+ }
2082
2061
  }
2083
2062
  }
2084
- } catch (e) {
2085
- ui.addLog('error', `[${symbolName}] Exit error: ${e.message}`);
2063
+ ui.addLog('success', 'Flatten all complete');
2064
+ } else if (service && typeof service.emergencyStop === 'function') {
2065
+ // Fallback to emergencyStop
2066
+ await withTimeout(service.emergencyStop(rithmicAccountId), TIMEOUT_MS);
2067
+ ui.addLog('success', 'Emergency stop complete');
2086
2068
  }
2069
+ } catch (e) {
2070
+ ui.addLog('warning', `Flatten: ${e.message}`);
2087
2071
  }
2088
2072
 
2089
- // Step 3: Fallback - call service.emergencyStop for any remaining
2090
- try {
2091
- if (service && typeof service.emergencyStop === 'function') {
2092
- await withTimeout(service.emergencyStop(rithmicAccountId), TIMEOUT_MS);
2073
+ // Step 3: Reset local state
2074
+ for (const [symbolName, symStats] of Object.entries(stats.symbolStats)) {
2075
+ symStats.position = 0;
2076
+ symStats.openPnl = 0;
2077
+ // Also reset position manager
2078
+ const pm = positionManagers[symbolName];
2079
+ if (pm) {
2080
+ try { pm.reset?.(); } catch {}
2093
2081
  }
2094
- } catch (e) {
2095
- // Silent - already tried individual closes
2096
2082
  }
2083
+ stats.openPnl = 0;
2097
2084
 
2098
2085
  ui.addLog('success', '████ EMERGENCY STOP COMPLETE ████');
2099
2086
 
@@ -2207,4 +2194,4 @@ const launchMultiSymbolRithmic = async (service, account, contracts, config) =>
2207
2194
  await prompts.waitForEnter();
2208
2195
  };
2209
2196
 
2210
- module.exports = { oneAccountMenu };
2197
+ module.exports = { oneAccountMenu, launchMultiSymbolRithmic };