feat(simple): polish status and market fallback ux
This commit is contained in:
parent
fc4d4c85d1
commit
beb75c1d89
@ -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>
|
||||
</>
|
||||
) : (
|
||||
|
||||
Loading…
Reference in New Issue
Block a user