import { useEffect, useMemo, 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 { 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; quantity: 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; }; const SIMPLE_AUTO_PROFILE_NAME = 'Simple Auto Profile'; const SIMPLE_AUTO_PROFILE_KEY = SIMPLE_AUTO_PROFILE_NAME.toLowerCase(); const DEFAULT_DRAFT: SimpleSetupDraft = { symbol: '', side: 'buy', quantity: '', 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 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 computeBuyTriggerPrice(draft: SimpleSetupDraft): number | null { const currentMarketPrice = parsePositiveNumber(draft.currentMarketPrice); const dropValue = parsePositiveNumber(draft.dropValue); if (!currentMarketPrice || !dropValue) 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; }): 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('Current market price is unavailable. Refresh market data and try again.'); } const quantity = parsePositiveNumber(input.draft.quantity); if (!quantity) { throw new Error('Quantity is required'); } 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; if (side === 'sell' && !holding) { throw new Error('Sell setups require an existing Simple holding for this symbol.'); } if (side === 'buy' && !parsePositiveNumber(input.draft.dropValue)) { throw new Error('Drop trigger is required for buy setups'); } 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 : 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' ? parsePositiveNumber(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, 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 dropText = 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`; return [ `Buy ${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 a Simple holding already exists.`; } 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 buildDraftFromEntry(entry: ManualEntryPayload): SimpleSetupDraft { return { symbol: String(entry.symbol || '').trim().toUpperCase(), side: normalizeSetupSide(entry.simple_side), quantity: entry.quantity ? String(entry.quantity) : '', currentMarketPrice: entry.reference_price ? Number(entry.reference_price).toFixed(4) : '', dropMode: normalizeMode(entry.drop_trigger_mode, 'percent'), dropValue: entry.drop_threshold_for_buy ? String(entry.drop_threshold_for_buy) : '', profitMode: normalizeMode(entry.profit_target_mode, 'percent'), profitValue: entry.gain_threshold_for_sell ? String(entry.gain_threshold_for_sell) : '', notes: String(entry.notes || ''), }; } 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 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 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); if (side === 'buy') { const triggerPrice = computeBuyTriggerPrice(buildDraftFromEntry(entry)); const dropText = dropMode === 'dollar' ? `$${dropValue.toFixed(2)} below` : `${dropValue}% below`; const profitText = profitMode === 'dollar' ? `$${profitValue.toFixed(2)} above purchase` : `${profitValue}% above purchase`; return `Buy ${symbol} ${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 ${symbol} at ${profitText}${profitTargetPrice ? ` (${profitTargetPrice.toFixed(4)})` : ''}.`; } export function SimpleView() { const { botState } = useAppContext(); 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 [message, setMessage] = useState(null); const [error, setError] = useState(null); const normalizedSymbol = draft.symbol.trim().toUpperCase(); const livePrice = normalizedSymbol ? Number(botState.symbols?.[normalizedSymbol]?.price || 0) : 0; const simpleAutoProfile = useMemo( () => profiles.find((profile) => matchesSimpleAutoProfile(profile)) || null, [profiles], ); const simpleHoldings = useMemo(() => { const simpleProfileId = simpleAutoProfile?.id; return botState.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 matchingHolding = useMemo( () => simpleHoldings.find((holding) => holding.symbol === normalizedSymbol) || null, [simpleHoldings, normalizedSymbol], ); 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; setDraft((prev) => ( prev.currentMarketPrice === livePrice.toFixed(4) ? prev : { ...prev, currentMarketPrice: livePrice.toFixed(4) } )); }, [livePrice]); useEffect(() => { if (!normalizedSymbol || livePrice > 0 || draft.currentMarketPrice.trim() || loadingPrice) { return; } void handleLoadMarketPrice(); }, [normalizedSymbol, livePrice]); const previewText = useMemo( () => buildPreviewText(draft, draft.side === 'sell' ? matchingHolding : null), [draft, matchingHolding], ); function updateDraft(key: K, value: SimpleSetupDraft[K]) { setDraft((prev) => ({ ...prev, [key]: value })); } async function refreshSetupList() { const [profileRows, entryRows] = await Promise.all([ fetchTradeProfiles(), fetchManualEntries(), ]); setProfiles(profileRows); setSavedSetups(normalizeSimpleEntries(entryRows)); } async function handleLoadMarketPrice() { if (!normalizedSymbol) return; setLoadingPrice(true); setError(null); setMessage(null); try { if (livePrice > 0) { updateDraft('currentMarketPrice', livePrice.toFixed(4)); return; } const bars = await fetchChartBars(normalizedSymbol, '1D'); const lastClose = Number(bars?.[bars.length - 1]?.close || 0); if (Number.isFinite(lastClose) && lastClose > 0) { updateDraft('currentMarketPrice', lastClose.toFixed(4)); } else { throw new Error('No recent market price available'); } } catch (err: any) { setError(err?.message ?? 'Failed to load market price'); } finally { setLoadingPrice(false); } } async function handleSubmit(e: FormEvent) { e.preventDefault(); setSubmitting(true); setError(null); setMessage(null); try { const payload = buildSimpleSetupPayload({ draft, existingId: editingSetupId || undefined, holding: draft.side === 'sell' ? matchingHolding : null, }); 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); 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 || '')); 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); setDraft(DEFAULT_DRAFT); } await refreshSetupList(); } catch (err: any) { setError(err?.message ?? 'Failed to delete Simple setup'); } } const saveButtonLabel = editingSetupId ? 'Update setup' : 'Save setup'; const saveButtonDisabled = submitting || loadingPrice || (draft.side === 'sell' && !matchingHolding); return (
{editingSetupId ? 'Edit setup' : 'New setup'} Saved trigger workflow, not an immediate broker order
{draft.side === 'buy' && (

Drop trigger

)}

Profit exit

{draft.side === 'sell' && (
{matchingHolding ? `Simple holding ready: ${matchingHolding.symbol} · ${matchingHolding.size} shares at ${matchingHolding.entryPrice.toFixed(4)}` : 'No existing Simple holding found for this symbol. Sell setups only arm against a current Simple holding.'}
)} {previewText && (
{previewText}
)} {message && (
{message}
)} {error && (
{error}
)}
Saved setups Review and update armed simple workflows in the same layout style used across the app.
{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; return (

{entry.symbol}

{side}

{describeSavedSetup(entry)}

{formatSetupStatus(entry.status)} {entry.reference_price ? ( Ref {Number(entry.reference_price).toFixed(4)} ) : null} {entry.entry_price ? ( Entry {Number(entry.entry_price).toFixed(4)} ) : null} {entry.linked_trade_id ? ( Trade linked ) : null}
); })}
); }