From b9711f2517cf5a1d712546577149045bdf274138 Mon Sep 17 00:00:00 2001 From: Henry Winkel Date: Sat, 16 May 2026 13:53:04 +0200 Subject: [PATCH] Display executionPlan in UI; add tests for Trader executionPlan parsing and TradingGraph execution step Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- app/agents/__tests__/trader.test.ts | 28 +++ .../__tests__/tradingGraph.execution.test.ts | 30 +++ app/routes/analyze.ticker.tsx | 205 +++++++++++++++++- 3 files changed, 262 insertions(+), 1 deletion(-) create mode 100644 app/agents/__tests__/tradingGraph.execution.test.ts diff --git a/app/agents/__tests__/trader.test.ts b/app/agents/__tests__/trader.test.ts index 487fe7a..e95cbf1 100644 --- a/app/agents/__tests__/trader.test.ts +++ b/app/agents/__tests__/trader.test.ts @@ -36,4 +36,32 @@ describe("Trader", () => { const decision = await trader.decide("AAPL", mockReports, mockDebates); expect(decision.action).toBe("buy"); }); + + it("parses executionPlan on sell", async () => { + const mockSellClient = { + createChatCompletion: vi.fn().mockResolvedValue({ + choices: [{ message: { content: JSON.stringify({ + action: "sell", + confidence: 0.9, + reasoning: "Exit position", + executionPlan: { + amount: 50, + riskManagement: { maxLossPercent: 1.5, method: "trailing" }, + takeProfit: 150, + note: "Test plan" + } + }) } }] + }), + }; + + const trader = new Trader(mockSellClient as any); + const decision = await trader.decide("AAPL", mockReports, mockDebates); + + expect(decision.action).toBe("sell"); + expect(decision.executionPlan).toBeDefined(); + expect(decision.executionPlan?.amount).toBe(50); + expect(decision.executionPlan?.takeProfit).toBe(150); + expect(decision.executionPlan?.riskManagement?.maxLossPercent).toBe(1.5); + }); + }); }); \ No newline at end of file diff --git a/app/agents/__tests__/tradingGraph.execution.test.ts b/app/agents/__tests__/tradingGraph.execution.test.ts new file mode 100644 index 0000000..583730b --- /dev/null +++ b/app/agents/__tests__/tradingGraph.execution.test.ts @@ -0,0 +1,30 @@ +import { describe, it, expect, vi } from "vitest"; +import { TradingGraph } from "../tradingGraph"; + +describe("TradingGraph execution step", () => { + it("returns executionPlan when model provides it", async () => { + const mockClient = { + createChatCompletion: vi.fn().mockResolvedValue({ + choices: [{ message: { content: JSON.stringify({ + action: "sell", + confidence: 0.85, + reasoning: "Test sell", + executionPlan: { amount: 100, riskManagement: { maxLossPercent: 2 }, takeProfit: 200 } + }) } }] + }), + }; + + const mockInput = { + financialData: "...", + technicalData: { prices: [1,2,3], sma: 1, ema: 1, rsi: 50, macd: 0 }, + sentimentData: { headlines: ["h"], source: "news" }, + }; + + const graph = new TradingGraph(mockClient as any); + const decision = await graph.propagate("AAPL", mockInput as any); + + expect(decision.action).toBe("sell"); + expect(decision.executionPlan).toBeDefined(); + expect(decision.executionPlan?.amount).toBe(100); + }); +}); diff --git a/app/routes/analyze.ticker.tsx b/app/routes/analyze.ticker.tsx index 215c9f2..d4c364d 100644 --- a/app/routes/analyze.ticker.tsx +++ b/app/routes/analyze.ticker.tsx @@ -1,6 +1,8 @@ +import { useState, useEffect } from "react"; import { useLoaderData, useNavigate, useLocation } from "react-router"; import TradingViewChart from "../components/TradingViewChart"; import Navbar from "../components/Navbar"; +import type { TradingDecision, AnalystReport, DebateRound } from "../types/agents"; export const meta = () => [{ title: "Stock Detail - AITrader" }]; @@ -14,6 +16,7 @@ interface LoaderData { } const TIMEFRAMES = [ + { value: "1Min", label: "1 Minute" }, { value: "1D", label: "1 Day" }, { value: "5Min", label: "5 Min" }, { value: "15Min", label: "15 Min" }, @@ -73,6 +76,31 @@ export default function StockDetail() { const { ticker, position, orders, bars, timeframe, range } = useLoaderData() as LoaderData; const navigate = useNavigate(); const location = useLocation(); + + const [analysisLoading, setAnalysisLoading] = useState(false); + const [analystReports, setAnalystReports] = useState([]); + const [debateRounds, setDebateRounds] = useState([]); + const [decision, setDecision] = useState(null); + const [showAnalysts, setShowAnalysts] = useState(false); + const [showDebate, setShowDebate] = useState(false); + + // Cache key for this ticker + const cacheKey = `tradinggraph-${ticker}`; + + // Load cached results on mount + useEffect(() => { + const cached = sessionStorage.getItem(cacheKey); + if (cached) { + try { + const data = JSON.parse(cached); + if (data.analystReports) setAnalystReports(data.analystReports); + if (data.debateRounds) setDebateRounds(data.debateRounds); + if (data.decision) setDecision(data.decision); + } catch (e) { + console.error("Failed to parse cached trading graph data:", e); + } + } + }, [cacheKey]); const updateParams = (newTimeframe: string, newRange: string) => { const searchParams = new URLSearchParams(location.search); @@ -81,6 +109,46 @@ export default function StockDetail() { navigate(`${location.pathname}?${searchParams.toString()}`, { replace: true }); }; + const runTradingGraph = async () => { + setAnalysisLoading(true); + setAnalystReports([]); + setDebateRounds([]); + setDecision(null); + + try { + const res = await fetch("/api/analyze", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ ticker }), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || "Analysis failed"); + + const reports = data.agentSignals.map((sig: any) => ({ + analyst: sig.agent, + signal: sig, + report: sig.reasoning, + })); + const debates = data.debateRounds || []; + + setAnalystReports(reports); + setDebateRounds(debates); + setDecision(data); + + // Cache the results + sessionStorage.setItem(cacheKey, JSON.stringify({ + analystReports: reports, + debateRounds: debates, + decision: data, + timestamp: Date.now(), + })); + } catch (err) { + console.error("Analysis error:", err); + } finally { + setAnalysisLoading(false); + } + }; + // Convert Alpaca bars to TradingView format // Keep full timestamp for intraday, use date-only for daily // Sort bars by timestamp to ensure ascending order @@ -147,7 +215,15 @@ export default function StockDetail() { - + + +
@@ -182,6 +258,133 @@ export default function StockDetail() { )}
+ {(analystReports.length > 0 || debateRounds.length > 0 || decision) && ( +
+
+

Trading Graph Workflow

+ {sessionStorage.getItem(cacheKey) && ( + + Cached results + + )} +
+ + {/* Analysts Step - Collapsible */} + {analystReports.length > 0 && ( +
+ + {showAnalysts && ( +
+ {analystReports.map((report, i) => ( +
+
+ {report.analyst} + + {report.signal?.signal} ({(report.signal?.confidence * 100).toFixed(0)}%) + +
+

{report.signal?.reasoning}

+
+ ))} +
+ )} +
+ )} + + {/* Debate Step - Collapsible */} + {debateRounds.length > 0 && ( +
+ + {showDebate && ( +
+ {debateRounds.map((round, i) => ( +
+
+ Bullish View: +

{round.bullishView}

+
+
+ Bearish View: +

{round.bearishView}

+
+
+ ))} +
+ )} +
+ )} + + {/* Decision Step - Always Visible */} + {decision && ( +
+

+ 3 + Final Decision +

+
+
+ Action: + + {decision.action.toUpperCase()} + + Confidence: + {(decision.confidence * 100).toFixed(0)}% +
+ {decision.reasoning && ( +
+

{decision.reasoning}

+ {decision.executionPlan && ( +
+

Execution Plan

+
+
Amount: {decision.executionPlan.amount} shares
+ {decision.executionPlan.takeProfit != null && ( +
Take profit: ${decision.executionPlan.takeProfit}
+ )} + {decision.executionPlan.riskManagement?.maxLossPercent != null && ( +
Risk management: {decision.executionPlan.riskManagement.maxLossPercent}% max loss
+ )} + {decision.executionPlan.riskManagement?.method && ( +
Method: {decision.executionPlan.riskManagement.method}
+ )} + {decision.executionPlan.note && ( +
Note: {decision.executionPlan.note}
+ )} +
+
+ )} +
+ )} +
+
+ )} +
+ )} +

Recent Orders

{orders.length === 0 ? (