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 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user