import { useState } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { TrendingUp, TrendingDown, Target, AlertTriangle, CheckCircle, Info, Play, RefreshCw, BookOpen, Zap, ChevronDown, ChevronUp } from 'lucide-react' import api from '../lib/api' // ── 타입 정의 ───────────────────────────────────────────────────────────────── interface PerformanceSummary { total_trades: number win_trades: number loss_trades: number win_rate: number avg_win_pct: number avg_loss_pct: number profit_factor: number gross_profit: number gross_loss: number net_pnl: number max_consecutive_loss: number risk_reward_ratio: number } interface PerformanceData { summary: PerformanceSummary by_strategy: Record by_exit_type: Record worst_stocks: { stock_code: string; net_pnl: number }[] best_stocks: { stock_code: string; net_pnl: number }[] period_days: number total_trades: number generated_at: string } interface Insight { level: 'info' | 'warning' | 'danger' message: string } interface OptimizationLog { id: number run_date: string trigger: string best_sharpe: number | null current_sharpe: number | null sharpe_improvement_pct: number | null best_win_rate: number | null best_mdd: number | null applied: boolean apply_reason: string best_params: Record | null previous_params: Record | null total_combinations: number | null created_at: string } // ── 유틸 함수 ───────────────────────────────────────────────────────────────── const fmt = (n: number | null | undefined, digits = 2) => n == null ? '-' : n.toFixed(digits) const fmtKRW = (n: number | null | undefined) => n == null ? '-' : `${n >= 0 ? '+' : ''}${n.toLocaleString('ko-KR')}원` // ── 서브 컴포넌트: 통계 카드 ────────────────────────────────────────────────── const StatCard = ({ label, value, sub, color = 'blue', icon: Icon, }: { label: string value: string | number sub?: string color?: 'blue' | 'green' | 'red' | 'yellow' | 'purple' icon?: React.ElementType }) => { const colorMap = { blue: 'border-blue-500/30 bg-blue-500/5', green: 'border-green-500/30 bg-green-500/5', red: 'border-red-500/30 bg-red-500/5', yellow: 'border-yellow-500/30 bg-yellow-500/5', purple: 'border-purple-500/30 bg-purple-500/5', } const textMap = { blue: 'text-blue-400', green: 'text-green-400', red: 'text-red-400', yellow: 'text-yellow-400', purple: 'text-purple-400', } return (
{Icon && } {label}
{value}
{sub &&
{sub}
}
) } // ── 서브 컴포넌트: 인사이트 배지 ───────────────────────────────────────────── const InsightBadge = ({ insight }: { insight: Insight }) => { const map = { info: { bg: 'bg-blue-500/10 border-blue-500/20', text: 'text-blue-300', Icon: Info }, warning: { bg: 'bg-yellow-500/10 border-yellow-500/20', text: 'text-yellow-300', Icon: AlertTriangle }, danger: { bg: 'bg-red-500/10 border-red-500/20', text: 'text-red-300', Icon: AlertTriangle }, } const { bg, text, Icon } = map[insight.level] return (
{insight.message}
) } // ── 메인 컴포넌트 ───────────────────────────────────────────────────────────── export default function Analysis() { const queryClient = useQueryClient() const [days, setDays] = useState(30) const [expandedOptLog, setExpandedOptLog] = useState(null) // 성과 데이터 const { data: perf, isLoading: perfLoading } = useQuery({ queryKey: ['analysis-performance', days], queryFn: async () => (await api.get(`/analysis/performance?days=${days}`)).data, refetchInterval: 60_000, }) // 인사이트 const { data: insightsData, isLoading: insightsLoading } = useQuery<{ insights: Insight[] last_optimization_date: string | null }>({ queryKey: ['analysis-insights', days], queryFn: async () => (await api.get(`/analysis/insights?days=${days}`)).data, refetchInterval: 60_000, }) // 최적화 이력 const { data: optHistory = [], isLoading: optHistLoading } = useQuery({ queryKey: ['optimization-history'], queryFn: async () => (await api.get('/analysis/optimization-history?limit=10')).data, refetchInterval: 30_000, }) // 최적화 상태 const { data: optStatus } = useQuery<{ running: boolean }>({ queryKey: ['optimization-status'], queryFn: async () => (await api.get('/analysis/optimization-status')).data, refetchInterval: 5_000, }) // 일지 생성 뮤테이션 const generateJournals = useMutation({ mutationFn: () => api.post('/analysis/generate-journals?days=7'), onSuccess: () => { alert('일지 생성을 시작했습니다. 잠시 후 매매 로그에서 확인하세요.') }, }) // 전략 최적화 뮤테이션 const optimizeStrategy = useMutation({ mutationFn: () => api.post(`/analysis/optimize-strategy?days=${days}&period_days=90`), onSuccess: () => { alert('전략 최적화를 시작했습니다. 수 분 후 최적화 이력에서 결과를 확인하세요.') setTimeout(() => queryClient.invalidateQueries({ queryKey: ['optimization-history'] }), 10_000) }, }) const summary = perf?.summary const insights = insightsData?.insights || [] const isOptimizing = optStatus?.running || optimizeStrategy.isPending return (
{/* 헤더 */}

성과 분석

매매 일지 자동 생성 & 전략 자동 최적화

{/* 성과 요약 카드 */} {perfLoading ? (
성과 데이터 로딩 중...
) : summary && summary.total_trades > 0 ? (
= 50 ? 'green' : summary.win_rate >= 40 ? 'yellow' : 'red'} icon={Target} /> = 1.5 ? 'green' : summary.profit_factor >= 1.0 ? 'yellow' : 'red'} icon={TrendingUp} /> = 1.5 ? 'green' : summary.risk_reward_ratio >= 1.0 ? 'yellow' : 'red'} icon={TrendingDown} /> = 0 ? 'green' : 'red'} icon={summary.net_pnl >= 0 ? TrendingUp : TrendingDown} />
) : (
최근 {days}일 내 완료된 매도 거래가 없습니다.
)}
{/* 전략별 성과 */}

전략별 성과

{perf && Object.keys(perf.by_strategy).length > 0 ? (
{Object.entries(perf.by_strategy).map(([strat, stats]) => (
{strat} {stats.total}건
= 50 ? 'text-green-400' : 'text-red-400'}> 승률 {fmt(stats.win_rate)}% = 0 ? 'text-green-400' : 'text-red-400'}> 평균 {stats.avg_return >= 0 ? '+' : ''}{fmt(stats.avg_return)}%
))}
) : (

데이터 없음

)}
{/* 청산 유형별 */}

청산 유형별 통계

{perf && Object.keys(perf.by_exit_type).length > 0 ? (
{Object.entries(perf.by_exit_type).map(([etype, stats]) => (
{etype} {stats.count}건
= 0 ? 'text-green-400' : 'text-red-400'}`}> 평균 {stats.avg_return >= 0 ? '+' : ''}{fmt(stats.avg_return)}%
))}
) : (

데이터 없음

)}
{/* 인사이트 */}

전략 인사이트

{insightsLoading ? (

분석 중...

) : insights.length > 0 ? (
{insights.map((ins, i) => )}
) : (

분석할 데이터가 없습니다.

)}
{/* 매매일지 */}

매매일지 자동 생성

최근 7일 내 일지가 없는 완료 거래에 대해 템플릿 기반 매매일지를 자동 생성합니다. 장 종료 후 16:00에 자동으로도 실행됩니다.

{/* 전략 자동 최적화 */}

전략 자동 최적화

{insightsData?.last_optimization_date && (

마지막 최적화: {new Date(insightsData.last_optimization_date).toLocaleDateString('ko-KR')}

)}

RSI, 손절선, ATR 배수, 볼린저 밴드 파라미터를 그리드 서치로 최적화합니다. Sharpe Ratio 10% 이상 개선 시 자동으로 전략 설정에 적용됩니다. (매일 16:00 KST 자동 실행, 최근 7일 거래 5건 이상 시)

{/* 최적화 이력 */}

최적화 이력

{optHistLoading ? (

로딩 중...

) : optHistory.length > 0 ? (
{optHistory.map((log) => (
{expandedOptLog === log.id && (
최적 Sharpe

{fmt(log.best_sharpe, 3)}

최적 승률

{fmt(log.best_win_rate, 1)}%

최적 MDD

{fmt(log.best_mdd, 1)}%

사유: {log.apply_reason || '-'}
{log.best_params && (
최적 파라미터:
                                                        {JSON.stringify(log.best_params, null, 2)}
                                                    
)}
)}
))}
) : (

최적화 이력이 없습니다.

)}
{/* 최악/최고 종목 */} {perf && (perf.worst_stocks.length > 0 || perf.best_stocks.length > 0) && (

최대 손실 종목 Top5

{perf.worst_stocks.map((s) => (
{s.stock_code} {fmtKRW(s.net_pnl)}
))}

최대 수익 종목 Top5

{perf.best_stocks.map((s) => (
{s.stock_code} 0 ? "text-green-400" : "text-red-400"}>{fmtKRW(s.net_pnl)}
))}
)}
) }