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.
- package/package.json +1 -1
- package/src/app.js +64 -42
- package/src/menus/connect.js +17 -14
- package/src/menus/dashboard.js +76 -58
- package/src/pages/accounts.js +49 -38
- package/src/pages/ai-agents-ui.js +388 -0
- package/src/pages/ai-agents.js +494 -0
- package/src/pages/ai-models.js +389 -0
- package/src/pages/algo/algo-executor.js +307 -0
- package/src/pages/algo/copy-executor.js +331 -0
- package/src/pages/algo/copy-trading.js +178 -546
- package/src/pages/algo/custom-strategy.js +313 -0
- package/src/pages/algo/index.js +75 -18
- package/src/pages/algo/one-account.js +57 -322
- package/src/pages/algo/ui.js +15 -15
- package/src/pages/orders.js +22 -19
- package/src/pages/positions.js +22 -19
- package/src/pages/stats/index.js +16 -15
- package/src/pages/user.js +11 -7
- package/src/services/ai-supervision/consensus.js +284 -0
- package/src/services/ai-supervision/context.js +275 -0
- package/src/services/ai-supervision/directive.js +167 -0
- package/src/services/ai-supervision/health.js +47 -35
- package/src/services/ai-supervision/index.js +359 -0
- package/src/services/ai-supervision/parser.js +278 -0
- package/src/services/ai-supervision/symbols.js +259 -0
- package/src/services/cliproxy/index.js +256 -0
- package/src/services/cliproxy/installer.js +111 -0
- package/src/services/cliproxy/manager.js +387 -0
- package/src/services/index.js +9 -1
- package/src/services/llmproxy/index.js +166 -0
- package/src/services/llmproxy/manager.js +411 -0
- package/src/services/rithmic/accounts.js +6 -8
- package/src/ui/box.js +5 -9
- package/src/ui/index.js +18 -5
- 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
|
+
};
|