hedgequantx 2.7.99 → 2.8.0

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.
@@ -0,0 +1,309 @@
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
+ * SupervisionEngine class - manages multi-agent supervision
18
+ */
19
+ class SupervisionEngine {
20
+ constructor(config = {}) {
21
+ this.agents = config.agents || [];
22
+ this.timeout = config.timeout || 30000;
23
+ this.minAgents = config.minAgents || 1;
24
+ this.directive = getFullDirective();
25
+ this.activeAgents = new Map();
26
+ this.rateLimitedAgents = new Set();
27
+
28
+ // Initialize active agents
29
+ for (const agent of this.agents) {
30
+ if (agent.active) {
31
+ this.activeAgents.set(agent.id, agent);
32
+ }
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Get count of active (non-rate-limited) agents
38
+ */
39
+ getActiveCount() {
40
+ return this.activeAgents.size - this.rateLimitedAgents.size;
41
+ }
42
+
43
+ /**
44
+ * Check if supervision is available
45
+ */
46
+ isAvailable() {
47
+ return this.getActiveCount() >= this.minAgents;
48
+ }
49
+
50
+ /**
51
+ * Mark agent as rate limited
52
+ */
53
+ markRateLimited(agentId) {
54
+ this.rateLimitedAgents.add(agentId);
55
+ }
56
+
57
+ /**
58
+ * Reset rate limited agents (call periodically)
59
+ */
60
+ resetRateLimits() {
61
+ this.rateLimitedAgents.clear();
62
+ }
63
+
64
+ /**
65
+ * Build prompt for AI agent
66
+ */
67
+ buildPrompt(context) {
68
+ const contextStr = formatContextForPrompt(context);
69
+ return `${this.directive}\n\n${contextStr}\n\nAnalyze this signal and respond with JSON only.`;
70
+ }
71
+
72
+ /**
73
+ * Query a single agent
74
+ */
75
+ async queryAgent(agent, prompt) {
76
+ const startTime = Date.now();
77
+
78
+ try {
79
+ let response;
80
+
81
+ if (agent.connectionType === 'cliproxy') {
82
+ // Use CLIProxy API
83
+ response = await cliproxy.chat(agent.provider, agent.modelId, prompt, this.timeout);
84
+ } else if (agent.connectionType === 'apikey' && agent.apiKey) {
85
+ // Direct API call (implement per provider)
86
+ response = await this.callDirectAPI(agent, prompt);
87
+ } else {
88
+ throw new Error('Invalid agent configuration');
89
+ }
90
+
91
+ const latency = Date.now() - startTime;
92
+
93
+ if (!response.success) {
94
+ // Check for rate limit
95
+ if (response.error?.includes('rate') || response.error?.includes('limit')) {
96
+ this.markRateLimited(agent.id);
97
+ }
98
+ return { success: false, error: response.error, latency };
99
+ }
100
+
101
+ const parsed = parseAgentResponse(response.content || response.text);
102
+
103
+ return {
104
+ success: true,
105
+ response: parsed,
106
+ latency,
107
+ raw: response
108
+ };
109
+
110
+ } catch (error) {
111
+ const latency = Date.now() - startTime;
112
+
113
+ // Check for rate limit errors
114
+ if (error.message?.includes('429') || error.message?.includes('rate')) {
115
+ this.markRateLimited(agent.id);
116
+ }
117
+
118
+ return {
119
+ success: false,
120
+ error: error.message,
121
+ latency
122
+ };
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Direct API call for API key connections
128
+ */
129
+ async callDirectAPI(agent, prompt) {
130
+ // This would be implemented per provider
131
+ // For now, return error to use CLIProxy instead
132
+ return {
133
+ success: false,
134
+ error: 'Direct API not implemented - use CLIProxy'
135
+ };
136
+ }
137
+
138
+ /**
139
+ * Query all agents in parallel
140
+ */
141
+ async queryAllAgents(prompt) {
142
+ const availableAgents = Array.from(this.activeAgents.values())
143
+ .filter(agent => !this.rateLimitedAgents.has(agent.id));
144
+
145
+ if (availableAgents.length === 0) {
146
+ return [];
147
+ }
148
+
149
+ // Query all agents in parallel with timeout
150
+ const queries = availableAgents.map(agent =>
151
+ Promise.race([
152
+ this.queryAgent(agent, prompt),
153
+ new Promise((_, reject) =>
154
+ setTimeout(() => reject(new Error('Timeout')), this.timeout)
155
+ )
156
+ ]).then(result => ({
157
+ agentId: agent.id,
158
+ agentName: agent.name,
159
+ weight: agent.weight || 100,
160
+ ...result
161
+ })).catch(error => ({
162
+ agentId: agent.id,
163
+ agentName: agent.name,
164
+ weight: agent.weight || 100,
165
+ success: false,
166
+ error: error.message
167
+ }))
168
+ );
169
+
170
+ return Promise.all(queries);
171
+ }
172
+
173
+ /**
174
+ * Main supervision method - analyze a signal
175
+ */
176
+ async supervise(params) {
177
+ const {
178
+ symbolId,
179
+ signal,
180
+ recentTicks = [],
181
+ recentSignals = [],
182
+ recentTrades = [],
183
+ domData = null,
184
+ position = null,
185
+ stats = {},
186
+ config = {}
187
+ } = params;
188
+
189
+ // Check availability
190
+ if (!this.isAvailable()) {
191
+ return {
192
+ success: false,
193
+ error: 'No agents available',
194
+ decision: 'approve',
195
+ reason: 'No AI supervision - passing through'
196
+ };
197
+ }
198
+
199
+ // Build context and prompt
200
+ const context = buildMarketContext({
201
+ symbolId,
202
+ signal,
203
+ recentTicks,
204
+ recentSignals,
205
+ recentTrades,
206
+ domData,
207
+ position,
208
+ stats,
209
+ config
210
+ });
211
+
212
+ const prompt = this.buildPrompt(context);
213
+
214
+ // Query all agents
215
+ const results = await this.queryAllAgents(prompt);
216
+
217
+ // Filter successful responses
218
+ const successfulResults = results.filter(r => r.success);
219
+
220
+ if (successfulResults.length === 0) {
221
+ return {
222
+ success: false,
223
+ error: 'All agents failed',
224
+ decision: 'approve',
225
+ reason: 'Agent errors - passing through',
226
+ agentResults: results
227
+ };
228
+ }
229
+
230
+ // Calculate consensus
231
+ const consensus = calculateConsensus(
232
+ successfulResults.map(r => ({
233
+ agentId: r.agentId,
234
+ response: r.response,
235
+ weight: r.weight
236
+ })),
237
+ { minAgents: this.minAgents }
238
+ );
239
+
240
+ // Apply optimizations if approved
241
+ const optimizedSignal = isApproved(consensus)
242
+ ? applyOptimizations(signal, consensus)
243
+ : signal;
244
+
245
+ return {
246
+ success: true,
247
+ decision: consensus.decision,
248
+ confidence: consensus.confidence,
249
+ reason: consensus.reason,
250
+ optimizedSignal,
251
+ consensus,
252
+ agentResults: results,
253
+ context
254
+ };
255
+ }
256
+
257
+ /**
258
+ * Get engine status
259
+ */
260
+ getStatus() {
261
+ return {
262
+ totalAgents: this.agents.length,
263
+ activeAgents: this.activeAgents.size,
264
+ rateLimitedAgents: this.rateLimitedAgents.size,
265
+ availableAgents: this.getActiveCount(),
266
+ isAvailable: this.isAvailable(),
267
+ agents: Array.from(this.activeAgents.values()).map(a => ({
268
+ id: a.id,
269
+ name: a.name,
270
+ provider: a.provider,
271
+ weight: a.weight,
272
+ rateLimited: this.rateLimitedAgents.has(a.id)
273
+ }))
274
+ };
275
+ }
276
+
277
+ /**
278
+ * Run pre-flight check on all agents
279
+ * Verifies CLIProxy is running and all agents respond correctly
280
+ * @returns {Promise<Object>} Pre-flight results
281
+ */
282
+ async preflightCheck() {
283
+ const agents = Array.from(this.activeAgents.values());
284
+ return runPreflightCheck(agents);
285
+ }
286
+ }
287
+
288
+ /**
289
+ * Create supervision engine from config
290
+ */
291
+ const createSupervisionEngine = (config) => {
292
+ return new SupervisionEngine(config);
293
+ };
294
+
295
+ module.exports = {
296
+ SupervisionEngine,
297
+ createSupervisionEngine,
298
+ // Re-export utilities
299
+ buildMarketContext,
300
+ formatContextForPrompt,
301
+ parseAgentResponse,
302
+ calculateConsensus,
303
+ isApproved,
304
+ applyOptimizations,
305
+ // Health check
306
+ runPreflightCheck,
307
+ formatPreflightResults,
308
+ getPreflightSummary
309
+ };
@@ -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
+ };