learning_ai_invt_trdg/web/src/views/SimpleView.tsx

751 lines
29 KiB
TypeScript

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<TradeProfilePayload, 'name'> | 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<TradeProfilePayload[]>([]);
const [savedSetups, setSavedSetups] = useState<ManualEntryPayload[]>([]);
const [editingSetupId, setEditingSetupId] = useState<string | null>(null);
const [draft, setDraft] = useState<SimpleSetupDraft>(DEFAULT_DRAFT);
const [submitting, setSubmitting] = useState(false);
const [loadingPrice, setLoadingPrice] = useState(false);
const [message, setMessage] = useState<string | null>(null);
const [error, setError] = useState<string | null>(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<K extends keyof SimpleSetupDraft>(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 (
<div className="space-y-8">
<PageHeader
title="Simple"
description="Create saved dip-buy and profit-exit setups with the same workspace patterns used across Research, Markets, and Settings."
/>
<div className="grid gap-8 xl:grid-cols-[minmax(0,1.1fr)_minmax(0,0.9fr)]">
<Card>
<CardHeader>
<div>
<CardTitle className="uppercase">{editingSetupId ? 'Edit setup' : 'New setup'}</CardTitle>
<CardDescription>Saved trigger workflow, not an immediate broker order</CardDescription>
</div>
<Button
type="button"
onClick={() => {
setEditingSetupId(null);
setDraft({
...DEFAULT_DRAFT,
currentMarketPrice: draft.currentMarketPrice,
});
setMessage(null);
setError(null);
}}
variant="outline"
size="sm"
className="uppercase tracking-[0.2em]"
>
Reset
</Button>
</CardHeader>
<CardContent>
<form className="space-y-6" onSubmit={handleSubmit}>
<div className="grid gap-4 md:grid-cols-2">
<label className="space-y-2">
<span className="text-[11px] font-black uppercase tracking-[0.24em] text-zinc-700">Symbol</span>
<Input
value={draft.symbol}
onChange={(e) => setDraft((prev) => ({
...prev,
symbol: e.target.value.toUpperCase(),
currentMarketPrice: '',
}))}
placeholder="AAPL"
/>
</label>
<label className="space-y-2">
<span className="text-[11px] font-black uppercase tracking-[0.24em] text-zinc-700">Setup type</span>
<Select
value={draft.side}
onChange={(e) => updateDraft('side', e.target.value as SimpleSide)}
>
<option value="buy">Buy the dip + profit exit</option>
<option value="sell">Sell existing Simple holding at profit</option>
</Select>
</label>
</div>
<div className="grid gap-4 md:grid-cols-[minmax(0,1fr)_auto]">
<label className="space-y-2">
<span className="text-[11px] font-black uppercase tracking-[0.24em] text-zinc-700">Market price (auto-fetched)</span>
<Input
value={draft.currentMarketPrice}
readOnly
className="bg-[var(--muted)] text-[var(--foreground)]"
/>
</label>
<Button
type="button"
onClick={() => void handleLoadMarketPrice()}
className="self-end uppercase tracking-[0.2em]"
variant="outline"
disabled={loadingPrice || !normalizedSymbol}
>
<span className="inline-flex items-center gap-2">
<RefreshCw size={14} className={loadingPrice ? 'animate-spin' : ''} />
Refresh
</span>
</Button>
</div>
<div className="grid gap-4 md:grid-cols-2">
<label className="space-y-2">
<span className="text-[11px] font-black uppercase tracking-[0.24em] text-zinc-500">
{draft.side === 'buy' ? 'Planned quantity' : 'Holding size'}
</span>
<Input
value={draft.side === 'sell' && matchingHolding ? String(matchingHolding.size) : draft.quantity}
onChange={(e) => updateDraft('quantity', e.target.value)}
readOnly={draft.side === 'sell' && !!matchingHolding}
className="read-only:bg-[var(--muted)]"
placeholder="10"
/>
</label>
<label className="space-y-2">
<span className="text-[11px] font-black uppercase tracking-[0.24em] text-zinc-700">Notes</span>
<Input
value={draft.notes}
onChange={(e) => updateDraft('notes', e.target.value)}
placeholder="Optional context"
/>
</label>
</div>
{draft.side === 'buy' && (
<div className="grid gap-4 rounded-[1.5rem] border border-[var(--border)] bg-[var(--card-elevated)] p-5 md:grid-cols-[0.55fr_0.45fr]">
<div className="space-y-3">
<p className="text-[11px] font-black uppercase tracking-[0.24em] text-zinc-700">Drop trigger</p>
<Select
value={draft.dropMode}
onChange={(e) => updateDraft('dropMode', e.target.value as TriggerMode)}
>
<option value="dollar">Dollar drop from current market</option>
<option value="percent">Percent drop from current market</option>
</Select>
</div>
<label className="space-y-3">
<span className="text-[11px] font-black uppercase tracking-[0.24em] text-zinc-700">
{draft.dropMode === 'dollar' ? 'Drop amount ($)' : 'Drop amount (%)'}
</span>
<Input
value={draft.dropValue}
onChange={(e) => updateDraft('dropValue', e.target.value)}
placeholder={draft.dropMode === 'dollar' ? '5.00' : '8'}
/>
</label>
</div>
)}
<div className="grid gap-4 rounded-[1.5rem] border border-[var(--border)] bg-[var(--card-elevated)] p-5 md:grid-cols-[0.55fr_0.45fr]">
<div className="space-y-3">
<p className="text-[11px] font-black uppercase tracking-[0.24em] text-zinc-700">Profit exit</p>
<Select
value={draft.profitMode}
onChange={(e) => updateDraft('profitMode', e.target.value as TriggerMode)}
>
<option value="dollar">Dollar gain from purchase</option>
<option value="percent">Percent gain from purchase</option>
</Select>
</div>
<label className="space-y-3">
<span className="text-[11px] font-black uppercase tracking-[0.24em] text-zinc-700">
{draft.profitMode === 'dollar' ? 'Profit target ($)' : 'Profit target (%)'}
</span>
<Input
value={draft.profitValue}
onChange={(e) => updateDraft('profitValue', e.target.value)}
placeholder={draft.profitMode === 'dollar' ? '7.50' : '10'}
/>
</label>
</div>
{draft.side === 'sell' && (
<div className={`rounded-[1.5rem] border px-4 py-4 text-sm ${
matchingHolding
? 'border-emerald-500/20 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300'
: 'border-red-500/20 bg-red-500/10 text-red-700 dark:text-red-300'
}`}>
{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.'}
</div>
)}
{previewText && (
<div className="rounded-[1.5rem] border border-[var(--border)] bg-[var(--accent-soft)] px-5 py-4 text-sm text-[var(--foreground)]">
{previewText}
</div>
)}
{message && (
<div className="rounded-2xl border border-emerald-500/20 bg-emerald-500/10 px-4 py-3 text-sm text-emerald-700 dark:text-emerald-300">
{message}
</div>
)}
{error && (
<div className="rounded-2xl border border-red-500/20 bg-red-500/10 px-4 py-3 text-sm text-red-700 dark:text-red-300">
{error}
</div>
)}
<Button
type="submit"
disabled={saveButtonDisabled}
className="w-full uppercase tracking-[0.24em]"
size="lg"
>
{submitting ? 'Saving...' : saveButtonLabel}
</Button>
</form>
</CardContent>
</Card>
<Card>
<CardHeader className="block">
<CardTitle className="uppercase">Saved setups</CardTitle>
<CardDescription>Review and update armed simple workflows in the same layout style used across the app.</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{savedSetups.length === 0 && (
<div className="rounded-[1.5rem] border border-dashed border-[var(--border)] bg-[var(--card-elevated)] px-5 py-8 text-sm text-[var(--muted-foreground)]">
No Simple setups saved yet.
</div>
)}
{savedSetups.map((entry) => {
const entryId = String(entry.stock_instance_id || '');
const side = normalizeSetupSide(entry.simple_side);
const isEditing = editingSetupId === entryId;
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">
<div>
<div className="flex items-center gap-3">
<h3 className="text-lg font-black uppercase text-[var(--foreground)]">{entry.symbol}</h3>
<span className={`rounded-full px-3 py-1 text-[10px] font-black uppercase tracking-[0.2em] ${
side === 'buy' ? 'bg-emerald-500/10 text-emerald-700 dark:text-emerald-300' : 'bg-violet-500/10 text-violet-700 dark:text-violet-300'
}`}>
{side}
</span>
</div>
<p className="mt-2 text-sm text-[var(--muted-foreground)]">{describeSavedSetup(entry)}</p>
</div>
<div className="flex items-center gap-2">
<Button
type="button"
onClick={() => handleEdit(entry)}
variant="outline"
size="sm"
className={isEditing ? 'border-emerald-500/30 text-emerald-700 dark:text-emerald-300' : 'uppercase tracking-[0.18em]'}
>
<span className="inline-flex items-center gap-2">
<Pencil size={14} />
Edit
</span>
</Button>
<Button
type="button"
onClick={() => void handleDelete(entryId)}
variant="destructive"
size="sm"
className="uppercase tracking-[0.18em]"
>
<span className="inline-flex items-center gap-2">
<Trash2 size={14} />
Delete
</span>
</Button>
</div>
</div>
<div className="flex flex-wrap items-center gap-3 text-[11px] uppercase tracking-[0.2em] text-[var(--muted-foreground)]">
<span className="rounded-full border border-[var(--border)] px-3 py-1">
{formatSetupStatus(entry.status)}
</span>
{entry.reference_price ? (
<span className="rounded-full border border-[var(--border)] px-3 py-1">
Ref {Number(entry.reference_price).toFixed(4)}
</span>
) : null}
{entry.entry_price ? (
<span className="rounded-full border border-[var(--border)] px-3 py-1">
Entry {Number(entry.entry_price).toFixed(4)}
</span>
) : null}
{entry.linked_trade_id ? (
<span className="rounded-full border border-[var(--border)] px-3 py-1">
Trade linked
</span>
) : null}
</div>
</div>
);
})}
</div>
</CardContent>
</Card>
</div>
</div>
);
}