hedgequantx 2.7.18 → 2.7.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hedgequantx",
3
- "version": "2.7.18",
3
+ "version": "2.7.20",
4
4
  "description": "HedgeQuantX - Prop Futures Trading CLI",
5
5
  "main": "src/app.js",
6
6
  "bin": {
package/src/app.js CHANGED
@@ -17,6 +17,7 @@ const log = logger.scope('App');
17
17
  const { showStats } = require('./pages/stats');
18
18
  const { showAccounts } = require('./pages/accounts');
19
19
  const { algoTradingMenu } = require('./pages/algo');
20
+ const { aiAgentsMenu, getActiveAgentCount } = require('./pages/ai-agents');
20
21
 
21
22
  // Menus
22
23
  const { rithmicMenu, dashboardMenu, handleUpdate } = require('./menus');
@@ -296,9 +297,7 @@ const run = async () => {
296
297
  break;
297
298
 
298
299
  case 'aiagents':
299
- console.log(chalk.yellow('\n AI Agents - Coming soon!'));
300
- console.log(chalk.gray(' Configure AI trading agents for automated strategies.'));
301
- await prompts.waitForEnter();
300
+ await aiAgentsMenu();
302
301
  break;
303
302
 
304
303
  case 'update':
@@ -0,0 +1,441 @@
1
+ /**
2
+ * AI Agents Configuration Page
3
+ *
4
+ * Allows users to configure AI providers for trading strategies.
5
+ * Supports both CLIProxy (paid plans) and direct API keys.
6
+ */
7
+
8
+ const chalk = require('chalk');
9
+ const os = require('os');
10
+ const path = require('path');
11
+ const fs = require('fs');
12
+
13
+ const { getLogoWidth, centerText, visibleLength } = require('../ui');
14
+ const { prompts } = require('../utils');
15
+ const { getModelsForProvider, getModelById } = require('./ai-models');
16
+
17
+ // Config file path
18
+ const CONFIG_DIR = path.join(os.homedir(), '.hqx');
19
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'ai-config.json');
20
+
21
+ // AI Providers list
22
+ const AI_PROVIDERS = [
23
+ { id: 'anthropic', name: 'Anthropic (Claude)', color: 'magenta' },
24
+ { id: 'openai', name: 'OpenAI (GPT)', color: 'green' },
25
+ { id: 'google', name: 'Google (Gemini)', color: 'blue' },
26
+ { id: 'mistral', name: 'Mistral AI', color: 'yellow' },
27
+ { id: 'groq', name: 'Groq', color: 'cyan' },
28
+ { id: 'xai', name: 'xAI (Grok)', color: 'white' },
29
+ { id: 'perplexity', name: 'Perplexity', color: 'blue' },
30
+ { id: 'openrouter', name: 'OpenRouter', color: 'gray' },
31
+ ];
32
+
33
+ /**
34
+ * Load AI config from file
35
+ * @returns {Object} Config object with provider settings
36
+ */
37
+ const loadConfig = () => {
38
+ try {
39
+ if (fs.existsSync(CONFIG_FILE)) {
40
+ const data = fs.readFileSync(CONFIG_FILE, 'utf8');
41
+ return JSON.parse(data);
42
+ }
43
+ } catch (error) {
44
+ // Config file doesn't exist or is invalid
45
+ }
46
+ return { providers: {} };
47
+ };
48
+
49
+ /**
50
+ * Save AI config to file
51
+ * @param {Object} config - Config object to save
52
+ * @returns {boolean} Success status
53
+ */
54
+ const saveConfig = (config) => {
55
+ try {
56
+ if (!fs.existsSync(CONFIG_DIR)) {
57
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
58
+ }
59
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
60
+ return true;
61
+ } catch (error) {
62
+ return false;
63
+ }
64
+ };
65
+
66
+ /**
67
+ * Mask API key for display
68
+ * @param {string} key - API key
69
+ * @returns {string} Masked key
70
+ */
71
+ const maskKey = (key) => {
72
+ if (!key || key.length < 16) return '****';
73
+ return key.substring(0, 8) + '...' + key.substring(key.length - 4);
74
+ };
75
+
76
+ /**
77
+ * Draw a 2-column row
78
+ */
79
+ const draw2ColRow = (leftText, rightText, W) => {
80
+ const col1Width = Math.floor(W / 2);
81
+ const col2Width = W - col1Width;
82
+ const leftLen = visibleLength(leftText);
83
+ const leftPad = col1Width - leftLen;
84
+ const leftPadL = Math.floor(leftPad / 2);
85
+ const rightLen = visibleLength(rightText || '');
86
+ const rightPad = col2Width - rightLen;
87
+ const rightPadL = Math.floor(rightPad / 2);
88
+ console.log(
89
+ chalk.cyan('║') +
90
+ ' '.repeat(leftPadL) + leftText + ' '.repeat(leftPad - leftPadL) +
91
+ ' '.repeat(rightPadL) + (rightText || '') + ' '.repeat(rightPad - rightPadL) +
92
+ chalk.cyan('║')
93
+ );
94
+ };
95
+
96
+ /**
97
+ * Draw 2-column table
98
+ */
99
+ const draw2ColTable = (title, titleColor, items, backText, W) => {
100
+ console.log(chalk.cyan('╔' + '═'.repeat(W) + '╗'));
101
+ console.log(chalk.cyan('║') + titleColor(centerText(title, W)) + chalk.cyan('║'));
102
+ console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
103
+
104
+ const rows = Math.ceil(items.length / 2);
105
+ for (let row = 0; row < rows; row++) {
106
+ const left = items[row];
107
+ const right = items[row + rows];
108
+ draw2ColRow(left || '', right || '', W);
109
+ }
110
+
111
+ console.log(chalk.cyan('╠' + '─'.repeat(W) + '╣'));
112
+ console.log(chalk.cyan('║') + chalk.red(centerText(backText, W)) + chalk.cyan('║'));
113
+ console.log(chalk.cyan('╚' + '═'.repeat(W) + '╝'));
114
+ };
115
+
116
+ /**
117
+ * Draw providers table
118
+ */
119
+ const drawProvidersTable = (config, boxWidth) => {
120
+ const W = boxWidth - 2;
121
+ const items = AI_PROVIDERS.map((p, i) => {
122
+ const status = config.providers[p.id]?.active ? chalk.green(' ●') : '';
123
+ return chalk.cyan(`[${i + 1}]`) + ' ' + chalk[p.color](p.name) + status;
124
+ });
125
+ draw2ColTable('AI AGENTS CONFIGURATION', chalk.yellow.bold, items, '[B] Back to Menu', W);
126
+ };
127
+
128
+ /**
129
+ * Draw models table
130
+ */
131
+ const drawModelsTable = (provider, models, boxWidth) => {
132
+ const W = boxWidth - 2;
133
+ const items = models.map((m, i) => chalk.cyan(`[${i + 1}]`) + ' ' + chalk.white(m.name));
134
+ draw2ColTable(`${provider.name.toUpperCase()} - MODELS`, chalk[provider.color].bold, items, '[B] Back', W);
135
+ };
136
+
137
+ /**
138
+ * Select a model for a provider
139
+ * @param {Object} provider - Provider object
140
+ * @returns {Object|null} Selected model or null if cancelled
141
+ */
142
+ const selectModel = async (provider) => {
143
+ const boxWidth = getLogoWidth();
144
+ const models = getModelsForProvider(provider.id);
145
+
146
+ if (models.length === 0) {
147
+ return null;
148
+ }
149
+
150
+ while (true) {
151
+ console.clear();
152
+ drawModelsTable(provider, models, boxWidth);
153
+
154
+ const input = await prompts.textInput(chalk.cyan('Select model: '));
155
+ const choice = (input || '').toLowerCase().trim();
156
+
157
+ if (choice === 'b' || choice === '') {
158
+ return null;
159
+ }
160
+
161
+ const num = parseInt(choice);
162
+ if (!isNaN(num) && num >= 1 && num <= models.length) {
163
+ return models[num - 1];
164
+ }
165
+
166
+ console.log(chalk.red(' Invalid option.'));
167
+ await new Promise(r => setTimeout(r, 1000));
168
+ }
169
+ };
170
+
171
+ /**
172
+ * Draw provider configuration window
173
+ * @param {Object} provider - Provider object
174
+ * @param {Object} config - Current config
175
+ * @param {number} boxWidth - Box width
176
+ */
177
+ const drawProviderWindow = (provider, config, boxWidth) => {
178
+ const W = boxWidth - 2;
179
+ const col1Width = Math.floor(W / 2);
180
+ const col2Width = W - col1Width;
181
+ const providerConfig = config.providers[provider.id] || {};
182
+
183
+ // Header
184
+ console.log(chalk.cyan('╔' + '═'.repeat(W) + '╗'));
185
+ console.log(chalk.cyan('║') + chalk[provider.color].bold(centerText(provider.name.toUpperCase(), W)) + chalk.cyan('║'));
186
+ console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
187
+
188
+ // Empty line
189
+ console.log(chalk.cyan('║') + ' '.repeat(W) + chalk.cyan('║'));
190
+
191
+ // Options in 2 columns
192
+ const opt1Title = '[1] Connect via Paid Plan';
193
+ const opt1Desc = 'Uses CLIProxy - No API key needed';
194
+ const opt2Title = '[2] Connect via API Key';
195
+ const opt2Desc = 'Enter your own API key';
196
+
197
+ // Row 1: Titles
198
+ const left1 = chalk.green(opt1Title);
199
+ const right1 = chalk.yellow(opt2Title);
200
+ const left1Len = visibleLength(left1);
201
+ const right1Len = visibleLength(right1);
202
+ const left1PadTotal = col1Width - left1Len;
203
+ const left1PadL = Math.floor(left1PadTotal / 2);
204
+ const left1PadR = left1PadTotal - left1PadL;
205
+ const right1PadTotal = col2Width - right1Len;
206
+ const right1PadL = Math.floor(right1PadTotal / 2);
207
+ const right1PadR = right1PadTotal - right1PadL;
208
+
209
+ console.log(
210
+ chalk.cyan('║') +
211
+ ' '.repeat(left1PadL) + left1 + ' '.repeat(left1PadR) +
212
+ ' '.repeat(right1PadL) + right1 + ' '.repeat(right1PadR) +
213
+ chalk.cyan('║')
214
+ );
215
+
216
+ // Row 2: Descriptions
217
+ const left2 = chalk.gray(opt1Desc);
218
+ const right2 = chalk.gray(opt2Desc);
219
+ const left2Len = visibleLength(left2);
220
+ const right2Len = visibleLength(right2);
221
+ const left2PadTotal = col1Width - left2Len;
222
+ const left2PadL = Math.floor(left2PadTotal / 2);
223
+ const left2PadR = left2PadTotal - left2PadL;
224
+ const right2PadTotal = col2Width - right2Len;
225
+ const right2PadL = Math.floor(right2PadTotal / 2);
226
+ const right2PadR = right2PadTotal - right2PadL;
227
+
228
+ console.log(
229
+ chalk.cyan('║') +
230
+ ' '.repeat(left2PadL) + left2 + ' '.repeat(left2PadR) +
231
+ ' '.repeat(right2PadL) + right2 + ' '.repeat(right2PadR) +
232
+ chalk.cyan('║')
233
+ );
234
+
235
+ // Empty line
236
+ console.log(chalk.cyan('║') + ' '.repeat(W) + chalk.cyan('║'));
237
+
238
+ // Status bar
239
+ console.log(chalk.cyan('╠' + '─'.repeat(W) + '╣'));
240
+
241
+ let statusText = '';
242
+ if (providerConfig.active) {
243
+ const connType = providerConfig.connectionType === 'cliproxy' ? 'CLIProxy' : 'API Key';
244
+ const modelName = providerConfig.modelName || 'N/A';
245
+ statusText = chalk.green('● ACTIVE') + chalk.gray(' Model: ') + chalk.yellow(modelName) + chalk.gray(' via ') + chalk.cyan(connType);
246
+ } else if (providerConfig.apiKey || providerConfig.connectionType) {
247
+ statusText = chalk.yellow('● CONFIGURED') + chalk.gray(' (not active)');
248
+ } else {
249
+ statusText = chalk.gray('○ NOT CONFIGURED');
250
+ }
251
+ console.log(chalk.cyan('║') + centerText(statusText, W) + chalk.cyan('║'));
252
+
253
+ // Disconnect option if active
254
+ if (providerConfig.active) {
255
+ console.log(chalk.cyan('╠' + '─'.repeat(W) + '╣'));
256
+ console.log(chalk.cyan('║') + chalk.red(centerText('[D] Disconnect', W)) + chalk.cyan('║'));
257
+ }
258
+
259
+ // Back
260
+ console.log(chalk.cyan('╠' + '─'.repeat(W) + '╣'));
261
+ console.log(chalk.cyan('║') + chalk.red(centerText('[B] Back', W)) + chalk.cyan('║'));
262
+ console.log(chalk.cyan('╚' + '═'.repeat(W) + '╝'));
263
+ };
264
+
265
+ /**
266
+ * Handle provider configuration
267
+ * @param {Object} provider - Provider to configure
268
+ * @param {Object} config - Current config
269
+ * @returns {Object} Updated config
270
+ */
271
+ const handleProviderConfig = async (provider, config) => {
272
+ const boxWidth = getLogoWidth();
273
+
274
+ while (true) {
275
+ console.clear();
276
+ drawProviderWindow(provider, config, boxWidth);
277
+
278
+ const input = await prompts.textInput(chalk.cyan('Select option: '));
279
+ const choice = (input || '').toLowerCase().trim();
280
+
281
+ if (choice === 'b' || choice === '') {
282
+ break;
283
+ }
284
+
285
+ if (choice === 'd') {
286
+ // Disconnect
287
+ if (config.providers[provider.id]) {
288
+ config.providers[provider.id].active = false;
289
+ saveConfig(config);
290
+ console.log(chalk.yellow(`\n ${provider.name} disconnected.`));
291
+ await prompts.waitForEnter();
292
+ }
293
+ continue;
294
+ }
295
+
296
+ if (choice === '1') {
297
+ // CLIProxy connection - select model first
298
+ const selectedModel = await selectModel(provider);
299
+ if (!selectedModel) continue;
300
+
301
+ // Deactivate all other providers
302
+ Object.keys(config.providers).forEach(id => {
303
+ if (config.providers[id]) config.providers[id].active = false;
304
+ });
305
+
306
+ if (!config.providers[provider.id]) config.providers[provider.id] = {};
307
+ config.providers[provider.id].connectionType = 'cliproxy';
308
+ config.providers[provider.id].modelId = selectedModel.id;
309
+ config.providers[provider.id].modelName = selectedModel.name;
310
+ config.providers[provider.id].active = true;
311
+ config.providers[provider.id].configuredAt = new Date().toISOString();
312
+
313
+ if (saveConfig(config)) {
314
+ console.log(chalk.green(`\n ✓ ${provider.name} connected via CLIProxy.`));
315
+ console.log(chalk.cyan(` Model: ${selectedModel.name}`));
316
+ } else {
317
+ console.log(chalk.red('\n Failed to save config.'));
318
+ }
319
+ await prompts.waitForEnter();
320
+ continue;
321
+ }
322
+
323
+ if (choice === '2') {
324
+ // API Key connection - select model first
325
+ const selectedModel = await selectModel(provider);
326
+ if (!selectedModel) continue;
327
+
328
+ console.clear();
329
+ console.log(chalk.yellow(`\n Enter your ${provider.name} API key:`));
330
+ console.log(chalk.gray(' (Press Enter to cancel)'));
331
+ console.log();
332
+
333
+ const apiKey = await prompts.textInput(chalk.cyan(' API Key: '), true);
334
+
335
+ if (!apiKey || apiKey.trim() === '') {
336
+ console.log(chalk.gray(' Cancelled.'));
337
+ await prompts.waitForEnter();
338
+ continue;
339
+ }
340
+
341
+ if (apiKey.length < 20) {
342
+ console.log(chalk.red(' Invalid API key format (too short).'));
343
+ await prompts.waitForEnter();
344
+ continue;
345
+ }
346
+
347
+ // Deactivate all other providers
348
+ Object.keys(config.providers).forEach(id => {
349
+ if (config.providers[id]) config.providers[id].active = false;
350
+ });
351
+
352
+ if (!config.providers[provider.id]) config.providers[provider.id] = {};
353
+ config.providers[provider.id].connectionType = 'apikey';
354
+ config.providers[provider.id].apiKey = apiKey.trim();
355
+ config.providers[provider.id].modelId = selectedModel.id;
356
+ config.providers[provider.id].modelName = selectedModel.name;
357
+ config.providers[provider.id].active = true;
358
+ config.providers[provider.id].configuredAt = new Date().toISOString();
359
+
360
+ if (saveConfig(config)) {
361
+ console.log(chalk.green(`\n ✓ ${provider.name} connected via API Key.`));
362
+ console.log(chalk.cyan(` Model: ${selectedModel.name}`));
363
+ } else {
364
+ console.log(chalk.red('\n Failed to save config.'));
365
+ }
366
+ await prompts.waitForEnter();
367
+ continue;
368
+ }
369
+ }
370
+
371
+ return config;
372
+ };
373
+
374
+ /**
375
+ * Get active AI provider config
376
+ * @returns {Object|null} Active provider config or null
377
+ */
378
+ const getActiveProvider = () => {
379
+ const config = loadConfig();
380
+ for (const provider of AI_PROVIDERS) {
381
+ const providerConfig = config.providers[provider.id];
382
+ if (providerConfig && providerConfig.active) {
383
+ return {
384
+ id: provider.id,
385
+ name: provider.name,
386
+ connectionType: providerConfig.connectionType,
387
+ apiKey: providerConfig.apiKey || null,
388
+ modelId: providerConfig.modelId || null,
389
+ modelName: providerConfig.modelName || null
390
+ };
391
+ }
392
+ }
393
+ return null;
394
+ };
395
+
396
+ /**
397
+ * Count active AI agents
398
+ * @returns {number} Number of active agents (0 or 1)
399
+ */
400
+ const getActiveAgentCount = () => {
401
+ const active = getActiveProvider();
402
+ return active ? 1 : 0;
403
+ };
404
+
405
+ /**
406
+ * Main AI Agents menu
407
+ */
408
+ const aiAgentsMenu = async () => {
409
+ let config = loadConfig();
410
+ const boxWidth = getLogoWidth();
411
+
412
+ while (true) {
413
+ console.clear();
414
+ drawProvidersTable(config, boxWidth);
415
+
416
+ const input = await prompts.textInput(chalk.cyan('Select provider: '));
417
+ const choice = (input || '').toLowerCase().trim();
418
+
419
+ if (choice === 'b' || choice === '') {
420
+ break;
421
+ }
422
+
423
+ const num = parseInt(choice);
424
+ if (!isNaN(num) && num >= 1 && num <= AI_PROVIDERS.length) {
425
+ config = await handleProviderConfig(AI_PROVIDERS[num - 1], config);
426
+ continue;
427
+ }
428
+
429
+ console.log(chalk.red(' Invalid option.'));
430
+ await new Promise(r => setTimeout(r, 1000));
431
+ }
432
+ };
433
+
434
+ module.exports = {
435
+ aiAgentsMenu,
436
+ getActiveProvider,
437
+ getActiveAgentCount,
438
+ loadConfig,
439
+ saveConfig,
440
+ AI_PROVIDERS
441
+ };
@@ -0,0 +1,84 @@
1
+ /**
2
+ * AI Models Configuration
3
+ *
4
+ * Lists available models for each AI provider.
5
+ * These are technical configuration values, not trading data.
6
+ */
7
+
8
+ // Models by provider ID
9
+ const PROVIDER_MODELS = {
10
+ anthropic: [
11
+ { id: 'claude-opus-4-20250514', name: 'Claude Opus 4', tier: 'flagship' },
12
+ { id: 'claude-sonnet-4-20250514', name: 'Claude Sonnet 4', tier: 'balanced' },
13
+ { id: 'claude-3-5-sonnet-20241022', name: 'Claude Sonnet 3.5', tier: 'balanced' },
14
+ { id: 'claude-3-5-haiku-20241022', name: 'Claude Haiku 3.5', tier: 'fast' },
15
+ ],
16
+ openai: [
17
+ { id: 'gpt-4o', name: 'GPT-4o', tier: 'flagship' },
18
+ { id: 'gpt-4o-mini', name: 'GPT-4o Mini', tier: 'fast' },
19
+ { id: 'gpt-4-turbo', name: 'GPT-4 Turbo', tier: 'balanced' },
20
+ { id: 'o1', name: 'o1', tier: 'reasoning' },
21
+ { id: 'o1-mini', name: 'o1-mini', tier: 'reasoning' },
22
+ { id: 'o3-mini', name: 'o3-mini', tier: 'reasoning' },
23
+ ],
24
+ google: [
25
+ { id: 'gemini-2.0-flash', name: 'Gemini 2.0 Flash', tier: 'flagship' },
26
+ { id: 'gemini-1.5-pro', name: 'Gemini 1.5 Pro', tier: 'balanced' },
27
+ { id: 'gemini-1.5-flash', name: 'Gemini 1.5 Flash', tier: 'fast' },
28
+ { id: 'gemini-1.0-pro', name: 'Gemini 1.0 Pro', tier: 'legacy' },
29
+ ],
30
+ mistral: [
31
+ { id: 'mistral-large-latest', name: 'Mistral Large', tier: 'flagship' },
32
+ { id: 'mistral-medium-latest', name: 'Mistral Medium', tier: 'balanced' },
33
+ { id: 'mistral-small-latest', name: 'Mistral Small', tier: 'fast' },
34
+ { id: 'codestral-latest', name: 'Codestral', tier: 'code' },
35
+ ],
36
+ groq: [
37
+ { id: 'llama-3.3-70b-versatile', name: 'Llama 3.3 70B', tier: 'flagship' },
38
+ { id: 'llama-3.1-8b-instant', name: 'Llama 3.1 8B', tier: 'fast' },
39
+ { id: 'mixtral-8x7b-32768', name: 'Mixtral 8x7B', tier: 'balanced' },
40
+ { id: 'gemma2-9b-it', name: 'Gemma 2 9B', tier: 'fast' },
41
+ ],
42
+ xai: [
43
+ { id: 'grok-2', name: 'Grok 2', tier: 'flagship' },
44
+ { id: 'grok-2-mini', name: 'Grok 2 Mini', tier: 'fast' },
45
+ { id: 'grok-beta', name: 'Grok Beta', tier: 'beta' },
46
+ ],
47
+ perplexity: [
48
+ { id: 'sonar-pro', name: 'Sonar Pro', tier: 'flagship' },
49
+ { id: 'sonar', name: 'Sonar', tier: 'balanced' },
50
+ { id: 'sonar-reasoning', name: 'Sonar Reasoning', tier: 'reasoning' },
51
+ ],
52
+ openrouter: [
53
+ { id: 'anthropic/claude-opus-4', name: 'Claude Opus 4', tier: 'flagship' },
54
+ { id: 'openai/gpt-4o', name: 'GPT-4o', tier: 'flagship' },
55
+ { id: 'google/gemini-2.0-flash', name: 'Gemini 2.0 Flash', tier: 'flagship' },
56
+ { id: 'meta-llama/llama-3.3-70b', name: 'Llama 3.3 70B', tier: 'open' },
57
+ ],
58
+ };
59
+
60
+ /**
61
+ * Get models for a provider
62
+ * @param {string} providerId - Provider ID
63
+ * @returns {Array} List of models
64
+ */
65
+ const getModelsForProvider = (providerId) => {
66
+ return PROVIDER_MODELS[providerId] || [];
67
+ };
68
+
69
+ /**
70
+ * Get model by ID
71
+ * @param {string} providerId - Provider ID
72
+ * @param {string} modelId - Model ID
73
+ * @returns {Object|null} Model object or null
74
+ */
75
+ const getModelById = (providerId, modelId) => {
76
+ const models = PROVIDER_MODELS[providerId] || [];
77
+ return models.find(m => m.id === modelId) || null;
78
+ };
79
+
80
+ module.exports = {
81
+ PROVIDER_MODELS,
82
+ getModelsForProvider,
83
+ getModelById
84
+ };