fix(simple): harden form runtime behavior
This commit is contained in:
parent
f57f0fc205
commit
943cfda6b5
@ -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<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 {
|
||||
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<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const marketPriceRequestSymbolRef = useRef<string>('');
|
||||
|
||||
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<typeof botState.positions[number]>(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<Record<string, any>>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user