diff --git a/web/src/views/SimpleView.tsx b/web/src/views/SimpleView.tsx index 87af9d8..f3a198f 100644 --- a/web/src/views/SimpleView.tsx +++ b/web/src/views/SimpleView.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import type { FormEvent } from 'react'; import { Pencil, RefreshCw, Trash2 } from 'lucide-react'; import { useAppContext } from '../context/AppContext'; @@ -323,6 +323,22 @@ function normalizeKnownSymbol(value: unknown): string | null { return normalized ? normalized : null; } +function normalizeRuntimeArray(value: unknown): T[] { + if (Array.isArray(value)) return value as T[]; + if (value && typeof value === 'object' && Symbol.iterator in (value as object)) { + try { + return Array.from(value as Iterable); + } catch { + return []; + } + } + return []; +} + +function isLikelySymbolCandidate(symbol: string): boolean { + return /^[A-Z0-9./-]{1,20}$/.test(symbol); +} + function describeSavedSetup(entry: ManualEntryPayload): string { const side = normalizeSetupSide(entry.simple_side); const symbol = String(entry.symbol || '').trim().toUpperCase(); @@ -367,17 +383,20 @@ export function SimpleView() { const [loadingPrice, setLoadingPrice] = useState(false); const [message, setMessage] = useState(null); const [error, setError] = useState(null); + const marketPriceRequestSymbolRef = useRef(''); const normalizedSymbol = draft.symbol.trim().toUpperCase(); - const livePrice = normalizedSymbol ? Number(botState.symbols?.[normalizedSymbol]?.price || 0) : 0; + const symbolState = botState?.symbols && typeof botState.symbols === 'object' ? botState.symbols : {}; + const livePrice = normalizedSymbol ? Number(symbolState?.[normalizedSymbol]?.price || 0) : 0; const simpleAutoProfile = useMemo( () => profiles.find((profile) => matchesSimpleAutoProfile(profile)) || null, [profiles], ); const simpleHoldings = useMemo(() => { + const positions = normalizeRuntimeArray(botState?.positions); const simpleProfileId = simpleAutoProfile?.id; - return botState.positions + return positions .filter((position) => !simpleProfileId || position.profileId === simpleProfileId) .map((position) => ({ symbol: String(position.symbol || '').trim().toUpperCase(), @@ -387,14 +406,15 @@ export function SimpleView() { tradeId: position.tradeId, })) .filter((position) => position.symbol && position.size > 0 && position.entryPrice > 0); - }, [botState.positions, simpleAutoProfile?.id]); + }, [botState?.positions, simpleAutoProfile?.id]); const runtimeOrders = useMemo(() => { - return Array.from(botState.orders.values()).filter((order) => { + const orders = normalizeRuntimeArray>(botState?.orders); + return orders.filter((order) => { if (!simpleAutoProfile?.id) return true; return String(order.profileId || '').trim() === simpleAutoProfile.id; }); - }, [botState.orders, simpleAutoProfile?.id]); + }, [botState?.orders, simpleAutoProfile?.id]); const matchingHolding = useMemo( () => simpleHoldings.find((holding) => holding.symbol === normalizedSymbol) || null, @@ -402,11 +422,11 @@ export function SimpleView() { ); const supportedSymbols = useMemo(() => { - const fromState = Object.keys(botState.symbols || {}).map(normalizeKnownSymbol).filter(Boolean) as string[]; + const fromState = Object.keys(symbolState || {}).map(normalizeKnownSymbol).filter(Boolean) as string[]; const fromSetups = savedSetups.map((entry) => normalizeKnownSymbol(entry.symbol)).filter(Boolean) as string[]; const fromHoldings = simpleHoldings.map((holding) => normalizeKnownSymbol(holding.symbol)).filter(Boolean) as string[]; return Array.from(new Set([...fromState, ...fromSetups, ...fromHoldings, ...COMMON_SIMPLE_SYMBOLS])).sort((left, right) => left.localeCompare(right)); - }, [botState.symbols, savedSetups, simpleHoldings]); + }, [symbolState, savedSetups, simpleHoldings]); const filteredSymbolSuggestions = useMemo(() => { if (!normalizedSymbol) { @@ -457,8 +477,22 @@ export function SimpleView() { return; } - void handleLoadMarketPrice(); - }, [normalizedSymbol, livePrice]); + if (!isLikelySymbolCandidate(normalizedSymbol)) { + return; + } + + if (!supportedSymbols.includes(normalizedSymbol)) { + return; + } + + const timer = window.setTimeout(() => { + void handleLoadMarketPrice(); + }, 250); + + return () => { + window.clearTimeout(timer); + }; + }, [normalizedSymbol, livePrice, draft.currentMarketPrice, loadingPrice, supportedSymbols]); const previewText = useMemo( () => buildPreviewText(draft, draft.side === 'sell' ? matchingHolding : null), @@ -480,27 +514,37 @@ export function SimpleView() { async function handleLoadMarketPrice() { if (!normalizedSymbol) return; + const requestSymbol = normalizedSymbol; + marketPriceRequestSymbolRef.current = requestSymbol; setLoadingPrice(true); setError(null); setMessage(null); try { if (livePrice > 0) { - updateDraft('currentMarketPrice', livePrice.toFixed(4)); + if (marketPriceRequestSymbolRef.current === requestSymbol) { + updateDraft('currentMarketPrice', livePrice.toFixed(4)); + } return; } - const bars = await fetchChartBars(normalizedSymbol, '1D'); + const bars = await fetchChartBars(requestSymbol, '1D'); const lastClose = Number(bars?.[bars.length - 1]?.close || 0); if (Number.isFinite(lastClose) && lastClose > 0) { - updateDraft('currentMarketPrice', lastClose.toFixed(4)); + if (marketPriceRequestSymbolRef.current === requestSymbol) { + updateDraft('currentMarketPrice', lastClose.toFixed(4)); + } } else { throw new Error('No recent market data is available for this symbol right now.'); } } catch (err: any) { - setError(err?.message ?? 'Failed to load market data'); + if (marketPriceRequestSymbolRef.current === requestSymbol) { + setError(err?.message ?? 'Failed to load market data'); + } } finally { - setLoadingPrice(false); + if (marketPriceRequestSymbolRef.current === requestSymbol) { + setLoadingPrice(false); + } } }