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