From f8a3b7840fa68c94f60a10eb256179c85dd1c1f4 Mon Sep 17 00:00:00 2001 From: Henry Winkel Date: Sat, 16 May 2026 22:14:21 +0200 Subject: [PATCH] fix: consolidate 3 fetchBars calls into 1 per stock and add 500ms delay between sequential loads to avoid rate limiting --- app/routes/analyze.tsx | 28 +++++++++++------ app/routes/api/indicators.ts | 58 +++++++++++------------------------- 2 files changed, 36 insertions(+), 50 deletions(-) diff --git a/app/routes/analyze.tsx b/app/routes/analyze.tsx index 61dee98..df5ddfe 100644 --- a/app/routes/analyze.tsx +++ b/app/routes/analyze.tsx @@ -210,20 +210,29 @@ export default function Analyze() { loadPortfolio(); }, []); - // Auto-load indicators for stocks that don't have them yet + // Auto-load indicators for stocks that don't have them yet (sequential with delay to avoid rate limits) useEffect(() => { const unloaded = stocks.filter((s) => !s.indicatorsLoading && s.indicators.rsi == null); if (unloaded.length === 0) return; - unloaded.forEach(async (stock) => { - 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)); + let cancelled = false; + const loadSequential = async () => { + for (const stock of unloaded) { + if (cancelled) break; + setStocks((s) => s.map((st) => st.id === stock.id ? { ...st, indicatorsLoading: true } : st)); + const indicators = await loadIndicators(stock.ticker); + if (cancelled) break; + 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)); + } + // 500ms delay between each stock to avoid rate limiting + await new Promise((r) => setTimeout(r, 500)); } - }); + }; + loadSequential(); + return () => { cancelled = true; }; }, [stocks]); useEffect(() => { @@ -411,6 +420,7 @@ export default function Analyze() { } else { setStocks((s) => s.map((st) => st.id === stock.id ? { ...st, indicatorsLoading: false } : st)); } + await new Promise((r) => setTimeout(r, 500)); } }; diff --git a/app/routes/api/indicators.ts b/app/routes/api/indicators.ts index 5556518..5d34710 100644 --- a/app/routes/api/indicators.ts +++ b/app/routes/api/indicators.ts @@ -2,46 +2,25 @@ import { type IndicatorData } from "../../types"; import { calculateSMA, calculateEMA, calculateRSI, calculateMACD, calculateBollingerBands, calculateATR, calculateVolumeAvg } from "../../utils/indicators"; import alpacaService from "../../lib/alpacaClient"; -async function fetchHistoricPrices(symbol: string): Promise { - 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 fetchBarsOnce(symbol: string): Promise<{ prices: number[]; volumes: number[]; highs: number[]; lows: number[] }> { + const bars = await alpacaService.fetchBars(symbol, "1Day", { limit: 60 }); + const prices: number[] = []; + const volumes: number[] = []; + const highs: number[] = []; + const lows: number[] = []; -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 []; + for (const b of bars) { + const c = b.ClosePrice ?? b.c ?? 0; + const v = b.Volume ?? b.v ?? 0; + const h = b.HighPrice ?? b.h ?? 0; + const l = b.LowPrice ?? b.l ?? 0; + if (typeof c === "number" && c > 0) prices.push(c); + if (typeof v === "number" && v > 0) volumes.push(v); + if (typeof h === "number" && h > 0) highs.push(h); + if (typeof l === "number" && l > 0) lows.push(l); } -} -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: [] }; - } + return { prices, volumes, highs, lows }; } export async function loader({ request }: { request: Request }) { @@ -53,14 +32,11 @@ export async function loader({ request }: { request: Request }) { } try { - const prices = await fetchHistoricPrices(symbol.toUpperCase()); + const { prices, volumes, highs, lows } = await fetchBarsOnce(symbol.toUpperCase()); if (prices.length < 26) { return Response.json({ error: "Insufficient price data" }, { status: 404 }); } - 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);