feat(simple): improve setup status and market fallback

This commit is contained in:
root 2026-05-06 08:10:16 +00:00
parent 92747b76a7
commit fc4d4c85d1

View File

@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef, useState } from 'react';
import type { FormEvent } from 'react';
import { Pencil, RefreshCw, Trash2 } from 'lucide-react';
import { useAppContext } from '../context/AppContext';
import { fetchChartBars } from '../lib/marketApi';
import { fetchChartBars, fetchResearchProfile } from '../lib/marketApi';
import {
createManualEntry,
deleteManualEntry,
@ -42,6 +42,14 @@ type SimpleHolding = {
tradeId?: string;
};
type SimpleRuntimeSnapshot = {
stage: 'armed' | 'entry_submitted' | 'filled' | 'exit_submitted' | 'closed' | 'unknown';
label: string;
tone: 'neutral' | 'info' | 'success';
tradeId?: string;
orderId?: string;
};
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';
@ -247,27 +255,44 @@ function buildPreviewText(draft: SimpleSetupDraft, holding: SimpleHolding | null
return `Exit ${symbol} when price reaches ${profitTargetPrice.toFixed(4)} (${profitText}).`;
}
function deriveRuntimeOrderStatus(
function deriveRuntimeSnapshot(
entry: ManualEntryPayload,
orders: Array<Record<string, any>>,
holdings: SimpleHolding[],
): string | null {
): SimpleRuntimeSnapshot | null {
const linkedTradeId = String(entry.linked_trade_id || '').trim();
if (linkedTradeId) {
const matchingOrder = orders.find((order) => String(order?.trade_id || order?.tradeId || '').trim() === linkedTradeId);
if (matchingOrder) {
const status = String(matchingOrder.status || '').trim();
return status ? `Order: ${status}` : null;
const status = String(matchingOrder.status || '').trim().toLowerCase();
const action = String(matchingOrder.action || '').trim().toUpperCase();
const orderId = String(matchingOrder.orderId || matchingOrder.order_id || matchingOrder.id || '').trim() || undefined;
if (action === 'EXIT') {
if (status === 'filled') {
return { stage: 'closed', label: 'Exit filled', tone: 'success', tradeId: linkedTradeId, orderId };
}
return { stage: 'exit_submitted', label: `Exit ${status || 'submitted'}`, tone: 'info', tradeId: linkedTradeId, orderId };
}
if (status === 'filled') {
return { stage: 'filled', label: 'Entry filled', tone: 'success', tradeId: linkedTradeId, orderId };
}
return { stage: 'entry_submitted', label: `Entry ${status || 'submitted'}`, tone: 'info', tradeId: linkedTradeId, orderId };
}
}
const symbol = String(entry.symbol || '').trim().toUpperCase();
const side = normalizeSetupSide(entry.simple_side);
if (side === 'buy' && holdings.some((holding) => holding.symbol === symbol)) {
return 'Portfolio: open holding';
const matchingHolding = holdings.find((holding) => holding.symbol === symbol && (!linkedTradeId || holding.tradeId === linkedTradeId || !holding.tradeId));
if (side === 'buy' && matchingHolding) {
return {
stage: 'filled',
label: 'Portfolio holding open',
tone: 'success',
tradeId: matchingHolding.tradeId || linkedTradeId || undefined,
};
}
if (String(entry.status || '').trim().toLowerCase() === 'sellcompleted') {
return 'Portfolio: closed';
return { stage: 'closed', label: 'Position closed', tone: 'success', tradeId: linkedTradeId || undefined };
}
return null;
}
@ -308,6 +333,25 @@ function formatSetupStatus(status?: string | null): string {
}
}
function statusToneClasses(tone: SimpleRuntimeSnapshot['tone']): string {
switch (tone) {
case 'success':
return 'border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300';
case 'info':
return 'border-sky-500/30 bg-sky-500/10 text-sky-700 dark:text-sky-300';
default:
return 'border-[var(--border)] text-[var(--muted-foreground)]';
}
}
function formatSetupUpdatedAt(entry: ManualEntryPayload): string | null {
const raw = String(entry.sell_time || entry.buy_time || '').trim();
if (!raw) return null;
const parsed = new Date(raw);
if (Number.isNaN(parsed.getTime())) return null;
return parsed.toLocaleString();
}
function normalizeSimpleEntries(entries: ManualEntryPayload[]): ManualEntryPayload[] {
return entries
.filter((entry) => String(entry.workflow_type || '').trim().toLowerCase() === 'simple')
@ -339,6 +383,23 @@ function isLikelySymbolCandidate(symbol: string): boolean {
return /^[A-Z0-9./-]{1,20}$/.test(symbol);
}
function extractReferencePriceFromResearchProfile(profile: any): number | null {
const candidates = [
profile?.price,
profile?.lastPrice,
profile?.currentPrice,
profile?.previousClose,
profile?.prevClose,
];
for (const candidate of candidates) {
const value = Number(candidate);
if (Number.isFinite(value) && value > 0) return value;
}
return null;
}
function describeSavedSetup(entry: ManualEntryPayload): string {
const side = normalizeSetupSide(entry.simple_side);
const symbol = String(entry.symbol || '').trim().toUpperCase();
@ -535,7 +596,13 @@ export function SimpleView() {
updateDraft('currentMarketPrice', lastClose.toFixed(4));
}
} else {
throw new Error('No recent market data is available for this symbol right now.');
const researchProfile = await fetchResearchProfile(requestSymbol).catch(() => null);
const fallbackReferencePrice = extractReferencePriceFromResearchProfile(researchProfile);
if (fallbackReferencePrice && marketPriceRequestSymbolRef.current === requestSymbol) {
updateDraft('currentMarketPrice', fallbackReferencePrice.toFixed(4));
return;
}
throw new Error('No live price or latest close is available for this symbol right now.');
}
} catch (err: any) {
if (marketPriceRequestSymbolRef.current === requestSymbol) {
@ -899,7 +966,8 @@ export function SimpleView() {
const entryId = String(entry.stock_instance_id || '');
const side = normalizeSetupSide(entry.simple_side);
const isEditing = editingSetupId === entryId;
const runtimeStatus = deriveRuntimeOrderStatus(entry, runtimeOrders, simpleHoldings);
const runtimeSnapshot = deriveRuntimeSnapshot(entry, runtimeOrders, simpleHoldings);
const updatedAt = formatSetupUpdatedAt(entry);
return (
<div key={entryId} className="rounded-[1.5rem] border border-[var(--border)] bg-[var(--card-elevated)] p-5">
<div className="mb-3 flex items-start justify-between gap-4">
@ -946,6 +1014,16 @@ export function SimpleView() {
<span className="rounded-full border border-[var(--border)] px-3 py-1">
{formatSetupStatus(entry.status)}
</span>
{runtimeSnapshot ? (
<span className={`rounded-full border px-3 py-1 ${statusToneClasses(runtimeSnapshot.tone)}`}>
{runtimeSnapshot.label}
</span>
) : null}
<span className="rounded-full border border-[var(--border)] px-3 py-1">
{String(entry.sizing_mode || '').trim().toLowerCase() === 'amount'
? `Budget $${Number(entry.amount_usd || 0).toFixed(2)}`
: `Qty ${entry.quantity || entry.filled_quantity || 0}`}
</span>
{entry.reference_price ? (
<span className="rounded-full border border-[var(--border)] px-3 py-1">
Ref {Number(entry.reference_price).toFixed(4)}
@ -956,14 +1034,19 @@ export function SimpleView() {
Entry {Number(entry.entry_price).toFixed(4)}
</span>
) : null}
{entry.linked_trade_id ? (
{(runtimeSnapshot?.tradeId || entry.linked_trade_id) ? (
<span className="rounded-full border border-[var(--border)] px-3 py-1">
Trade linked
Trade {String(runtimeSnapshot?.tradeId || entry.linked_trade_id).slice(0, 18)}
</span>
) : null}
{runtimeStatus ? (
{runtimeSnapshot?.orderId ? (
<span className="rounded-full border border-[var(--border)] px-3 py-1">
{runtimeStatus}
Order {runtimeSnapshot.orderId.slice(0, 12)}
</span>
) : null}
{updatedAt ? (
<span className="rounded-full border border-[var(--border)] px-3 py-1 normal-case tracking-normal">
Updated {updatedAt}
</span>
) : null}
</div>