import React, { useState, useEffect, useMemo } from 'react'; import { X, RotateCcw, ChevronDown, Zap, Plus, Minus, Calendar, ToggleLeft, ToggleRight, CheckSquare, Square, TrendingUp, TrendingDown, Wallet, Target, ShieldAlert, BadgePercent, Save, PlayCircle, Info, BarChart3, Maximize2, Circle, CheckCircle2 } from 'lucide-react'; import { StockItem, OrderType, MarketType, ReservedOrder, TradeOrder } from '../types'; import { DbService, HoldingItem } from '../services/dbService'; interface TradeModalProps { stock: StockItem; type: OrderType; onClose: () => void; onExecute: (order: Omit) => Promise; onImmediateOrder?: (order: Omit) => Promise; } type StrategyType = 'NONE' | 'PROFIT' | 'LOSS' | 'TRAILING_STOP'; const TradeModal: React.FC = ({ stock, type: initialType, onClose, onExecute, onImmediateOrder }) => { const [orderType, setOrderType] = useState(initialType); const isBuyMode = orderType === OrderType.BUY; const [holding, setHolding] = useState(null); const [buyingPower, setBuyingPower] = useState(0); const dbService = useMemo(() => new DbService(), []); useEffect(() => { const fetchData = async () => { const holdings = await dbService.getHoldings(); const currentHolding = holdings.find(h => h.code === stock.code) || null; setHolding(currentHolding); const summary = await dbService.getAccountSummary(); setBuyingPower(summary.buyingPower); }; fetchData(); }, [stock.code, dbService, orderType]); const plInfo = useMemo(() => { if (!holding) return null; const pl = (stock.price - holding.avgPrice) * holding.quantity; const percent = ((stock.price - holding.avgPrice) / holding.avgPrice) * 100; return { pl, percent }; }, [holding, stock.price]); const [monitoringEnabled, setMonitoringEnabled] = useState(false); const [immediateStart, setImmediateStart] = useState(true); // 전략 선택 (라디오 버튼 방식) const [activeStrategy, setActiveStrategy] = useState(isBuyMode ? 'TRAILING_STOP' : 'NONE'); // TS 설정 const [tsValue, setTsValue] = useState(3); const [tsUnit, setTsUnit] = useState<'PERCENT' | 'TICK' | 'AMOUNT'>('PERCENT'); // TS 내부 감시 시작 조건 (Trigger) const [triggerEnabled, setTriggerEnabled] = useState(false); const [triggerType, setTriggerType] = useState<'CURRENT' | 'HIGH' | 'LOW' | 'VOLUME'>('CURRENT'); const [triggerValue, setTriggerValue] = useState(stock.price); const [monCondition, setMonCondition] = useState<'ABOVE' | 'BELOW'>(isBuyMode ? 'BELOW' : 'ABOVE'); // 자동 조건 결정 로직 const applyAutoCondition = (type: string, buyMode: boolean) => { if (!buyMode) { setMonCondition('ABOVE'); // 매도시에는 현재/고/저/거래량 모두 이상 } else { // 매수시 if (type === 'CURRENT' || type === 'HIGH') { setMonCondition('BELOW'); // 현재가, 고가는 이하 } else { setMonCondition('ABOVE'); // 저가, 거래량은 이상 } } }; // 모드 변경 시 전략 및 조건 초기화 useEffect(() => { if (isBuyMode) { setActiveStrategy('TRAILING_STOP'); } else { setActiveStrategy('NONE'); } applyAutoCondition(triggerType, isBuyMode); if (triggerType !== 'VOLUME') { setTriggerValue(stock.price); } }, [isBuyMode, stock.price]); const [monTargetUnit, setMonTargetUnit] = useState<'PRICE' | 'PERCENT' | 'AMOUNT'>('PRICE'); const [profitValue, setProfitValue] = useState(stock.price * 1.1); const [lossValue, setLossValue] = useState(stock.price * 0.9); const [expiryDays, setExpiryDays] = useState(1); const [customExpiryDate, setCustomExpiryDate] = useState( new Date(Date.now() + 86400000).toISOString().split('T')[0] ); const [priceMethod, setPriceMethod] = useState<'CURRENT' | 'MARKET' | 'HIGH' | 'LOW'>('CURRENT'); const [tickOffset, setTickOffset] = useState(0); const [quantityMode, setQuantityMode] = useState<'DIRECT' | 'RATIO'>('DIRECT'); const [quantity, setQuantity] = useState(1); const [quantityRatio, setQuantityRatio] = useState(isBuyMode ? 10 : 100); const currencySymbol = stock.market === MarketType.DOMESTIC ? '원' : '$'; const handleMaxQuantity = () => { if (quantityMode === 'RATIO') { setQuantityRatio(100); } else { if (isBuyMode) { const maxQty = Math.floor(buyingPower / stock.price); setQuantity(maxQty > 0 ? maxQty : 1); } else if (holding) { setQuantity(holding.quantity); } } }; const handleReset = () => { setMonitoringEnabled(false); setImmediateStart(true); setActiveStrategy(isBuyMode ? 'TRAILING_STOP' : 'NONE'); setTriggerEnabled(false); setTriggerType('CURRENT'); setTriggerValue(stock.price); applyAutoCondition('CURRENT', isBuyMode); setPriceMethod('CURRENT'); setTickOffset(0); setQuantity(1); setQuantityRatio(isBuyMode ? 10 : 100); setExpiryDays(1); }; const finalQuantity = useMemo(() => { if (quantityMode === 'RATIO') { if (isBuyMode) { const targetBudget = buyingPower * (quantityRatio / 100); return Math.floor(targetBudget / stock.price) || 0; } else if (holding) { return Math.floor(holding.quantity * (quantityRatio / 100)); } } return quantity; }, [isBuyMode, quantityMode, quantity, quantityRatio, holding, buyingPower, stock.price]); const handleExecute = () => { if (monitoringEnabled) { let finalExpiry = new Date(); if (expiryDays > 0) finalExpiry.setDate(finalExpiry.getDate() + expiryDays); else finalExpiry = new Date(customExpiryDate); onExecute({ stockCode: stock.code, stockName: stock.name, type: orderType, quantity: finalQuantity, monitoringType: activeStrategy === 'TRAILING_STOP' ? 'TRAILING_STOP' : 'PRICE_TRIGGER', triggerPrice: triggerEnabled ? triggerValue : stock.price, trailingType: tsUnit === 'PERCENT' ? 'PERCENT' : 'AMOUNT', trailingValue: tsValue, market: stock.market, expiryDate: finalExpiry }); } else { if (onImmediateOrder) { onImmediateOrder({ stockCode: stock.code, stockName: stock.name, type: orderType, price: priceMethod === 'MARKET' ? 0 : stock.price, quantity: finalQuantity }); } } onClose(); }; const StrategyRadio = ({ type, label, icon: Icon }: { type: StrategyType, label: string, icon: any }) => { const isSelected = activeStrategy === type; return (
setActiveStrategy(type)} className={`p-4 rounded-2xl border-2 transition-all cursor-pointer flex items-center justify-between ${isSelected ? 'bg-amber-50/40 border-amber-300 shadow-sm' : 'bg-transparent border-slate-100 opacity-60'}`} >
{isSelected ? : } {label}
{isSelected && type === 'TRAILING_STOP' && (
e.stopPropagation()}> setTsValue(Number(e.target.value))} />
)} {isSelected && type === 'PROFIT' && (
e.stopPropagation()}> setProfitValue(Number(e.target.value))} /> {monTargetUnit === 'PERCENT' ? '%' : currencySymbol}
)} {isSelected && type === 'LOSS' && (
e.stopPropagation()}> setLossValue(Number(e.target.value))} /> {monTargetUnit === 'PERCENT' ? '%' : currencySymbol}
)}
); }; return (
{/* 헤더 */}
{/* 상단 통합 정보 */}
{stock.name[0]}

{stock.name}

{stock.market === MarketType.DOMESTIC ? 'KRX' : 'NYSE'} {stock.code}

= 0 ? 'text-rose-500' : 'text-blue-600'}`}> {stock.market === MarketType.DOMESTIC ? stock.price.toLocaleString() : `$${stock.price}`}

= 0 ? 'text-rose-500' : 'text-blue-600'}`}> {stock.changePercent >= 0 ? : } {Math.abs(stock.changePercent)}%

거래량

{stock.volume.toLocaleString()}

+5.2%

사용가능 예수금

{buyingPower.toLocaleString()}{currencySymbol}

{holding ? (

보유수량

{holding.quantity} EA

평가수익금

= 0 ? 'text-rose-500' : 'text-blue-600'}`}> {plInfo!.pl > 0 ? '+' : ''}{plInfo!.pl.toLocaleString()} ({plInfo!.percent.toFixed(2)}%)

) : (

미보유 종목

)}

1. 자동 감시 전략

{!isBuyMode ? (
{['PRICE', 'PERCENT', 'AMOUNT'].map(unit => ( ))}
) : (
)} {/* TS 내부 감시 시작 조건 설정 (TS 선택 시에만 표시) */} {activeStrategy === 'TRAILING_STOP' && (
setTriggerEnabled(!triggerEnabled)} className="flex items-center gap-2 cursor-pointer group" > {triggerEnabled ? : } 목표가 설정
{triggerEnabled && (
setTriggerValue(Number(e.target.value))} />
)} {!triggerEnabled && (

※ 조건 미설정 시 현재 시점부터 즉시 반등 감시를 시작합니다.

)}
)}
유효기간
{[1, 5, 30].map(d => ( ))}

2. 매매 실행 조건

{['CURRENT', 'MARKET', 'HIGH', 'LOW'].map((method) => ( ))}
{tickOffset > 0 ? `+${tickOffset}` : tickOffset}
{isBuyMode ? `예수금` : `보유수량`}
{quantityMode === 'DIRECT' ? (
setQuantity(Math.max(1, parseInt(e.target.value) || 0))} />
EA / UNIT
) : (
setQuantityRatio(Math.min(100, Math.max(0, parseInt(e.target.value) || 0)))} />
PERCENT
)}

{finalQuantity}주를 {isBuyMode ? '매수' : '매도'}합니다.

{/* 푸터 */}
{monitoringEnabled && (
즉시 실행 엔진 자동 가동
)}
); }; export default TradeModal;