hedgequantx 2.7.86 → 2.7.88

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.7.86",
3
+ "version": "2.7.88",
4
4
  "description": "HedgeQuantX - Prop Futures Trading CLI",
5
5
  "main": "src/app.js",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  /**
2
- * @fileoverview Copy Trading Mode
3
- * @module pages/algo/copy-trading
2
+ * Copy Trading Mode - HQX Ultra Scalping
3
+ * Same as One Account but copies trades to multiple followers
4
4
  */
5
5
 
6
6
  const chalk = require('chalk');
@@ -9,18 +9,18 @@ const readline = require('readline');
9
9
 
10
10
  const { connections } = require('../../services');
11
11
  const { AlgoUI, renderSessionSummary } = require('./ui');
12
- const { logger, prompts } = require('../../utils');
12
+ const { prompts } = require('../../utils');
13
13
  const { checkMarketHours } = require('../../services/rithmic/market');
14
14
 
15
- const log = logger.scope('CopyTrading');
15
+ // Strategy & Market Data
16
+ const { M1 } = require('../../lib/m/s1');
17
+ const { MarketDataFeed } = require('../../lib/data');
16
18
 
17
19
  /**
18
20
  * Copy Trading Menu
19
21
  */
20
22
  const copyTradingMenu = async () => {
21
- log.info('Copy Trading menu opened');
22
-
23
- // Check market hours
23
+ // Check if market is open
24
24
  const market = checkMarketHours();
25
25
  if (!market.isOpen && !market.message.includes('early')) {
26
26
  console.log();
@@ -30,326 +30,372 @@ const copyTradingMenu = async () => {
30
30
  await prompts.waitForEnter();
31
31
  return;
32
32
  }
33
-
34
- const allConns = connections.getAll();
35
-
36
- if (allConns.length < 2) {
37
- console.log();
38
- console.log(chalk.yellow(` Copy Trading requires 2 connected accounts (found: ${allConns.length})`));
33
+
34
+ const spinner = ora({ text: 'Fetching active accounts...', color: 'yellow' }).start();
35
+
36
+ const allAccounts = await connections.getAllAccounts();
37
+
38
+ if (!allAccounts?.length) {
39
+ spinner.fail('No accounts found');
40
+ await prompts.waitForEnter();
41
+ return;
42
+ }
43
+
44
+ const activeAccounts = allAccounts.filter(acc => acc.status === 0);
45
+
46
+ if (activeAccounts.length < 2) {
47
+ spinner.fail(`Need at least 2 active accounts (found: ${activeAccounts.length})`);
39
48
  console.log(chalk.gray(' Connect to another PropFirm first'));
40
- console.log();
41
49
  await prompts.waitForEnter();
42
50
  return;
43
51
  }
44
-
52
+
53
+ spinner.succeed(`Found ${activeAccounts.length} active accounts`);
54
+
55
+ // Step 1: Select LEAD Account
45
56
  console.log();
46
- console.log(chalk.yellow.bold(' Copy Trading Setup'));
57
+ console.log(chalk.cyan.bold(' STEP 1: SELECT LEAD ACCOUNT'));
58
+ const leadOptions = activeAccounts.map(acc => {
59
+ const name = acc.accountName || acc.rithmicAccountId || acc.accountId;
60
+ const balance = acc.balance !== null && acc.balance !== undefined
61
+ ? ` - $${acc.balance.toLocaleString()}`
62
+ : '';
63
+ return {
64
+ label: `${name} (${acc.propfirm || acc.platform || 'Unknown'})${balance}`,
65
+ value: acc
66
+ };
67
+ });
68
+ leadOptions.push({ label: '< Back', value: 'back' });
69
+
70
+ const leadAccount = await prompts.selectOption('Lead Account:', leadOptions);
71
+ if (!leadAccount || leadAccount === 'back') return;
72
+
73
+ // Step 2: Select FOLLOWER Account(s)
47
74
  console.log();
48
-
49
- // Fetch all accounts
50
- const spinner = ora({ text: 'Fetching accounts...', color: 'yellow' }).start();
51
- const allAccounts = await fetchAllAccounts(allConns);
52
-
53
- if (allAccounts.length < 2) {
54
- spinner.fail('Need at least 2 active accounts');
75
+ console.log(chalk.yellow.bold(' STEP 2: SELECT FOLLOWER ACCOUNT(S)'));
76
+ console.log(chalk.gray(' (Select accounts to copy trades to)'));
77
+
78
+ const followers = [];
79
+ const availableFollowers = activeAccounts.filter(a => a.accountId !== leadAccount.accountId);
80
+
81
+ while (availableFollowers.length > 0) {
82
+ const remaining = availableFollowers.filter(a => !followers.find(f => f.accountId === a.accountId));
83
+ if (remaining.length === 0) break;
84
+
85
+ const followerOptions = remaining.map(acc => {
86
+ const name = acc.accountName || acc.rithmicAccountId || acc.accountId;
87
+ const balance = acc.balance !== null && acc.balance !== undefined
88
+ ? ` - $${acc.balance.toLocaleString()}`
89
+ : '';
90
+ return {
91
+ label: `${name} (${acc.propfirm || acc.platform || 'Unknown'})${balance}`,
92
+ value: acc
93
+ };
94
+ });
95
+
96
+ if (followers.length > 0) {
97
+ followerOptions.push({ label: chalk.green('✓ Done selecting followers'), value: 'done' });
98
+ }
99
+ followerOptions.push({ label: '< Back', value: 'back' });
100
+
101
+ const msg = followers.length === 0 ? 'Select Follower:' : `Add another follower (${followers.length} selected):`;
102
+ const selected = await prompts.selectOption(msg, followerOptions);
103
+
104
+ if (!selected || selected === 'back') {
105
+ if (followers.length === 0) return;
106
+ break;
107
+ }
108
+ if (selected === 'done') break;
109
+
110
+ followers.push(selected);
111
+ console.log(chalk.green(` ✓ Added: ${selected.accountName || selected.accountId}`));
112
+ }
113
+
114
+ if (followers.length === 0) {
115
+ console.log(chalk.red(' No followers selected'));
55
116
  await prompts.waitForEnter();
56
117
  return;
57
118
  }
58
-
59
- spinner.succeed(`Found ${allAccounts.length} active accounts`);
60
-
61
- // Step 1: Select Lead Account
62
- console.log(chalk.cyan(' Step 1: Select LEAD Account'));
63
- const leadIdx = await selectAccount('Lead Account:', allAccounts, -1);
64
- if (leadIdx === null || leadIdx === -1) return;
65
- const lead = allAccounts[leadIdx];
66
-
67
- // Step 2: Select Follower Account
68
- console.log();
69
- console.log(chalk.cyan(' Step 2: Select FOLLOWER Account'));
70
- const followerIdx = await selectAccount('Follower Account:', allAccounts, leadIdx);
71
- if (followerIdx === null || followerIdx === -1) return;
72
- const follower = allAccounts[followerIdx];
73
-
119
+
74
120
  // Step 3: Select Symbol
75
121
  console.log();
76
- console.log(chalk.cyan(' Step 3: Select Trading Symbol'));
77
- const symbol = await selectSymbol(lead.service);
78
- if (!symbol) return;
79
-
122
+ console.log(chalk.magenta.bold(' STEP 3: SELECT SYMBOL'));
123
+ const leadService = leadAccount.service || connections.getServiceForAccount(leadAccount.accountId);
124
+ const contract = await selectSymbol(leadService);
125
+ if (!contract) return;
126
+
80
127
  // Step 4: Configure Parameters
81
128
  console.log();
82
- console.log(chalk.cyan(' Step 4: Configure Parameters'));
83
-
129
+ console.log(chalk.cyan.bold(' STEP 4: CONFIGURE PARAMETERS'));
130
+ console.log();
131
+
84
132
  const leadContracts = await prompts.numberInput('Lead contracts:', 1, 1, 10);
85
133
  if (leadContracts === null) return;
86
-
87
- const followerContracts = await prompts.numberInput('Follower contracts:', leadContracts, 1, 10);
134
+
135
+ const followerContracts = await prompts.numberInput('Follower contracts (each):', leadContracts, 1, 10);
88
136
  if (followerContracts === null) return;
89
-
137
+
90
138
  const dailyTarget = await prompts.numberInput('Daily target ($):', 400, 1, 10000);
91
139
  if (dailyTarget === null) return;
92
-
140
+
93
141
  const maxRisk = await prompts.numberInput('Max risk ($):', 200, 1, 5000);
94
142
  if (maxRisk === null) return;
95
-
96
- // Step 5: Privacy
97
- const showNames = await prompts.selectOption('Account names:', [
98
- { label: 'Hide account names', value: false },
99
- { label: 'Show account names', value: true },
100
- ]);
143
+
144
+ const showNames = await prompts.confirmPrompt('Show account names?', false);
101
145
  if (showNames === null) return;
102
-
103
- // Confirm
146
+
147
+ // Summary
104
148
  console.log();
105
- console.log(chalk.white(' Summary:'));
106
- console.log(chalk.cyan(` Symbol: ${symbol.name}`));
107
- console.log(chalk.cyan(` Lead: ${lead.propfirm} x${leadContracts}`));
108
- console.log(chalk.cyan(` Follower: ${follower.propfirm} x${followerContracts}`));
149
+ console.log(chalk.white.bold(' SUMMARY:'));
150
+ console.log(chalk.cyan(` Symbol: ${contract.name}`));
151
+ console.log(chalk.cyan(` Lead: ${leadAccount.propfirm} x${leadContracts}`));
152
+ console.log(chalk.yellow(` Followers (${followers.length}):`));
153
+ for (const f of followers) {
154
+ console.log(chalk.yellow(` - ${f.propfirm} x${followerContracts}`));
155
+ }
109
156
  console.log(chalk.cyan(` Target: $${dailyTarget} | Risk: $${maxRisk}`));
110
157
  console.log();
111
-
158
+
112
159
  const confirm = await prompts.confirmPrompt('Start Copy Trading?', true);
113
160
  if (!confirm) return;
114
-
115
- // Launch
161
+
116
162
  await launchCopyTrading({
117
- lead: { ...lead, symbol, contracts: leadContracts },
118
- follower: { ...follower, symbol, contracts: followerContracts },
163
+ lead: { account: leadAccount, contracts: leadContracts },
164
+ followers: followers.map(f => ({ account: f, contracts: followerContracts })),
165
+ contract,
119
166
  dailyTarget,
120
167
  maxRisk,
121
- showNames,
168
+ showNames
122
169
  });
123
170
  };
124
171
 
125
172
  /**
126
- * Fetch all active accounts from connections
127
- * @param {Array} allConns - All connections
128
- * @returns {Promise<Array>}
129
- */
130
- const fetchAllAccounts = async (allConns) => {
131
- const allAccounts = [];
132
-
133
- for (const conn of allConns) {
134
- try {
135
- const result = await conn.service.getTradingAccounts();
136
- if (result.success && result.accounts) {
137
- const active = result.accounts.filter(a => a.status === 0);
138
- for (const acc of active) {
139
- allAccounts.push({
140
- account: acc,
141
- service: conn.service,
142
- propfirm: conn.propfirm,
143
- type: conn.type,
144
- });
145
- }
146
- }
147
- } catch (err) {
148
- log.warn('Failed to get accounts', { type: conn.type, error: err.message });
149
- }
150
- }
151
-
152
- return allAccounts;
153
- };
154
-
155
- /**
156
- * Select account from list
157
- * @param {string} message - Prompt message
158
- * @param {Array} accounts - Available accounts
159
- * @param {number} excludeIdx - Index to exclude
160
- * @returns {Promise<number|null>}
161
- */
162
- const selectAccount = async (message, accounts, excludeIdx) => {
163
- const options = accounts
164
- .map((a, i) => ({ a, i }))
165
- .filter(x => x.i !== excludeIdx)
166
- .map(x => {
167
- const acc = x.a.account;
168
- const balance = acc.balance !== null ? ` ($${acc.balance.toLocaleString()})` : '';
169
- return {
170
- label: `${x.a.propfirm} - ${acc.accountName || acc.rithmicAccountId || acc.name || acc.accountId}${balance}`,
171
- value: x.i,
172
- };
173
- });
174
-
175
- options.push({ label: '< Cancel', value: -1 });
176
- return prompts.selectOption(message, options);
177
- };
178
-
179
- /**
180
- * Select trading symbol
181
- * @param {Object} service - Service instance
182
- * @returns {Promise<Object|null>}
173
+ * Symbol selection - sorted with popular indices first
183
174
  */
184
175
  const selectSymbol = async (service) => {
185
176
  const spinner = ora({ text: 'Loading symbols...', color: 'yellow' }).start();
186
-
187
- try {
188
- // Try Rithmic API first for consistency
189
- let contracts = await getContractsFromAPI();
190
-
191
- // Fallback to service
192
- if (!contracts && typeof service.getContracts === 'function') {
193
- const result = await service.getContracts();
194
- if (result.success && result.contracts?.length > 0) {
195
- contracts = result.contracts;
196
- }
197
- }
198
-
199
- if (!contracts || !contracts.length) {
200
- spinner.fail('No contracts available');
201
- await prompts.waitForEnter();
202
- return null;
203
- }
204
-
205
- spinner.succeed(`Found ${contracts.length} contracts`);
206
-
207
- // Build options from RAW API data - no static mapping
208
- const options = [];
209
- let currentGroup = null;
210
-
211
- for (const c of contracts) {
212
- // Use RAW API field: contractGroup
213
- if (c.contractGroup && c.contractGroup !== currentGroup) {
214
- currentGroup = c.contractGroup;
215
- options.push({
216
- label: chalk.cyan.bold(`── ${currentGroup} ──`),
217
- value: null,
218
- disabled: true,
219
- });
220
- }
221
-
222
- // Use RAW API fields: symbol (trading symbol), name (product name), exchange
223
- const label = ` ${c.symbol} - ${c.name} (${c.exchange})`;
224
- options.push({ label, value: c });
225
- }
226
-
227
- options.push({ label: '', value: null, disabled: true });
228
- options.push({ label: chalk.gray('< Cancel'), value: null });
229
-
230
- return prompts.selectOption('Trading Symbol:', options);
231
- } catch (err) {
232
- spinner.fail(`Error loading contracts: ${err.message}`);
233
- await prompts.waitForEnter();
177
+
178
+ const contractsResult = await service.getContracts();
179
+ if (!contractsResult.success || !contractsResult.contracts?.length) {
180
+ spinner.fail('Failed to load contracts');
234
181
  return null;
235
182
  }
183
+
184
+ let contracts = contractsResult.contracts;
185
+
186
+ // Sort: Popular indices first
187
+ const popularPrefixes = ['ES', 'NQ', 'MES', 'MNQ', 'M2K', 'RTY', 'YM', 'MYM', 'NKD', 'GC', 'SI', 'CL'];
188
+
189
+ contracts.sort((a, b) => {
190
+ const baseA = a.baseSymbol || a.symbol || '';
191
+ const baseB = b.baseSymbol || b.symbol || '';
192
+ const idxA = popularPrefixes.findIndex(p => baseA === p || baseA.startsWith(p));
193
+ const idxB = popularPrefixes.findIndex(p => baseB === p || baseB.startsWith(p));
194
+ if (idxA !== -1 && idxB !== -1) return idxA - idxB;
195
+ if (idxA !== -1) return -1;
196
+ if (idxB !== -1) return 1;
197
+ return baseA.localeCompare(baseB);
198
+ });
199
+
200
+ spinner.succeed(`Found ${contracts.length} contracts`);
201
+
202
+ const options = contracts.map(c => ({
203
+ label: `${c.symbol} - ${c.name} (${c.exchange})`,
204
+ value: c
205
+ }));
206
+ options.push({ label: chalk.gray('< Back'), value: 'back' });
207
+
208
+ const selected = await prompts.selectOption(chalk.yellow('Select Symbol:'), options);
209
+ return selected === 'back' || selected === null ? null : selected;
236
210
  };
237
211
 
238
212
  /**
239
- * Get contracts from Rithmic API - RAW data only
240
- * @returns {Promise<Array|null>}
241
- */
242
- const getContractsFromAPI = async () => {
243
- const allConns = connections.getAll();
244
- const rithmicConn = allConns.find(c => c.type === 'rithmic');
245
-
246
- if (rithmicConn && typeof rithmicConn.service.getContracts === 'function') {
247
- const result = await rithmicConn.service.getContracts();
248
- if (result.success && result.contracts?.length > 0) {
249
- // Return RAW API data - no mapping
250
- return result.contracts;
251
- }
252
- }
253
-
254
- return null;
255
- };
256
-
257
- /**
258
- * Launch Copy Trading session
259
- * @param {Object} config - Session configuration
213
+ * Launch Copy Trading - HQX Ultra Scalping with trade copying
260
214
  */
261
215
  const launchCopyTrading = async (config) => {
262
- const { lead, follower, dailyTarget, maxRisk, showNames } = config;
263
-
264
- // Account names (masked for privacy)
265
- const leadName = showNames ? lead.account.accountId : 'HQX Lead *****';
266
- const followerName = showNames ? follower.account.accountId : 'HQX Follower *****';
267
-
216
+ const { lead, followers, contract, dailyTarget, maxRisk, showNames } = config;
217
+
218
+ const leadAccount = lead.account;
219
+ const leadService = leadAccount.service || connections.getServiceForAccount(leadAccount.accountId);
220
+ const leadName = showNames
221
+ ? (leadAccount.accountName || leadAccount.rithmicAccountId || leadAccount.accountId)
222
+ : 'HQX Lead *****';
223
+ const symbolName = contract.name;
224
+ const contractId = contract.id;
225
+ const tickSize = contract.tickSize || 0.25;
226
+
227
+ const followerNames = followers.map((f, i) =>
228
+ showNames ? (f.account.accountName || f.account.accountId) : `HQX Follower ${i + 1} *****`
229
+ );
230
+
268
231
  const ui = new AlgoUI({ subtitle: 'HQX Copy Trading', mode: 'copy-trading' });
269
-
232
+
270
233
  const stats = {
271
- leadName,
272
- followerName,
273
- leadSymbol: lead.symbol.name,
274
- followerSymbol: follower.symbol.name,
275
- leadQty: lead.contracts,
276
- followerQty: follower.contracts,
234
+ accountName: leadName,
235
+ followerNames,
236
+ symbol: symbolName,
237
+ qty: lead.contracts,
238
+ followerQty: followers[0]?.contracts || lead.contracts,
277
239
  target: dailyTarget,
278
240
  risk: maxRisk,
241
+ propfirm: leadAccount.propfirm || 'Unknown',
242
+ platform: leadAccount.platform || 'Rithmic',
279
243
  pnl: 0,
244
+ followerPnl: 0,
280
245
  trades: 0,
281
246
  wins: 0,
282
247
  losses: 0,
283
248
  latency: 0,
284
249
  connected: false,
285
- platform: lead.account.platform || 'Rithmic',
250
+ startTime: Date.now(),
251
+ followersCount: followers.length
286
252
  };
287
-
253
+
288
254
  let running = true;
289
255
  let stopReason = null;
256
+ let startingPnL = null;
257
+ let currentPosition = 0;
258
+ let pendingOrder = false;
259
+ let tickCount = 0;
290
260
 
291
- // Measure API latency (CLI <-> API)
292
- const measureLatency = async () => {
293
- try {
294
- const start = Date.now();
295
- await lead.service.getPositions(lead.account.accountId);
296
- stats.latency = Date.now() - start;
297
- } catch (e) {
298
- stats.latency = 0;
299
- }
300
- };
301
-
302
- // Local copy trading - no external server needed
303
- ui.addLog('info', `Starting copy trading on ${stats.platform}...`);
304
- ui.addLog('info', `Lead: ${stats.leadName} -> Follower: ${stats.followerName}`);
305
- ui.addLog('info', `Symbol: ${stats.symbol} | Target: $${dailyTarget} | Risk: $${maxRisk}`);
306
- stats.connected = true;
261
+ // Initialize Strategy
262
+ const strategy = new M1({ tickSize });
263
+ strategy.initialize(contractId, tickSize);
307
264
 
308
- // Track lead positions and copy to follower
309
- let lastLeadPositions = [];
265
+ // Initialize Market Data Feed
266
+ const marketFeed = new MarketDataFeed({ propfirm: leadAccount.propfirm });
310
267
 
311
- const pollAndCopy = async () => {
268
+ // Log startup
269
+ ui.addLog('info', `Lead: ${leadName} | Followers: ${followers.length}`);
270
+ ui.addLog('info', `Symbol: ${symbolName} | Lead Qty: ${lead.contracts} | Follower Qty: ${followers[0]?.contracts}`);
271
+ ui.addLog('info', `Target: $${dailyTarget} | Max Risk: $${maxRisk}`);
272
+ ui.addLog('info', 'Connecting to market data...');
273
+
274
+ // Handle strategy signals - place on lead AND all followers
275
+ strategy.on('signal', async (signal) => {
276
+ if (!running || pendingOrder || currentPosition !== 0) return;
277
+
278
+ const { direction, entry, stopLoss, takeProfit, confidence } = signal;
279
+
280
+ ui.addLog('signal', `${direction.toUpperCase()} signal @ ${entry.toFixed(2)} (${(confidence * 100).toFixed(0)}%)`);
281
+
282
+ pendingOrder = true;
312
283
  try {
313
- // Get lead positions
314
- const leadResult = await lead.service.getPositions(lead.account.accountId);
315
- if (!leadResult.success) return;
284
+ const orderSide = direction === 'long' ? 0 : 1;
316
285
 
317
- const currentPositions = leadResult.positions || [];
286
+ // Place order on LEAD
287
+ const leadResult = await leadService.placeOrder({
288
+ accountId: leadAccount.accountId,
289
+ contractId: contractId,
290
+ type: 2,
291
+ side: orderSide,
292
+ size: lead.contracts
293
+ });
318
294
 
319
- // Detect new positions on lead
320
- for (const pos of currentPositions) {
321
- const existing = lastLeadPositions.find(p => p.contractId === pos.contractId);
322
- if (!existing && pos.quantity !== 0) {
323
- // New position opened - copy to follower
324
- ui.addLog('trade', `Lead opened: ${pos.quantity > 0 ? 'LONG' : 'SHORT'} ${Math.abs(pos.quantity)}x ${pos.symbol || pos.contractId}`);
325
- // TODO: Place order on follower account
295
+ if (leadResult.success) {
296
+ currentPosition = direction === 'long' ? lead.contracts : -lead.contracts;
297
+ stats.trades++;
298
+ ui.addLog('trade', `LEAD: ${direction.toUpperCase()} ${lead.contracts}x @ market`);
299
+
300
+ // Place orders on ALL FOLLOWERS
301
+ for (let i = 0; i < followers.length; i++) {
302
+ const f = followers[i];
303
+ const fService = f.account.service || connections.getServiceForAccount(f.account.accountId);
304
+
305
+ try {
306
+ const fResult = await fService.placeOrder({
307
+ accountId: f.account.accountId,
308
+ contractId: contractId,
309
+ type: 2,
310
+ side: orderSide,
311
+ size: f.contracts
312
+ });
313
+
314
+ if (fResult.success) {
315
+ ui.addLog('trade', `FOLLOWER ${i + 1}: ${direction.toUpperCase()} ${f.contracts}x @ market`);
316
+ } else {
317
+ ui.addLog('error', `FOLLOWER ${i + 1}: Order failed`);
318
+ }
319
+ } catch (e) {
320
+ ui.addLog('error', `FOLLOWER ${i + 1}: ${e.message}`);
321
+ }
326
322
  }
327
- }
328
-
329
- // Detect closed positions
330
- for (const oldPos of lastLeadPositions) {
331
- const stillOpen = currentPositions.find(p => p.contractId === oldPos.contractId);
332
- if (!stillOpen || stillOpen.quantity === 0) {
333
- ui.addLog('info', `Lead closed: ${oldPos.symbol || oldPos.contractId}`);
334
- // TODO: Close position on follower account
323
+
324
+ // Place bracket orders on lead (SL/TP)
325
+ if (stopLoss && takeProfit) {
326
+ await leadService.placeOrder({
327
+ accountId: leadAccount.accountId, contractId, type: 4,
328
+ side: direction === 'long' ? 1 : 0, size: lead.contracts, stopPrice: stopLoss
329
+ });
330
+ await leadService.placeOrder({
331
+ accountId: leadAccount.accountId, contractId, type: 1,
332
+ side: direction === 'long' ? 1 : 0, size: lead.contracts, limitPrice: takeProfit
333
+ });
334
+ ui.addLog('info', `SL: ${stopLoss.toFixed(2)} | TP: ${takeProfit.toFixed(2)}`);
335
335
  }
336
+ } else {
337
+ ui.addLog('error', `Lead order failed: ${leadResult.error}`);
336
338
  }
337
-
338
- lastLeadPositions = currentPositions;
339
-
340
- // Update P&L from lead
341
- const leadPnL = currentPositions.reduce((sum, p) => sum + (p.profitAndLoss || 0), 0);
342
- if (leadPnL !== stats.pnl) {
343
- const diff = leadPnL - stats.pnl;
344
- if (Math.abs(diff) > 0.01 && stats.pnl !== 0) {
345
- stats.trades++;
346
- if (diff >= 0) stats.wins++;
347
- else stats.losses++;
339
+ } catch (e) {
340
+ ui.addLog('error', `Order error: ${e.message}`);
341
+ }
342
+ pendingOrder = false;
343
+ });
344
+
345
+ // Handle market data ticks
346
+ marketFeed.on('tick', (tick) => {
347
+ tickCount++;
348
+ const latencyStart = Date.now();
349
+
350
+ strategy.processTick({
351
+ contractId: tick.contractId || contractId,
352
+ price: tick.price,
353
+ bid: tick.bid,
354
+ ask: tick.ask,
355
+ volume: tick.volume || 1,
356
+ side: tick.lastTradeSide || 'unknown',
357
+ timestamp: tick.timestamp || Date.now()
358
+ });
359
+
360
+ stats.latency = Date.now() - latencyStart;
361
+
362
+ if (tickCount % 100 === 0) {
363
+ ui.addLog('info', `Tick #${tickCount} @ ${tick.price?.toFixed(2) || 'N/A'}`);
364
+ }
365
+ });
366
+
367
+ marketFeed.on('connected', () => {
368
+ stats.connected = true;
369
+ ui.addLog('success', 'Market data connected!');
370
+ });
371
+
372
+ marketFeed.on('error', (err) => ui.addLog('error', `Market: ${err.message}`));
373
+ marketFeed.on('disconnected', () => { stats.connected = false; ui.addLog('error', 'Market data disconnected'); });
374
+
375
+ // Connect to market data
376
+ try {
377
+ const token = leadService.token || leadService.getToken?.();
378
+ const propfirmKey = (leadAccount.propfirm || 'topstep').toLowerCase().replace(/\s+/g, '_');
379
+ await marketFeed.connect(token, propfirmKey, contractId);
380
+ await marketFeed.subscribe(symbolName, contractId);
381
+ } catch (e) {
382
+ ui.addLog('error', `Failed to connect: ${e.message}`);
383
+ }
384
+
385
+ // Poll P&L from lead and followers
386
+ const pollPnL = async () => {
387
+ try {
388
+ // Lead P&L
389
+ const leadResult = await leadService.getTradingAccounts();
390
+ if (leadResult.success && leadResult.accounts) {
391
+ const acc = leadResult.accounts.find(a => a.accountId === leadAccount.accountId);
392
+ if (acc && acc.profitAndLoss !== undefined) {
393
+ if (startingPnL === null) startingPnL = acc.profitAndLoss;
394
+ stats.pnl = acc.profitAndLoss - startingPnL;
348
395
  }
349
- stats.pnl = leadPnL;
350
396
  }
351
397
 
352
- // Check target/risk limits
398
+ // Check target/risk
353
399
  if (stats.pnl >= dailyTarget) {
354
400
  stopReason = 'target';
355
401
  running = false;
@@ -357,80 +403,66 @@ const launchCopyTrading = async (config) => {
357
403
  } else if (stats.pnl <= -maxRisk) {
358
404
  stopReason = 'risk';
359
405
  running = false;
360
- ui.addLog('error', `MAX RISK HIT! -$${Math.abs(stats.pnl).toFixed(2)}`);
406
+ ui.addLog('error', `MAX RISK! -$${Math.abs(stats.pnl).toFixed(2)}`);
361
407
  }
362
- } catch (e) {
363
- // Silent fail - will retry
364
- }
408
+ } catch (e) { /* silent */ }
365
409
  };
366
-
367
- // UI refresh loop
368
- const refreshInterval = setInterval(() => {
369
- if (running) ui.render(stats);
370
- }, 250);
371
-
372
- // Measure API latency every 5 seconds
373
- measureLatency(); // Initial measurement
374
- const latencyInterval = setInterval(() => { if (running) measureLatency(); }, 5000);
375
-
376
- // Poll and copy every 2 seconds
377
- pollAndCopy(); // Initial poll
378
- const copyInterval = setInterval(() => { if (running) pollAndCopy(); }, 2000);
379
-
380
- // Keyboard handling
381
- const cleanupKeys = setupKeyboardHandler(() => {
382
- running = false;
383
- stopReason = 'manual';
384
- });
385
-
410
+
411
+ // Start intervals
412
+ const refreshInterval = setInterval(() => { if (running) ui.render(stats); }, 250);
413
+ const pnlInterval = setInterval(() => { if (running) pollPnL(); }, 2000);
414
+ pollPnL();
415
+
416
+ // Keyboard handler
417
+ const setupKeyHandler = () => {
418
+ if (!process.stdin.isTTY) return;
419
+ readline.emitKeypressEvents(process.stdin);
420
+ process.stdin.setRawMode(true);
421
+ process.stdin.resume();
422
+
423
+ const onKey = (str, key) => {
424
+ if (key && (key.name === 'x' || key.name === 'X' || (key.ctrl && key.name === 'c'))) {
425
+ running = false;
426
+ stopReason = 'manual';
427
+ }
428
+ };
429
+ process.stdin.on('keypress', onKey);
430
+ return () => {
431
+ process.stdin.removeListener('keypress', onKey);
432
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
433
+ };
434
+ };
435
+
436
+ const cleanupKeys = setupKeyHandler();
437
+
386
438
  // Wait for stop
387
- await new Promise((resolve) => {
439
+ await new Promise(resolve => {
388
440
  const check = setInterval(() => {
389
- if (!running) {
390
- clearInterval(check);
391
- resolve();
392
- }
441
+ if (!running) { clearInterval(check); resolve(); }
393
442
  }, 100);
394
443
  });
395
-
444
+
396
445
  // Cleanup
397
446
  clearInterval(refreshInterval);
398
- clearInterval(latencyInterval);
399
- clearInterval(copyInterval);
447
+ clearInterval(pnlInterval);
448
+ await marketFeed.disconnect();
400
449
  if (cleanupKeys) cleanupKeys();
401
450
  ui.cleanup();
402
-
403
- // Show summary
404
- renderSessionSummary(stats, stopReason);
405
- await prompts.waitForEnter();
406
- };
407
-
408
- /**
409
- * Setup keyboard handler
410
- * @param {Function} onStop - Stop callback
411
- * @returns {Function|null} Cleanup function
412
- */
413
- const setupKeyboardHandler = (onStop) => {
414
- if (!process.stdin.isTTY) return null;
415
-
416
- readline.emitKeypressEvents(process.stdin);
417
- process.stdin.setRawMode(true);
451
+
452
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
418
453
  process.stdin.resume();
419
-
420
- const handler = (str, key) => {
421
- if (key && (key.name === 'x' || (key.ctrl && key.name === 'c'))) {
422
- onStop();
423
- }
424
- };
425
-
426
- process.stdin.on('keypress', handler);
427
-
428
- return () => {
429
- process.stdin.removeListener('keypress', handler);
430
- if (process.stdin.isTTY) {
431
- process.stdin.setRawMode(false);
432
- }
433
- };
454
+
455
+ // Duration
456
+ const durationMs = Date.now() - stats.startTime;
457
+ const hours = Math.floor(durationMs / 3600000);
458
+ const minutes = Math.floor((durationMs % 3600000) / 60000);
459
+ const seconds = Math.floor((durationMs % 60000) / 1000);
460
+ stats.duration = hours > 0 ? `${hours}h ${minutes}m ${seconds}s` : minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
461
+
462
+ renderSessionSummary(stats, stopReason);
463
+
464
+ console.log('\n Returning to menu in 3 seconds...');
465
+ await new Promise(resolve => setTimeout(resolve, 3000));
434
466
  };
435
467
 
436
468
  module.exports = { copyTradingMenu };
@@ -0,0 +1,394 @@
1
+ /**
2
+ * Custom Strategy - AI-powered modular strategy builder
3
+ * Each strategy is a folder with modular components
4
+ */
5
+
6
+ const chalk = require('chalk');
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const os = require('os');
10
+ const ora = require('ora');
11
+
12
+ const { getLogoWidth, centerText, displayBanner } = require('../../ui');
13
+ const { prompts } = require('../../utils');
14
+ const { getActiveProvider } = require('../ai-agents');
15
+ const cliproxy = require('../../services/cliproxy');
16
+
17
+ // Base strategies directory
18
+ const STRATEGIES_DIR = path.join(os.homedir(), '.hqx', 'strategies');
19
+
20
+ /** Ensure strategies directory exists */
21
+ const ensureStrategiesDir = () => {
22
+ if (!fs.existsSync(STRATEGIES_DIR)) fs.mkdirSync(STRATEGIES_DIR, { recursive: true });
23
+ };
24
+
25
+ /** Load all saved strategies (folders) */
26
+ const loadStrategies = () => {
27
+ ensureStrategiesDir();
28
+ try {
29
+ const items = fs.readdirSync(STRATEGIES_DIR, { withFileTypes: true });
30
+ return items.filter(i => i.isDirectory()).map(dir => {
31
+ const configPath = path.join(STRATEGIES_DIR, dir.name, 'config.json');
32
+ if (fs.existsSync(configPath)) {
33
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
34
+ return { folder: dir.name, path: path.join(STRATEGIES_DIR, dir.name), ...config };
35
+ }
36
+ return { folder: dir.name, path: path.join(STRATEGIES_DIR, dir.name), name: dir.name };
37
+ });
38
+ } catch (e) { return []; }
39
+ };
40
+
41
+ /** Create strategy folder structure */
42
+ const createStrategyFolder = (name) => {
43
+ ensureStrategiesDir();
44
+ const folderName = name.toLowerCase().replace(/[^a-z0-9]/g, '-').replace(/-+/g, '-');
45
+ const strategyPath = path.join(STRATEGIES_DIR, folderName);
46
+
47
+ if (fs.existsSync(strategyPath)) {
48
+ return { success: false, error: 'Strategy folder already exists', path: null };
49
+ }
50
+
51
+ fs.mkdirSync(strategyPath, { recursive: true });
52
+ return { success: true, path: strategyPath, folder: folderName };
53
+ };
54
+
55
+ /** Save strategy module */
56
+ const saveModule = (strategyPath, moduleName, content) => {
57
+ const filePath = path.join(strategyPath, moduleName);
58
+ fs.writeFileSync(filePath, typeof content === 'string' ? content : JSON.stringify(content, null, 2));
59
+ return filePath;
60
+ };
61
+
62
+ /** Delete strategy folder */
63
+ const deleteStrategy = (strategyPath) => {
64
+ if (fs.existsSync(strategyPath)) {
65
+ fs.rmSync(strategyPath, { recursive: true, force: true });
66
+ return true;
67
+ }
68
+ return false;
69
+ };
70
+
71
+ /** Custom Strategy Menu */
72
+ const customStrategyMenu = async (service) => {
73
+ while (true) {
74
+ console.clear();
75
+ displayBanner();
76
+
77
+ const boxWidth = getLogoWidth();
78
+ const W = boxWidth - 2;
79
+ const aiProvider = getActiveProvider();
80
+ const strategies = loadStrategies();
81
+
82
+ console.log(chalk.cyan('╔' + '═'.repeat(W) + '╗'));
83
+ console.log(chalk.cyan('║') + chalk.green.bold(centerText('CUSTOM STRATEGY BUILDER', W)) + chalk.cyan('║'));
84
+ console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
85
+
86
+ // AI Status
87
+ if (aiProvider) {
88
+ const status = `AI: ${aiProvider.name} (${aiProvider.modelName || 'default'})`;
89
+ console.log(chalk.cyan('║') + chalk.green(centerText('● ' + status, W)) + chalk.cyan('║'));
90
+ } else {
91
+ console.log(chalk.cyan('║') + chalk.red(centerText('○ NO AI AGENT CONNECTED', W)) + chalk.cyan('║'));
92
+ }
93
+
94
+ console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
95
+
96
+ // Options
97
+ const col1 = '[1] CREATE NEW';
98
+ const col2 = `[2] MY STRATEGIES (${strategies.length})`;
99
+ const colWidth = Math.floor(W / 2);
100
+ const pad1 = Math.floor((colWidth - col1.length) / 2);
101
+ const pad2 = Math.floor((W - colWidth - col2.length) / 2);
102
+ console.log(chalk.cyan('║') +
103
+ ' '.repeat(pad1) + chalk.yellow(col1) + ' '.repeat(colWidth - col1.length - pad1) +
104
+ ' '.repeat(pad2) + chalk.cyan(col2) + ' '.repeat(W - colWidth - col2.length - pad2) +
105
+ chalk.cyan('║'));
106
+
107
+ console.log(chalk.cyan('╠' + '─'.repeat(W) + '╣'));
108
+ console.log(chalk.cyan('║') + chalk.red(centerText('[B] BACK', W)) + chalk.cyan('║'));
109
+ console.log(chalk.cyan('╚' + '═'.repeat(W) + '╝'));
110
+
111
+ const input = await prompts.textInput(chalk.cyan('SELECT (1/2/B): '));
112
+ const choice = (input || '').toLowerCase().trim();
113
+
114
+ if (choice === 'b' || choice === '') return;
115
+
116
+ if (choice === '1') {
117
+ if (!aiProvider) {
118
+ console.log(chalk.red('\n Connect an AI Agent first (AI Agents menu)'));
119
+ await prompts.waitForEnter();
120
+ continue;
121
+ }
122
+ await createStrategyWizard(aiProvider);
123
+ } else if (choice === '2') {
124
+ await myStrategiesMenu(strategies, service);
125
+ }
126
+ }
127
+ };
128
+
129
+ /** AI Wizard to create modular strategy */
130
+ const createStrategyWizard = async (aiProvider) => {
131
+ console.clear();
132
+ displayBanner();
133
+
134
+ const boxWidth = getLogoWidth();
135
+ const W = boxWidth - 2;
136
+
137
+ console.log(chalk.cyan('╔' + '═'.repeat(W) + '╗'));
138
+ console.log(chalk.cyan('║') + chalk.green.bold(centerText('CREATE STRATEGY WITH AI', W)) + chalk.cyan('║'));
139
+ console.log(chalk.cyan('╚' + '═'.repeat(W) + '╝'));
140
+ console.log();
141
+
142
+ // Step 1: Strategy name
143
+ console.log(chalk.yellow(' STEP 1: Name your strategy'));
144
+ const name = await prompts.textInput(chalk.cyan(' Strategy name: '));
145
+ if (!name || !name.trim()) {
146
+ console.log(chalk.red(' Cancelled'));
147
+ await prompts.waitForEnter();
148
+ return;
149
+ }
150
+
151
+ // Create folder
152
+ const folder = createStrategyFolder(name.trim());
153
+ if (!folder.success) {
154
+ console.log(chalk.red(` Error: ${folder.error}`));
155
+ await prompts.waitForEnter();
156
+ return;
157
+ }
158
+
159
+ console.log(chalk.green(` ✓ Created: ${folder.path}`));
160
+ console.log();
161
+
162
+ // Step 2: Chat with AI to build strategy
163
+ console.log(chalk.yellow(' STEP 2: Describe your strategy to the AI'));
164
+ console.log(chalk.gray(' Type your strategy idea in plain English.'));
165
+ console.log(chalk.gray(' The AI will help you build each module.'));
166
+ console.log(chalk.gray(' Type "done" when finished, "cancel" to abort.'));
167
+ console.log();
168
+
169
+ const systemPrompt = `You are an expert algo trading strategy builder for futures (ES, NQ, MES, MNQ, etc).
170
+ Help the user create a modular trading strategy. Build these components:
171
+
172
+ 1. ENTRY CONDITIONS - When to open a position (long/short signals)
173
+ 2. EXIT CONDITIONS - Take profit, stop loss, trailing stops
174
+ 3. RISK MANAGEMENT - Position sizing, max loss, max positions
175
+ 4. FILTERS - Market conditions when NOT to trade
176
+
177
+ Ask clarifying questions. Be concise. When ready, output each module.
178
+
179
+ For each module, output JavaScript code in this format:
180
+ \`\`\`javascript:entry.js
181
+ module.exports = {
182
+ checkLongEntry: (data) => { /* return true/false */ },
183
+ checkShortEntry: (data) => { /* return true/false */ }
184
+ };
185
+ \`\`\`
186
+
187
+ The 'data' object contains: { price, bid, ask, volume, atr, ema20, ema50, rsi, macd, vwap, high, low, open, close }`;
188
+
189
+ const messages = [{ role: 'system', content: systemPrompt }];
190
+ const modules = {};
191
+
192
+ console.log(chalk.green(' AI: ') + 'What kind of trading strategy do you want to create?');
193
+ console.log(chalk.gray(' Example: "A mean reversion strategy that buys when RSI < 30"'));
194
+ console.log();
195
+
196
+ while (true) {
197
+ const userInput = await prompts.textInput(chalk.yellow(' You: '));
198
+
199
+ if (!userInput) continue;
200
+
201
+ if (userInput.toLowerCase() === 'cancel') {
202
+ deleteStrategy(folder.path);
203
+ console.log(chalk.gray('\n Strategy cancelled and folder deleted.'));
204
+ await prompts.waitForEnter();
205
+ return;
206
+ }
207
+
208
+ if (userInput.toLowerCase() === 'done') {
209
+ // Save config
210
+ saveModule(folder.path, 'config.json', {
211
+ name: name.trim(),
212
+ description: modules.description || '',
213
+ createdAt: new Date().toISOString(),
214
+ modules: Object.keys(modules).filter(k => k !== 'description')
215
+ });
216
+
217
+ console.log(chalk.green('\n ✓ Strategy saved!'));
218
+ console.log(chalk.cyan(` Location: ${folder.path}`));
219
+ console.log(chalk.gray(' Modules created:'));
220
+ for (const m of Object.keys(modules)) {
221
+ if (m !== 'description') console.log(chalk.gray(` - ${m}`));
222
+ }
223
+ await prompts.waitForEnter();
224
+ return;
225
+ }
226
+
227
+ messages.push({ role: 'user', content: userInput });
228
+
229
+ const spinner = ora({ text: 'AI thinking...', color: 'yellow' }).start();
230
+
231
+ try {
232
+ const modelId = aiProvider.modelId || getDefaultModel(aiProvider.id);
233
+ const result = await cliproxy.chatCompletion(modelId, messages);
234
+
235
+ if (!result.success) {
236
+ spinner.fail(`AI Error: ${result.error}`);
237
+ messages.pop();
238
+ continue;
239
+ }
240
+
241
+ const response = result.response?.choices?.[0]?.message?.content || '';
242
+ messages.push({ role: 'assistant', content: response });
243
+
244
+ spinner.stop();
245
+ console.log();
246
+
247
+ // Extract and save code modules
248
+ const codeBlocks = response.matchAll(/```javascript:(\w+\.js)\n([\s\S]*?)```/g);
249
+ for (const match of codeBlocks) {
250
+ const [, filename, code] = match;
251
+ saveModule(folder.path, filename, code.trim());
252
+ modules[filename] = true;
253
+ console.log(chalk.green(` ✓ Saved module: ${filename}`));
254
+ }
255
+
256
+ // Extract description if present
257
+ const descMatch = response.match(/description[:\s]*["']?([^"'\n]+)/i);
258
+ if (descMatch) modules.description = descMatch[1];
259
+
260
+ // Print AI response (without code blocks for cleaner output)
261
+ const cleanResponse = response.replace(/```[\s\S]*?```/g, '[code saved]');
262
+ console.log(chalk.green(' AI: ') + formatResponse(cleanResponse));
263
+ console.log();
264
+
265
+ } catch (e) {
266
+ spinner.fail(`Error: ${e.message}`);
267
+ messages.pop();
268
+ }
269
+ }
270
+ };
271
+
272
+ /** Get default model */
273
+ const getDefaultModel = (providerId) => {
274
+ const defaults = {
275
+ anthropic: 'claude-sonnet-4-20250514',
276
+ google: 'gemini-2.5-pro',
277
+ openai: 'gpt-4o'
278
+ };
279
+ return defaults[providerId] || 'claude-sonnet-4-20250514';
280
+ };
281
+
282
+ /** Format response for terminal */
283
+ const formatResponse = (text) => {
284
+ const lines = text.split('\n');
285
+ return lines.map((l, i) => i === 0 ? l : ' ' + l).join('\n');
286
+ };
287
+
288
+ /** My Strategies Menu */
289
+ const myStrategiesMenu = async (strategies, service) => {
290
+ while (true) {
291
+ console.clear();
292
+ displayBanner();
293
+
294
+ const boxWidth = getLogoWidth();
295
+ const W = boxWidth - 2;
296
+
297
+ console.log(chalk.cyan('╔' + '═'.repeat(W) + '╗'));
298
+ console.log(chalk.cyan('║') + chalk.yellow.bold(centerText('MY STRATEGIES', W)) + chalk.cyan('║'));
299
+ console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
300
+
301
+ if (strategies.length === 0) {
302
+ console.log(chalk.cyan('║') + chalk.gray(centerText('No strategies yet', W)) + chalk.cyan('║'));
303
+ } else {
304
+ for (let i = 0; i < strategies.length; i++) {
305
+ const s = strategies[i];
306
+ const num = `[${i + 1}]`.padEnd(4);
307
+ const sname = (s.name || s.folder).substring(0, 30).padEnd(32);
308
+ const modules = s.modules ? `${s.modules.length} modules` : '';
309
+ const line = `${num} ${sname} ${chalk.gray(modules)}`;
310
+ console.log(chalk.cyan('║') + ' ' + chalk.white(num) + chalk.cyan(sname) + chalk.gray(modules.padEnd(W - 38)) + chalk.cyan('║'));
311
+ }
312
+ }
313
+
314
+ console.log(chalk.cyan('╠' + '─'.repeat(W) + '╣'));
315
+ console.log(chalk.cyan('║') + chalk.red(centerText('[B] BACK', W)) + chalk.cyan('║'));
316
+ console.log(chalk.cyan('╚' + '═'.repeat(W) + '╝'));
317
+
318
+ if (strategies.length === 0) {
319
+ await prompts.waitForEnter();
320
+ return;
321
+ }
322
+
323
+ const input = await prompts.textInput(chalk.cyan(`SELECT (1-${strategies.length}/B): `));
324
+ const choice = (input || '').toLowerCase().trim();
325
+
326
+ if (choice === 'b' || choice === '') return;
327
+
328
+ const num = parseInt(choice);
329
+ if (!isNaN(num) && num >= 1 && num <= strategies.length) {
330
+ await strategyDetailMenu(strategies[num - 1], service);
331
+ strategies.length = 0;
332
+ strategies.push(...loadStrategies());
333
+ }
334
+ }
335
+ };
336
+
337
+ /** Strategy Detail Menu */
338
+ const strategyDetailMenu = async (strategy, service) => {
339
+ console.clear();
340
+ displayBanner();
341
+
342
+ const boxWidth = getLogoWidth();
343
+ const W = boxWidth - 2;
344
+
345
+ console.log(chalk.cyan('╔' + '═'.repeat(W) + '╗'));
346
+ console.log(chalk.cyan('║') + chalk.green.bold(centerText((strategy.name || strategy.folder).toUpperCase(), W)) + chalk.cyan('║'));
347
+ console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
348
+
349
+ // Show modules
350
+ const files = fs.readdirSync(strategy.path);
351
+ console.log(chalk.cyan('║') + chalk.gray(centerText(`Path: ${strategy.path}`, W)) + chalk.cyan('║'));
352
+ console.log(chalk.cyan('╠' + '─'.repeat(W) + '╣'));
353
+ console.log(chalk.cyan('║') + chalk.white(centerText('MODULES:', W)) + chalk.cyan('║'));
354
+
355
+ for (const f of files) {
356
+ const icon = f.endsWith('.js') ? '📄' : f.endsWith('.json') ? '⚙️' : '📁';
357
+ console.log(chalk.cyan('║') + centerText(`${icon} ${f}`, W) + chalk.cyan('║'));
358
+ }
359
+
360
+ console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
361
+
362
+ // Options: Run, Edit with AI, Delete
363
+ const opts = ['[1] RUN', '[2] EDIT WITH AI', '[3] DELETE'];
364
+ const optLine = opts.join(' ');
365
+ console.log(chalk.cyan('║') + centerText(
366
+ chalk.green(opts[0]) + ' ' + chalk.yellow(opts[1]) + ' ' + chalk.red(opts[2]), W
367
+ ) + chalk.cyan('║'));
368
+
369
+ console.log(chalk.cyan('╠' + '─'.repeat(W) + '╣'));
370
+ console.log(chalk.cyan('║') + chalk.red(centerText('[B] BACK', W)) + chalk.cyan('║'));
371
+ console.log(chalk.cyan('╚' + '═'.repeat(W) + '╝'));
372
+
373
+ const input = await prompts.textInput(chalk.cyan('SELECT (1/2/3/B): '));
374
+ const choice = (input || '').toLowerCase().trim();
375
+
376
+ if (choice === '1') {
377
+ console.log(chalk.yellow('\n Running custom strategy...'));
378
+ console.log(chalk.gray(' This will use your connected accounts and market data.'));
379
+ console.log(chalk.gray(' (Full execution coming soon)'));
380
+ await prompts.waitForEnter();
381
+ } else if (choice === '2') {
382
+ console.log(chalk.yellow('\n Edit with AI coming soon...'));
383
+ await prompts.waitForEnter();
384
+ } else if (choice === '3') {
385
+ const confirm = await prompts.confirmPrompt(`Delete "${strategy.name || strategy.folder}"?`, false);
386
+ if (confirm) {
387
+ deleteStrategy(strategy.path);
388
+ console.log(chalk.green('\n ✓ Strategy deleted'));
389
+ await prompts.waitForEnter();
390
+ }
391
+ }
392
+ };
393
+
394
+ module.exports = { customStrategyMenu, loadStrategies, createStrategyFolder, saveModule };
@@ -10,6 +10,7 @@ const log = logger.scope('AlgoMenu');
10
10
 
11
11
  const { oneAccountMenu } = require('./one-account');
12
12
  const { copyTradingMenu } = require('./copy-trading');
13
+ const { customStrategyMenu } = require('./custom-strategy');
13
14
 
14
15
  /**
15
16
  * Algo Trading Menu
@@ -87,27 +88,4 @@ const algoTradingMenu = async (service) => {
87
88
  }
88
89
  };
89
90
 
90
- /**
91
- * Custom Strategy Menu - AI-powered strategy creation
92
- */
93
- const customStrategyMenu = async (service) => {
94
- console.clear();
95
- displayBanner();
96
-
97
- const boxWidth = getLogoWidth();
98
- const W = boxWidth - 2;
99
-
100
- console.log(chalk.cyan('╔' + '═'.repeat(W) + '╗'));
101
- console.log(chalk.cyan('║') + chalk.green.bold(centerText('CUSTOM STRATEGY', W)) + chalk.cyan('║'));
102
- console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
103
- console.log(chalk.cyan('║') + centerText('Create your own trading strategy with AI assistance', W) + chalk.cyan('║'));
104
- console.log(chalk.cyan('╠' + '─'.repeat(W) + '╣'));
105
- console.log(chalk.cyan('║') + chalk.gray(centerText('Coming soon...', W)) + chalk.cyan('║'));
106
- console.log(chalk.cyan('╠' + '─'.repeat(W) + '╣'));
107
- console.log(chalk.cyan('║') + chalk.red(centerText('[B] BACK', W)) + chalk.cyan('║'));
108
- console.log(chalk.cyan('╚' + '═'.repeat(W) + '╝'));
109
-
110
- await prompts.waitForEnter();
111
- };
112
-
113
91
  module.exports = { algoTradingMenu };
package/src/ui/index.js CHANGED
@@ -70,7 +70,7 @@ const displayBanner = () => {
70
70
 
71
71
  console.log(chalk.cyan('╠' + '═'.repeat(innerWidth) + '╣'));
72
72
  const tagline = isMobile ? `HQX V${version}` : `PROP FUTURES ALGO TRADING V${version}`;
73
- console.log(chalk.cyan('║') + chalk.white(centerText(tagline, innerWidth)) + chalk.cyan('║'));
73
+ console.log(chalk.cyan('║') + chalk.yellow(centerText(tagline, innerWidth)) + chalk.cyan('║'));
74
74
 
75
75
  // ALWAYS close the banner
76
76
  console.log(chalk.cyan('╚' + '═'.repeat(innerWidth) + '╝'));