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