import { useEffect, useMemo, useState } from 'react'; import { Link } from 'react-router-dom'; import { RefreshCw, Target, Trash2 } from 'lucide-react'; import { useAuth } from '../components/AuthContext'; import { useAppContext } from '../context/AppContext'; import { fetchChartBars } from '../lib/marketApi'; import { createManualEntry, deleteManualEntry, fetchManualEntries, type ManualEntryPayload, } from '../lib/manualEntriesApi'; type SimpleSide = 'buy' | 'sell'; type SimpleTriggerMode = 'target_price' | 'profit_percent' | 'drop_percent'; type SimpleRuleDraft = { symbol: string; side: SimpleSide; triggerMode: SimpleTriggerMode; quantity: string; targetPrice: string; purchasePrice: string; currentMarketPrice: string; percentValue: string; isCrypto: boolean; notes: string; }; type SimpleRuleEntry = ManualEntryPayload & { stock_instance_id?: string; label?: string | null; notes?: string | null; }; const SIMPLE_LABEL_PREFIX = 'SIMPLE '; const DEFAULT_DRAFT: SimpleRuleDraft = { symbol: '', side: 'buy', triggerMode: 'target_price', quantity: '', targetPrice: '', purchasePrice: '', currentMarketPrice: '', percentValue: '', isCrypto: false, 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)); } export function computeSimpleTriggerPrice(draft: Pick): number | null { if (draft.triggerMode === 'target_price') { return parsePositiveNumber(draft.targetPrice); } const percentValue = parsePositiveNumber(draft.percentValue); if (percentValue === null) return null; if (draft.triggerMode === 'profit_percent') { const purchasePrice = parsePositiveNumber(draft.purchasePrice); if (purchasePrice === null) return null; return roundPrice(purchasePrice * (1 + percentValue / 100)); } const currentMarketPrice = parsePositiveNumber(draft.currentMarketPrice); if (currentMarketPrice === null) return null; return roundPrice(currentMarketPrice * (1 - percentValue / 100)); } function buildSimpleRuleNote(symbol: string, side: SimpleSide, triggerMode: SimpleTriggerMode, triggerPrice: number, percentValue: number | null): string { if (triggerMode === 'target_price') { return `${side.toUpperCase()} ${symbol} when price hits ${triggerPrice.toFixed(4)}`; } if (triggerMode === 'profit_percent') { return `SELL ${symbol} at ${percentValue?.toFixed(2)}% profit (${triggerPrice.toFixed(4)}) from purchase price`; } return `BUY ${symbol} after ${percentValue?.toFixed(2)}% drop from current market (${triggerPrice.toFixed(4)})`; } export function buildSimpleEntryPayload(userId: string, draft: SimpleRuleDraft): ManualEntryPayload { const symbol = draft.symbol.trim().toUpperCase(); if (!symbol) { throw new Error('Symbol is required'); } const triggerPrice = computeSimpleTriggerPrice(draft); if (triggerPrice === null) { throw new Error('A valid trigger price could not be calculated'); } const quantity = parsePositiveNumber(draft.quantity); const purchasePrice = parsePositiveNumber(draft.purchasePrice); const percentValue = parsePositiveNumber(draft.percentValue); const notePrefix = buildSimpleRuleNote(symbol, draft.side, draft.triggerMode, triggerPrice, percentValue); const notes = draft.notes.trim() ? `${notePrefix}. ${draft.notes.trim()}` : notePrefix; return { symbol, active: true, status: 'active', user_id: userId, quantity, is_crypto: draft.isCrypto, is_real_trade: false, label: `${SIMPLE_LABEL_PREFIX}${draft.side.toUpperCase()}`, notes, entry_price: purchasePrice, buy_price: draft.side === 'buy' ? triggerPrice : purchasePrice, sell_price: draft.side === 'sell' ? triggerPrice : null, gain_threshold_for_sell: draft.triggerMode === 'profit_percent' ? percentValue : null, drop_threshold_for_buy: draft.triggerMode === 'drop_percent' ? percentValue : null, }; } function isSimpleRule(entry: SimpleRuleEntry): boolean { return String(entry.label || '').toUpperCase().startsWith(SIMPLE_LABEL_PREFIX); } export function SimpleView() { const { user } = useAuth(); const { botState } = useAppContext(); const [draft, setDraft] = useState(DEFAULT_DRAFT); const [savedRules, setSavedRules] = useState([]); 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 computedTriggerPrice = computeSimpleTriggerPrice(draft); const triggerOptions = draft.side === 'buy' ? [ { value: 'target_price' as const, label: 'Buy when price hits target' }, { value: 'drop_percent' as const, label: 'Buy after % drop from current market' }, ] : [ { value: 'target_price' as const, label: 'Sell when price hits target' }, { value: 'profit_percent' as const, label: 'Sell at % profit from purchase' }, ]; async function loadSavedRules() { try { const rows = await fetchManualEntries(); setSavedRules(rows.filter(isSimpleRule)); } catch (err: any) { setError(err?.message ?? 'Failed to load simple rules'); } } useEffect(() => { if (user) { loadSavedRules(); } }, [user]); useEffect(() => { if (draft.side === 'buy' && draft.triggerMode === 'profit_percent') { setDraft(prev => ({ ...prev, triggerMode: 'target_price' })); } if (draft.side === 'sell' && draft.triggerMode === 'drop_percent') { setDraft(prev => ({ ...prev, triggerMode: 'target_price' })); } }, [draft.side, draft.triggerMode]); useEffect(() => { if (draft.triggerMode !== 'drop_percent' || !livePrice || draft.currentMarketPrice.trim()) { return; } setDraft(prev => ({ ...prev, currentMarketPrice: livePrice.toFixed(4) })); }, [draft.triggerMode, draft.currentMarketPrice, livePrice]); const rulePreview = useMemo(() => { if (!normalizedSymbol || computedTriggerPrice === null) return null; return buildSimpleRuleNote(normalizedSymbol, draft.side, draft.triggerMode, computedTriggerPrice, parsePositiveNumber(draft.percentValue)); }, [normalizedSymbol, computedTriggerPrice, draft.side, draft.triggerMode, draft.percentValue]); function updateDraft(key: K, value: SimpleRuleDraft[K]) { setDraft(prev => ({ ...prev, [key]: value })); } async function handleLoadMarketPrice() { if (!normalizedSymbol) { setError('Enter a symbol first'); return; } setLoadingPrice(true); setError(null); setMessage(null); try { const bars = await fetchChartBars(normalizedSymbol, '1D'); const latestClose = Number(bars[bars.length - 1]?.close || 0); if (!Number.isFinite(latestClose) || latestClose <= 0) { throw new Error(`No recent market price found for ${normalizedSymbol}`); } updateDraft('currentMarketPrice', latestClose.toFixed(4)); setMessage(`Loaded current market price for ${normalizedSymbol}`); } catch (err: any) { setError(err?.message ?? 'Failed to load current market price'); } finally { setLoadingPrice(false); } } async function handleSubmit(event: React.FormEvent) { event.preventDefault(); if (!user?.id) { setError('Not authenticated'); return; } setSubmitting(true); setError(null); setMessage(null); try { const payload = buildSimpleEntryPayload(user.id, draft); await createManualEntry(payload); setDraft(DEFAULT_DRAFT); setMessage('Simple rule saved to your watchlist'); await loadSavedRules(); } catch (err: any) { setError(err?.message ?? 'Failed to save simple rule'); } finally { setSubmitting(false); } } async function handleDelete(ruleId: string) { if (!confirm('Delete this simple rule?')) return; try { await deleteManualEntry(ruleId); await loadSavedRules(); setMessage('Simple rule deleted'); setError(null); } catch (err: any) { setError(err?.message ?? 'Failed to delete simple rule'); } } return (

Simple

Simple Triggers
Save a buy or sell rule without the full strategy builder
This creates a tracked rule in your watchlist using the existing manual-entry system. It does not place a broker-native conditional order by itself.
Open Watchlist
{draft.triggerMode === 'target_price' && ( )} {draft.triggerMode === 'profit_percent' && ( <> )} {draft.triggerMode === 'drop_percent' && ( <> )} {draft.side === 'sell' && draft.triggerMode === 'target_price' && ( )}
Rule Preview
{rulePreview ?? 'Complete the fields to preview the simple rule.'}
{computedTriggerPrice !== null && (
Trigger Price: {computedTriggerPrice.toFixed(4)}
)}
{(error || message) && (
{error || message}
)}
Saved Simple Rules
{savedRules.length === 0 ? (
No saved simple rules yet.
) : (
{savedRules.map(rule => (
{rule.symbol} {rule.label}
{rule.notes || 'Simple trigger rule'}
{rule.buy_price != null && Buy trigger: {Number(rule.buy_price).toFixed(4)}} {rule.sell_price != null && Sell trigger: {Number(rule.sell_price).toFixed(4)}} {rule.quantity != null && Qty: {rule.quantity}}
{rule.stock_instance_id && ( )}
))}
)}
); }