fix(simple): harden form runtime behavior

This commit is contained in:
root 2026-05-06 07:13:37 +00:00
parent f57f0fc205
commit 943cfda6b5

View File

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