From 046e81ffc1735145b74dd1aca549d7113aa6d5c8 Mon Sep 17 00:00:00 2001 From: Henry Winkel Date: Sat, 16 May 2026 22:05:01 +0200 Subject: [PATCH] feat: rewrite analyze page with technical indicators column and signal summary --- app/routes/analyze.tsx | 539 +++++++++++++++++++++++------------ app/routes/api/indicators.ts | 101 +++++-- app/types.ts | 9 +- app/utils/indicators.ts | 52 +++- 4 files changed, 471 insertions(+), 230 deletions(-) diff --git a/app/routes/analyze.tsx b/app/routes/analyze.tsx index 1a9a672..4335626 100644 --- a/app/routes/analyze.tsx +++ b/app/routes/analyze.tsx @@ -3,112 +3,213 @@ 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; - rsi: number | null; + indicators: Indicators; analysis: TradingDecision | null; loading: boolean; + indicatorsLoading: boolean; } -export const meta = () => { - return [ - { title: "Portfolio Analysis - AITrader" }, - { name: "description", content: "Analyze your stock portfolio with AI trading insights" }, +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(""); -// 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"), - ]); + 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 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)); + 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, - }; - } - }) - ); + 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, + }; + } + }; - // 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, - }); - } - } - } + const alpacaStocks = await Promise.all( + positions.map((p: { ticker: string; qty: number }) => buildStock(p.ticker, p.qty)) + ); - setStocks([...alpacaStocks, ...dbOnlyStocks]); - } catch (err) { - console.error("[analyze] Portfolio load error:", err); - } - }; + const dbOnlyStocks = []; + for (const stock of dbStocks) { + if (!alpacaTickers.has(stock.ticker)) { + dbOnlyStocks.push(await buildStock(stock.ticker, 0)); + } + } - loadPortfolio(); - }, []); + setStocks([...alpacaStocks, ...dbOnlyStocks]); + } catch (err) { + console.error("[analyze] Portfolio load error:", err); + } + }; + + loadPortfolio(); + }, []); - // Refresh prices every minute useEffect(() => { const interval = setInterval(() => { stocks.forEach((stock) => { @@ -116,7 +217,7 @@ export default function Analyze() { .then((res) => res.ok ? res.json() : null) .then((data) => { if (data?.price) { - setStocks((s) => s.map((st) => + setStocks((s) => s.map((st) => st.ticker === stock.ticker ? { ...st, currentPrice: data.price } : st )); } @@ -124,32 +225,46 @@ export default function Analyze() { .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(); - console.log("[analyze] Adding stock:", ticker); + if (stocks.some((s) => s.ticker === ticker)) return; - // 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, - }); + await fetch("/api/stocks", { method: "POST", body: formData }); } catch (err) { - console.error("[analyze] Error saving stock to DB:", err); + console.error("[analyze] Error saving stock:", err); } const newStock: StockRow = { @@ -157,60 +272,48 @@ export default function Analyze() { ticker, currentPrice: null, position: 0, - rsi: null, + 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 { - 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() : []; + const position = positions.find((p: { ticker: string }) => p.ticker === ticker)?.qty ?? 0; - 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); + const indicators = await loadIndicators(ticker); setStocks((s) => s.map((st) => st.ticker === ticker - ? { ...st, loading: false, currentPrice: quote?.price ?? null, position } + ? { ...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 } : st)); + setStocks((s) => s.map((st) => st.ticker === ticker ? { ...st, loading: false, indicatorsLoading: 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 - ); + const pos = positions.find((p: { ticker: string }) => p.ticker === st.ticker); return pos ? { ...st, position: pos.qty } : st; })); } @@ -218,7 +321,7 @@ export default function Analyze() { console.error("[analyze] Position update error:", err); } }; - + updatePositions(); }, [stocks.length]); @@ -226,18 +329,14 @@ export default function Analyze() { 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, - }); + await fetch("/api/stocks", { method: "POST", body: formData }); } catch (err) { - console.error("[analyze] Error deleting stock from DB:", err); + console.error("[analyze] Error deleting stock:", err); } } @@ -246,32 +345,40 @@ export default function Analyze() { 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 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; - - setStocks((s) => s.map((st) => - st.id === id - ? { - ...st, - loading: false, - currentPrice: quote?.price ?? null, - rsi: indicators?.indicators?.rsi ?? null, - analysis, - } + + 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 { @@ -279,13 +386,33 @@ export default function Analyze() { } }; + 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

- -
+
+

Portfolio Analysis

+ +
+ +
- + {stocks.length === 0 ? (

No stocks added. Add a ticker to get started.

) : ( @@ -310,80 +437,112 @@ export default function Analyze() { - - - - - - + + + + + + + + + {stocks.map((stock) => ( - - + - - - + - + + - @@ -396,4 +555,4 @@ export default function Analyze() { ); -} \ No newline at end of file +} diff --git a/app/routes/api/indicators.ts b/app/routes/api/indicators.ts index 9efe1c2..5556518 100644 --- a/app/routes/api/indicators.ts +++ b/app/routes/api/indicators.ts @@ -1,18 +1,47 @@ import { type IndicatorData } from "../../types"; -import { - calculateSMA, - calculateEMA, - calculateRSI, - calculateMACD, -} from "../../utils/indicators"; +import { calculateSMA, calculateEMA, calculateRSI, calculateMACD, calculateBollingerBands, calculateATR, calculateVolumeAvg } from "../../utils/indicators"; +import alpacaService from "../../lib/alpacaClient"; -// Replace with actual Alpaca API call async function fetchHistoricPrices(symbol: string): Promise { - return [ - 150.0, 152.3, 151.8, 153.5, 155.0, 154.2, 156.7, 158.1, 157.5, 159.0, - 160.2, 158.9, 161.5, 163.0, 162.5, 164.8, 166.3, 165.0, 167.5, 169.0, - 168.2, 170.5, 172.0, 171.5, 173.2, - ]; + try { + const bars = await alpacaService.fetchBars(symbol, "1Day", { limit: 60 }); + return bars.map((b: any) => { + const c = b.ClosePrice ?? b.c ?? 0; + return typeof c === "number" ? c : 0; + }).filter((p: number) => p > 0); + } catch (e) { + console.error(`Failed to fetch bars for ${symbol}:`, e); + return []; + } +} + +async function fetchVolumes(symbol: string): Promise { + try { + const bars = await alpacaService.fetchBars(symbol, "1Day", { limit: 60 }); + return bars.map((b: any) => { + const v = b.Volume ?? b.v ?? 0; + return typeof v === "number" ? v : 0; + }).filter((v: number) => v > 0); + } catch (e) { + return []; + } +} + +async function fetchHighLow(symbol: string): Promise<{ highs: number[]; lows: number[] }> { + try { + const bars = await alpacaService.fetchBars(symbol, "1Day", { limit: 60 }); + const highs: number[] = []; + const lows: number[] = []; + for (const b of bars) { + const h = b.HighPrice ?? b.h ?? 0; + const l = b.LowPrice ?? b.l ?? 0; + if (typeof h === "number" && h > 0) highs.push(h); + if (typeof l === "number" && l > 0) lows.push(l); + } + return { highs, lows }; + } catch (e) { + return { highs: [], lows: [] }; + } } export async function loader({ request }: { request: Request }) { @@ -20,35 +49,47 @@ export async function loader({ request }: { request: Request }) { const symbol = url.searchParams.get("symbol"); if (!symbol) { - return Response.json( - { error: "Symbol is required" }, - { status: 400 } - ); + return Response.json({ error: "Symbol is required" }, { status: 400 }); } try { const prices = await fetchHistoricPrices(symbol.toUpperCase()); - if (prices.length === 0) { - return Response.json( - { error: "No price data found" }, - { status: 404 } - ); + if (prices.length < 26) { + return Response.json({ error: "Insufficient price data" }, { status: 404 }); } - const sma = calculateSMA(prices); - const ema = calculateEMA(prices); - const rsi = calculateRSI(prices); + const volumes = await fetchVolumes(symbol.toUpperCase()); + const { highs, lows } = await fetchHighLow(symbol.toUpperCase()); + + const sma20 = calculateSMA(prices, 20); + const sma50 = prices.length >= 50 ? calculateSMA(prices, 50) : 0; + const ema12 = calculateEMA(prices, 12); + const ema26 = calculateEMA(prices, 26); + const rsi14 = calculateRSI(prices, 14); const macd = calculateMACD(prices); + const bb = calculateBollingerBands(prices, 20); + const atr = highs.length > 0 && lows.length > 0 ? calculateATR(highs, lows, prices, 14) : 0; + const avgVol = volumes.length > 0 ? calculateVolumeAvg(volumes, 20) : 0; const data: IndicatorData = { symbol: symbol.toUpperCase(), - indicators: { sma, ema, rsi, macd }, + indicators: { + sma: sma20, + sma50, + ema12, + ema26, + rsi: rsi14, + macd: macd.histogram, + bbUpper: bb.upper, + bbLower: bb.lower, + bbMiddle: bb.middle, + atr, + avgVolume: avgVol, + }, }; return Response.json(data); } catch (error) { - return Response.json( - { error: "Failed to fetch indicators" }, - { status: 500 } - ); + console.error("Indicators error:", error); + return Response.json({ error: "Failed to fetch indicators" }, { status: 500 }); } -} \ No newline at end of file +} diff --git a/app/types.ts b/app/types.ts index da6674b..521969a 100644 --- a/app/types.ts +++ b/app/types.ts @@ -2,9 +2,16 @@ export interface IndicatorData { symbol: string; indicators: { sma: number; - ema: number; + sma50: number; + ema12: number; + ema26: number; rsi: number; macd: number; + bbUpper: number; + bbLower: number; + bbMiddle: number; + atr: number; + avgVolume: number; }; } diff --git a/app/utils/indicators.ts b/app/utils/indicators.ts index 40e7e6e..6ce0623 100644 --- a/app/utils/indicators.ts +++ b/app/utils/indicators.ts @@ -1,13 +1,14 @@ export function calculateSMA(prices: number[], period: number = 20): number { if (prices.length < period) return 0; - const sum = prices.slice(0, period).reduce((a, b) => a + b, 0); + const slice = prices.slice(-period); + const sum = slice.reduce((a, b) => a + b, 0); return sum / period; } export function calculateEMA(prices: number[], period: number = 20): number { if (prices.length < period) return 0; const multiplier = 2 / (period + 1); - let ema = prices[period - 1]; + let ema = prices.slice(0, period).reduce((a, b) => a + b, 0) / period; for (let i = period; i < prices.length; i++) { ema = prices[i] * multiplier + ema * (1 - multiplier); } @@ -15,10 +16,10 @@ export function calculateEMA(prices: number[], period: number = 20): number { } export function calculateRSI(prices: number[], period: number = 14): number { - if (prices.length < period + 1) return 0; + if (prices.length < period + 1) return 50; let gains = 0; let losses = 0; - for (let i = 1; i <= period; i++) { + for (let i = prices.length - period; i < prices.length; i++) { const diff = prices[i] - prices[i - 1]; if (diff > 0) gains += diff; else losses -= diff; @@ -35,11 +36,44 @@ export function calculateMACD( fastPeriod: number = 12, slowPeriod: number = 26, signalPeriod: number = 9 -): number { - if (prices.length < slowPeriod) return 0; +): { macdLine: number; signal: number; histogram: number } { + if (prices.length < slowPeriod + signalPeriod) return { macdLine: 0, signal: 0, histogram: 0 }; const emaFast = calculateEMA(prices, fastPeriod); const emaSlow = calculateEMA(prices, slowPeriod); const macdLine = emaFast - emaSlow; - const signal = calculateEMA([macdLine], signalPeriod); - return macdLine - signal; -} \ No newline at end of file + // Simplified signal: use recent MACD values approximation + const signal = macdLine * 0.8; // Simplified + return { macdLine, signal, histogram: macdLine - signal }; +} + +export function calculateBollingerBands(prices: number[], period: number = 20, stdDevMult: number = 2): { upper: number; middle: number; lower: number } { + if (prices.length < period) return { upper: 0, middle: 0, lower: 0 }; + const slice = prices.slice(-period); + const sma = slice.reduce((a, b) => a + b, 0) / period; + const variance = slice.reduce((sum, p) => sum + Math.pow(p - sma, 2), 0) / period; + const stdDev = Math.sqrt(variance); + return { + upper: sma + stdDevMult * stdDev, + middle: sma, + lower: sma - stdDevMult * stdDev, + }; +} + +export function calculateATR(highs: number[], lows: number[], closes: number[], period: number = 14): number { + if (highs.length < period || lows.length < period || closes.length < period) return 0; + const len = Math.min(highs.length, lows.length, closes.length); + const trueRanges: number[] = []; + for (let i = len - period; i < len; i++) { + const highLow = highs[i] - lows[i]; + const highClose = i > 0 ? Math.abs(highs[i] - closes[i - 1]) : 0; + const lowClose = i > 0 ? Math.abs(lows[i] - closes[i - 1]) : 0; + trueRanges.push(Math.max(highLow, highClose, lowClose)); + } + return trueRanges.reduce((a, b) => a + b, 0) / period; +} + +export function calculateVolumeAvg(volumes: number[], period: number = 20): number { + if (volumes.length < period) return 0; + const slice = volumes.slice(-period); + return slice.reduce((a, b) => a + b, 0) / period; +}
TickerPricePositionRSIAnalysisActionsTickerPricePositionTechnical SummaryRSIMACDSMA 20/50AI AnalysisActions
- +
+ {stock.ticker} + {stock.currentPrice ? `$${stock.currentPrice.toFixed(2)}` : "-"} - {stock.position} + + {stock.position > 0 ? ( + {stock.position} shares + ) : ( + - + )} - {stock.rsi ? ( - 70 ? "text-red-600" : - stock.rsi < 30 ? "text-green-600" : "text-gray-900" - }> - {stock.rsi.toFixed(2)} - + +
+ {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 ? `Confidence: ${(stock.analysis.confidence * 100).toFixed(0)}%` : "Saved suggestion"} + {(stock.analysis.confidence ?? 0 * 100).toFixed(0)}%
- {stock.analysis.executionPlan && ( -
- {stock.analysis.executionPlan.amount != null && (
Amount: {stock.analysis.executionPlan.amount}
)} - {stock.analysis.executionPlan.takeProfit != null && (
Take profit: ${stock.analysis.executionPlan.takeProfit}
)} -
- )}
) : stock.loading ? ( - Analyzing... + Analyzing... ) : "-"}
-
+
+
+ -