import { useState, useEffect } from "react"; import { Link } from "react-router"; import Navbar from "../components/Navbar"; import type { TradingDecision } from "../types/agents"; interface Indicators { rsi: number | null; sma20: number | null; sma50: number | null; ema12: number | null; ema26: number | null; macd: number | null; bbUpper: number | null; bbMiddle: number | null; bbLower: number | null; atr: number | null; avgVolume: number | null; } interface StockRow { id: string; ticker: string; currentPrice: number | null; position: number; indicators: Indicators; analysis: TradingDecision | null; loading: boolean; indicatorsLoading: boolean; } export const meta = () => [ { title: "Portfolio Analysis - AITrader" }, { name: "description", content: "Analyze your stock portfolio with AI trading insights" }, ]; function RsiBadge({ value }: { value: number }) { const color = value > 70 ? "bg-red-100 text-red-700" : value < 30 ? "bg-green-100 text-green-700" : "bg-gray-100 text-gray-700"; const label = value > 70 ? "Overbought" : value < 30 ? "Oversold" : "Neutral"; return {value.toFixed(0)} {label}; } function MacdBadge({ value }: { value: number }) { const color = value > 0 ? "text-green-600" : "text-red-600"; return {value > 0 ? "▲" : "▼"} {value.toFixed(2)}; } function PriceVsSma({ price, sma, label }: { price: number; sma: number; label: string }) { if (!price || !sma) return -; const above = price > sma; const pct = ((price - sma) / sma * 100).toFixed(1); return ( {above ? "▲" : "▼"} {pct}% ); } function SignalSummary({ price, indicators }: { price: number | null; indicators: Indicators }) { if (!price) return No data; const signals: string[] = []; if (indicators.rsi != null) { if (indicators.rsi > 70) signals.push("RSI overbought"); else if (indicators.rsi < 30) signals.push("RSI oversold"); } if (indicators.sma20 != null && indicators.sma50 != null) { if (indicators.sma20 > indicators.sma50) signals.push("SMA bullish cross"); else signals.push("SMA bearish cross"); } if (indicators.macd != null) { if (indicators.macd > 0) signals.push("MACD positive"); else signals.push("MACD negative"); } if (indicators.bbUpper != null && indicators.bbLower != null) { if (price > indicators.bbUpper) signals.push("Above BB upper"); else if (price < indicators.bbLower) signals.push("Below BB lower"); } if (signals.length === 0) return -; const bullish = signals.filter(s => s.includes("oversold") || s.includes("bullish") || s.includes("positive") || s.includes("Below BB")).length; const bearish = signals.filter(s => s.includes("overbought") || s.includes("bearish") || s.includes("negative") || s.includes("Above BB")).length; const net = bullish - bearish; const bias = net > 0 ? "bullish" : net < 0 ? "bearish" : "neutral"; const biasColor = bias === "bullish" ? "text-green-600" : bias === "bearish" ? "text-red-600" : "text-gray-500"; return (
{bias}
{signals.slice(0, 3).map((s, i) =>
{s}
)}
); } function IndicatorsPopover({ indicators, price, visible, onClose }: { indicators: Indicators; price: number | null; visible: boolean; onClose: () => void }) { if (!visible) return null; const rows = [ { label: "RSI (14)", value: indicators.rsi != null ? : "-" }, { label: "SMA 20", value: indicators.sma20 != null ? `${indicators.sma20.toFixed(2)}` : "-" }, { label: "SMA 50", value: indicators.sma50 != null ? `${indicators.sma50.toFixed(2)}` : "-" }, { label: "EMA 12", value: indicators.ema12 != null ? `${indicators.ema12.toFixed(2)}` : "-" }, { label: "EMA 26", value: indicators.ema26 != null ? `${indicators.ema26.toFixed(2)}` : "-" }, { label: "MACD", value: indicators.macd != null ? : "-" }, { label: "BB Upper", value: indicators.bbUpper != null ? `$${indicators.bbUpper.toFixed(2)}` : "-" }, { label: "BB Middle", value: indicators.bbMiddle != null ? `$${indicators.bbMiddle.toFixed(2)}` : "-" }, { label: "BB Lower", value: indicators.bbLower != null ? `$${indicators.bbLower.toFixed(2)}` : "-" }, { label: "ATR (14)", value: indicators.atr != null ? `$${indicators.atr.toFixed(2)}` : "-" }, { label: "Avg Vol (20)", value: indicators.avgVolume != null ? indicators.avgVolume.toFixed(0) : "-" }, ]; return ( <>

Technical Indicators

{rows.map((r) => (
{r.label} {r.value}
))}
{price && indicators.sma20 && indicators.sma50 && (
Price vs SMA20
Price vs SMA50
)}
); } export default function Analyze() { const [stocks, setStocks] = useState([]); const [newTicker, setNewTicker] = useState(""); useEffect(() => { const loadPortfolio = async () => { try { 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() : []; const alpacaTickers = new Set(positions.map((p: { ticker: string }) => p.ticker)); const buildStock = async (ticker: string, qty: number) => { try { const quoteRes = await fetch(`/api/alpaca/quote/${ticker}`); const quote = quoteRes.ok ? await quoteRes.json() : null; return { id: `alpaca-${ticker}`, ticker, currentPrice: quote?.price ?? null, position: qty, indicators: { rsi: null, sma20: null, sma50: null, ema12: null, ema26: null, macd: null, bbUpper: null, bbMiddle: null, bbLower: null, atr: null, avgVolume: null }, analysis: null, loading: false, indicatorsLoading: false, }; } catch { return { id: `alpaca-${ticker}`, ticker, currentPrice: null, position: qty, indicators: { rsi: null, sma20: null, sma50: null, ema12: null, ema26: null, macd: null, bbUpper: null, bbMiddle: null, bbLower: null, atr: null, avgVolume: null }, analysis: null, loading: false, indicatorsLoading: false, }; } }; const alpacaStocks = await Promise.all( positions.map((p: { ticker: string; qty: number }) => buildStock(p.ticker, p.qty)) ); const dbOnlyStocks = []; for (const stock of dbStocks) { if (!alpacaTickers.has(stock.ticker)) { dbOnlyStocks.push(await buildStock(stock.ticker, 0)); } } setStocks([...alpacaStocks, ...dbOnlyStocks]); } catch (err) { console.error("[analyze] Portfolio load error:", err); } }; loadPortfolio(); }, []); 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 loadIndicators = async (ticker: string) => { try { const res = await fetch(`/api/indicators?symbol=${ticker}`); if (!res.ok) return null; const data = await res.json(); const ind = data.indicators || {}; return { rsi: ind.rsi ?? null, sma20: ind.sma ?? null, sma50: ind.sma50 ?? null, ema12: ind.ema12 ?? null, ema26: ind.ema26 ?? null, macd: ind.macd ?? null, bbUpper: ind.bbUpper ?? null, bbMiddle: ind.bbMiddle ?? null, bbLower: ind.bbLower ?? null, atr: ind.atr ?? null, avgVolume: ind.avgVolume ?? null, }; } catch { return null; } }; const addStock = async () => { if (!newTicker.trim()) return; const ticker = newTicker.trim().toUpperCase(); if (stocks.some((s) => s.ticker === ticker)) return; 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:", err); } const newStock: StockRow = { id: `db-${ticker}`, ticker, currentPrice: null, position: 0, indicators: { rsi: null, sma20: null, sma50: null, ema12: null, ema26: null, macd: null, bbUpper: null, bbMiddle: null, bbLower: null, atr: null, avgVolume: null }, analysis: null, loading: true, indicatorsLoading: true, }; setStocks((s) => [...s, newStock]); setNewTicker(""); try { const [quoteRes, positionsRes] = await Promise.all([ fetch(`/api/alpaca/quote/${ticker}`), fetch("/api/alpaca/positions"), ]); const quote = quoteRes.ok ? await quoteRes.json() : null; const positions = positionsRes.ok ? await positionsRes.json() : []; const position = positions.find((p: { ticker: string }) => p.ticker === ticker)?.qty ?? 0; const indicators = await loadIndicators(ticker); setStocks((s) => s.map((st) => st.ticker === ticker ? { ...st, loading: false, indicatorsLoading: false, currentPrice: quote?.price ?? null, position, indicators: indicators || st.indicators } : st )); } catch (err) { console.error("[analyze] Error adding stock:", err); setStocks((s) => s.map((st) => st.ticker === ticker ? { ...st, loading: false, indicatorsLoading: false } : st)); } }; 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 }) => 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; 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:", 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 indicatorsData = 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; const indicators: Indicators = { rsi: indicatorsData?.indicators?.rsi ?? null, sma20: indicatorsData?.indicators?.sma ?? null, sma50: indicatorsData?.indicators?.sma50 ?? null, ema12: indicatorsData?.indicators?.ema12 ?? null, ema26: indicatorsData?.indicators?.ema26 ?? null, macd: indicatorsData?.indicators?.macd ?? null, bbUpper: indicatorsData?.indicators?.bbUpper ?? null, bbMiddle: indicatorsData?.indicators?.bbMiddle ?? null, bbLower: indicatorsData?.indicators?.bbLower ?? null, atr: indicatorsData?.indicators?.atr ?? null, avgVolume: indicatorsData?.indicators?.avgVolume ?? null, }; setStocks((s) => s.map((st) => st.id === id ? { ...st, loading: false, currentPrice: quote?.price ?? null, indicators, analysis } : st )); } catch { setStocks((s) => s.map((st) => st.id === id ? { ...st, loading: false } : st)); } }; const loadAllIndicators = async () => { for (const stock of stocks) { if (stock.indicators.rsi != null) continue; setStocks((s) => s.map((st) => st.id === stock.id ? { ...st, indicatorsLoading: true } : st)); const indicators = await loadIndicators(stock.ticker); if (indicators) { setStocks((s) => s.map((st) => st.id === stock.id ? { ...st, indicators, indicatorsLoading: false } : st)); } else { setStocks((s) => s.map((st) => st.id === stock.id ? { ...st, indicatorsLoading: false } : st)); } } }; const [openIndicatorId, setOpenIndicatorId] = useState(null); 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 Technical Summary RSI MACD SMA 20/50 AI Analysis Actions
{stock.ticker} {stock.currentPrice ? `$${stock.currentPrice.toFixed(2)}` : "-"} {stock.position > 0 ? ( {stock.position} shares ) : ( - )}
{stock.indicatorsLoading ? ( Loading... ) : ( <> )}
setOpenIndicatorId(null)} />
{stock.indicators.rsi != null ? ( ) : stock.indicatorsLoading ? ( ... ) : "-"} {stock.indicators.macd != null ? ( ) : "-"} {stock.currentPrice && stock.indicators.sma20 && stock.indicators.sma50 ? (
) : "-"}
{stock.analysis ? (
{stock.analysis.action.toUpperCase()}
{(stock.analysis.confidence ?? 0 * 100).toFixed(0)}%
) : stock.loading ? ( Analyzing... ) : "-"}
)}
); }