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,275 @@
1
+ /**
2
+ * Context Builder for AI Supervision
3
+ *
4
+ * Builds the market context from real Rithmic data
5
+ * to send to AI agents for signal analysis.
6
+ */
7
+
8
+ const { getSymbol, getCurrentSession, isGoodSessionForSymbol } = require('./symbols');
9
+
10
+ /**
11
+ * Build DOM (Depth of Market) summary from raw data
12
+ */
13
+ const buildDOMSummary = (domData) => {
14
+ if (!domData || !domData.bids || !domData.asks) {
15
+ return { available: false };
16
+ }
17
+
18
+ const bids = domData.bids.slice(0, 10);
19
+ const asks = domData.asks.slice(0, 10);
20
+
21
+ const totalBidSize = bids.reduce((sum, b) => sum + (b.size || 0), 0);
22
+ const totalAskSize = asks.reduce((sum, a) => sum + (a.size || 0), 0);
23
+ const imbalance = totalBidSize - totalAskSize;
24
+ const imbalanceRatio = totalAskSize > 0 ? totalBidSize / totalAskSize : 1;
25
+
26
+ return {
27
+ available: true,
28
+ topBid: bids[0]?.price || null,
29
+ topAsk: asks[0]?.price || null,
30
+ spread: asks[0] && bids[0] ? asks[0].price - bids[0].price : null,
31
+ totalBidSize,
32
+ totalAskSize,
33
+ imbalance,
34
+ imbalanceRatio: Math.round(imbalanceRatio * 100) / 100,
35
+ bidLevels: bids.length,
36
+ askLevels: asks.length,
37
+ dominantSide: imbalance > 0 ? 'buyers' : imbalance < 0 ? 'sellers' : 'neutral'
38
+ };
39
+ };
40
+
41
+ /**
42
+ * Build Order Flow summary from recent ticks
43
+ */
44
+ const buildOrderFlowSummary = (recentTicks, windowSize = 50) => {
45
+ if (!recentTicks || recentTicks.length === 0) {
46
+ return { available: false };
47
+ }
48
+
49
+ const ticks = recentTicks.slice(-windowSize);
50
+
51
+ let buyVolume = 0;
52
+ let sellVolume = 0;
53
+ let totalVolume = 0;
54
+ let highPrice = -Infinity;
55
+ let lowPrice = Infinity;
56
+
57
+ for (const tick of ticks) {
58
+ const vol = tick.volume || 1;
59
+ totalVolume += vol;
60
+
61
+ if (tick.side === 'buy' || tick.lastTradeSide === 'buy') {
62
+ buyVolume += vol;
63
+ } else if (tick.side === 'sell' || tick.lastTradeSide === 'sell') {
64
+ sellVolume += vol;
65
+ }
66
+
67
+ if (tick.price > highPrice) highPrice = tick.price;
68
+ if (tick.price < lowPrice) lowPrice = tick.price;
69
+ }
70
+
71
+ const delta = buyVolume - sellVolume;
72
+ const deltaPercent = totalVolume > 0 ? (delta / totalVolume) * 100 : 0;
73
+
74
+ return {
75
+ available: true,
76
+ tickCount: ticks.length,
77
+ totalVolume,
78
+ buyVolume,
79
+ sellVolume,
80
+ delta,
81
+ deltaPercent: Math.round(deltaPercent),
82
+ highPrice: highPrice === -Infinity ? null : highPrice,
83
+ lowPrice: lowPrice === Infinity ? null : lowPrice,
84
+ range: highPrice !== -Infinity && lowPrice !== Infinity ? highPrice - lowPrice : null,
85
+ lastPrice: ticks[ticks.length - 1]?.price || null,
86
+ trend: delta > 0 ? 'bullish' : delta < 0 ? 'bearish' : 'neutral'
87
+ };
88
+ };
89
+
90
+ /**
91
+ * Build trade history summary from recent signals/trades
92
+ */
93
+ const buildTradeHistory = (recentSignals, recentTrades) => {
94
+ const signals = recentSignals || [];
95
+ const trades = recentTrades || [];
96
+
97
+ const wins = trades.filter(t => t.pnl > 0).length;
98
+ const losses = trades.filter(t => t.pnl < 0).length;
99
+ const totalTrades = wins + losses;
100
+ const winRate = totalTrades > 0 ? (wins / totalTrades) * 100 : 0;
101
+
102
+ const totalPnL = trades.reduce((sum, t) => sum + (t.pnl || 0), 0);
103
+ const avgWin = wins > 0 ? trades.filter(t => t.pnl > 0).reduce((s, t) => s + t.pnl, 0) / wins : 0;
104
+ const avgLoss = losses > 0 ? Math.abs(trades.filter(t => t.pnl < 0).reduce((s, t) => s + t.pnl, 0) / losses) : 0;
105
+
106
+ return {
107
+ recentSignals: signals.length,
108
+ totalTrades,
109
+ wins,
110
+ losses,
111
+ winRate: Math.round(winRate),
112
+ totalPnL: Math.round(totalPnL * 100) / 100,
113
+ avgWin: Math.round(avgWin * 100) / 100,
114
+ avgLoss: Math.round(avgLoss * 100) / 100,
115
+ profitFactor: avgLoss > 0 ? Math.round((avgWin / avgLoss) * 100) / 100 : 0
116
+ };
117
+ };
118
+
119
+ /**
120
+ * Build current position info
121
+ */
122
+ const buildPositionInfo = (position, currentPrice) => {
123
+ if (!position || position.quantity === 0) {
124
+ return { hasPosition: false, quantity: 0 };
125
+ }
126
+
127
+ const qty = position.quantity;
128
+ const entryPrice = position.averagePrice || position.entryPrice;
129
+ const unrealizedPnL = position.profitAndLoss || 0;
130
+ const side = qty > 0 ? 'long' : 'short';
131
+
132
+ return {
133
+ hasPosition: true,
134
+ side,
135
+ quantity: Math.abs(qty),
136
+ entryPrice,
137
+ currentPrice,
138
+ unrealizedPnL: Math.round(unrealizedPnL * 100) / 100,
139
+ ticksInProfit: entryPrice && currentPrice
140
+ ? Math.round((side === 'long' ? currentPrice - entryPrice : entryPrice - currentPrice) * 4)
141
+ : 0
142
+ };
143
+ };
144
+
145
+ /**
146
+ * Build complete market context for AI analysis
147
+ */
148
+ const buildMarketContext = ({
149
+ symbolId,
150
+ signal,
151
+ recentTicks = [],
152
+ recentSignals = [],
153
+ recentTrades = [],
154
+ domData = null,
155
+ position = null,
156
+ stats = {},
157
+ config = {}
158
+ }) => {
159
+ const symbol = getSymbol(symbolId);
160
+ const session = getCurrentSession();
161
+ const sessionCheck = isGoodSessionForSymbol(symbolId);
162
+ const orderFlow = buildOrderFlowSummary(recentTicks);
163
+ const dom = buildDOMSummary(domData);
164
+ const history = buildTradeHistory(recentSignals, recentTrades);
165
+ const positionInfo = buildPositionInfo(position, orderFlow.lastPrice);
166
+
167
+ return {
168
+ timestamp: new Date().toISOString(),
169
+
170
+ // Symbol info
171
+ symbol: {
172
+ id: symbolId,
173
+ name: symbol?.name || symbolId,
174
+ tickSize: symbol?.tickSize || 0.25,
175
+ tickValue: symbol?.tickValue || 12.50,
176
+ characteristics: symbol?.characteristics || {},
177
+ correlations: symbol?.correlations || {}
178
+ },
179
+
180
+ // Current session
181
+ session: {
182
+ name: session.name,
183
+ description: session.description,
184
+ isGoodTime: sessionCheck.good,
185
+ sessionNote: sessionCheck.reason
186
+ },
187
+
188
+ // The signal to analyze
189
+ signal: {
190
+ direction: signal.direction,
191
+ entry: signal.entry,
192
+ stopLoss: signal.stopLoss,
193
+ takeProfit: signal.takeProfit,
194
+ confidence: signal.confidence,
195
+ pattern: signal.pattern || 'unknown',
196
+ timestamp: signal.timestamp || Date.now()
197
+ },
198
+
199
+ // Market data
200
+ orderFlow,
201
+ dom,
202
+
203
+ // Position and history
204
+ position: positionInfo,
205
+ history,
206
+
207
+ // Session stats
208
+ sessionStats: {
209
+ pnl: stats.pnl || 0,
210
+ trades: stats.trades || 0,
211
+ wins: stats.wins || 0,
212
+ losses: stats.losses || 0,
213
+ target: config.dailyTarget || stats.target || 500,
214
+ maxRisk: config.maxRisk || stats.risk || 300,
215
+ progressToTarget: stats.pnl && config.dailyTarget
216
+ ? Math.round((stats.pnl / config.dailyTarget) * 100)
217
+ : 0
218
+ }
219
+ };
220
+ };
221
+
222
+ /**
223
+ * Format context as a string for AI prompt
224
+ */
225
+ const formatContextForPrompt = (context) => {
226
+ return `
227
+ ## MARKET CONTEXT
228
+
229
+ **Symbol**: ${context.symbol.name} (${context.symbol.id})
230
+ **Tick Size**: ${context.symbol.tickSize} | **Tick Value**: $${context.symbol.tickValue}
231
+ **Session**: ${context.session.description} ${context.session.isGoodTime ? '✓' : '⚠'}
232
+
233
+ ### SIGNAL TO ANALYZE
234
+ - Direction: ${context.signal.direction.toUpperCase()}
235
+ - Entry: ${context.signal.entry}
236
+ - Stop Loss: ${context.signal.stopLoss}
237
+ - Take Profit: ${context.signal.takeProfit}
238
+ - Strategy Confidence: ${Math.round(context.signal.confidence * 100)}%
239
+
240
+ ### ORDER FLOW (Last ${context.orderFlow.tickCount || 0} ticks)
241
+ - Delta: ${context.orderFlow.delta || 0} (${context.orderFlow.deltaPercent || 0}%)
242
+ - Buy Volume: ${context.orderFlow.buyVolume || 0}
243
+ - Sell Volume: ${context.orderFlow.sellVolume || 0}
244
+ - Trend: ${context.orderFlow.trend || 'unknown'}
245
+ - Range: ${context.orderFlow.range?.toFixed(2) || 'N/A'}
246
+
247
+ ### DOM ANALYSIS
248
+ ${context.dom.available
249
+ ? `- Spread: ${context.dom.spread?.toFixed(2) || 'N/A'}
250
+ - Bid Size: ${context.dom.totalBidSize} | Ask Size: ${context.dom.totalAskSize}
251
+ - Imbalance Ratio: ${context.dom.imbalanceRatio}x
252
+ - Dominant Side: ${context.dom.dominantSide}`
253
+ : '- DOM data not available'}
254
+
255
+ ### POSITION
256
+ ${context.position.hasPosition
257
+ ? `- ${context.position.side.toUpperCase()} ${context.position.quantity}x @ ${context.position.entryPrice}
258
+ - Unrealized P&L: $${context.position.unrealizedPnL}`
259
+ : '- No open position'}
260
+
261
+ ### SESSION PERFORMANCE
262
+ - P&L: $${context.sessionStats.pnl} / $${context.sessionStats.target} target
263
+ - Trades: ${context.sessionStats.trades} (W: ${context.sessionStats.wins} / L: ${context.sessionStats.losses})
264
+ - Progress: ${context.sessionStats.progressToTarget}%
265
+ `;
266
+ };
267
+
268
+ module.exports = {
269
+ buildMarketContext,
270
+ formatContextForPrompt,
271
+ buildDOMSummary,
272
+ buildOrderFlowSummary,
273
+ buildTradeHistory,
274
+ buildPositionInfo
275
+ };
@@ -0,0 +1,167 @@
1
+ /**
2
+ * AI Supervision Directive - The 7 Phases
3
+ *
4
+ * This directive is sent to ALL AI agents for trading supervision.
5
+ * Each agent receives the same instructions and analyzes signals in parallel.
6
+ */
7
+
8
+ const DIRECTIVE_PHASES = {
9
+ phase1_strategy: `
10
+ ## PHASE 1: MAÎTRISER LA STRATÉGIE
11
+ Tu connais cette stratégie par coeur. Tu sais exactement:
12
+ - Les conditions d'entrée (setup patterns, confirmations requises)
13
+ - Les règles de sortie (targets, stops, trailing)
14
+ - La gestion du risque (sizing, max loss, drawdown limits)
15
+ - Les filtres de marché (sessions, volatilité, corrélations)
16
+ Tu ne devines jamais. Tu appliques la stratégie avec précision chirurgicale.
17
+ `,
18
+
19
+ phase2_role: `
20
+ ## PHASE 2: TON RÔLE
21
+ Tu es une arme de précision intégrée à l'algo de trading.
22
+ Tu n'es PAS un conseiller - tu es un avantage décisif.
23
+ Ton job: optimiser chaque signal pour maximiser le edge.
24
+ Tu travailles EN TEMPS RÉEL avec latence minimale.
25
+ Chaque milliseconde compte. Chaque tick compte.
26
+ `,
27
+
28
+ phase3_analysis: `
29
+ ## PHASE 3: CE QUE TU ANALYSES
30
+ Pour chaque signal reçu, tu analyses:
31
+ - DOM (Depth of Market): déséquilibres bid/ask, absorption, spoofing
32
+ - Order Flow: delta, CVD, volume profile, footprint
33
+ - Manipulations: stop hunts, fakeouts, liquidity grabs
34
+ - Patterns: structure de marché, S/R, pivots, VWAP
35
+ - Historique: trades récents, winrate par setup, performance horaire
36
+ - Corrélations: autres symboles liés (ES/NQ, GC/SI, etc.)
37
+ `,
38
+
39
+ phase4_output: `
40
+ ## PHASE 4: CE QUE TU FOURNIS
41
+ Pour chaque signal, tu retournes:
42
+ 1. DECISION: approve/reject/modify
43
+ 2. CONFIDENCE: score 0-100
44
+ 3. OPTIMIZATIONS (si approve/modify):
45
+ - entry: prix d'entrée optimisé (ou null)
46
+ - stopLoss: stop optimisé (ou null)
47
+ - takeProfit: target optimisé (ou null)
48
+ - size: ajustement de taille (-50% à +50%)
49
+ - timing: "now" | "wait" | "cancel"
50
+ 4. REASON: explication courte (max 50 chars)
51
+ 5. ALERTS: warnings importants (optionnel)
52
+ `,
53
+
54
+ phase5_restrictions: `
55
+ ## PHASE 5: CE QUE TU NE FAIS JAMAIS
56
+ - Tu ne BLOQUES jamais l'algo sans raison valide
57
+ - Tu ne RALENTIS jamais l'exécution (réponse < 2 secondes)
58
+ - Tu ne fais pas de VAGUE - décision claire et directe
59
+ - Tu n'INVENTES pas de données - utilise uniquement ce qui est fourni
60
+ - Tu ne CHANGES pas la stratégie - tu l'optimises dans ses règles
61
+ `,
62
+
63
+ phase6_symbols: `
64
+ ## PHASE 6: CONNAISSANCE DES SYMBOLES
65
+ Tu trades ces symboles avec leurs caractéristiques:
66
+ - NQ (Nasdaq): volatile, tech-driven, corrélé ES
67
+ - ES (S&P500): référence, plus stable, leader
68
+ - YM (Dow): value stocks, moins volatile
69
+ - RTY (Russell): small caps, plus volatile que ES
70
+ - GC (Gold): safe haven, inverse USD, sessions Asia/London
71
+ - SI (Silver): suit GC avec plus de volatilité
72
+ - CL (Crude): news-driven, inventories, géopolitique
73
+
74
+ Sessions importantes:
75
+ - Asia: 18:00-03:00 ET (GC/SI actifs)
76
+ - London: 03:00-08:00 ET (préparation US)
77
+ - US Open: 09:30-11:30 ET (max volatilité)
78
+ - US Close: 15:00-16:00 ET (rebalancing)
79
+ `,
80
+
81
+ phase7_mindset: `
82
+ ## PHASE 7: TA MENTALITÉ
83
+ - OBJECTIF: Gagner. Pas "essayer". Gagner.
84
+ - PRÉCISION: Chaque décision compte
85
+ - RAPIDITÉ: Temps = argent. Sois rapide.
86
+ - RESPONSABILITÉ: Tu assumes tes recommandations
87
+ - ADAPTATION: Le marché change, tu t'adaptes
88
+ - DISCIPLINE: Les règles sont les règles
89
+ `
90
+ };
91
+
92
+ /**
93
+ * Build the complete directive string
94
+ */
95
+ const buildDirective = () => {
96
+ return Object.values(DIRECTIVE_PHASES).join('\n');
97
+ };
98
+
99
+ /**
100
+ * Expected output format from AI agents
101
+ */
102
+ const OUTPUT_FORMAT = {
103
+ schema: {
104
+ decision: 'approve | reject | modify',
105
+ confidence: 'number 0-100',
106
+ optimizations: {
107
+ entry: 'number | null',
108
+ stopLoss: 'number | null',
109
+ takeProfit: 'number | null',
110
+ size: 'number (-0.5 to 0.5) | null',
111
+ timing: 'now | wait | cancel'
112
+ },
113
+ reason: 'string (max 50 chars)',
114
+ alerts: 'string[] | null'
115
+ },
116
+ example: {
117
+ decision: 'modify',
118
+ confidence: 85,
119
+ optimizations: {
120
+ entry: 21450.25,
121
+ stopLoss: 21445.00,
122
+ takeProfit: 21462.50,
123
+ size: 0,
124
+ timing: 'now'
125
+ },
126
+ reason: 'Strong bid stack, tighten stop',
127
+ alerts: null
128
+ }
129
+ };
130
+
131
+ /**
132
+ * Build the output format instructions
133
+ */
134
+ const buildOutputInstructions = () => {
135
+ return `
136
+ ## OUTPUT FORMAT (JSON STRICT)
137
+ Tu dois TOUJOURS répondre en JSON valide avec ce format exact:
138
+
139
+ \`\`\`json
140
+ ${JSON.stringify(OUTPUT_FORMAT.example, null, 2)}
141
+ \`\`\`
142
+
143
+ IMPORTANT:
144
+ - decision: "approve" (exécuter tel quel), "reject" (ne pas exécuter), "modify" (exécuter avec optimisations)
145
+ - confidence: 0-100, ton niveau de confiance dans la décision
146
+ - optimizations: null si decision="reject", sinon les ajustements
147
+ - size: 0 = garder la taille, -0.5 = réduire de 50%, +0.5 = augmenter de 50%
148
+ - timing: "now" = exécuter immédiatement, "wait" = attendre meilleur prix, "cancel" = annuler
149
+ - reason: TOUJOURS fournir une raison courte
150
+ - Pas de texte avant ou après le JSON
151
+ `;
152
+ };
153
+
154
+ /**
155
+ * Get the complete directive with output format
156
+ */
157
+ const getFullDirective = () => {
158
+ return buildDirective() + '\n' + buildOutputInstructions();
159
+ };
160
+
161
+ module.exports = {
162
+ DIRECTIVE_PHASES,
163
+ OUTPUT_FORMAT,
164
+ buildDirective,
165
+ buildOutputInstructions,
166
+ getFullDirective
167
+ };
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  const cliproxy = require('../cliproxy');
13
- const { extractJSON } = require('./parser');
13
+ const https = require('https');
14
14
 
15
15
  /** Test prompt to verify agent understands directive format */
16
16
  const TEST_PROMPT = `You are being tested. Respond ONLY with this exact JSON, nothing else:
@@ -161,7 +161,6 @@ const testAgentConnection = async (agent) => {
161
161
 
162
162
  /**
163
163
  * Validate that response matches expected JSON format
164
- * Uses robust extractJSON from parser.js to handle MiniMax <think> tags and other edge cases
165
164
  * @param {string} content - Response content from agent
166
165
  * @returns {Object} { valid, error }
167
166
  */
@@ -170,40 +169,53 @@ const validateResponseFormat = (content) => {
170
169
  return { valid: false, error: 'Empty response' };
171
170
  }
172
171
 
173
- // Use robust JSON extraction from parser.js
174
- // Handles: direct JSON, markdown code blocks, JSON with extra text (MiniMax <think> tags)
175
- const json = extractJSON(content);
176
-
177
- if (!json) {
178
- return { valid: false, error: 'No valid JSON found in response' };
179
- }
180
-
181
- // Check required fields
182
- if (!json.decision) {
183
- return { valid: false, error: 'Missing "decision" field' };
184
- }
185
-
186
- if (json.confidence === undefined) {
187
- return { valid: false, error: 'Missing "confidence" field' };
188
- }
189
-
190
- if (!json.reason) {
191
- return { valid: false, error: 'Missing "reason" field' };
192
- }
193
-
194
- // Validate decision value
195
- const validDecisions = ['approve', 'reject', 'modify'];
196
- if (!validDecisions.includes(json.decision)) {
197
- return { valid: false, error: `Invalid decision: ${json.decision}` };
198
- }
199
-
200
- // Validate confidence is number 0-100
201
- const conf = Number(json.confidence);
202
- if (isNaN(conf) || conf < 0 || conf > 100) {
203
- return { valid: false, error: `Invalid confidence: ${json.confidence}` };
172
+ try {
173
+ // Try to extract JSON from response
174
+ let json;
175
+
176
+ // Direct parse
177
+ try {
178
+ json = JSON.parse(content.trim());
179
+ } catch (e) {
180
+ // Try to find JSON in response
181
+ const match = content.match(/\{[\s\S]*\}/);
182
+ if (match) {
183
+ json = JSON.parse(match[0]);
184
+ } else {
185
+ return { valid: false, error: 'No JSON in response' };
186
+ }
187
+ }
188
+
189
+ // Check required fields
190
+ if (!json.decision) {
191
+ return { valid: false, error: 'Missing "decision" field' };
192
+ }
193
+
194
+ if (json.confidence === undefined) {
195
+ return { valid: false, error: 'Missing "confidence" field' };
196
+ }
197
+
198
+ if (!json.reason) {
199
+ return { valid: false, error: 'Missing "reason" field' };
200
+ }
201
+
202
+ // Validate decision value
203
+ const validDecisions = ['approve', 'reject', 'modify'];
204
+ if (!validDecisions.includes(json.decision)) {
205
+ return { valid: false, error: `Invalid decision: ${json.decision}` };
206
+ }
207
+
208
+ // Validate confidence is number 0-100
209
+ const conf = Number(json.confidence);
210
+ if (isNaN(conf) || conf < 0 || conf > 100) {
211
+ return { valid: false, error: `Invalid confidence: ${json.confidence}` };
212
+ }
213
+
214
+ return { valid: true, error: null };
215
+
216
+ } catch (error) {
217
+ return { valid: false, error: `Parse error: ${error.message}` };
204
218
  }
205
-
206
- return { valid: true, error: null };
207
219
  };
208
220
 
209
221
  /**