hedgequantx 2.7.88 → 2.7.89
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/pages/algo/custom-strategy.js +273 -288
- package/src/pages/algo/index.js +41 -16
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Custom Strategy - AI-powered
|
|
3
|
-
*
|
|
2
|
+
* Custom Strategy - AI-powered strategy builder
|
|
3
|
+
* Same config flow as One Account, then AI chat to create strategy
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const chalk = require('chalk');
|
|
@@ -8,13 +8,16 @@ const fs = require('fs');
|
|
|
8
8
|
const path = require('path');
|
|
9
9
|
const os = require('os');
|
|
10
10
|
const ora = require('ora');
|
|
11
|
+
const readline = require('readline');
|
|
11
12
|
|
|
12
13
|
const { getLogoWidth, centerText, displayBanner } = require('../../ui');
|
|
13
14
|
const { prompts } = require('../../utils');
|
|
15
|
+
const { connections } = require('../../services');
|
|
14
16
|
const { getActiveProvider } = require('../ai-agents');
|
|
15
17
|
const cliproxy = require('../../services/cliproxy');
|
|
18
|
+
const { checkMarketHours } = require('../../services/rithmic/market');
|
|
16
19
|
|
|
17
|
-
//
|
|
20
|
+
// Strategies directory
|
|
18
21
|
const STRATEGIES_DIR = path.join(os.homedir(), '.hqx', 'strategies');
|
|
19
22
|
|
|
20
23
|
/** Ensure strategies directory exists */
|
|
@@ -22,175 +25,203 @@ const ensureStrategiesDir = () => {
|
|
|
22
25
|
if (!fs.existsSync(STRATEGIES_DIR)) fs.mkdirSync(STRATEGIES_DIR, { recursive: true });
|
|
23
26
|
};
|
|
24
27
|
|
|
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
28
|
/** Custom Strategy Menu */
|
|
72
29
|
const customStrategyMenu = async (service) => {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
}
|
|
30
|
+
// Check AI provider first
|
|
31
|
+
const aiProvider = getActiveProvider();
|
|
32
|
+
if (!aiProvider) {
|
|
33
|
+
console.log(chalk.red('\n No AI Agent connected. Go to AI Agents menu first.'));
|
|
34
|
+
await prompts.waitForEnter();
|
|
35
|
+
return;
|
|
126
36
|
}
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
/** AI Wizard to create modular strategy */
|
|
130
|
-
const createStrategyWizard = async (aiProvider) => {
|
|
131
|
-
console.clear();
|
|
132
|
-
displayBanner();
|
|
133
37
|
|
|
134
|
-
|
|
135
|
-
const
|
|
38
|
+
// Check market hours
|
|
39
|
+
const market = checkMarketHours();
|
|
40
|
+
if (!market.isOpen && !market.message.includes('early')) {
|
|
41
|
+
console.log();
|
|
42
|
+
console.log(chalk.red(` ${market.message}`));
|
|
43
|
+
console.log(chalk.gray(' Custom strategy requires market to be open'));
|
|
44
|
+
console.log();
|
|
45
|
+
await prompts.waitForEnter();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
136
48
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
console.log();
|
|
49
|
+
// Step 1: Fetch accounts
|
|
50
|
+
const spinner = ora({ text: 'Fetching active accounts...', color: 'yellow' }).start();
|
|
51
|
+
const allAccounts = await connections.getAllAccounts();
|
|
141
52
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const name = await prompts.textInput(chalk.cyan(' Strategy name: '));
|
|
145
|
-
if (!name || !name.trim()) {
|
|
146
|
-
console.log(chalk.red(' Cancelled'));
|
|
53
|
+
if (!allAccounts?.length) {
|
|
54
|
+
spinner.fail('No accounts found');
|
|
147
55
|
await prompts.waitForEnter();
|
|
148
56
|
return;
|
|
149
57
|
}
|
|
150
58
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
console.log(chalk.red(` Error: ${folder.error}`));
|
|
59
|
+
const activeAccounts = allAccounts.filter(acc => acc.status === 0);
|
|
60
|
+
if (!activeAccounts.length) {
|
|
61
|
+
spinner.fail('No active accounts');
|
|
155
62
|
await prompts.waitForEnter();
|
|
156
63
|
return;
|
|
157
64
|
}
|
|
158
65
|
|
|
159
|
-
|
|
66
|
+
spinner.succeed(`Found ${activeAccounts.length} active account(s)`);
|
|
67
|
+
|
|
68
|
+
// Step 2: Select account
|
|
69
|
+
console.log();
|
|
70
|
+
console.log(chalk.cyan.bold(' STEP 1: SELECT ACCOUNT'));
|
|
71
|
+
const accountOptions = activeAccounts.map(acc => {
|
|
72
|
+
const name = acc.accountName || acc.rithmicAccountId || acc.accountId;
|
|
73
|
+
const balance = acc.balance !== null && acc.balance !== undefined
|
|
74
|
+
? ` - $${acc.balance.toLocaleString()}` : '';
|
|
75
|
+
return {
|
|
76
|
+
label: `${name} (${acc.propfirm || acc.platform || 'Unknown'})${balance}`,
|
|
77
|
+
value: acc
|
|
78
|
+
};
|
|
79
|
+
});
|
|
80
|
+
accountOptions.push({ label: '< Back', value: 'back' });
|
|
81
|
+
|
|
82
|
+
const selectedAccount = await prompts.selectOption('Select Account:', accountOptions);
|
|
83
|
+
if (!selectedAccount || selectedAccount === 'back') return;
|
|
84
|
+
|
|
85
|
+
const accountService = selectedAccount.service || connections.getServiceForAccount(selectedAccount.accountId) || service;
|
|
86
|
+
|
|
87
|
+
// Step 3: Select symbol
|
|
88
|
+
console.log();
|
|
89
|
+
console.log(chalk.cyan.bold(' STEP 2: SELECT SYMBOL'));
|
|
90
|
+
const contract = await selectSymbol(accountService);
|
|
91
|
+
if (!contract) return;
|
|
92
|
+
|
|
93
|
+
// Step 4: Configure parameters
|
|
94
|
+
console.log();
|
|
95
|
+
console.log(chalk.cyan.bold(' STEP 3: CONFIGURE PARAMETERS'));
|
|
96
|
+
console.log();
|
|
97
|
+
|
|
98
|
+
const contracts = await prompts.numberInput('Number of contracts:', 1, 1, 10);
|
|
99
|
+
if (contracts === null) return;
|
|
100
|
+
|
|
101
|
+
const dailyTarget = await prompts.numberInput('Daily target ($):', 200, 1, 10000);
|
|
102
|
+
if (dailyTarget === null) return;
|
|
103
|
+
|
|
104
|
+
const maxRisk = await prompts.numberInput('Max risk ($):', 100, 1, 5000);
|
|
105
|
+
if (maxRisk === null) return;
|
|
106
|
+
|
|
107
|
+
const showName = await prompts.confirmPrompt('Show account name?', false);
|
|
108
|
+
if (showName === null) return;
|
|
109
|
+
|
|
110
|
+
// Step 5: AI Supervision
|
|
160
111
|
console.log();
|
|
112
|
+
console.log(chalk.cyan.bold(' STEP 4: AI SUPERVISION'));
|
|
113
|
+
const aiSupervision = await prompts.confirmPrompt('Enable AI supervision during execution?', true);
|
|
114
|
+
if (aiSupervision === null) return;
|
|
161
115
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
116
|
+
const config = {
|
|
117
|
+
account: selectedAccount,
|
|
118
|
+
contract,
|
|
119
|
+
contracts,
|
|
120
|
+
dailyTarget,
|
|
121
|
+
maxRisk,
|
|
122
|
+
showName,
|
|
123
|
+
aiSupervision,
|
|
124
|
+
aiProvider
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// Step 6: AI Chat to create/configure strategy
|
|
128
|
+
console.log();
|
|
129
|
+
console.log(chalk.cyan.bold(' STEP 5: CREATE YOUR STRATEGY WITH AI'));
|
|
130
|
+
console.log(chalk.gray(' Describe your trading strategy in plain English.'));
|
|
131
|
+
console.log(chalk.gray(' The AI will help you build and validate it.'));
|
|
167
132
|
console.log();
|
|
168
133
|
|
|
169
|
-
|
|
170
|
-
|
|
134
|
+
await strategyChat(config, accountService);
|
|
135
|
+
};
|
|
171
136
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
137
|
+
/** Select symbol - same as one-account */
|
|
138
|
+
const selectSymbol = async (service) => {
|
|
139
|
+
const spinner = ora({ text: 'Loading symbols...', color: 'yellow' }).start();
|
|
140
|
+
|
|
141
|
+
const contractsResult = await service.getContracts();
|
|
142
|
+
if (!contractsResult.success || !contractsResult.contracts?.length) {
|
|
143
|
+
spinner.fail('Failed to load contracts');
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let contracts = contractsResult.contracts;
|
|
148
|
+
|
|
149
|
+
// Sort popular indices first
|
|
150
|
+
const popular = ['ES', 'NQ', 'MES', 'MNQ', 'M2K', 'RTY', 'YM', 'MYM', 'NKD', 'GC', 'SI', 'CL'];
|
|
151
|
+
contracts.sort((a, b) => {
|
|
152
|
+
const baseA = a.baseSymbol || a.symbol || '';
|
|
153
|
+
const baseB = b.baseSymbol || b.symbol || '';
|
|
154
|
+
const idxA = popular.findIndex(p => baseA === p || baseA.startsWith(p));
|
|
155
|
+
const idxB = popular.findIndex(p => baseB === p || baseB.startsWith(p));
|
|
156
|
+
if (idxA !== -1 && idxB !== -1) return idxA - idxB;
|
|
157
|
+
if (idxA !== -1) return -1;
|
|
158
|
+
if (idxB !== -1) return 1;
|
|
159
|
+
return baseA.localeCompare(baseB);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
spinner.succeed(`Found ${contracts.length} contracts`);
|
|
163
|
+
|
|
164
|
+
const options = contracts.map(c => ({
|
|
165
|
+
label: `${c.symbol} - ${c.name} (${c.exchange})`,
|
|
166
|
+
value: c
|
|
167
|
+
}));
|
|
168
|
+
options.push({ label: chalk.gray('< Back'), value: 'back' });
|
|
169
|
+
|
|
170
|
+
const selected = await prompts.selectOption(chalk.yellow('Select Symbol:'), options);
|
|
171
|
+
return selected === 'back' || selected === null ? null : selected;
|
|
172
|
+
};
|
|
176
173
|
|
|
177
|
-
|
|
174
|
+
/** AI Chat for strategy creation */
|
|
175
|
+
const strategyChat = async (config, service) => {
|
|
176
|
+
const { account, contract, contracts, dailyTarget, maxRisk, showName, aiSupervision, aiProvider } = config;
|
|
177
|
+
|
|
178
|
+
const accountName = showName
|
|
179
|
+
? (account.accountName || account.rithmicAccountId || account.accountId)
|
|
180
|
+
: 'HQX *****';
|
|
181
|
+
|
|
182
|
+
console.clear();
|
|
183
|
+
displayBanner();
|
|
184
|
+
|
|
185
|
+
const boxWidth = getLogoWidth();
|
|
186
|
+
const W = boxWidth - 2;
|
|
187
|
+
|
|
188
|
+
console.log(chalk.cyan('╔' + '═'.repeat(W) + '╗'));
|
|
189
|
+
console.log(chalk.cyan('║') + chalk.green.bold(centerText('CUSTOM STRATEGY - AI CHAT', W)) + chalk.cyan('║'));
|
|
190
|
+
console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
|
|
191
|
+
console.log(chalk.cyan('║') + centerText(`Account: ${accountName} | Symbol: ${contract.name} | Qty: ${contracts}`, W) + chalk.cyan('║'));
|
|
192
|
+
console.log(chalk.cyan('║') + centerText(`Target: $${dailyTarget} | Risk: $${maxRisk} | AI Supervision: ${aiSupervision ? 'ON' : 'OFF'}`, W) + chalk.cyan('║'));
|
|
193
|
+
console.log(chalk.cyan('║') + chalk.gray(centerText(`AI: ${aiProvider.name} (${aiProvider.modelName || 'default'})`, W)) + chalk.cyan('║'));
|
|
194
|
+
console.log(chalk.cyan('╠' + '─'.repeat(W) + '╣'));
|
|
195
|
+
console.log(chalk.cyan('║') + chalk.gray(centerText('Type your strategy. "run" to execute, "save" to save, "cancel" to abort', W)) + chalk.cyan('║'));
|
|
196
|
+
console.log(chalk.cyan('╚' + '═'.repeat(W) + '╝'));
|
|
197
|
+
console.log();
|
|
198
|
+
|
|
199
|
+
const systemPrompt = `You are an expert algorithmic trading assistant for futures trading.
|
|
178
200
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
185
|
-
|
|
201
|
+
Current setup:
|
|
202
|
+
- Account: ${accountName}
|
|
203
|
+
- Symbol: ${contract.name} (${contract.symbol})
|
|
204
|
+
- Contracts: ${contracts}
|
|
205
|
+
- Daily Target: $${dailyTarget}
|
|
206
|
+
- Max Risk: $${maxRisk}
|
|
207
|
+
- AI Supervision: ${aiSupervision ? 'Enabled' : 'Disabled'}
|
|
186
208
|
|
|
187
|
-
|
|
209
|
+
Help the user create a trading strategy. When they describe what they want:
|
|
210
|
+
1. Understand their entry/exit logic
|
|
211
|
+
2. Validate the strategy makes sense
|
|
212
|
+
3. Suggest improvements if needed
|
|
213
|
+
4. When ready, confirm the strategy is valid
|
|
214
|
+
|
|
215
|
+
Keep responses concise (2-3 sentences max unless explaining strategy details).
|
|
216
|
+
When the user says "run", output: [STRATEGY_READY]
|
|
217
|
+
Include the strategy parameters in JSON format when ready.`;
|
|
188
218
|
|
|
189
219
|
const messages = [{ role: 'system', content: systemPrompt }];
|
|
190
|
-
|
|
220
|
+
let strategyReady = false;
|
|
221
|
+
let strategyConfig = null;
|
|
191
222
|
|
|
192
|
-
console.log(chalk.green(' AI: ') + '
|
|
193
|
-
console.log(chalk.
|
|
223
|
+
console.log(chalk.green(' AI: ') + `Hello! I'll help you create a custom strategy for ${contract.name}.`);
|
|
224
|
+
console.log(chalk.green(' ') + 'What kind of trading strategy do you want to build?');
|
|
194
225
|
console.log();
|
|
195
226
|
|
|
196
227
|
while (true) {
|
|
@@ -198,30 +229,32 @@ The 'data' object contains: { price, bid, ask, volume, atr, ema20, ema50, rsi, m
|
|
|
198
229
|
|
|
199
230
|
if (!userInput) continue;
|
|
200
231
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
232
|
+
const cmd = userInput.toLowerCase().trim();
|
|
233
|
+
|
|
234
|
+
if (cmd === 'cancel' || cmd === 'exit' || cmd === 'quit') {
|
|
235
|
+
console.log(chalk.gray('\n Strategy creation cancelled.'));
|
|
204
236
|
await prompts.waitForEnter();
|
|
205
237
|
return;
|
|
206
238
|
}
|
|
207
239
|
|
|
208
|
-
if (
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
240
|
+
if (cmd === 'save') {
|
|
241
|
+
if (strategyConfig) {
|
|
242
|
+
await saveStrategy(strategyConfig, config);
|
|
243
|
+
} else {
|
|
244
|
+
console.log(chalk.yellow('\n No strategy to save yet. Keep describing your strategy.'));
|
|
245
|
+
}
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (cmd === 'run') {
|
|
250
|
+
if (strategyReady && strategyConfig) {
|
|
251
|
+
console.log(chalk.green('\n Launching strategy...'));
|
|
252
|
+
await launchCustomStrategy(config, strategyConfig, service);
|
|
253
|
+
return;
|
|
254
|
+
} else {
|
|
255
|
+
console.log(chalk.yellow('\n Strategy not ready yet. Describe your entry/exit conditions first.'));
|
|
256
|
+
continue;
|
|
222
257
|
}
|
|
223
|
-
await prompts.waitForEnter();
|
|
224
|
-
return;
|
|
225
258
|
}
|
|
226
259
|
|
|
227
260
|
messages.push({ role: 'user', content: userInput });
|
|
@@ -243,25 +276,24 @@ The 'data' object contains: { price, bid, ask, volume, atr, ema20, ema50, rsi, m
|
|
|
243
276
|
|
|
244
277
|
spinner.stop();
|
|
245
278
|
console.log();
|
|
279
|
+
console.log(chalk.green(' AI: ') + formatResponse(response));
|
|
280
|
+
console.log();
|
|
246
281
|
|
|
247
|
-
//
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
282
|
+
// Check if strategy is ready
|
|
283
|
+
if (response.includes('[STRATEGY_READY]') || response.toLowerCase().includes('strategy is ready')) {
|
|
284
|
+
strategyReady = true;
|
|
285
|
+
// Try to extract JSON config
|
|
286
|
+
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
|
287
|
+
if (jsonMatch) {
|
|
288
|
+
try { strategyConfig = JSON.parse(jsonMatch[0]); } catch (e) {}
|
|
289
|
+
}
|
|
290
|
+
if (!strategyConfig) {
|
|
291
|
+
strategyConfig = { description: userInput, messages: messages.slice(1) };
|
|
292
|
+
}
|
|
293
|
+
console.log(chalk.cyan(' [Strategy ready! Type "run" to execute or "save" to save for later]'));
|
|
294
|
+
console.log();
|
|
254
295
|
}
|
|
255
296
|
|
|
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
297
|
} catch (e) {
|
|
266
298
|
spinner.fail(`Error: ${e.message}`);
|
|
267
299
|
messages.pop();
|
|
@@ -269,126 +301,79 @@ The 'data' object contains: { price, bid, ask, volume, atr, ema20, ema50, rsi, m
|
|
|
269
301
|
}
|
|
270
302
|
};
|
|
271
303
|
|
|
272
|
-
/** Get default model */
|
|
304
|
+
/** Get default model for provider */
|
|
273
305
|
const getDefaultModel = (providerId) => {
|
|
274
|
-
const defaults = {
|
|
275
|
-
anthropic: 'claude-sonnet-4-20250514',
|
|
276
|
-
google: 'gemini-2.5-pro',
|
|
277
|
-
openai: 'gpt-4o'
|
|
278
|
-
};
|
|
306
|
+
const defaults = { anthropic: 'claude-sonnet-4-20250514', google: 'gemini-2.5-pro', openai: 'gpt-4o' };
|
|
279
307
|
return defaults[providerId] || 'claude-sonnet-4-20250514';
|
|
280
308
|
};
|
|
281
309
|
|
|
282
|
-
/** Format response
|
|
310
|
+
/** Format AI response */
|
|
283
311
|
const formatResponse = (text) => {
|
|
284
|
-
const
|
|
312
|
+
const clean = text.replace(/\[STRATEGY_READY\]/g, '').trim();
|
|
313
|
+
const lines = clean.split('\n');
|
|
285
314
|
return lines.map((l, i) => i === 0 ? l : ' ' + l).join('\n');
|
|
286
315
|
};
|
|
287
316
|
|
|
288
|
-
/**
|
|
289
|
-
const
|
|
290
|
-
|
|
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('║'));
|
|
317
|
+
/** Save strategy to disk */
|
|
318
|
+
const saveStrategy = async (strategyConfig, config) => {
|
|
319
|
+
ensureStrategiesDir();
|
|
354
320
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
console.log(chalk.
|
|
321
|
+
const name = await prompts.textInput(chalk.cyan(' Strategy name: '));
|
|
322
|
+
if (!name || !name.trim()) {
|
|
323
|
+
console.log(chalk.gray(' Save cancelled.'));
|
|
324
|
+
return;
|
|
358
325
|
}
|
|
359
326
|
|
|
360
|
-
|
|
327
|
+
const folderName = name.trim().toLowerCase().replace(/[^a-z0-9]/g, '-');
|
|
328
|
+
const strategyPath = path.join(STRATEGIES_DIR, folderName);
|
|
361
329
|
|
|
362
|
-
|
|
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('║'));
|
|
330
|
+
if (!fs.existsSync(strategyPath)) fs.mkdirSync(strategyPath, { recursive: true });
|
|
368
331
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
332
|
+
const configFile = {
|
|
333
|
+
name: name.trim(),
|
|
334
|
+
symbol: config.contract.name,
|
|
335
|
+
contracts: config.contracts,
|
|
336
|
+
dailyTarget: config.dailyTarget,
|
|
337
|
+
maxRisk: config.maxRisk,
|
|
338
|
+
aiSupervision: config.aiSupervision,
|
|
339
|
+
strategy: strategyConfig,
|
|
340
|
+
createdAt: new Date().toISOString()
|
|
341
|
+
};
|
|
372
342
|
|
|
373
|
-
|
|
374
|
-
|
|
343
|
+
fs.writeFileSync(path.join(strategyPath, 'config.json'), JSON.stringify(configFile, null, 2));
|
|
344
|
+
console.log(chalk.green(`\n ✓ Strategy saved: ${strategyPath}`));
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
/** Launch custom strategy execution */
|
|
348
|
+
const launchCustomStrategy = async (config, strategyConfig, service) => {
|
|
349
|
+
const { account, contract, contracts, dailyTarget, maxRisk, showName, aiSupervision, aiProvider } = config;
|
|
375
350
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
351
|
+
console.log(chalk.yellow('\n Custom strategy execution coming soon...'));
|
|
352
|
+
console.log(chalk.gray(' Your strategy will use the HQX engine with AI supervision.'));
|
|
353
|
+
console.log();
|
|
354
|
+
console.log(chalk.white(' Strategy Summary:'));
|
|
355
|
+
console.log(chalk.gray(` - Symbol: ${contract.name}`));
|
|
356
|
+
console.log(chalk.gray(` - Contracts: ${contracts}`));
|
|
357
|
+
console.log(chalk.gray(` - Target: $${dailyTarget}`));
|
|
358
|
+
console.log(chalk.gray(` - Risk: $${maxRisk}`));
|
|
359
|
+
console.log(chalk.gray(` - AI Supervision: ${aiSupervision ? 'Enabled' : 'Disabled'}`));
|
|
360
|
+
|
|
361
|
+
await prompts.waitForEnter();
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
/** Load saved strategies */
|
|
365
|
+
const loadStrategies = () => {
|
|
366
|
+
ensureStrategiesDir();
|
|
367
|
+
try {
|
|
368
|
+
const items = fs.readdirSync(STRATEGIES_DIR, { withFileTypes: true });
|
|
369
|
+
return items.filter(i => i.isDirectory()).map(dir => {
|
|
370
|
+
const configPath = path.join(STRATEGIES_DIR, dir.name, 'config.json');
|
|
371
|
+
if (fs.existsSync(configPath)) {
|
|
372
|
+
return { folder: dir.name, ...JSON.parse(fs.readFileSync(configPath, 'utf8')) };
|
|
373
|
+
}
|
|
374
|
+
return { folder: dir.name, name: dir.name };
|
|
375
|
+
});
|
|
376
|
+
} catch (e) { return []; }
|
|
392
377
|
};
|
|
393
378
|
|
|
394
|
-
module.exports = { customStrategyMenu, loadStrategies
|
|
379
|
+
module.exports = { customStrategyMenu, loadStrategies };
|
package/src/pages/algo/index.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
const chalk = require('chalk');
|
|
6
6
|
const { getLogoWidth, centerText, displayBanner } = require('../../ui');
|
|
7
7
|
const { logger, prompts } = require('../../utils');
|
|
8
|
+
const { getActiveProvider } = require('../ai-agents');
|
|
8
9
|
|
|
9
10
|
const log = logger.scope('AlgoMenu');
|
|
10
11
|
|
|
@@ -26,33 +27,52 @@ const algoTradingMenu = async (service) => {
|
|
|
26
27
|
const boxWidth = getLogoWidth();
|
|
27
28
|
const W = boxWidth - 2;
|
|
28
29
|
|
|
30
|
+
// Check if AI agent is connected
|
|
31
|
+
const aiProvider = getActiveProvider();
|
|
32
|
+
const hasAI = !!aiProvider;
|
|
33
|
+
|
|
29
34
|
// Draw menu rectangle
|
|
30
35
|
console.log(chalk.cyan('╔' + '═'.repeat(W) + '╗'));
|
|
31
36
|
console.log(chalk.cyan('║') + chalk.magenta.bold(centerText('ALGO-TRADING', W)) + chalk.cyan('║'));
|
|
32
37
|
console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
|
|
33
38
|
|
|
34
|
-
// 3 columns layout
|
|
39
|
+
// 2 or 3 columns layout based on AI availability
|
|
35
40
|
const col1 = '[1] ONE ACCOUNT';
|
|
36
41
|
const col2 = '[2] COPY TRADING';
|
|
37
|
-
const col3 = '[3] CUSTOM STRATEGY';
|
|
38
|
-
const colWidth = Math.floor(W / 3);
|
|
39
|
-
const lastColWidth = W - 2 * colWidth;
|
|
40
|
-
|
|
41
|
-
const pad1 = Math.floor((colWidth - col1.length) / 2);
|
|
42
|
-
const pad2 = Math.floor((colWidth - col2.length) / 2);
|
|
43
|
-
const pad3 = Math.floor((lastColWidth - col3.length) / 2);
|
|
42
|
+
const col3 = hasAI ? '[3] CUSTOM STRATEGY' : '';
|
|
44
43
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
44
|
+
if (hasAI) {
|
|
45
|
+
// 3 columns
|
|
46
|
+
const colWidth = Math.floor(W / 3);
|
|
47
|
+
const lastColWidth = W - 2 * colWidth;
|
|
48
|
+
|
|
49
|
+
const pad1 = Math.floor((colWidth - col1.length) / 2);
|
|
50
|
+
const pad2 = Math.floor((colWidth - col2.length) / 2);
|
|
51
|
+
const pad3 = Math.floor((lastColWidth - col3.length) / 2);
|
|
52
|
+
|
|
53
|
+
const col1Str = ' '.repeat(pad1) + chalk.cyan(col1) + ' '.repeat(colWidth - col1.length - pad1);
|
|
54
|
+
const col2Str = ' '.repeat(pad2) + chalk.yellow(col2) + ' '.repeat(colWidth - col2.length - pad2);
|
|
55
|
+
const col3Str = ' '.repeat(pad3) + chalk.green(col3) + ' '.repeat(lastColWidth - col3.length - pad3);
|
|
56
|
+
|
|
57
|
+
console.log(chalk.cyan('║') + col1Str + col2Str + col3Str + chalk.cyan('║'));
|
|
58
|
+
} else {
|
|
59
|
+
// 2 columns only (no AI connected)
|
|
60
|
+
const colWidth = Math.floor(W / 2);
|
|
61
|
+
const pad1 = Math.floor((colWidth - col1.length) / 2);
|
|
62
|
+
const pad2 = Math.floor((W - colWidth - col2.length) / 2);
|
|
63
|
+
|
|
64
|
+
const col1Str = ' '.repeat(pad1) + chalk.cyan(col1) + ' '.repeat(colWidth - col1.length - pad1);
|
|
65
|
+
const col2Str = ' '.repeat(pad2) + chalk.yellow(col2) + ' '.repeat(W - colWidth - col2.length - pad2);
|
|
66
|
+
|
|
67
|
+
console.log(chalk.cyan('║') + col1Str + col2Str + chalk.cyan('║'));
|
|
68
|
+
}
|
|
50
69
|
|
|
51
70
|
console.log(chalk.cyan('╠' + '─'.repeat(W) + '╣'));
|
|
52
71
|
console.log(chalk.cyan('║') + chalk.red(centerText('[B] BACK', W)) + chalk.cyan('║'));
|
|
53
72
|
console.log(chalk.cyan('╚' + '═'.repeat(W) + '╝'));
|
|
54
73
|
|
|
55
|
-
const
|
|
74
|
+
const promptText = hasAI ? 'SELECT (1/2/3/B): ' : 'SELECT (1/2/B): ';
|
|
75
|
+
const input = await prompts.textInput(chalk.cyan(promptText));
|
|
56
76
|
const choice = (input || '').toLowerCase().trim();
|
|
57
77
|
|
|
58
78
|
log.debug('Algo mode selected', { choice });
|
|
@@ -71,8 +91,13 @@ const algoTradingMenu = async (service) => {
|
|
|
71
91
|
await copyTradingMenu();
|
|
72
92
|
break;
|
|
73
93
|
case '3':
|
|
74
|
-
|
|
75
|
-
|
|
94
|
+
if (hasAI) {
|
|
95
|
+
log.info('Starting Custom Strategy mode');
|
|
96
|
+
await customStrategyMenu(service);
|
|
97
|
+
} else {
|
|
98
|
+
console.log(chalk.red(' INVALID OPTION'));
|
|
99
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
100
|
+
}
|
|
76
101
|
break;
|
|
77
102
|
default:
|
|
78
103
|
console.log(chalk.red(' INVALID OPTION'));
|