import { useState, useEffect } from "react"; import { Link } from "react-router"; import Navbar from "../components/Navbar"; import type { TradingDecision } from "../types/agents"; interface StockRow { id: string; ticker: string; currentPrice: number | null; position: number; rsi: number | null; analysis: TradingDecision | null; loading: boolean; } export const meta = () => { return [ { title: "Portfolio Analysis - AITrader" }, { name: "description", content: "Analyze your stock portfolio with AI trading insights" }, ]; }; export default function Analyze() { const [stocks, setStocks] = useState([]); const [newTicker, setNewTicker] = useState(""); // Load Alpaca portfolio and database stocks on mount useEffect(() => { const loadPortfolio = async () => { try { // Fetch both Alpaca positions and database stocks const [positionsRes, dbStocksRes] = await Promise.all([ fetch("/api/alpaca/positions"), fetch("/api/stocks"), ]); const positions = positionsRes.ok ? await positionsRes.json() : []; const dbStocks = dbStocksRes.ok ? await dbStocksRes.json() : []; // Create a set of tickers from Alpaca positions for quick lookup const alpacaTickers = new Set(positions.map((p: { ticker: string }) => p.ticker)); // Build stocks array for Alpaca positions const alpacaStocks = await Promise.all( positions.map(async (p: { ticker: string; qty: number }) => { try { const quoteRes = await fetch(`/api/alpaca/quote/${p.ticker}`); const quote = quoteRes.ok ? await quoteRes.json() : null; return { id: `alpaca-${p.ticker}`, ticker: p.ticker, currentPrice: quote?.price ?? null, position: p.qty, rsi: null, analysis: null, loading: false, }; } catch { return { id: `alpaca-${p.ticker}`, ticker: p.ticker, currentPrice: null, position: p.qty, rsi: null, analysis: null, loading: false, }; } }) ); // Add database stocks that are not in Alpaca positions with position=0 const dbOnlyStocks = []; for (const stock of dbStocks) { if (!alpacaTickers.has(stock.ticker)) { try { const quoteRes = await fetch(`/api/alpaca/quote/${stock.ticker}`); const quote = quoteRes.ok ? await quoteRes.json() : null; dbOnlyStocks.push({ id: `db-${stock.ticker}`, ticker: stock.ticker, currentPrice: quote?.price ?? null, position: 0, rsi: null, analysis: null, loading: false, }); } catch { dbOnlyStocks.push({ id: `db-${stock.ticker}`, ticker: stock.ticker, currentPrice: null, position: 0, rsi: null, analysis: null, loading: false, }); } } } setStocks([...alpacaStocks, ...dbOnlyStocks]); } catch (err) { console.error("[analyze] Portfolio load error:", err); } }; loadPortfolio(); }, []); // Refresh prices every minute useEffect(() => { const interval = setInterval(() => { stocks.forEach((stock) => { fetch(`/api/alpaca/quote/${stock.ticker}`) .then((res) => res.ok ? res.json() : null) .then((data) => { if (data?.price) { setStocks((s) => s.map((st) => st.ticker === stock.ticker ? { ...st, currentPrice: data.price } : st )); } }) .catch(() => {}); }); }, 60000); return () => clearInterval(interval); }, [stocks]); const addStock = async () => { if (!newTicker.trim()) return; const ticker = newTicker.trim().toUpperCase(); console.log("[analyze] Adding stock:", ticker); // Check if ticker already exists if (stocks.some((s) => s.ticker === ticker)) { console.log("[analyze] Ticker already exists:", ticker); return; } // Save to database first try { const formData = new FormData(); formData.append("ticker", ticker); await fetch("/api/stocks", { method: "POST", body: formData, }); } catch (err) { console.error("[analyze] Error saving stock to DB:", err); } const newStock: StockRow = { id: `db-${ticker}`, ticker, currentPrice: null, position: 0, rsi: null, analysis: null, loading: true, }; setStocks((s) => [...s, newStock]); setNewTicker(""); try { console.log("[analyze] Fetching quote and positions for", ticker); const [quoteRes, positionsRes] = await Promise.all([ fetch(`/api/alpaca/quote/${ticker}`), fetch("/api/alpaca/positions"), ]); console.log("[analyze] Quote response:", quoteRes.status); console.log("[analyze] Positions response:", positionsRes.status); const quote = quoteRes.ok ? await quoteRes.json() : null; const positions = positionsRes.ok ? await positionsRes.json() : []; console.log("[analyze] Quote data:", quote); console.log("[analyze] Positions data:", positions); const position = positions.find((p: { ticker: string; qty: number }) => p.ticker === ticker )?.qty ?? 0; console.log("[analyze] Found position:", position); setStocks((s) => s.map((st) => st.ticker === ticker ? { ...st, loading: false, currentPrice: quote?.price ?? null, position } : st )); } catch (err) { console.error("[analyze] Error adding stock:", err); setStocks((s) => s.map((st) => st.ticker === ticker ? { ...st, loading: false } : st)); } }; // Update all positions on mount and when stocks change useEffect(() => { if (stocks.length === 0) return; const updatePositions = async () => { try { const res = await fetch("/api/alpaca/positions"); if (res.ok) { const positions = await res.json(); setStocks((s) => s.map((st) => { const pos = positions.find((p: { ticker: string; qty: number }) => p.ticker === st.ticker ); return pos ? { ...st, position: pos.qty } : st; })); } } catch (err) { console.error("[analyze] Position update error:", err); } }; updatePositions(); }, [stocks.length]); const removeStock = async (id: string) => { const stock = stocks.find((s) => s.id === id); if (!stock) return; // Delete from database if this was a manually added stock (db- prefix) if (id.startsWith("db-")) { try { const formData = new FormData(); formData.append("_method", "DELETE"); formData.append("ticker", stock.ticker); await fetch("/api/stocks", { method: "POST", body: formData, }); } catch (err) { console.error("[analyze] Error deleting stock from DB:", err); } } setStocks((s) => s.filter((stock) => stock.id !== id)); }; const runAnalysis = async (id: string, ticker: string) => { setStocks((s) => s.map((st) => st.id === id ? { ...st, loading: true } : st)); try { const [quoteRes, indicatorsRes] = await Promise.all([ fetch(`/api/alpaca/quote/${ticker}`), fetch(`/api/indicators?symbol=${ticker}`), ]); const quote = quoteRes.ok ? await quoteRes.json() : null; const indicators = indicatorsRes.ok ? await indicatorsRes.json() : null; const analysisRes = await fetch("/api/analyze", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ticker }), }); const analysis = analysisRes.ok ? await analysisRes.json() : null; setStocks((s) => s.map((st) => st.id === id ? { ...st, loading: false, currentPrice: quote?.price ?? null, rsi: indicators?.indicators?.rsi ?? null, analysis, } : st )); } catch { setStocks((s) => s.map((st) => st.id === id ? { ...st, loading: false } : st)); } }; return (

Portfolio Analysis

setNewTicker(e.target.value.toUpperCase())} placeholder="Add ticker (e.g. AAPL)" className="flex-1 border border-gray-300 rounded-lg px-4 py-2.5 text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500" onKeyDown={(e) => e.key === "Enter" && addStock()} />
{stocks.length === 0 ? (

No stocks added. Add a ticker to get started.

) : (
{stocks.map((stock) => ( ))}
Ticker Price Position RSI Analysis Actions
{stock.ticker} {stock.currentPrice ? `$${stock.currentPrice.toFixed(2)}` : "-"} {stock.position} {stock.rsi ? ( 70 ? "text-red-600" : stock.rsi < 30 ? "text-green-600" : "text-gray-900" }> {stock.rsi.toFixed(2)} ) : "-"} {stock.analysis ? (
{stock.analysis.action.toUpperCase()}
Confidence: {(stock.analysis.confidence * 100).toFixed(0)}%
) : stock.loading ? ( Analyzing... ) : "-"}
)}
); }