Fix(types): LLM types, execution LLM call safety, analyze defaults; skip tests in tsconfig for dev typecheck\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

This commit is contained in:
2026-05-16 17:57:53 +02:00
parent c4873daf3b
commit b2e0568bfd
5 changed files with 175 additions and 8 deletions
+135
View File
@@ -0,0 +1,135 @@
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<TradingDecision> {
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;
}
}
+35 -6
View File
@@ -1,6 +1,7 @@
import { OpenRouterClient } from "../../lib/openrouter";
import { TradingGraph } from "../../agents/tradingGraph";
import { db } from "../../lib/db.server";
import { fetchAccount, fetchRecentCloses } from "../../lib/alpacaClient";
export async function action({ request }: { request: Request }) {
console.log("[analyze] Request received:", request.method, request.url);
@@ -56,19 +57,31 @@ export async function action({ request }: { request: Request }) {
const client = new OpenRouterClient(apiKey);
const graph = new TradingGraph(client);
// Fetch latest Alpaca account and recent prices; abort if unavailable
let account: any = undefined;
let prices: number[] = [];
try {
account = await fetchAccount();
prices = await fetchRecentCloses(ticker);
} catch (e) {
console.error("[analyze] Failed to fetch Alpaca data before analysis:", e);
return Response.json({ error: "Failed to fetch Alpaca data: " + String(e) }, { status: 502 });
}
const input = {
financialData: `Financial data for ${ticker} as of ${date}`,
technicalData: {
prices: [100, 102, 101, 103, 105],
sma: 102,
ema: 103,
rsi: 55,
macd: 0.5,
prices,
sma: 0,
ema: 0,
rsi: 0,
macd: 0,
},
sentimentData: {
headlines: [`${ticker} showing positive momentum`],
source: "news" as const,
},
account,
};
try {
@@ -86,7 +99,23 @@ export async function action({ request }: { request: Request }) {
}
}
const decision = await graph.propagate(ticker, input);
let decision = await graph.propagate(ticker, input);
// Enrich executionPlan deterministically on server-side
try {
const { enrichExecutionPlan, verifyExecutionPlanWithLLM } = await import("../../lib/execution");
decision = enrichExecutionPlan(decision, input);
// Optionally ask LLM to verify/adjust the computed plan if API key is present
if (process.env.OPENROUTER_API_KEY) {
try {
decision = await verifyExecutionPlanWithLLM(decision, input);
} catch (e) {
console.warn("LLM verification failed:", e);
}
}
} catch (e) {
console.warn("Failed to enrich execution plan:", e);
}
console.log("[analyze] Decision received:", JSON.stringify(decision));
return Response.json(decision);
} catch (error) {
+2
View File
@@ -27,7 +27,9 @@ export interface ExecutionPlan {
method?: string
}
takeProfit?: number // target price for take-profit
stopLoss?: number // stop-loss price or absolute value
note?: string
_llmReview?: { approved: boolean; notes?: string | null }
}
export interface TradingDecision {