import { useState, useEffect } from 'react'; import { createPortal } from 'react-dom'; import { aggregateHistoryLedger, buildHistoryLedger } from '../lib/tradeHistoryLedger'; import { publishMarketplacePreset } from '../lib/marketplaceApi'; import { fetchTradeHistory } from '../lib/tradeHistoryApi'; import { createTradeProfile, deleteTradeProfile, fetchCurrentUserProfile, fetchTradeProfiles, setTradeProfileActive, type TradeProfilePayload, updateTradeProfile } from '../lib/profileApi'; import { Trash2, Edit3, X, Check, Zap, Play, Save, RotateCcw, Info, Search, Cpu, TrendingUp, Activity, Settings, Shield, Plus, DollarSign, BarChart3, Coins, AlertTriangle, Layers, Target, Clock, Share2 } from 'lucide-react'; import { useAuth } from './AuthContext'; import type { BotState } from '../hooks/useWebSocket'; import { DEFAULT_BOT_STATE } from '../hooks/useWebSocket'; import { Button } from './ui/button'; import { Card } from './ui/card'; import { Input } from './ui/input'; import { Select } from './ui/select'; import { cn } from '../lib/utils'; // ChatControl is now rendered globally in App.tsx // --- TYPES --- interface RuleConfig { ruleId: string; enabled: boolean; ruleType?: 'mandatory' | 'voting'; params?: Record; } interface StrategyConfig { rules: RuleConfig[]; riskLimits: { maxDailyLossUsd: number; dailyProfitTargetUsd?: number; maxOpenTrades: number; maxConsecutiveLosses: number; }; execution: { orderType: 'market' | 'limit'; cooldownMinutes: number; minRulePassRatio?: number; entryMode: 'both' | 'long_only'; }; } interface Profile { id: string; user_id: string; name: string; allocated_capital: number; risk_per_trade_percent: number; symbols: string; is_active: boolean; strategy_config?: StrategyConfig; created_at?: string; } interface User { user_id: string; email: string; } export interface ProfileTradeStats { winRate: number; totalPnl: number; tradeCount: number; } // --- CONSTANTS --- const AVAILABLE_RULES = [ { id: 'TrendBiasRule', name: 'Trend Bias', desc: 'EMA50/200 direction check on 4H timeframe', icon: TrendingUp, color: '#3b82f6', defaultParams: { fastPeriod: 50, slowPeriod: 200 } }, { id: 'MomentumRule', name: 'Momentum', desc: 'RSI overbought/oversold confirmation', icon: Activity, color: '#06b6d4', defaultParams: { rsiPeriod: 14, overbought: 70, oversold: 30 } }, { id: 'ZoneRule', name: 'Zone Proximity', desc: 'Price relative to EMA value zones', icon: Target, color: '#a855f7', defaultParams: { zonePercent: 1.5 } }, { id: 'SessionRule', name: 'Session Filter', desc: 'Trade only during major sessions', icon: Clock, color: '#f59e0b', defaultParams: { sessions: 'LDN,NY' } }, { id: 'EntryTriggerRule', name: 'Entry Trigger', desc: 'Pattern-based precise entry logic', icon: Play, color: '#10b981', defaultParams: { showPatterns: true } }, { id: 'RiskManagementRule', name: 'Risk Guard', desc: 'ATR-based stop loss & position sizing', icon: Shield, color: '#ef4444', defaultParams: { maxRisk: 2.0 } }, { id: 'AIAnalysisRule', name: 'AI Sentiment', desc: 'LLM-powered market analysis', icon: Cpu, color: '#8b5cf6', defaultParams: { minConfidence: 70 } } ]; const SESSION_PRESET_OPTIONS = [ { value: '24/7', label: '24/7 (All Sessions)', hint: 'No session-time restriction' }, { value: 'LDN,NY', label: 'London + New York', hint: 'High-liquidity overlap' }, { value: 'TOK,SYD', label: 'Tokyo + Sydney', hint: 'Asia-Pacific market hours' }, { value: 'LDN', label: 'London Only', hint: 'Only London session' }, { value: 'NY', label: 'New York Only', hint: 'Only New York session' }, { value: 'TOK', label: 'Tokyo Only', hint: 'Only Tokyo session' }, { value: 'SYD', label: 'Sydney Only', hint: 'Only Sydney session' } ] as const; const SESSION_ALIAS_MAP: Record = { LDN: 'LDN', LONDON: 'LDN', NY: 'NY', NEWYORK: 'NY', TOK: 'TOK', TOKYO: 'TOK', SYD: 'SYD', SYDNEY: 'SYD', '24/7': '24/7', '24X7': '24/7', '247': '24/7', ALL: '24/7', ALLSESSIONS: '24/7' }; const SESSION_TOKEN_ORDER = ['LDN', 'NY', 'TOK', 'SYD'] as const; type SessionPresetValue = (typeof SESSION_PRESET_OPTIONS)[number]['value']; const PARAM_LABELS: Record = { fastPeriod: 'Fast EMA Period', slowPeriod: 'Slow EMA Period', rsiPeriod: 'RSI Period', overbought: 'Overbought Level', oversold: 'Oversold Level', zonePercent: 'Zone Width %', sessions: 'Active Sessions', showPatterns: 'Show Patterns', maxRisk: 'Max Risk %', minConfidence: 'Min Confidence', }; const labelClass = 'text-[10px] font-semibold text-[var(--muted-foreground)] uppercase tracking-wider'; const helpTextClass = 'text-[10px] text-[var(--muted-foreground)]'; const panelClass = 'rounded-xl border border-[var(--border)] bg-[var(--card-elevated)]'; const subtlePanelClass = 'rounded-xl border border-[var(--border)] bg-[var(--muted)]/35'; const iconButtonClass = 'h-8 w-8 rounded-xl'; const accentTextClass = 'text-[var(--accent)]'; export const normalizeSessionPresetValue = (raw: unknown): string => { if (raw === undefined || raw === null) return 'LDN,NY'; const tokens = (Array.isArray(raw) ? raw.map((token) => String(token)) : String(raw).split(/[,|]/)) .map((token) => token.trim().toUpperCase().replace(/[\s_-]+/g, '')) .filter(Boolean); if (tokens.length === 0) return 'LDN,NY'; const mappedTokens = tokens.map((token) => SESSION_ALIAS_MAP[token] || token); if (mappedTokens.includes('24/7')) return '24/7'; const normalizedSessionTokens: string[] = []; mappedTokens.forEach((token) => { if (SESSION_TOKEN_ORDER.includes(token as any) && !normalizedSessionTokens.includes(token)) { normalizedSessionTokens.push(token); } }); const orderedTokens = SESSION_TOKEN_ORDER.filter((token) => normalizedSessionTokens.includes(token)); if (orderedTokens.length === SESSION_TOKEN_ORDER.length) return '24/7'; if (orderedTokens.length > 0) return orderedTokens.join(','); return String(raw).trim(); }; export const resolveSessionPresetSelection = (raw: unknown): SessionPresetValue | 'custom' => { const normalizedValue = normalizeSessionPresetValue(raw); if (SESSION_PRESET_OPTIONS.some((option) => option.value === normalizedValue)) { return normalizedValue as SessionPresetValue; } return 'custom'; }; export const normalizeStrategyConfig = (rawConfig?: StrategyConfig): StrategyConfig => { const safeConfig = rawConfig && typeof rawConfig === 'object' ? rawConfig : {} as StrategyConfig; const rawRules = Array.isArray(safeConfig.rules) ? safeConfig.rules : []; const normalizedRules: RuleConfig[] = rawRules .filter((rule: any) => rule && typeof rule.ruleId === 'string') .map((rule: any) => { const safeParams = rule.params && typeof rule.params === 'object' ? { ...rule.params } : {}; if (rule.ruleId === 'SessionRule' && safeParams.allowedSessions && !safeParams.sessions) { safeParams.sessions = safeParams.allowedSessions; } if (rule.ruleId === 'AIAnalysisRule') { const rawConfidence = Number(safeParams.minConfidence ?? safeParams.confidenceThreshold ?? 70); safeParams.minConfidence = Number.isFinite(rawConfidence) && rawConfidence >= 0 ? (rawConfidence <= 1 ? rawConfidence * 100 : rawConfidence) : 70; } return { ruleId: String(rule.ruleId), enabled: Boolean(rule.enabled), ruleType: (rule.ruleType === 'mandatory' || rule.ruleType === 'voting') ? rule.ruleType : undefined, params: safeParams }; }); const rawRisk = safeConfig.riskLimits || {}; const maxDailyLossUsd = Number((rawRisk as any).maxDailyLossUsd); const dailyProfitTargetUsd = Number((rawRisk as any).dailyProfitTargetUsd); const maxOpenTrades = Number((rawRisk as any).maxOpenTrades); const maxConsecutiveLosses = Number((rawRisk as any).maxConsecutiveLosses); const rawExecution = safeConfig.execution || {}; const rawOrderType = String((rawExecution as any).orderType || 'market').toLowerCase(); const cooldownMinutes = Number((rawExecution as any).cooldownMinutes); const minRulePassRatio = Number((rawExecution as any).minRulePassRatio); const rawEntryMode = String( (rawExecution as any).entryMode ?? ((rawExecution as any).longOnly ? 'long_only' : 'both') ).toLowerCase(); const entryMode: 'both' | 'long_only' = (rawEntryMode === 'long_only' || rawEntryMode === 'longonly' || rawEntryMode === 'buy_only') ? 'long_only' : 'both'; return { rules: normalizedRules, riskLimits: { maxDailyLossUsd: Number.isFinite(maxDailyLossUsd) && maxDailyLossUsd > 0 ? maxDailyLossUsd : 50, dailyProfitTargetUsd: Number.isFinite(dailyProfitTargetUsd) && dailyProfitTargetUsd > 0 ? dailyProfitTargetUsd : undefined, maxOpenTrades: Number.isFinite(maxOpenTrades) && maxOpenTrades > 0 ? Math.floor(maxOpenTrades) : 3, maxConsecutiveLosses: Number.isFinite(maxConsecutiveLosses) && maxConsecutiveLosses >= 0 ? Math.floor(maxConsecutiveLosses) : 2, }, execution: { orderType: rawOrderType === 'limit' ? 'limit' : 'market', cooldownMinutes: Number.isFinite(cooldownMinutes) && cooldownMinutes >= 0 ? cooldownMinutes : 30, minRulePassRatio: Number.isFinite(minRulePassRatio) && minRulePassRatio >= 0 && minRulePassRatio <= 1 ? minRulePassRatio : 1.0, entryMode } }; }; export const filterProfilesBySearch = (profiles: Profile[], searchTerm: string) => { const term = searchTerm.toLowerCase(); return profiles.filter((profile) => profile.name.toLowerCase().includes(term)); }; export const summarizePortfolioStats = ( profiles: Profile[], tradeStats: Record ) => ({ activeCount: profiles.filter((profile) => profile.is_active).length, totalCapital: profiles.reduce((sum, profile) => sum + (profile.allocated_capital || 0), 0), totalPnl: Object.values(tradeStats).reduce((sum, stat) => sum + stat.totalPnl, 0), totalTrades: Object.values(tradeStats).reduce((sum, stat) => sum + stat.tradeCount, 0) }); // --- UI COMPONENTS --- const ToggleSwitch = ({ checked, onChange }: { checked: boolean, onChange: (v: boolean) => void }) => ( ); const Slider = ({ value, onChange, min, max, step, unit, label }: { value: number, onChange: (n: number) => void, min: number, max: number, step?: number, unit?: string, label: string }) => (
{label} {value}{unit}
onChange(Number(e.target.value))} className="h-1 w-full accent-[var(--accent)]" />
{min}{unit} {max}{unit}
); const StatPill = ({ icon: Icon, label, value, color }: { icon: any, label: string, value: string, color: string }) => (

{label}

{value}

); interface TradeProfileManagerProps { botState?: BotState; } // --- MAIN COMPONENT --- export const TradeProfileManager = ({ botState = DEFAULT_BOT_STATE }: TradeProfileManagerProps) => { const { user: authUser, profile } = useAuth(); const [profiles, setProfiles] = useState([]); const [tradeStats, setTradeStats] = useState>({}); const [loading, setLoading] = useState(false); const [searchTerm, setSearchTerm] = useState(''); const [currentUserProfile, setCurrentUserProfile] = useState(null); // Drawer state const [isDrawerOpen, setIsDrawerOpen] = useState(false); const [isAddingMode, setIsAddingMode] = useState(false); const [editingProfile, setEditingProfile] = useState>({}); const [drawerTab, setDrawerTab] = useState<'settings' | 'logic' | 'advanced'>('settings'); // Utilities const [toasts, setToasts] = useState<{ id: number, msg: string, type: 'success' | 'error' | 'info' }[]>([]); const [deleteConfirm, setDeleteConfirm] = useState(null); const addToast = (msg: string, type: 'success' | 'error' | 'info' = 'info') => { const id = Date.now(); setToasts(prev => [...prev, { id, msg, type }]); setTimeout(() => setToasts(prev => prev.filter(t => t.id !== id)), 4000); }; // --- DATA --- const fetchData = async () => { setLoading(true); try { const [profilesData, meProfile, hRes] = await Promise.all([ fetchTradeProfiles({ scope: profile?.role === 'admin' ? 'all' : 'user' }), fetchCurrentUserProfile().catch(() => null), fetchTradeHistory({ scope: profile?.role === 'admin' ? 'all' : 'user' }) ]); const normalizedProfiles = (profilesData || []).map((profile: any) => ({ ...profile, strategy_config: normalizeStrategyConfig(profile.strategy_config as StrategyConfig) })); setProfiles(normalizedProfiles); setCurrentUserProfile(meProfile ? { user_id: String(meProfile.user_id || authUser?.id || ''), email: String(meProfile.email || authUser?.email || '') } : (authUser?.id || authUser?.email ? { user_id: String(authUser?.id || ''), email: String(authUser?.email || '') } : null)); const historyLedger = buildHistoryLedger({ dbRows: hRes || [], includeRealtime: false }); const historyAggregate = aggregateHistoryLedger(historyLedger); const stats: Record = {}; normalizedProfiles.forEach((p: any) => { const profileStats = historyAggregate.byProfile[p.id]; stats[p.id] = { tradeCount: profileStats?.tradeCount || 0, totalPnl: profileStats?.realizedPnl || 0, winRate: profileStats?.winRate || 0 }; }); setTradeStats(stats); } catch (e) { console.error('[ProfileManager] Fetch error', e); addToast('Failed to load profiles', 'error'); } setLoading(false); }; useEffect(() => { fetchData(); const interval = setInterval(fetchData, 30000); // Listen for chat-created profile updates to refresh instantly const onProfilesUpdated = () => fetchData(); window.addEventListener('profiles-updated', onProfilesUpdated); return () => { clearInterval(interval); window.removeEventListener('profiles-updated', onProfilesUpdated); }; }, []); // --- ACTIONS --- const handleOpenEdit = (profile: Profile) => { setEditingProfile({ ...profile, strategy_config: normalizeStrategyConfig(profile.strategy_config) }); setIsAddingMode(false); setDrawerTab('settings'); setIsDrawerOpen(true); }; const handleOpenAdd = () => { setEditingProfile({ name: '', user_id: authUser?.id || currentUserProfile?.user_id || '', allocated_capital: 1000, risk_per_trade_percent: 1, is_active: true, symbols: 'BTC/USDT, ETH/USDT', strategy_config: { rules: AVAILABLE_RULES.map(r => ({ ruleId: r.id, enabled: true, params: { ...r.defaultParams } })), riskLimits: { maxDailyLossUsd: 50, dailyProfitTargetUsd: 100, maxOpenTrades: 3, maxConsecutiveLosses: 2 }, execution: { orderType: 'market', cooldownMinutes: 30, minRulePassRatio: 1.0, entryMode: 'both' } } }); setIsAddingMode(true); setDrawerTab('settings'); setIsDrawerOpen(true); }; const handleSave = async () => { if (!editingProfile.name) return addToast('Profile name is required', 'error'); setLoading(true); const id = isAddingMode ? null : editingProfile.id; const payload: TradeProfilePayload = { name: editingProfile.name, user_id: editingProfile.user_id || authUser?.id || currentUserProfile?.user_id, allocated_capital: Number(editingProfile.allocated_capital), risk_per_trade_percent: Number(editingProfile.risk_per_trade_percent), symbols: editingProfile.symbols || '', is_active: editingProfile.is_active ?? true, strategy_config: normalizeStrategyConfig(editingProfile.strategy_config as StrategyConfig) as unknown as Record }; try { if (id) { await updateTradeProfile(id, payload); } else { await createTradeProfile(payload); } addToast(isAddingMode ? 'Profile created successfully' : 'Profile updated', 'success'); setIsDrawerOpen(false); await fetchData(); } catch (error: any) { addToast(error?.message || 'Failed to save profile', 'error'); } setLoading(false); }; const handleDelete = async (profileId: string) => { try { await deleteTradeProfile(profileId); addToast('Profile deleted', 'success'); await fetchData(); } catch (error: any) { addToast(`Delete failed: ${error?.message || 'Unknown error'}`, 'error'); } setDeleteConfirm(null); }; const toggleRule = (ruleId: string) => { const rules = [...(editingProfile.strategy_config?.rules || [])]; const idx = rules.findIndex(r => r.ruleId === ruleId); if (idx !== -1) { rules[idx] = { ...rules[idx], enabled: !rules[idx].enabled }; } else { rules.push({ ruleId, enabled: true, params: {} }); } setEditingProfile({ ...editingProfile, strategy_config: { ...editingProfile.strategy_config, rules } as StrategyConfig }); }; const updateRuleParam = (ruleId: string, key: string, value: string | number | boolean) => { const newRules = editingProfile.strategy_config?.rules?.map((rule) => rule.ruleId === ruleId ? { ...rule, params: { ...rule.params, [key]: value } } : rule ); setEditingProfile({ ...editingProfile, strategy_config: { ...editingProfile.strategy_config, rules: newRules } as StrategyConfig }); }; const toggleProfileActive = async (profile: Profile) => { const newState = !profile.is_active; try { await setTradeProfileActive(profile.id, newState); addToast(newState ? 'Profile activated' : 'Profile suspended', 'success'); await fetchData(); } catch (error: any) { addToast(error?.message || 'Failed to update profile state', 'error'); } }; const handlePublish = async (p: Profile) => { if (profile?.role !== 'admin') return; setLoading(true); // Find risk style based on pass ratio const passRatio = p.strategy_config?.execution?.minRulePassRatio || 1.0; const styleId = passRatio < 0.9 ? 'aggressive' : (passRatio >= 1.0 ? 'safe' : 'balanced'); const payload = { id: `template-${p.id}-${Date.now()}`, name: p.name, description: `Admin-published strategy based on ${p.name}. Features ${p.strategy_config?.rules?.filter(r => r.enabled).length} optimized rules.`, risk_style_id: styleId, recommended_assets: p.symbols.split(',').map(s => s.trim()), typical_trades_per_day: styleId === 'aggressive' ? '8-12' : (styleId === 'safe' ? '1-2' : '3-5'), performance_tag: 'Institutional Template', is_popular: true, created_by: authUser?.id, original_profile_id: p.id, strategy_config: p.strategy_config || {} }; try { await publishMarketplacePreset(payload); addToast('Strategy published to Marketplace!', 'success'); } catch (error: any) { addToast(`Publish failed: ${error?.message || 'Unknown error'}`, 'error'); } setLoading(false); }; const filtered = filterProfilesBySearch(profiles, searchTerm); const { activeCount, totalCapital, totalPnl, totalTrades } = summarizePortfolioStats(profiles, tradeStats); const getUserEmail = (userId: string) => { if (userId && userId === currentUserProfile?.user_id) { return currentUserProfile.email; } if (userId && userId === authUser?.id) { return authUser?.email || ''; } return ''; }; // --- RENDER --- return (
{/* ─── PAGE HEADER ─── */}

Strategy Clusters

{loading && }

Manage trading profiles, rules, and capital allocation

setSearchTerm(e.target.value)} className="h-10 w-44 pl-8 pr-3 text-[11px]" />
{/* Aggregate stats */}
= 0 ? '+' : ''}$${totalPnl.toFixed(2)}`} color={totalPnl >= 0 ? '#00ff88' : '#ef4444'} />
{/* ─── EMPTY STATE ─── */} {filtered.length === 0 && !loading && (

{searchTerm ? 'No matching profiles' : 'No profiles yet'}

{searchTerm ? 'Try a different search term' : 'Create your first strategy profile to start trading'}

{!searchTerm && ( )}
)} {/* ─── PROFILE CARDS ─── */}
{filtered.map(p => { const stats = tradeStats[p.id] || { winRate: 0, totalPnl: 0, tradeCount: 0 }; const enabledRules = p.strategy_config?.rules?.filter(r => r.enabled) || []; const ruleCount = enabledRules.length; const email = getUserEmail(p.user_id); const maxLoss = p.strategy_config?.riskLimits?.maxDailyLossUsd ?? 50; const profitTarget = p.strategy_config?.riskLimits?.dailyProfitTargetUsd; const maxOpen = p.strategy_config?.riskLimits?.maxOpenTrades ?? 3; const orderType = p.strategy_config?.execution?.orderType ?? 'market'; const cooldown = p.strategy_config?.execution?.cooldownMinutes ?? 30; const minPassRatio = p.strategy_config?.execution?.minRulePassRatio ?? 1.0; const entryMode = p.strategy_config?.execution?.entryMode ?? 'both'; const usedNotional = botState.positions .filter(pos => pos.profileId === p.id) .reduce((sum, pos) => sum + Math.abs((pos.entryPrice || 0) * (pos.size || 0)), 0); const capitalHealth = usedNotional > (p.allocated_capital || 0) ? 'Issue' : 'OK'; // --- NEW: Capital Coverage Check (Allocated vs Buying Power) --- // This is observational only. const bp = botState.accountSnapshot?.buying_power ?? 0; const isCovered = (p.allocated_capital || 0) <= bp; const coverageStatus = isCovered ? 'Covered' : 'Insufficient funds'; const coverageColor = isCovered ? 'ok' : 'drift'; // reuse 'drift' style for warning const lifecycleStatus = stats.tradeCount > 0 ? 'OK' : (p.is_active ? 'Blocked' : 'Idle'); const reconciliationDrift = (botState.health?.reconciliationMismatchCount || 0) > 0 ? 'Drift' : 'Clean'; const lockStatus = (botState.health?.lockContentionCount || 0) > 0 ? 'Contended' : 'Stable'; return ( {/* Top accent bar */}
{/* Card header area */}

{p.name}

{p.is_active ? 'Active' : 'Paused'} {email && ( {email} )}
{/* Action buttons */}
{profile?.role === 'admin' && ( )}
{/* ── Elevated inner panel: Key Metrics ── */}
{/* Stats header row */}
{[ { icon: DollarSign, label: 'Capital', val: `$${p.allocated_capital.toLocaleString()}`, color: '#3b82f6', valClass: 'text-[var(--foreground)]' }, { icon: BarChart3, label: 'Realized', val: `${stats.totalPnl >= 0 ? '+' : ''}${stats.totalPnl.toFixed(2)}`, color: stats.totalPnl >= 0 ? '#16a34a' : '#dc2626', valClass: stats.totalPnl >= 0 ? 'text-emerald-600 dark:text-emerald-400' : 'text-red-600 dark:text-red-400' }, { icon: AlertTriangle, label: 'Risk', val: `${p.risk_per_trade_percent}%`, color: '#f59e0b', valClass: 'text-[var(--foreground)]' }, ].map((item, i) => (
{item.label}
{item.val}
))}
Capital: {capitalHealth} {botState.accountSnapshot && ( LP: {coverageStatus} )} Lifecycle: {lifecycleStatus} Reconciliation: {reconciliationDrift} Locks: {lockStatus}
{/* Win rate section */}
Win Rate {stats.winRate.toFixed(1)}% · {stats.tradeCount} trades
= 50 ? '#16a34a' : stats.winRate >= 30 ? '#f59e0b' : '#dc2626', }} />
{/* ── Elevated inner panel: Details Table ── */}
{/* Table header */}
Configuration
{profitTarget && ( )}
Rules Active
{enabledRules.slice(0, 5).map(r => { const ruleDef = AVAILABLE_RULES.find(ar => ar.id === r.ruleId); return ruleDef ? ( ) : null; })} {ruleCount > 5 && ( +{ruleCount - 5} )}
Max Daily Loss ${maxLoss}
Max Open Trades {maxOpen}
Order Type {orderType}
Cooldown {cooldown}m
Entry Mode {entryMode === 'long_only' ? 'LONG ONLY' : 'BOTH SIDES'}
Profit Target ${profitTarget}
Voting Threshold {(minPassRatio * 100).toFixed(0)}%
{/* ── Bottom: Symbol tags ── */}
{p.symbols.split(',').slice(0, 5).map(s => ( {s.trim()} ))} {p.symbols.split(',').length > 5 && ( +{p.symbols.split(',').length - 5} )}
); })}
{/* ─── EDIT / CREATE DRAWER ─── */} {isDrawerOpen && createPortal(
setIsDrawerOpen(false)} />
{/* Drawer header */}
{isAddingMode ? : }

{isAddingMode ? 'Create Profile' : 'Edit Profile'}

{isAddingMode ? 'Configure a new trading strategy' : editingProfile.name}

{/* Drawer tab nav */}
{([ { id: 'settings' as const, label: 'General', icon: Settings }, { id: 'logic' as const, label: 'Rules', icon: Cpu }, { id: 'advanced' as const, label: 'Risk & Execution', icon: Shield }, ]).map(t => ( ))}
{/* Drawer content */}
{drawerTab === 'settings' && ( <> {/* Profile Name */}
setEditingProfile({ ...editingProfile, name: e.target.value })} placeholder="e.g. BTC Momentum Scalper" />
{/* Current User (read-only) */}
{(authUser?.email || '?').charAt(0).toUpperCase()}
{authUser?.email || 'Current User'} You
{/* Trading Symbols */}