feat(simple): polish status and market fallback ux

This commit is contained in:
root 2026-05-06 08:13:41 +00:00
parent fc4d4c85d1
commit beb75c1d89

View File

@ -50,6 +50,8 @@ type SimpleRuntimeSnapshot = {
orderId?: string;
};
type MarketPriceSource = 'live' | 'latest_close' | 'reference_price' | null;
const SIMPLE_AUTO_PROFILE_NAME = 'Simple Auto Profile';
const SIMPLE_AUTO_PROFILE_KEY = SIMPLE_AUTO_PROFILE_NAME.toLowerCase();
const SIMPLE_SYMBOL_DATALIST_ID = 'simple-supported-symbols';
@ -313,6 +315,10 @@ function buildDraftFromEntry(entry: ManualEntryPayload): SimpleSetupDraft {
};
}
function inferMarketPriceSourceFromEntry(entry: ManualEntryPayload): MarketPriceSource {
return entry.reference_price ? 'reference_price' : null;
}
function formatSetupStatus(status?: string | null): string {
const normalized = String(status || '').trim().toLowerCase();
switch (normalized) {
@ -442,6 +448,7 @@ export function SimpleView() {
const [draft, setDraft] = useState<SimpleSetupDraft>(DEFAULT_DRAFT);
const [submitting, setSubmitting] = useState(false);
const [loadingPrice, setLoadingPrice] = useState(false);
const [marketPriceSource, setMarketPriceSource] = useState<MarketPriceSource>(null);
const [message, setMessage] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const marketPriceRequestSymbolRef = useRef<string>('');
@ -526,6 +533,7 @@ export function SimpleView() {
useEffect(() => {
if (!livePrice) return;
setMarketPriceSource('live');
setDraft((prev) => (
prev.currentMarketPrice === livePrice.toFixed(4)
? prev
@ -547,7 +555,7 @@ export function SimpleView() {
}
const timer = window.setTimeout(() => {
void handleLoadMarketPrice();
void handleLoadMarketPrice({ silent: true });
}, 250);
return () => {
@ -573,18 +581,25 @@ export function SimpleView() {
setSavedSetups(normalizeSimpleEntries(entryRows));
}
async function handleLoadMarketPrice() {
function setMarketPriceValue(value: string, source: MarketPriceSource) {
setMarketPriceSource(source);
updateDraft('currentMarketPrice', value);
}
async function handleLoadMarketPrice(options?: { silent?: boolean }) {
if (!normalizedSymbol) return;
const requestSymbol = normalizedSymbol;
marketPriceRequestSymbolRef.current = requestSymbol;
setLoadingPrice(true);
setError(null);
setMessage(null);
if (!options?.silent) {
setError(null);
setMessage(null);
}
try {
if (livePrice > 0) {
if (marketPriceRequestSymbolRef.current === requestSymbol) {
updateDraft('currentMarketPrice', livePrice.toFixed(4));
setMarketPriceValue(livePrice.toFixed(4), 'live');
}
return;
}
@ -593,19 +608,19 @@ export function SimpleView() {
const lastClose = Number(bars?.[bars.length - 1]?.close || 0);
if (Number.isFinite(lastClose) && lastClose > 0) {
if (marketPriceRequestSymbolRef.current === requestSymbol) {
updateDraft('currentMarketPrice', lastClose.toFixed(4));
setMarketPriceValue(lastClose.toFixed(4), 'latest_close');
}
} else {
const researchProfile = await fetchResearchProfile(requestSymbol).catch(() => null);
const fallbackReferencePrice = extractReferencePriceFromResearchProfile(researchProfile);
if (fallbackReferencePrice && marketPriceRequestSymbolRef.current === requestSymbol) {
updateDraft('currentMarketPrice', fallbackReferencePrice.toFixed(4));
setMarketPriceValue(fallbackReferencePrice.toFixed(4), 'reference_price');
return;
}
throw new Error('No live price or latest close is available for this symbol right now.');
}
} catch (err: any) {
if (marketPriceRequestSymbolRef.current === requestSymbol) {
if (!options?.silent && marketPriceRequestSymbolRef.current === requestSymbol) {
setError(err?.message ?? 'Failed to load market data');
}
} finally {
@ -641,6 +656,7 @@ export function SimpleView() {
await refreshSetupList();
setEditingSetupId(null);
setMarketPriceSource(draft.currentMarketPrice ? marketPriceSource : null);
setDraft({
...DEFAULT_DRAFT,
currentMarketPrice: draft.currentMarketPrice,
@ -654,6 +670,7 @@ export function SimpleView() {
function handleEdit(entry: ManualEntryPayload) {
setEditingSetupId(String(entry.stock_instance_id || ''));
setMarketPriceSource(inferMarketPriceSourceFromEntry(entry));
setDraft(buildDraftFromEntry(entry));
setMessage(null);
setError(null);
@ -665,6 +682,7 @@ export function SimpleView() {
await deleteManualEntry(entryId);
if (editingSetupId === entryId) {
setEditingSetupId(null);
setMarketPriceSource(null);
setDraft(DEFAULT_DRAFT);
}
await refreshSetupList();
@ -694,6 +712,7 @@ export function SimpleView() {
type="button"
onClick={() => {
setEditingSetupId(null);
setMarketPriceSource(draft.currentMarketPrice ? marketPriceSource : null);
setDraft({
...DEFAULT_DRAFT,
currentMarketPrice: draft.currentMarketPrice,
@ -716,11 +735,14 @@ export function SimpleView() {
<span className="text-[11px] font-black uppercase tracking-[0.24em] text-zinc-700">Symbol</span>
<Input
value={draft.symbol}
onChange={(e) => setDraft((prev) => ({
...prev,
symbol: e.target.value.toUpperCase(),
currentMarketPrice: '',
}))}
onChange={(e) => {
setMarketPriceSource(null);
setDraft((prev) => ({
...prev,
symbol: e.target.value.toUpperCase(),
currentMarketPrice: '',
}));
}}
list={SIMPLE_SYMBOL_DATALIST_ID}
placeholder="AAPL"
/>
@ -743,11 +765,14 @@ export function SimpleView() {
<button
key={symbol}
type="button"
onClick={() => setDraft((prev) => ({
...prev,
symbol,
currentMarketPrice: '',
}))}
onClick={() => {
setMarketPriceSource(null);
setDraft((prev) => ({
...prev,
symbol,
currentMarketPrice: '',
}));
}}
className={`rounded-full border px-3 py-1 text-[11px] font-semibold transition ${
symbol === normalizedSymbol
? 'border-[var(--primary)] bg-[var(--accent-soft)] text-[var(--primary)]'
@ -781,9 +806,23 @@ export function SimpleView() {
readOnly
className="bg-[var(--muted)] text-[var(--foreground)]"
/>
<span className="block text-[11px] text-[var(--muted-foreground)]">
Uses live market price when available. Outside market hours, it falls back to the latest close.
</span>
<div className="space-y-1">
<span className="block text-[11px] text-[var(--muted-foreground)]">
Uses live market price when available. Outside market hours, it falls back to the latest close.
</span>
<span className="block text-[11px] text-[var(--muted-foreground)]">
Price source:{' '}
<span className="font-semibold text-[var(--foreground)]">
{marketPriceSource === 'live'
? 'Live'
: marketPriceSource === 'latest_close'
? 'Latest close'
: marketPriceSource === 'reference_price'
? 'Reference price'
: 'Waiting for symbol data'}
</span>
</span>
</div>
</label>
<Button
type="button"
@ -831,6 +870,9 @@ export function SimpleView() {
<span className="block text-[11px] text-[var(--muted-foreground)]">
Quantity supports fractional shares/coins. Amount spends an approximate USD budget at trigger time.
</span>
<span className="block text-[11px] text-[var(--muted-foreground)]">
Use quantity when you know the units you want. Use amount to budget dollars and let the app derive fractional size at entry.
</span>
</label>
</>
) : (