import React, { useState } from 'react'; import { useDraggablePanel } from '../hooks/useDraggablePanel'; import { placePaperOrder, placeLiveOrder, loadLiveSummary } from '../utils/backendApi'; import { formatSignalTime } from '../utils/tradeSignalDisplay'; import { TRADE_BUY_COLOR, TRADE_SELL_COLOR } from '../utils/tradeSignalColors'; import { useTradeAlertTimeFormat } from '../utils/tradeAlertTimeFormat'; export interface TradeSignalInfo { market: string; signalType: 'BUY' | 'SELL'; price: number; candleTime: number; strategyName?: string | null; strategyId?: number | null; executionType?: string; candleType?: string; } interface Props { signal: TradeSignalInfo; onClose: () => void; centered?: boolean; tradingMode?: string; hasUpbitKeys?: boolean; paperTradingEnabled?: boolean; liveAutoTradeBudgetPct?: number; paperAutoTradeBudgetPct?: number; onOrderDone?: () => void; } const fmt = (n: number): string => n > 0 ? n.toLocaleString('ko-KR', { maximumFractionDigits: 0 }) : '0'; function execLabel(type?: string, candleType?: string): string { const base = type === 'REALTIME_TICK' ? '실시간 틱' : '봉 마감'; return candleType ? `${base}(${candleType})` : base; } const CandleIcon: React.FC<{ color: string }> = ({ color }) => ( ); export const TradeAlertModal: React.FC = ({ signal, onClose, centered = false, tradingMode = 'PAPER', hasUpbitKeys = false, paperTradingEnabled = true, liveAutoTradeBudgetPct = 95, paperAutoTradeBudgetPct = 95, onOrderDone, }) => { useTradeAlertTimeFormat(); const isBuy = signal.signalType === 'BUY'; const accentColor = isBuy ? TRADE_BUY_COLOR : TRADE_SELL_COLOR; const accentGlow = isBuy ? 'rgba(239, 83, 80, 0.28)' : 'rgba(77, 171, 247, 0.28)'; const signalLabel = isBuy ? 'BUY' : 'SELL'; const signalKo = isBuy ? '매수 알림' : '매도 알림'; const coinSymbol = signal.market.replace(/^[A-Z]+-/, ''); const fiatSymbol = signal.market.split('-')[0] ?? 'KRW'; const useLive = (tradingMode === 'LIVE' || tradingMode === 'BOTH') && hasUpbitKeys; const usePaper = (tradingMode === 'PAPER' || tradingMode === 'BOTH') && paperTradingEnabled; const [limitPrice, setLimitPrice] = useState(fmt(signal.price)); const [memo, setMemo] = useState(''); const [activePct, setActivePct] = useState(null); const [ordering, setOrdering] = useState(false); const { panelRef, dragging, onHeaderPointerDown, headerTouchStyle, panelStyle, headerCursor, } = useDraggablePanel({ centerOnMount: centered, initialPosition: { x: 40, y: 40 }, }); const handlePct = (pct: string) => { setActivePct(pct); }; const resolveBudgetPct = (): number => { if (activePct === '25%') return 25; if (activePct === '50%') return 50; if (activePct === '100%') return 100; return paperAutoTradeBudgetPct; }; const handleOrder = async (type: 'limit' | 'market') => { if (ordering) return; setOrdering(true); try { const side = signal.signalType; const budgetPct = resolveBudgetPct(); const orderPrice = type === 'limit' ? Number(limitPrice.replace(/,/g, '')) : signal.price; let executed = false; if (usePaper) { await placePaperOrder({ market: signal.market, side, orderKind: type === 'market' ? 'market' : 'limit', price: orderPrice, quantity: 0, budgetPct, source: 'MANUAL', strategyId: signal.strategyId ?? undefined, }); executed = true; } if (useLive && type === 'market') { const livePct = activePct === '25%' ? 25 : activePct === '50%' ? 50 : activePct === '100%' ? 100 : liveAutoTradeBudgetPct; const summary = isBuy ? await loadLiveSummary() : null; const krwAmount = isBuy && summary ? summary.krwBalance * livePct / 100 : undefined; await placeLiveOrder({ market: signal.market, side, orderKind: 'market', krwAmount, strategyId: signal.strategyId ?? undefined, }); executed = true; } if (!executed) { window.alert('주문 가능한 채널이 없습니다. 설정에서 모의투자 또는 실거래 API를 확인하세요.'); return; } onOrderDone?.(); onClose(); } catch (e) { window.alert((e as Error).message); } finally { setOrdering(false); } }; return (
e.stopPropagation()} >
{signalLabel} ALERT ({signalKo})
{signal.market} {isBuy ? '↑' : '↓'} ₩{fmt(signal.price)}
발생 시각: {formatSignalTime(signal.candleTime)}
전략명 {signal.strategyName ?? '(전략 미지정)'}
실행 방식 {execLabel(signal.executionType, signal.candleType)}
자산 비율
{['25%', '50%', '100%'].map(p => ( ))}
지정가
setLimitPrice(e.target.value)} inputMode="numeric" />
메모 setMemo(e.target.value)} placeholder="메모 입력 (선택)" />

{useLive && usePaper && '※ 모의·실거래 병행: 시장가는 모의·실거래 모두 체결, 지정가는 모의 계좌 체결'} {useLive && !usePaper && '※ 업비트 실거래 시장가 주문'} {!useLive && usePaper && '※ 모의투자 계좌 체결 (자동매매와 동일 내역 반영)'} {!useLive && !usePaper && '※ 설정에서 모의투자 또는 API 키를 활성화하세요'}

); }; export default TradeAlertModal;