import { useEffect, useMemo, useRef, useState } from 'react'; import type { FormEvent } from 'react'; import { Pencil, RefreshCw, Trash2 } from 'lucide-react'; import { useSearchParams } from 'react-router-dom'; import { useAppContext } from '../context/AppContext'; import { fetchChartBars, fetchResearchProfile } from '../lib/marketApi'; import { createManualEntry, deleteManualEntry, fetchManualEntries, updateManualEntry, type ManualEntryPayload, } from '../lib/manualEntriesApi'; import { fetchTradeProfiles, type TradeProfilePayload } from '../lib/profileApi'; import { Button } from '../components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../components/ui/card'; import { Input } from '../components/ui/input'; import { PageHeader } from '../components/ui/page-header'; import { Select } from '../components/ui/select'; type SimpleSide = 'buy' | 'sell'; type TriggerMode = 'dollar' | 'percent'; type SimpleSetupDraft = { symbol: string; side: SimpleSide; sizingMode: 'quantity' | 'amount'; quantity: string; amountUsd: string; currentMarketPrice: string; dropMode: TriggerMode; dropValue: string; profitMode: TriggerMode; profitValue: string; notes: string; }; type SimpleHolding = { symbol: string; size: number; entryPrice: number; profileId?: string; tradeId?: string; }; type SimpleRuntimeSnapshot = { stage: 'armed' | 'entry_submitted' | 'filled' | 'exit_submitted' | 'closed' | 'unknown'; label: string; tone: 'neutral' | 'info' | 'success'; tradeId?: string; orderId?: string; }; type SimpleOperationalEvent = NonNullable['botState']['operationalEvents']>[number]; 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'; const COMMON_SIMPLE_SYMBOLS = [ 'AAPL', 'MSFT', 'NVDA', 'TSLA', 'SPY', 'QQQ', 'AMZN', 'META', 'BTC/USD', 'ETH/USD', 'SOL/USD', ]; const DEFAULT_DRAFT: SimpleSetupDraft = { symbol: '', side: 'buy', sizingMode: 'quantity', quantity: '', amountUsd: '', currentMarketPrice: '', dropMode: 'percent', dropValue: '', profitMode: 'percent', profitValue: '', notes: '', }; function parsePositiveNumber(value: string): number | null { const trimmed = value.trim(); if (!trimmed) return null; const parsed = Number(trimmed); return Number.isFinite(parsed) && parsed > 0 ? parsed : null; } function parseNonNegativeNumber(value: string): number | null { const trimmed = value.trim(); if (!trimmed) return null; const parsed = Number(trimmed); return Number.isFinite(parsed) && parsed >= 0 ? parsed : null; } function roundPrice(value: number): number { return Number(value.toFixed(4)); } function matchesSimpleAutoProfile(profile: Pick | null | undefined) { return String(profile?.name || '').trim().toLowerCase() === SIMPLE_AUTO_PROFILE_KEY; } function normalizeSetupSide(value: unknown): SimpleSide { return String(value || '').trim().toLowerCase() === 'sell' ? 'sell' : 'buy'; } function normalizeMode(value: unknown, fallback: TriggerMode = 'percent'): TriggerMode { return String(value || '').trim().toLowerCase() === 'dollar' ? 'dollar' : fallback; } function normalizeHoldingMode(value: unknown): 'short_term' | 'long_term' { return String(value || '').trim().toLowerCase() === 'long_term' ? 'long_term' : 'short_term'; } function normalizeAutomationState(value: unknown, entry: ManualEntryPayload): string { const explicit = String(value || '').trim().toLowerCase(); if (explicit) return explicit; const status = String(entry.status || '').trim().toLowerCase(); const holdingMode = normalizeHoldingMode(entry.holding_mode); if (holdingMode === 'long_term') { return status === 'sellcompleted' ? 'closed' : 'paused_long_term'; } switch (status) { case 'simple_armed_buy': case 'simple_armed_sell': return 'armed'; case 'simple_entry_submitted': return 'entry_submitted'; case 'simple_bought': return 'holding_managed'; case 'simple_exit_submitted': return 'exit_submitted'; case 'sellcompleted': return 'closed'; default: return 'unknown'; } } function computeBuyTriggerPrice(draft: SimpleSetupDraft): number | null { const currentMarketPrice = parsePositiveNumber(draft.currentMarketPrice); const dropValue = parseNonNegativeNumber(draft.dropValue); if (!currentMarketPrice || dropValue === null) return null; if (draft.dropMode === 'dollar') { const trigger = currentMarketPrice - dropValue; return trigger > 0 ? roundPrice(trigger) : null; } const trigger = currentMarketPrice * (1 - dropValue / 100); return trigger > 0 ? roundPrice(trigger) : null; } function computeProfitTargetPrice(entryPrice: number | null, mode: TriggerMode, value: string): number | null { const numericEntryPrice = entryPrice && entryPrice > 0 ? entryPrice : null; const targetValue = parsePositiveNumber(value); if (!numericEntryPrice || !targetValue) return null; if (mode === 'dollar') { return roundPrice(numericEntryPrice + targetValue); } return roundPrice(numericEntryPrice * (1 + targetValue / 100)); } export function buildSimpleSetupPayload(input: { draft: SimpleSetupDraft; existingId?: string; holding?: SimpleHolding | null; existingEntry?: ManualEntryPayload | null; }): ManualEntryPayload { const symbol = input.draft.symbol.trim().toUpperCase(); if (!symbol) { throw new Error('Symbol is required'); } const currentMarketPrice = parsePositiveNumber(input.draft.currentMarketPrice); if (!currentMarketPrice) { throw new Error('No recent market price is available right now. The app uses live price when available, otherwise the latest close.'); } const quantity = parsePositiveNumber(input.draft.quantity); const amountUsd = parsePositiveNumber(input.draft.amountUsd); const profitValue = parsePositiveNumber(input.draft.profitValue); if (!profitValue) { throw new Error('Profit target is required'); } const side = input.draft.side; const holding = input.holding || null; const existingEntry = input.existingEntry || null; const holdingMode = normalizeHoldingMode(existingEntry?.holding_mode); const automationState = normalizeAutomationState(existingEntry?.automation_state, existingEntry || { status: side === 'buy' ? 'simple_armed_buy' : 'simple_armed_sell' } as ManualEntryPayload); if (side === 'sell' && !holding) { throw new Error('Sell setups require an existing holding for this symbol.'); } if (side === 'buy' && parseNonNegativeNumber(input.draft.dropValue) === null) { throw new Error('Drop trigger is required for buy setups'); } if (side === 'buy') { if (input.draft.sizingMode === 'amount') { if (!amountUsd) { throw new Error('USD amount is required'); } } else if (!quantity) { throw new Error('Quantity is required'); } } return { stock_instance_id: input.existingId, symbol, active: true, status: side === 'buy' ? 'simple_armed_buy' : 'simple_armed_sell', is_crypto: false, is_real_trade: false, label: 'Simple', quantity: side === 'sell' ? holding!.size : (input.draft.sizingMode === 'quantity' ? quantity : null), amount_usd: side === 'buy' && input.draft.sizingMode === 'amount' ? amountUsd : null, sizing_mode: side === 'buy' ? input.draft.sizingMode : 'quantity', filled_quantity: side === 'sell' ? holding!.size : null, notes: input.draft.notes.trim() || null, entry_price: side === 'sell' ? holding!.entryPrice : null, reference_price: currentMarketPrice, gain_threshold_for_sell: profitValue, drop_threshold_for_buy: side === 'buy' ? parseNonNegativeNumber(input.draft.dropValue) : null, workflow_type: 'simple', simple_side: side, drop_trigger_mode: side === 'buy' ? input.draft.dropMode : null, profit_target_mode: input.draft.profitMode, linked_trade_id: side === 'sell' ? holding!.tradeId || null : null, profile_id: side === 'sell' ? holding!.profileId || null : null, holding_mode: existingEntry ? holdingMode : 'short_term', automation_state: existingEntry ? automationState : 'armed', buy_price: null, sell_price: null, buy_time: null, sell_time: null, }; } function buildPreviewText(draft: SimpleSetupDraft, holding: SimpleHolding | null): string | null { const symbol = draft.symbol.trim().toUpperCase(); if (!symbol) return null; if (draft.side === 'buy') { const triggerPrice = computeBuyTriggerPrice(draft); const profitTargetPrice = computeProfitTargetPrice(triggerPrice, draft.profitMode, draft.profitValue); if (!triggerPrice) return `Buy ${symbol} after the configured drop trigger is hit.`; const dropMagnitude = parseNonNegativeNumber(draft.dropValue); const isImmediate = dropMagnitude === 0; const dropText = isImmediate ? 'at the current market reference' : draft.dropMode === 'dollar' ? `$${Number(draft.dropValue || 0).toFixed(2)} below current price` : `${draft.dropValue || '0'}% below current price`; const profitText = draft.profitMode === 'dollar' ? `$${Number(draft.profitValue || 0).toFixed(2)} above purchase` : `${draft.profitValue || '0'}% above purchase`; const sizeText = draft.sizingMode === 'amount' ? `Spend $${Number(draft.amountUsd || 0).toFixed(2)}` : `Buy ${draft.quantity || '0'} units`; return [ `${sizeText} of ${symbol} when price reaches ${triggerPrice.toFixed(4)} (${dropText}).`, profitTargetPrice ? `Exit target stays armed at ${profitTargetPrice.toFixed(4)} (${profitText}).` : `Exit target uses ${profitText}.`, ].join(' '); } if (!holding) { return `Sell ${symbol} only works when an eligible existing holding is available.`; } const profitTargetPrice = computeProfitTargetPrice(holding.entryPrice, draft.profitMode, draft.profitValue); const profitText = draft.profitMode === 'dollar' ? `$${Number(draft.profitValue || 0).toFixed(2)} above purchase` : `${draft.profitValue || '0'}% above purchase`; if (!profitTargetPrice) { return `Exit ${symbol} when the configured profit target is hit (${profitText}).`; } return `Exit ${symbol} when price reaches ${profitTargetPrice.toFixed(4)} (${profitText}).`; } function deriveRuntimeSnapshot( entry: ManualEntryPayload, orders: Array>, holdings: SimpleHolding[], ): 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().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); 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 { stage: 'closed', label: 'Position closed', tone: 'success', tradeId: linkedTradeId || undefined }; } return null; } function buildDraftFromEntry(entry: ManualEntryPayload): SimpleSetupDraft { return { symbol: String(entry.symbol || '').trim().toUpperCase(), side: normalizeSetupSide(entry.simple_side), sizingMode: String(entry.sizing_mode || '').trim().toLowerCase() === 'amount' ? 'amount' : 'quantity', quantity: entry.quantity ? String(entry.quantity) : '', amountUsd: entry.amount_usd ? String(entry.amount_usd) : '', currentMarketPrice: entry.reference_price ? Number(entry.reference_price).toFixed(4) : '', dropMode: normalizeMode(entry.drop_trigger_mode, 'percent'), dropValue: entry.drop_threshold_for_buy !== null && entry.drop_threshold_for_buy !== undefined ? String(entry.drop_threshold_for_buy) : '', profitMode: normalizeMode(entry.profit_target_mode, 'percent'), profitValue: entry.gain_threshold_for_sell !== null && entry.gain_threshold_for_sell !== undefined ? String(entry.gain_threshold_for_sell) : '', notes: String(entry.notes || ''), }; } 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) { case 'simple_armed_buy': return 'Buy armed'; case 'simple_entry_submitted': return 'Buy submitted'; case 'simple_bought': return 'Holding'; case 'simple_armed_sell': return 'Sell armed'; case 'simple_exit_submitted': return 'Exit submitted'; case 'sellcompleted': return 'Closed'; default: return normalized || 'Unknown'; } } function formatHoldingMode(mode?: string | null): string { return normalizeHoldingMode(mode) === 'long_term' ? 'Long-term' : 'Short-term'; } function formatAutomationState(entry: ManualEntryPayload): string { const state = normalizeAutomationState(entry.automation_state, entry); switch (state) { case 'armed': return 'Automation armed'; case 'entry_submitted': return 'Entry syncing'; case 'holding_managed': return 'Exit managed'; case 'paused_long_term': return 'Automation paused'; case 'exit_submitted': return 'Exit syncing'; case 'closed': return 'Closed'; default: return 'State syncing'; } } 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)]'; } } const SIMPLE_TIMELINE_STEPS: Array = [ 'armed', 'entry_submitted', 'filled', 'exit_submitted', 'closed', ]; function isTimelineStepComplete( current: SimpleRuntimeSnapshot['stage'] | undefined, step: SimpleRuntimeSnapshot['stage'], ): boolean { if (!current) return false; const currentIndex = SIMPLE_TIMELINE_STEPS.indexOf(current); const stepIndex = SIMPLE_TIMELINE_STEPS.indexOf(step); return currentIndex >= 0 && stepIndex >= 0 && stepIndex <= currentIndex; } function formatTimelineStepLabel(step: SimpleRuntimeSnapshot['stage']) { switch (step) { case 'armed': return 'Armed'; case 'entry_submitted': return 'Entry sent'; case 'filled': return 'Filled'; case 'exit_submitted': return 'Exit sent'; case 'closed': return 'Closed'; default: return step; } } function describeNextAction( entry: ManualEntryPayload, runtimeSnapshot: SimpleRuntimeSnapshot | null, ): string { const side = normalizeSetupSide(entry.simple_side); const symbol = String(entry.symbol || '').trim().toUpperCase() || 'This symbol'; if (!runtimeSnapshot) { if (normalizeHoldingMode(entry.holding_mode) === 'long_term') { return `${symbol} is being kept as a long-term hold. Automated exit monitoring is paused until you resume it.`; } return side === 'buy' ? `${symbol} is saved and waiting for the configured buy trigger.` : `${symbol} is saved and waiting for an eligible holding to manage.`; } if (normalizeHoldingMode(entry.holding_mode) === 'long_term' && runtimeSnapshot.stage === 'filled') { return `${symbol} is held as a long-term position. No automated profit exit is currently armed.`; } switch (runtimeSnapshot.stage) { case 'armed': return side === 'buy' ? `${symbol} is waiting for the configured drop trigger before sending an entry order.` : `${symbol} is waiting for the configured profit exit trigger.`; case 'entry_submitted': return `${symbol} entry order has been submitted and is waiting for exchange fill confirmation.`; case 'filled': return `${symbol} entry is filled. The setup is now monitoring for the configured profit exit.`; case 'exit_submitted': return `${symbol} exit order has been submitted and is waiting for exchange fill confirmation.`; case 'closed': return `${symbol} setup is complete. The linked position has been closed.`; default: return `${symbol} runtime state is syncing.`; } } 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 formatEventTimestamp(timestamp?: number): string | null { if (!timestamp || !Number.isFinite(timestamp)) return null; const parsed = new Date(timestamp); if (Number.isNaN(parsed.getTime())) return null; return parsed.toLocaleString(); } function deriveSimpleEventHistory( entry: ManualEntryPayload, operationalEvents: SimpleOperationalEvent[], ): SimpleOperationalEvent[] { const setupId = String(entry.stock_instance_id || '').trim(); const tradeId = String(entry.linked_trade_id || '').trim(); const symbol = String(entry.symbol || '').trim().toUpperCase(); return operationalEvents .filter((event): event is SimpleOperationalEvent => Boolean(event) && event.type === 'SIMPLE_SETUP_UPDATE') .filter((event) => { const eventSetupId = String(event.setupId || '').trim(); const eventTradeId = String(event.tradeId || '').trim(); const eventSymbol = String(event.symbol || '').trim().toUpperCase(); if (setupId && eventSetupId === setupId) return true; if (tradeId && eventTradeId === tradeId) return true; return !setupId && !tradeId && !!symbol && eventSymbol === symbol; }) .sort((left, right) => right.timestamp - left.timestamp) .slice(0, 5); } function eventSeverityClasses(severity?: string) { const normalized = String(severity || '').trim().toUpperCase(); if (normalized === 'ERROR') { return 'border-red-500/30 bg-red-500/10 text-red-700 dark:text-red-300'; } if (normalized === 'WARN') { return 'border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300'; } return 'border-sky-500/30 bg-sky-500/10 text-sky-700 dark:text-sky-300'; } function normalizeSimpleEntries(entries: ManualEntryPayload[]): ManualEntryPayload[] { return entries .filter((entry) => String(entry.workflow_type || '').trim().toLowerCase() === 'simple') .sort((left, right) => { const leftTimestamp = new Date(String(left.sell_time || left.buy_time || '')).getTime() || 0; const rightTimestamp = new Date(String(right.sell_time || right.buy_time || '')).getTime() || 0; return rightTimestamp - leftTimestamp; }); } function normalizeKnownSymbol(value: unknown): string | null { const normalized = String(value || '').trim().toUpperCase(); return normalized ? normalized : null; } function normalizeRuntimeArray(value: unknown): T[] { if (Array.isArray(value)) return value as T[]; if (value && typeof value === 'object' && Symbol.iterator in (value as object)) { try { return Array.from(value as Iterable); } catch { return []; } } return []; } 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(); const dropValue = Number(entry.drop_threshold_for_buy || 0); const profitValue = Number(entry.gain_threshold_for_sell || 0); const dropMode = normalizeMode(entry.drop_trigger_mode, 'percent'); const profitMode = normalizeMode(entry.profit_target_mode, 'percent'); const referencePrice = Number(entry.reference_price || 0); const entryPrice = Number(entry.entry_price || 0); const sizingMode = String(entry.sizing_mode || 'quantity').trim().toLowerCase(); const sizeText = sizingMode === 'amount' ? `$${Number(entry.amount_usd || 0).toFixed(2)} budget` : `${Number(entry.quantity || 0).toFixed(6).replace(/\.?0+$/, '')} units`; if (side === 'buy') { const triggerPrice = computeBuyTriggerPrice(buildDraftFromEntry(entry)); const dropText = dropValue === 0 ? 'at current reference' : dropMode === 'dollar' ? `$${dropValue.toFixed(2)} below` : `${dropValue}% below`; const profitText = profitMode === 'dollar' ? `$${profitValue.toFixed(2)} above purchase` : `${profitValue}% above purchase`; return `Buy ${symbol} using ${sizeText} ${dropText} ${referencePrice > 0 ? `(${referencePrice.toFixed(4)} ref` : ''}${triggerPrice ? ` → ${triggerPrice.toFixed(4)}` : ''}). Exit at ${profitText}.`; } const profitTargetPrice = computeProfitTargetPrice(entryPrice || null, profitMode, String(profitValue || '')); const profitText = profitMode === 'dollar' ? `$${profitValue.toFixed(2)} above purchase` : `${profitValue}% above purchase`; return `Exit full ${symbol} holding at ${profitText}${profitTargetPrice ? ` (${profitTargetPrice.toFixed(4)})` : ''}.`; } export function SimpleView() { const { botState } = useAppContext(); const [searchParams, setSearchParams] = useSearchParams(); const [profiles, setProfiles] = useState([]); const [savedSetups, setSavedSetups] = useState([]); const [editingSetupId, setEditingSetupId] = useState(null); const [draft, setDraft] = useState(DEFAULT_DRAFT); const [submitting, setSubmitting] = useState(false); const [loadingPrice, setLoadingPrice] = useState(false); const [marketPriceSource, setMarketPriceSource] = useState(null); const [copiedKey, setCopiedKey] = useState(null); const [message, setMessage] = useState(null); const [error, setError] = useState(null); const [selectedHoldingTradeId, setSelectedHoldingTradeId] = useState(null); const [focusedSetupId, setFocusedSetupId] = useState(null); const marketPriceRequestSymbolRef = useRef(''); const consumedPrefillRef = useRef(false); const consumedSetupFocusRef = useRef(false); const setupCardRefs = useRef>({}); const normalizedSymbol = draft.symbol.trim().toUpperCase(); const symbolState = botState?.symbols && typeof botState.symbols === 'object' ? botState.symbols : {}; const livePrice = normalizedSymbol ? Number(symbolState?.[normalizedSymbol]?.price || 0) : 0; const simpleAutoProfile = useMemo( () => profiles.find((profile) => matchesSimpleAutoProfile(profile)) || null, [profiles], ); const simpleHoldings = useMemo(() => { const positions = normalizeRuntimeArray(botState?.positions); const simpleProfileId = simpleAutoProfile?.id; return positions .filter((position) => !simpleProfileId || position.profileId === simpleProfileId) .map((position) => ({ symbol: String(position.symbol || '').trim().toUpperCase(), size: Number(position.size || 0), entryPrice: Number(position.entryPrice || 0), profileId: position.profileId, tradeId: position.tradeId, })) .filter((position) => position.symbol && position.size > 0 && position.entryPrice > 0); }, [botState?.positions, simpleAutoProfile?.id]); const availableSellHoldings = useMemo(() => { const positions = normalizeRuntimeArray(botState?.positions); return positions .map((position) => ({ symbol: String(position.symbol || '').trim().toUpperCase(), size: Number(position.size || 0), entryPrice: Number(position.entryPrice || 0), profileId: position.profileId, tradeId: position.tradeId, })) .filter((position) => position.symbol && position.size > 0 && position.entryPrice > 0 && position.profileId && position.tradeId) .sort((left, right) => { const bySymbol = left.symbol.localeCompare(right.symbol); if (bySymbol !== 0) return bySymbol; return String(left.tradeId || '').localeCompare(String(right.tradeId || '')); }); }, [botState?.positions]); const runtimeOrders = useMemo(() => { const orders = normalizeRuntimeArray>(botState?.orders); return orders.filter((order) => { if (!simpleAutoProfile?.id) return true; return String(order.profileId || '').trim() === simpleAutoProfile.id; }); }, [botState?.orders, simpleAutoProfile?.id]); const runtimeEvents = useMemo( () => normalizeRuntimeArray(botState?.operationalEvents), [botState?.operationalEvents], ); const selectedSellHolding = useMemo(() => { if (selectedHoldingTradeId) { return availableSellHoldings.find((holding) => holding.tradeId === selectedHoldingTradeId) || null; } return availableSellHoldings.find((holding) => holding.symbol === normalizedSymbol) || null; }, [availableSellHoldings, normalizedSymbol, selectedHoldingTradeId]); const supportedSymbols = useMemo(() => { const fromState = Object.keys(symbolState || {}).map(normalizeKnownSymbol).filter(Boolean) as string[]; const fromSetups = savedSetups.map((entry) => normalizeKnownSymbol(entry.symbol)).filter(Boolean) as string[]; const fromHoldings = availableSellHoldings.map((holding) => normalizeKnownSymbol(holding.symbol)).filter(Boolean) as string[]; return Array.from(new Set([...fromState, ...fromSetups, ...fromHoldings, ...COMMON_SIMPLE_SYMBOLS])).sort((left, right) => left.localeCompare(right)); }, [symbolState, savedSetups, availableSellHoldings]); const filteredSymbolSuggestions = useMemo(() => { if (!normalizedSymbol) { return supportedSymbols.slice(0, 8); } const startsWith = supportedSymbols.filter((symbol) => symbol.startsWith(normalizedSymbol)); const includes = supportedSymbols.filter((symbol) => !symbol.startsWith(normalizedSymbol) && symbol.includes(normalizedSymbol)); return [...startsWith, ...includes].slice(0, 8); }, [normalizedSymbol, supportedSymbols]); const isSuggestedSymbol = normalizedSymbol ? supportedSymbols.includes(normalizedSymbol) : false; useEffect(() => { let cancelled = false; async function loadData() { try { const [profileRows, entryRows] = await Promise.all([ fetchTradeProfiles(), fetchManualEntries(), ]); if (cancelled) return; setProfiles(profileRows); setSavedSetups(normalizeSimpleEntries(entryRows)); } catch (err: any) { if (cancelled) return; setError(err?.message ?? 'Failed to load Simple setups'); } } void loadData(); return () => { cancelled = true; }; }, []); useEffect(() => { if (!livePrice) return; setMarketPriceSource('live'); setDraft((prev) => ( prev.currentMarketPrice === livePrice.toFixed(4) ? prev : { ...prev, currentMarketPrice: livePrice.toFixed(4) } )); }, [livePrice]); useEffect(() => { if (!normalizedSymbol || livePrice > 0 || draft.currentMarketPrice.trim() || loadingPrice) { return; } if (!isLikelySymbolCandidate(normalizedSymbol)) { return; } if (!supportedSymbols.includes(normalizedSymbol)) { return; } const timer = window.setTimeout(() => { void handleLoadMarketPrice({ silent: true }); }, 250); return () => { window.clearTimeout(timer); }; }, [normalizedSymbol, livePrice, draft.currentMarketPrice, loadingPrice, supportedSymbols]); const previewText = useMemo( () => buildPreviewText(draft, draft.side === 'sell' ? selectedSellHolding : null), [draft, selectedSellHolding], ); function updateDraft(key: K, value: SimpleSetupDraft[K]) { if (key === 'side' && value === 'buy') { setSelectedHoldingTradeId(null); } if (key === 'symbol' && draft.side === 'sell') { setSelectedHoldingTradeId(null); } setDraft((prev) => ({ ...prev, [key]: value })); } function applyHoldingToDraft(holding: SimpleHolding) { setMarketPriceSource(null); setSelectedHoldingTradeId(holding.tradeId || null); setDraft((prev) => ({ ...prev, side: 'sell', symbol: holding.symbol, quantity: String(holding.size), currentMarketPrice: '', })); } useEffect(() => { if (draft.side !== 'sell') return; if (selectedSellHolding) return; if (availableSellHoldings.length === 0) return; applyHoldingToDraft(availableSellHoldings[0]); }, [draft.side, selectedSellHolding, availableSellHoldings]); useEffect(() => { if (consumedPrefillRef.current) return; const requestedMode = String(searchParams.get('mode') || '').trim().toLowerCase(); const requestedSymbol = String(searchParams.get('symbol') || '').trim().toUpperCase(); const requestedTradeId = String(searchParams.get('tradeId') || '').trim(); if (requestedMode !== 'sell') return; if (availableSellHoldings.length === 0) return; const selected = (requestedTradeId ? availableSellHoldings.find((holding) => holding.tradeId === requestedTradeId) : null) || (requestedSymbol ? availableSellHoldings.find((holding) => holding.symbol === requestedSymbol) : null) || availableSellHoldings[0]; if (selected) { applyHoldingToDraft(selected); setMessage(`Loaded ${selected.symbol} from Portfolio. Configure the profit target and save the plan.`); setError(null); } consumedPrefillRef.current = true; setSearchParams({}, { replace: true }); }, [availableSellHoldings, searchParams, setSearchParams]); useEffect(() => { if (consumedSetupFocusRef.current) return; const requestedSetupId = String(searchParams.get('setupId') || '').trim(); if (!requestedSetupId) return; if (savedSetups.length === 0) return; const targetEntry = savedSetups.find((entry) => String(entry.stock_instance_id || '') === requestedSetupId) || null; consumedSetupFocusRef.current = true; setSearchParams({}, { replace: true }); if (!targetEntry) return; setFocusedSetupId(requestedSetupId); setMessage(`Focused saved plan for ${targetEntry.symbol}.`); setError(null); window.setTimeout(() => setFocusedSetupId((prev) => (prev === requestedSetupId ? null : prev)), 2200); window.requestAnimationFrame(() => { setupCardRefs.current[requestedSetupId]?.scrollIntoView({ behavior: 'smooth', block: 'center' }); }); }, [savedSetups, searchParams, setSearchParams]); async function copyIdentifier(kind: 'trade' | 'order', value: string | null | undefined) { if (!value) return; try { await navigator.clipboard.writeText(value); const key = `${kind}:${value}`; setCopiedKey(key); window.setTimeout(() => { setCopiedKey((prev) => (prev === key ? null : prev)); }, 1200); } catch { setError(`Failed to copy ${kind} ID`); } } async function refreshSetupList() { const [profileRows, entryRows] = await Promise.all([ fetchTradeProfiles(), fetchManualEntries(), ]); setProfiles(profileRows); setSavedSetups(normalizeSimpleEntries(entryRows)); } async function updateSavedSetup(entryId: string, updater: (entry: ManualEntryPayload) => ManualEntryPayload) { const existing = savedSetups.find((entry) => String(entry.stock_instance_id || '') === entryId); if (!existing) return; const updated = await updateManualEntry(entryId, updater(existing)); setSavedSetups((prev) => normalizeSimpleEntries(prev.map((entry) => ( String(entry.stock_instance_id || '') === entryId ? updated : entry )))); } 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); if (!options?.silent) { setError(null); setMessage(null); } try { if (livePrice > 0) { if (marketPriceRequestSymbolRef.current === requestSymbol) { setMarketPriceValue(livePrice.toFixed(4), 'live'); } return; } const bars = await fetchChartBars(requestSymbol, '1D'); const lastClose = Number(bars?.[bars.length - 1]?.close || 0); if (Number.isFinite(lastClose) && lastClose > 0) { if (marketPriceRequestSymbolRef.current === requestSymbol) { setMarketPriceValue(lastClose.toFixed(4), 'latest_close'); } } else { const researchProfile = await fetchResearchProfile(requestSymbol).catch(() => null); const fallbackReferencePrice = extractReferencePriceFromResearchProfile(researchProfile); if (fallbackReferencePrice && marketPriceRequestSymbolRef.current === requestSymbol) { 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 (!options?.silent && marketPriceRequestSymbolRef.current === requestSymbol) { setError(err?.message ?? 'Failed to load market data'); } } finally { if (marketPriceRequestSymbolRef.current === requestSymbol) { setLoadingPrice(false); } } } async function handleSubmit(e: FormEvent) { e.preventDefault(); setSubmitting(true); setError(null); setMessage(null); try { const existingEntry = editingSetupId ? (savedSetups.find((entry) => String(entry.stock_instance_id || '') === editingSetupId) || null) : null; const payload = buildSimpleSetupPayload({ draft, existingId: editingSetupId || undefined, holding: draft.side === 'sell' ? selectedSellHolding : null, existingEntry, }); if (editingSetupId) { await updateManualEntry(editingSetupId, payload); setMessage(`Updated ${normalizedSymbol} Simple setup.`); } else { await createManualEntry({ ...payload, stock_instance_id: crypto.randomUUID(), }); setMessage(`Saved ${normalizedSymbol} Simple setup.`); } await refreshSetupList(); setEditingSetupId(null); setMarketPriceSource(draft.currentMarketPrice ? marketPriceSource : null); setDraft({ ...DEFAULT_DRAFT, currentMarketPrice: draft.currentMarketPrice, }); } catch (err: any) { setError(err?.message ?? 'Failed to save Simple setup'); } finally { setSubmitting(false); } } function handleEdit(entry: ManualEntryPayload) { setEditingSetupId(String(entry.stock_instance_id || '')); setMarketPriceSource(inferMarketPriceSourceFromEntry(entry)); setDraft(buildDraftFromEntry(entry)); setMessage(null); setError(null); } async function handleDelete(entryId: string) { if (!window.confirm('Delete this Simple setup?')) return; try { await deleteManualEntry(entryId); if (editingSetupId === entryId) { setEditingSetupId(null); setMarketPriceSource(null); setDraft(DEFAULT_DRAFT); } await refreshSetupList(); } catch (err: any) { setError(err?.message ?? 'Failed to delete Simple setup'); } } async function handleConvertToLongTerm(entry: ManualEntryPayload) { const entryId = String(entry.stock_instance_id || ''); if (!entryId) return; setError(null); setMessage(null); try { await updateSavedSetup(entryId, (current) => ({ ...current, holding_mode: 'long_term', automation_state: 'paused_long_term', status: 'simple_bought', active: true, })); setMessage(`${String(entry.symbol || '').trim().toUpperCase()} is now treated as a long-term hold. Automated exit monitoring is paused.`); } catch (err: any) { setError(err?.message ?? 'Failed to convert setup to long-term mode'); } } async function handleResumeExitManagement(entry: ManualEntryPayload) { const entryId = String(entry.stock_instance_id || ''); if (!entryId) return; setError(null); setMessage(null); try { await updateSavedSetup(entryId, (current) => ({ ...current, holding_mode: 'short_term', automation_state: 'holding_managed', status: 'simple_bought', active: true, })); setMessage(`${String(entry.symbol || '').trim().toUpperCase()} is back under short-term exit management.`); } catch (err: any) { setError(err?.message ?? 'Failed to resume short-term exit management'); } } const saveButtonLabel = editingSetupId ? 'Update setup' : 'Save setup'; const saveButtonDisabled = submitting || loadingPrice || (draft.side === 'sell' && !selectedSellHolding); return (
{editingSetupId ? 'Edit setup' : 'New setup'} Build a short-term buy plan or attach a managed profit exit to an existing holding.
{draft.side === 'sell' && ( )}
{draft.side === 'buy' ? ( <> ) : ( )}
{draft.side === 'buy' && (

Drop trigger

)}

Profit exit

{draft.side === 'sell' && (
{selectedSellHolding ? `Holding ready: ${selectedSellHolding.symbol} · ${selectedSellHolding.size} units at ${selectedSellHolding.entryPrice.toFixed(4)}. Executed buys also appear in Portfolio as live positions.` : 'No eligible live holding found for this symbol yet. Managed sell setups only arm against a current holding.'}
)} {previewText && (
{previewText}
)} {message && (
{message}
)} {error && (
{error}
)}
Saved setups Review armed setups, current runtime order state, and whether an executed setup is now showing in Portfolio as an open holding.
{savedSetups.length === 0 && (
No Simple setups saved yet.
)} {savedSetups.map((entry) => { const entryId = String(entry.stock_instance_id || ''); const side = normalizeSetupSide(entry.simple_side); const isEditing = editingSetupId === entryId; const runtimeSnapshot = deriveRuntimeSnapshot(entry, runtimeOrders, simpleHoldings); const nextActionText = describeNextAction(entry, runtimeSnapshot); const updatedAt = formatSetupUpdatedAt(entry); const eventHistory = deriveSimpleEventHistory(entry, runtimeEvents); const holdingMode = normalizeHoldingMode(entry.holding_mode); const canConvertToLongTerm = side === 'buy' && holdingMode === 'short_term' && runtimeSnapshot?.stage === 'filled'; const canResumeExitManagement = side === 'buy' && holdingMode === 'long_term' && runtimeSnapshot?.stage === 'filled'; return (
{ setupCardRefs.current[entryId] = node; }} className={`rounded-[1.5rem] border bg-[var(--card-elevated)] p-5 transition ${ focusedSetupId === entryId ? 'border-[var(--primary)] ring-2 ring-[var(--primary)]/20' : 'border-[var(--border)]' }`} >

{entry.symbol}

{side}

{describeSavedSetup(entry)}

{canConvertToLongTerm ? ( ) : null} {canResumeExitManagement ? ( ) : null}
{formatSetupStatus(entry.status)} {formatHoldingMode(entry.holding_mode)} {formatAutomationState(entry)} {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)} ) : null} {entry.entry_price ? ( Entry {Number(entry.entry_price).toFixed(4)} ) : null} {(runtimeSnapshot?.tradeId || entry.linked_trade_id) ? ( ) : null} {runtimeSnapshot?.orderId ? ( ) : null} {updatedAt ? ( Updated {updatedAt} ) : null}
{SIMPLE_TIMELINE_STEPS.map((step) => { const complete = isTimelineStepComplete(runtimeSnapshot?.stage, step); const isCurrent = runtimeSnapshot?.stage === step; return (
{formatTimelineStepLabel(step)}
); })}
Next action:{' '} {nextActionText}
{eventHistory.length > 0 && (
Recent activity
setup-level runtime history
{eventHistory.map((event) => (
{String(event.severity || 'INFO').toUpperCase()}
{formatEventTimestamp(event.timestamp) || 'Just now'}
{event.message}
{event.tradeId ? Trade {event.tradeId.slice(0, 18)}… : null} {event.orderId ? Order {event.orderId.slice(0, 12)}… : null}
))}
)}
); })}
); }