/* global React, AC_CONFIG, apiGet, apiPost, krw, Sparkline */ const { useState, useEffect, useRef, useMemo, useCallback } = React; // =============== STATE HOOK — live snapshot polling =============== function useBotState() { const [snapshot, setSnapshot] = useState(null); const [error, setError] = useState(null); const lastFetchRef = useRef(0); const refresh = useCallback(async () => { try { const data = await apiGet(AC_CONFIG.snapshotUrl); window.TICKERS = data.tickers || []; window.STRATEGIES = data.strategies || []; window.ORDERS_SEED = data.orders || []; window.AUDIT_SEED = data.audit || []; window.WEBHOOKS_SEED = data.webhooks || []; window.PORTFOLIO = data.portfolio || []; lastFetchRef.current = Date.now(); setSnapshot(data); setError(null); } catch (err) { setError(err.message || String(err)); } }, []); useEffect(() => { refresh(); const id = setInterval(refresh, AC_CONFIG.pollIntervalMs); return () => clearInterval(id); }, [refresh]); // ===== mutators (POST + immediate refresh) ===== const setRunning = useCallback(async (next) => { try { await apiPost(`${AC_CONFIG.controlUrl}/${next ? 'start' : 'stop'}`, {}); refresh(); } catch (err) { setError(err.message || String(err)); } }, [refresh]); const setMode = useCallback(async (next) => { try { await apiPost(`${AC_CONFIG.controlUrl}/mode`, { mode: next }); refresh(); } catch (err) { setError(err.message || String(err)); } }, [refresh]); const setKillSwitch = useCallback(async (next) => { try { await apiPost(`${AC_CONFIG.controlUrl}/kill-switch`, { value: !!next }); refresh(); } catch (err) { setError(err.message || String(err)); } }, [refresh]); const reload = useCallback(async () => { try { await apiPost(`${AC_CONFIG.controlUrl}/reload`, {}); refresh(); } catch (err) { setError(err.message || String(err)); } }, [refresh]); const setActiveStrat = useCallback(async (id) => { try { await apiPost(`${AC_CONFIG.controlUrl}/strategy`, { strategy: id }); refresh(); } catch (err) { setError(err.message || String(err)); } }, [refresh]); const setPortfolio = useCallback(async (tickers) => { try { await apiPost(`${AC_CONFIG.controlUrl}/portfolio`, { tickers }); await refresh(); return null; } catch (err) { const msg = err.message || String(err); setError(msg); return msg; } }, [refresh]); // 빈 스냅샷 fallback (최초 폴 직전 첫 렌더) const empty = useMemo(() => ({ running: false, mode: 'paper', is_live: false, live_view: false, kill_switch: false, tickers: [], portfolio: [], watchlist: [], orders: [], audit: [], webhooks: [], equity: [], strategies: [], slots_used: 0, slots_max: 3, krw_balance: 0, position_value: 0, position_cost: 0, total_cost_krw: 0, total_value_krw: 0, total_equity: 0, today_pnl: 0, today_pnl_pct: 0, cum_pnl: 0, cum_pnl_pct: 0, daily_loss_limit_krw: 0, daily_loss_limit_pct: 0, loss_used_pct: 0, win_rate_30d: 0, sharpe_30d: 0, active_strategy: '', tick_count: 0, risk: {}, schedule: {}, }), []); const data = snapshot || empty; return { error, refresh, running: !!data.running, mode: data.mode || 'paper', isLive: !!data.is_live, liveView: !!data.live_view, killSwitch: !!data.kill_switch, tickers: data.tickers || [], portfolio: data.portfolio || [], watchlist: data.watchlist || [], orders: data.orders || [], audit: data.audit || [], webhooks: data.webhooks || [], equity: data.equity || [], strategies: data.strategies || [], activeStrat: data.active_strategy || '', tickCount: data.tick_count || 0, positions: (data.tickers || []).filter(t => (t.qty || 0) > 0), slotsUsed: data.slots_used || 0, slotsMax: data.slots_max || 3, krwBalance: data.krw_balance || 0, positionValue: data.position_value || 0, positionCost: data.position_cost || 0, totalCostKrw: data.total_cost_krw || 0, totalValueKrw: data.total_value_krw || 0, totalEquity: data.total_equity || 0, todayPnl: data.today_pnl || 0, todayPnlPct: data.today_pnl_pct || 0, cumPnl: data.cum_pnl || 0, cumPnlPct: data.cum_pnl_pct || 0, dailyLossLimit: data.daily_loss_limit_krw || 0, dailyLossLimitPct: data.daily_loss_limit_pct || 0, lossUsedPct: data.loss_used_pct || 0, winRate30d: data.win_rate_30d || 0, sharpe30d: data.sharpe_30d || 0, risk: data.risk || {}, schedule: data.schedule || {}, setRunning, setMode, setKillSwitch, reload, setActiveStrat, setPortfolio, }; } // =============== TOP BAR =============== function TopBar({ page, setPage, mode, running, killSwitch }) { const [now, setNow] = useState(new Date()); useEffect(() => { const id = setInterval(() => setNow(new Date()), 1000); return () => clearInterval(id); }, []); const navItems = [ { id: 'dash', label: '대시보드', n: 'F1' }, { id: 'strat', label: '전략', n: 'F2' }, { id: 'pos', label: '포지션', n: 'F3' }, { id: 'orders', label: '주문', n: 'F4' }, { id: 'webhooks', label: '웹훅', n: 'F5' }, { id: 'risk', label: '리스크', n: 'F6' }, { id: 'audit', label: '감사', n: 'F7' }, { id: 'settings', label: '설정', n: 'F8' }, ]; const t = now.toTimeString().slice(0, 8); const d = now.toISOString().slice(0, 10); return ( AUTO_COIN v{(window.AC_CONFIG && window.AC_CONFIG.version) || ''} {navItems.map(item => ( setPage(item.id)}> {item.n} {item.label} ))} {running ? '스케줄 · 실행중' : '정지'} {mode === 'live' ? '실거래' : '페이퍼'} {killSwitch && ( 킬스위치 )} KST{d} {t} ); } // =============== SIDE RAIL =============== function SideRail(props) { const { running, setRunning, mode, setMode, killSwitch, setKillSwitch, reload, slotsUsed, slotsMax, krwBalance, positionValue, tickCount, activeStrat, strategies, schedule, } = props; const stratLabel = (strategies.find(s => s.id === activeStrat)?.code) || '—'; const tickInterval = (schedule.tick_seconds ? `${schedule.tick_seconds}s` : '—'); return ( ); } // =============== STATUS BAR (footer) =============== function StatusBar({ tickCount, mode, running, error }) { return ( ); } Object.assign(window, { useBotState, TopBar, SideRail, StatusBar });