import type { TradingDecision, ExecutionPlan } from "../types/agents"; export function enrichExecutionPlan(decision: TradingDecision, input: any): TradingDecision { try { const prices: number[] = input?.technicalData?.prices || []; const entryPrice = prices.length ? prices[prices.length - 1] : undefined; // simple ATR approximation: average absolute diff let atr = 0; if (prices && prices.length >= 2) { let sum = 0; for (let i = 1; i < prices.length; i++) sum += Math.abs(prices[i] - prices[i - 1]); atr = sum / (prices.length - 1); } else if (entryPrice) { atr = entryPrice * 0.01; // fallback 1% } const rr = 2; // default risk:reward const equity = Number(input?.account?.cash ?? input?.account?.buying_power ?? process.env.DEFAULT_ACCOUNT_EQUITY ?? 10000); if (!decision.executionPlan) decision.executionPlan = {} as ExecutionPlan; const plan = decision.executionPlan as any; const maxLossPercent = plan.riskManagement?.maxLossPercent ?? plan.maxLossPercent ?? 1; // default 1% const riskPercent = Number(maxLossPercent) / 100; // compute stop distance (price units) let stopDistanceByPercent = entryPrice ? Math.abs(entryPrice * riskPercent) : 0; const stopDistanceByAtr = atr ? atr * 1.5 : 0; // multiplier let stopDistance = Math.max(stopDistanceByAtr, stopDistanceByPercent, 0.0001); // compute stopLoss absolute price if missing if (plan.stopLoss == null && entryPrice != null) { if (decision.action === 'buy') { plan.stopLoss = Number((entryPrice - stopDistance).toFixed(2)); } else if (decision.action === 'sell') { // for sell (exit/short) place stop above entry plan.stopLoss = Number((entryPrice + stopDistance).toFixed(2)); } else { // default: buy-style stop plan.stopLoss = Number((entryPrice - stopDistance).toFixed(2)); } } // compute takeProfit if missing if (plan.takeProfit == null && entryPrice != null) { if (decision.action === 'buy') { plan.takeProfit = Number((entryPrice + stopDistance * rr).toFixed(2)); } else if (decision.action === 'sell') { plan.takeProfit = Number((entryPrice - stopDistance * rr).toFixed(2)); } else { plan.takeProfit = Number((entryPrice + stopDistance * rr).toFixed(2)); } } // compute shares if missing using risk-based sizing if (plan.amount == null) { const riskAmount = equity * riskPercent; // Protect against extremely small stopDistance (which can occur with missing/flat prices) if (!stopDistance || stopDistance < 0.01) { stopDistance = entryPrice ? Math.max(entryPrice * 0.01, 0.01) : 1; // default 1 unit when no price } const rawShares = Math.max(1, Math.floor(riskAmount / stopDistance)); // Cap shares to what is affordable with full equity and a reasonable absolute cap const affordableMax = Math.max(1, Math.floor(equity / Math.max(entryPrice || 1, 1))); const absoluteMax = 100000; // safety cap const shares = Math.min(rawShares, affordableMax, absoluteMax); plan.amount = shares; } // normalize nested riskManagement plan.riskManagement = plan.riskManagement || {}; if (plan.riskManagement.maxLossPercent == null) plan.riskManagement.maxLossPercent = maxLossPercent; decision.executionPlan = plan as ExecutionPlan; } catch (err) { console.warn("enrichExecutionPlan error:", err); } return decision; } // Optional LLM verification step: review computed executionPlan and suggest adjustments export async function verifyExecutionPlanWithLLM(decision: TradingDecision, input: any): Promise { try { const apiKey = process.env.OPENROUTER_API_KEY; if (!apiKey) return decision; const { OpenRouterClient } = await import("./openrouter"); const client = new OpenRouterClient(apiKey); const plan = decision.executionPlan || {}; const prices: number[] = input?.technicalData?.prices || []; const entryPrice = prices.length ? prices[prices.length - 1] : null; const userPrompt = `Review the following deterministic execution plan and either approve or provide corrected values. Respond with a JSON object only with the shape: { "approved": boolean, "executionPlan": { /* fields to use */ }, "notes": "..." }\n\nContext:\nentryPrice: ${entryPrice}\nrecentPrices: ${JSON.stringify(prices.slice(-10))}\nComputedPlan: ${JSON.stringify(plan)}\n\nGuidelines:\n- If values look reasonable, return {"approved": true, "executionPlan": {}} (empty executionPlan means keep computed values).\n- If any value should be adjusted, return an executionPlan object with corrected fields (amount, stopLoss, takeProfit, riskManagement).\n- Do not include any extra text outside the JSON object.`; const messages = [ { role: "system", content: "You are a conservative trading assistant. Validate risk sizing and stop levels. If you suggest changes, prefer conservative (smaller) position sizes and wider stops only if volatility justifies it." }, { role: "user", content: userPrompt }, ]; const res: any = await client.createChatCompletion(messages as any); const content = res?.choices?.[0]?.message?.content ?? ""; // try to parse JSON out of the content let parsed: any = null; try { parsed = JSON.parse(content); } catch (e) { // fallback: extract first JSON object const m = content.match(/(\{[\s\S]*\})/); if (m) { try { parsed = JSON.parse(m[1]); } catch (e2) { console.warn("verifyExecutionPlanWithLLM: failed to parse LLM response JSON"); } } } if (parsed && typeof parsed === "object") { const approved = parsed.approved !== false; // default true if missing const suggested = parsed.executionPlan || {}; // attach llmReview metadata const newPlan = { ...plan, ...suggested }; newPlan._llmReview = { approved: !!approved, notes: parsed.notes || null }; decision.executionPlan = newPlan as any; } return decision; } catch (err) { console.warn("verifyExecutionPlanWithLLM error:", err); return decision; } }