hedgequantx 2.9.20 → 2.9.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/package.json +1 -1
  2. package/src/app.js +64 -42
  3. package/src/menus/connect.js +17 -14
  4. package/src/menus/dashboard.js +76 -58
  5. package/src/pages/accounts.js +49 -38
  6. package/src/pages/ai-agents-ui.js +388 -0
  7. package/src/pages/ai-agents.js +494 -0
  8. package/src/pages/ai-models.js +389 -0
  9. package/src/pages/algo/algo-executor.js +307 -0
  10. package/src/pages/algo/copy-executor.js +331 -0
  11. package/src/pages/algo/copy-trading.js +178 -546
  12. package/src/pages/algo/custom-strategy.js +313 -0
  13. package/src/pages/algo/index.js +75 -18
  14. package/src/pages/algo/one-account.js +57 -322
  15. package/src/pages/algo/ui.js +15 -15
  16. package/src/pages/orders.js +22 -19
  17. package/src/pages/positions.js +22 -19
  18. package/src/pages/stats/index.js +16 -15
  19. package/src/pages/user.js +11 -7
  20. package/src/services/ai-supervision/consensus.js +284 -0
  21. package/src/services/ai-supervision/context.js +275 -0
  22. package/src/services/ai-supervision/directive.js +167 -0
  23. package/src/services/ai-supervision/health.js +47 -35
  24. package/src/services/ai-supervision/index.js +359 -0
  25. package/src/services/ai-supervision/parser.js +278 -0
  26. package/src/services/ai-supervision/symbols.js +259 -0
  27. package/src/services/cliproxy/index.js +256 -0
  28. package/src/services/cliproxy/installer.js +111 -0
  29. package/src/services/cliproxy/manager.js +387 -0
  30. package/src/services/index.js +9 -1
  31. package/src/services/llmproxy/index.js +166 -0
  32. package/src/services/llmproxy/manager.js +411 -0
  33. package/src/services/rithmic/accounts.js +6 -8
  34. package/src/ui/box.js +5 -9
  35. package/src/ui/index.js +18 -5
  36. package/src/ui/menu.js +4 -4
@@ -0,0 +1,359 @@
1
+ /**
2
+ * AI Supervision Engine - Main Entry Point
3
+ *
4
+ * Orchestrates multi-agent AI supervision for trading signals.
5
+ * Sends signals to all active agents in parallel and calculates
6
+ * weighted consensus for final decision.
7
+ */
8
+
9
+ const { getFullDirective } = require('./directive');
10
+ const { buildMarketContext, formatContextForPrompt } = require('./context');
11
+ const { parseAgentResponse } = require('./parser');
12
+ const { calculateConsensus, isApproved, applyOptimizations } = require('./consensus');
13
+ const { runPreflightCheck, formatPreflightResults, getPreflightSummary } = require('./health');
14
+ const cliproxy = require('../cliproxy');
15
+
16
+ /**
17
+ * API endpoints for direct API key providers
18
+ */
19
+ const API_CHAT_ENDPOINTS = {
20
+ minimax: 'https://api.minimaxi.chat/v1/chat/completions',
21
+ deepseek: 'https://api.deepseek.com/v1/chat/completions',
22
+ mistral: 'https://api.mistral.ai/v1/chat/completions',
23
+ xai: 'https://api.x.ai/v1/chat/completions',
24
+ openrouter: 'https://openrouter.ai/api/v1/chat/completions',
25
+ };
26
+
27
+ /**
28
+ * SupervisionEngine class - manages multi-agent supervision
29
+ */
30
+ class SupervisionEngine {
31
+ constructor(config = {}) {
32
+ this.agents = config.agents || [];
33
+ this.timeout = config.timeout || 30000;
34
+ this.minAgents = config.minAgents || 1;
35
+ this.directive = getFullDirective();
36
+ this.activeAgents = new Map();
37
+ this.rateLimitedAgents = new Set();
38
+
39
+ // Initialize active agents
40
+ for (const agent of this.agents) {
41
+ if (agent.active) {
42
+ this.activeAgents.set(agent.id, agent);
43
+ }
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Get count of active (non-rate-limited) agents
49
+ */
50
+ getActiveCount() {
51
+ return this.activeAgents.size - this.rateLimitedAgents.size;
52
+ }
53
+
54
+ /**
55
+ * Check if supervision is available
56
+ */
57
+ isAvailable() {
58
+ return this.getActiveCount() >= this.minAgents;
59
+ }
60
+
61
+ /**
62
+ * Mark agent as rate limited
63
+ */
64
+ markRateLimited(agentId) {
65
+ this.rateLimitedAgents.add(agentId);
66
+ }
67
+
68
+ /**
69
+ * Reset rate limited agents (call periodically)
70
+ */
71
+ resetRateLimits() {
72
+ this.rateLimitedAgents.clear();
73
+ }
74
+
75
+ /**
76
+ * Build prompt for AI agent
77
+ */
78
+ buildPrompt(context) {
79
+ const contextStr = formatContextForPrompt(context);
80
+ return `${this.directive}\n\n${contextStr}\n\nAnalyze this signal and respond with JSON only.`;
81
+ }
82
+
83
+ /**
84
+ * Query a single agent
85
+ */
86
+ async queryAgent(agent, prompt) {
87
+ const startTime = Date.now();
88
+
89
+ try {
90
+ let response;
91
+
92
+ if (agent.connectionType === 'cliproxy') {
93
+ // Use CLIProxy API
94
+ response = await cliproxy.chat(agent.provider, agent.modelId, prompt, this.timeout);
95
+ } else if (agent.connectionType === 'apikey' && agent.apiKey) {
96
+ // Direct API call (implement per provider)
97
+ response = await this.callDirectAPI(agent, prompt);
98
+ } else {
99
+ throw new Error('Invalid agent configuration');
100
+ }
101
+
102
+ const latency = Date.now() - startTime;
103
+
104
+ if (!response.success) {
105
+ // Check for rate limit
106
+ if (response.error?.includes('rate') || response.error?.includes('limit')) {
107
+ this.markRateLimited(agent.id);
108
+ }
109
+ return { success: false, error: response.error, latency };
110
+ }
111
+
112
+ const parsed = parseAgentResponse(response.content || response.text);
113
+
114
+ return {
115
+ success: true,
116
+ response: parsed,
117
+ latency,
118
+ raw: response
119
+ };
120
+
121
+ } catch (error) {
122
+ const latency = Date.now() - startTime;
123
+
124
+ // Check for rate limit errors
125
+ if (error.message?.includes('429') || error.message?.includes('rate')) {
126
+ this.markRateLimited(agent.id);
127
+ }
128
+
129
+ return {
130
+ success: false,
131
+ error: error.message,
132
+ latency
133
+ };
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Direct API call for API key connections
139
+ * Uses fetch API for direct HTTPS requests to provider endpoints
140
+ */
141
+ async callDirectAPI(agent, prompt) {
142
+ const endpoint = API_CHAT_ENDPOINTS[agent.provider];
143
+
144
+ if (!endpoint) {
145
+ return { success: false, error: `No endpoint for provider: ${agent.provider}` };
146
+ }
147
+
148
+ if (!agent.apiKey) {
149
+ return { success: false, error: 'Missing API key' };
150
+ }
151
+
152
+ try {
153
+ const controller = new AbortController();
154
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
155
+
156
+ const response = await fetch(endpoint, {
157
+ method: 'POST',
158
+ headers: {
159
+ 'Content-Type': 'application/json',
160
+ 'Authorization': `Bearer ${agent.apiKey}`
161
+ },
162
+ body: JSON.stringify({
163
+ model: agent.modelId,
164
+ messages: [{ role: 'user', content: prompt }],
165
+ max_tokens: 500,
166
+ stream: false
167
+ }),
168
+ signal: controller.signal
169
+ });
170
+
171
+ clearTimeout(timeoutId);
172
+ const data = await response.json();
173
+
174
+ if (response.ok) {
175
+ const content = data.choices?.[0]?.message?.content || '';
176
+ return { success: true, content, error: null };
177
+ } else {
178
+ return { success: false, error: data.error?.message || `HTTP ${response.status}` };
179
+ }
180
+ } catch (e) {
181
+ if (e.name === 'AbortError') {
182
+ return { success: false, error: 'Timeout' };
183
+ }
184
+ return { success: false, error: e.message };
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Query all agents in parallel
190
+ */
191
+ async queryAllAgents(prompt) {
192
+ const availableAgents = Array.from(this.activeAgents.values())
193
+ .filter(agent => !this.rateLimitedAgents.has(agent.id));
194
+
195
+ if (availableAgents.length === 0) {
196
+ return [];
197
+ }
198
+
199
+ // Query all agents in parallel with timeout
200
+ const queries = availableAgents.map(agent =>
201
+ Promise.race([
202
+ this.queryAgent(agent, prompt),
203
+ new Promise((_, reject) =>
204
+ setTimeout(() => reject(new Error('Timeout')), this.timeout)
205
+ )
206
+ ]).then(result => ({
207
+ agentId: agent.id,
208
+ agentName: agent.name,
209
+ weight: agent.weight || 100,
210
+ ...result
211
+ })).catch(error => ({
212
+ agentId: agent.id,
213
+ agentName: agent.name,
214
+ weight: agent.weight || 100,
215
+ success: false,
216
+ error: error.message
217
+ }))
218
+ );
219
+
220
+ return Promise.all(queries);
221
+ }
222
+
223
+ /**
224
+ * Main supervision method - analyze a signal
225
+ */
226
+ async supervise(params) {
227
+ const {
228
+ symbolId,
229
+ signal,
230
+ recentTicks = [],
231
+ recentSignals = [],
232
+ recentTrades = [],
233
+ domData = null,
234
+ position = null,
235
+ stats = {},
236
+ config = {}
237
+ } = params;
238
+
239
+ // Check availability
240
+ if (!this.isAvailable()) {
241
+ return {
242
+ success: false,
243
+ error: 'No agents available',
244
+ decision: 'approve',
245
+ reason: 'No AI supervision - passing through'
246
+ };
247
+ }
248
+
249
+ // Build context and prompt
250
+ const context = buildMarketContext({
251
+ symbolId,
252
+ signal,
253
+ recentTicks,
254
+ recentSignals,
255
+ recentTrades,
256
+ domData,
257
+ position,
258
+ stats,
259
+ config
260
+ });
261
+
262
+ const prompt = this.buildPrompt(context);
263
+
264
+ // Query all agents
265
+ const results = await this.queryAllAgents(prompt);
266
+
267
+ // Filter successful responses
268
+ const successfulResults = results.filter(r => r.success);
269
+
270
+ if (successfulResults.length === 0) {
271
+ return {
272
+ success: false,
273
+ error: 'All agents failed',
274
+ decision: 'approve',
275
+ reason: 'Agent errors - passing through',
276
+ agentResults: results
277
+ };
278
+ }
279
+
280
+ // Calculate consensus
281
+ const consensus = calculateConsensus(
282
+ successfulResults.map(r => ({
283
+ agentId: r.agentId,
284
+ response: r.response,
285
+ weight: r.weight
286
+ })),
287
+ { minAgents: this.minAgents }
288
+ );
289
+
290
+ // Apply optimizations if approved
291
+ const optimizedSignal = isApproved(consensus)
292
+ ? applyOptimizations(signal, consensus)
293
+ : signal;
294
+
295
+ return {
296
+ success: true,
297
+ decision: consensus.decision,
298
+ confidence: consensus.confidence,
299
+ reason: consensus.reason,
300
+ optimizedSignal,
301
+ consensus,
302
+ agentResults: results,
303
+ context
304
+ };
305
+ }
306
+
307
+ /**
308
+ * Get engine status
309
+ */
310
+ getStatus() {
311
+ return {
312
+ totalAgents: this.agents.length,
313
+ activeAgents: this.activeAgents.size,
314
+ rateLimitedAgents: this.rateLimitedAgents.size,
315
+ availableAgents: this.getActiveCount(),
316
+ isAvailable: this.isAvailable(),
317
+ agents: Array.from(this.activeAgents.values()).map(a => ({
318
+ id: a.id,
319
+ name: a.name,
320
+ provider: a.provider,
321
+ weight: a.weight,
322
+ rateLimited: this.rateLimitedAgents.has(a.id)
323
+ }))
324
+ };
325
+ }
326
+
327
+ /**
328
+ * Run pre-flight check on all agents
329
+ * Verifies CLIProxy is running and all agents respond correctly
330
+ * @returns {Promise<Object>} Pre-flight results
331
+ */
332
+ async preflightCheck() {
333
+ const agents = Array.from(this.activeAgents.values());
334
+ return runPreflightCheck(agents);
335
+ }
336
+ }
337
+
338
+ /**
339
+ * Create supervision engine from config
340
+ */
341
+ const createSupervisionEngine = (config) => {
342
+ return new SupervisionEngine(config);
343
+ };
344
+
345
+ module.exports = {
346
+ SupervisionEngine,
347
+ createSupervisionEngine,
348
+ // Re-export utilities
349
+ buildMarketContext,
350
+ formatContextForPrompt,
351
+ parseAgentResponse,
352
+ calculateConsensus,
353
+ isApproved,
354
+ applyOptimizations,
355
+ // Health check
356
+ runPreflightCheck,
357
+ formatPreflightResults,
358
+ getPreflightSummary
359
+ };
@@ -0,0 +1,278 @@
1
+ /**
2
+ * AI Response Parser
3
+ *
4
+ * Parses responses from AI agents (JSON or text)
5
+ * and normalizes them to a standard format.
6
+ */
7
+
8
+ /**
9
+ * Default response when parsing fails
10
+ */
11
+ const DEFAULT_RESPONSE = {
12
+ decision: 'approve',
13
+ confidence: 50,
14
+ optimizations: null,
15
+ reason: 'Parse failed - defaulting to approve',
16
+ alerts: null,
17
+ parseSuccess: false
18
+ };
19
+
20
+ /**
21
+ * Extract JSON from a string that may contain markdown or extra text
22
+ */
23
+ const extractJSON = (text) => {
24
+ if (!text || typeof text !== 'string') return null;
25
+
26
+ // Try direct parse first
27
+ try {
28
+ return JSON.parse(text.trim());
29
+ } catch (e) { /* continue */ }
30
+
31
+ // Try to find JSON in markdown code blocks
32
+ const codeBlockMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/);
33
+ if (codeBlockMatch) {
34
+ try {
35
+ return JSON.parse(codeBlockMatch[1].trim());
36
+ } catch (e) { /* continue */ }
37
+ }
38
+
39
+ // Try to find JSON object pattern
40
+ const jsonMatch = text.match(/\{[\s\S]*"decision"[\s\S]*\}/);
41
+ if (jsonMatch) {
42
+ try {
43
+ return JSON.parse(jsonMatch[0]);
44
+ } catch (e) { /* continue */ }
45
+ }
46
+
47
+ return null;
48
+ };
49
+
50
+ /**
51
+ * Validate and normalize the decision field
52
+ */
53
+ const normalizeDecision = (decision) => {
54
+ if (!decision) return 'approve';
55
+
56
+ const d = String(decision).toLowerCase().trim();
57
+
58
+ if (d === 'approve' || d === 'yes' || d === 'accept' || d === 'go') return 'approve';
59
+ if (d === 'reject' || d === 'no' || d === 'deny' || d === 'stop') return 'reject';
60
+ if (d === 'modify' || d === 'adjust' || d === 'optimize') return 'modify';
61
+
62
+ return 'approve';
63
+ };
64
+
65
+ /**
66
+ * Validate and normalize confidence score
67
+ */
68
+ const normalizeConfidence = (confidence) => {
69
+ if (confidence === undefined || confidence === null) return 50;
70
+
71
+ const c = Number(confidence);
72
+ if (isNaN(c)) return 50;
73
+
74
+ // Handle percentage strings like "85%"
75
+ if (typeof confidence === 'string' && confidence.includes('%')) {
76
+ const parsed = parseFloat(confidence);
77
+ if (!isNaN(parsed)) return Math.min(100, Math.max(0, parsed));
78
+ }
79
+
80
+ // Normalize to 0-100 range
81
+ if (c >= 0 && c <= 1) return Math.round(c * 100);
82
+ return Math.min(100, Math.max(0, Math.round(c)));
83
+ };
84
+
85
+ /**
86
+ * Validate and normalize optimizations
87
+ */
88
+ const normalizeOptimizations = (opts, signal) => {
89
+ if (!opts) return null;
90
+
91
+ const normalized = {
92
+ entry: null,
93
+ stopLoss: null,
94
+ takeProfit: null,
95
+ size: null,
96
+ timing: 'now'
97
+ };
98
+
99
+ // Entry price
100
+ if (opts.entry !== undefined && opts.entry !== null) {
101
+ const entry = Number(opts.entry);
102
+ if (!isNaN(entry) && entry > 0) normalized.entry = entry;
103
+ }
104
+
105
+ // Stop loss
106
+ if (opts.stopLoss !== undefined && opts.stopLoss !== null) {
107
+ const sl = Number(opts.stopLoss);
108
+ if (!isNaN(sl) && sl > 0) normalized.stopLoss = sl;
109
+ }
110
+
111
+ // Take profit
112
+ if (opts.takeProfit !== undefined && opts.takeProfit !== null) {
113
+ const tp = Number(opts.takeProfit);
114
+ if (!isNaN(tp) && tp > 0) normalized.takeProfit = tp;
115
+ }
116
+
117
+ // Size adjustment (-0.5 to +0.5)
118
+ if (opts.size !== undefined && opts.size !== null) {
119
+ const size = Number(opts.size);
120
+ if (!isNaN(size)) {
121
+ normalized.size = Math.min(0.5, Math.max(-0.5, size));
122
+ }
123
+ }
124
+
125
+ // Timing
126
+ if (opts.timing) {
127
+ const t = String(opts.timing).toLowerCase().trim();
128
+ if (t === 'now' || t === 'immediate') normalized.timing = 'now';
129
+ else if (t === 'wait' || t === 'delay') normalized.timing = 'wait';
130
+ else if (t === 'cancel' || t === 'abort') normalized.timing = 'cancel';
131
+ else normalized.timing = 'now';
132
+ }
133
+
134
+ return normalized;
135
+ };
136
+
137
+ /**
138
+ * Normalize reason string
139
+ */
140
+ const normalizeReason = (reason) => {
141
+ if (!reason) return 'No reason provided';
142
+
143
+ const r = String(reason).trim();
144
+ if (r.length > 100) return r.substring(0, 97) + '...';
145
+ return r;
146
+ };
147
+
148
+ /**
149
+ * Normalize alerts array
150
+ */
151
+ const normalizeAlerts = (alerts) => {
152
+ if (!alerts) return null;
153
+ if (!Array.isArray(alerts)) {
154
+ if (typeof alerts === 'string') return [alerts];
155
+ return null;
156
+ }
157
+ return alerts.filter(a => a && typeof a === 'string').slice(0, 5);
158
+ };
159
+
160
+ /**
161
+ * Parse text response when JSON parsing fails
162
+ * Attempts to extract decision from natural language
163
+ */
164
+ const parseTextResponse = (text, signal) => {
165
+ if (!text) return DEFAULT_RESPONSE;
166
+
167
+ const lower = text.toLowerCase();
168
+
169
+ // Determine decision from keywords
170
+ let decision = 'approve';
171
+ if (lower.includes('reject') || lower.includes('do not') || lower.includes("don't") ||
172
+ lower.includes('avoid') || lower.includes('skip') || lower.includes('no trade')) {
173
+ decision = 'reject';
174
+ } else if (lower.includes('modify') || lower.includes('adjust') || lower.includes('optimize') ||
175
+ lower.includes('tighten') || lower.includes('widen')) {
176
+ decision = 'modify';
177
+ }
178
+
179
+ // Try to extract confidence
180
+ let confidence = 60;
181
+ const confMatch = lower.match(/confidence[:\s]*(\d+)/i) ||
182
+ lower.match(/(\d+)%?\s*confiden/i) ||
183
+ lower.match(/score[:\s]*(\d+)/i);
184
+ if (confMatch) {
185
+ confidence = normalizeConfidence(confMatch[1]);
186
+ }
187
+
188
+ // Extract reason (first sentence or up to 100 chars)
189
+ let reason = text.split(/[.!?\n]/)[0]?.trim() || 'Parsed from text response';
190
+ reason = normalizeReason(reason);
191
+
192
+ return {
193
+ decision,
194
+ confidence,
195
+ optimizations: decision === 'modify' ? {
196
+ entry: signal?.entry || null,
197
+ stopLoss: signal?.stopLoss || null,
198
+ takeProfit: signal?.takeProfit || null,
199
+ size: null,
200
+ timing: 'now'
201
+ } : null,
202
+ reason,
203
+ alerts: null,
204
+ parseSuccess: false,
205
+ parsedFromText: true
206
+ };
207
+ };
208
+
209
+ /**
210
+ * Main parser function - parse AI response to standard format
211
+ */
212
+ const parseAgentResponse = (response, signal = null) => {
213
+ // Handle empty response
214
+ if (!response) {
215
+ return { ...DEFAULT_RESPONSE, reason: 'Empty response from agent' };
216
+ }
217
+
218
+ // Handle response object with content field (common API format)
219
+ let text = response;
220
+ if (typeof response === 'object') {
221
+ if (response.content) text = response.content;
222
+ else if (response.text) text = response.text;
223
+ else if (response.message) text = response.message;
224
+ else text = JSON.stringify(response);
225
+ }
226
+
227
+ // Try to extract and parse JSON
228
+ const json = extractJSON(text);
229
+
230
+ if (json && json.decision) {
231
+ // Successfully parsed JSON
232
+ return {
233
+ decision: normalizeDecision(json.decision),
234
+ confidence: normalizeConfidence(json.confidence),
235
+ optimizations: normalizeOptimizations(json.optimizations, signal),
236
+ reason: normalizeReason(json.reason),
237
+ alerts: normalizeAlerts(json.alerts),
238
+ parseSuccess: true
239
+ };
240
+ }
241
+
242
+ // Fallback to text parsing
243
+ return parseTextResponse(text, signal);
244
+ };
245
+
246
+ /**
247
+ * Validate a parsed response
248
+ */
249
+ const validateResponse = (parsed) => {
250
+ const errors = [];
251
+
252
+ if (!['approve', 'reject', 'modify'].includes(parsed.decision)) {
253
+ errors.push(`Invalid decision: ${parsed.decision}`);
254
+ }
255
+
256
+ if (parsed.confidence < 0 || parsed.confidence > 100) {
257
+ errors.push(`Invalid confidence: ${parsed.confidence}`);
258
+ }
259
+
260
+ if (parsed.decision === 'modify' && !parsed.optimizations) {
261
+ errors.push('Modify decision requires optimizations');
262
+ }
263
+
264
+ return {
265
+ valid: errors.length === 0,
266
+ errors
267
+ };
268
+ };
269
+
270
+ module.exports = {
271
+ parseAgentResponse,
272
+ validateResponse,
273
+ extractJSON,
274
+ normalizeDecision,
275
+ normalizeConfidence,
276
+ normalizeOptimizations,
277
+ DEFAULT_RESPONSE
278
+ };