diff --git a/web/src/views/SimpleView.tsx b/web/src/views/SimpleView.tsx index f3a198f..6dc0da6 100644 --- a/web/src/views/SimpleView.tsx +++ b/web/src/views/SimpleView.tsx @@ -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>, 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 (
@@ -946,6 +1014,16 @@ export function SimpleView() { {formatSetupStatus(entry.status)} + {runtimeSnapshot ? ( + + {runtimeSnapshot.label} + + ) : null} + + {String(entry.sizing_mode || '').trim().toLowerCase() === 'amount' + ? `Budget $${Number(entry.amount_usd || 0).toFixed(2)}` + : `Qty ${entry.quantity || entry.filled_quantity || 0}`} + {entry.reference_price ? ( Ref {Number(entry.reference_price).toFixed(4)} @@ -956,14 +1034,19 @@ export function SimpleView() { Entry {Number(entry.entry_price).toFixed(4)} ) : null} - {entry.linked_trade_id ? ( + {(runtimeSnapshot?.tradeId || entry.linked_trade_id) ? ( - Trade linked + Trade {String(runtimeSnapshot?.tradeId || entry.linked_trade_id).slice(0, 18)}… ) : null} - {runtimeStatus ? ( + {runtimeSnapshot?.orderId ? ( - {runtimeStatus} + Order {runtimeSnapshot.orderId.slice(0, 12)}… + + ) : null} + {updatedAt ? ( + + Updated {updatedAt} ) : null}