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:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -3,6 +3,6 @@ import type { Config } from "@react-router/dev/config";
|
||||
export default {
|
||||
// Config options...
|
||||
// Server-side render by default, to enable SPA mode set this to `false`
|
||||
// Disabled SSR in dev to avoid server-side JSX runtime mismatch causing crashes
|
||||
ssr: false,
|
||||
// Server-side render by default, to enable SPA mode set this to `false`
|
||||
ssr: true,
|
||||
} satisfies Config;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"**/.client/**/*",
|
||||
".react-router/types/**/*"
|
||||
],
|
||||
"exclude": ["node_modules", "tests", "app/lib/__tests__"],
|
||||
"compilerOptions": {
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||
"types": ["node", "vite/client"],
|
||||
|
||||
Reference in New Issue
Block a user