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);
setError(null); if (!options?.silent) {
setMessage(null); setError(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) => {
...prev, setMarketPriceSource(null);
symbol: e.target.value.toUpperCase(), setDraft((prev) => ({
currentMarketPrice: '', ...prev,
}))} symbol: e.target.value.toUpperCase(),
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={() => {
...prev, setMarketPriceSource(null);
symbol, setDraft((prev) => ({
currentMarketPrice: '', ...prev,
}))} symbol,
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)]"
/> />
<span className="block text-[11px] text-[var(--muted-foreground)]"> <div className="space-y-1">
Uses live market price when available. Outside market hours, it falls back to the latest close. <span className="block text-[11px] text-[var(--muted-foreground)]">
</span> 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> </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>
</> </>
) : ( ) : (