import React, { useState } from 'react'; import { portfolioAPI, rebalancingAPI } from '../../api/client'; interface PortfolioAsset { ticker: string; target_ratio: number; } interface CurrentHolding { ticker: string; quantity: number; } const RebalancingDashboard: React.FC = () => { const [portfolioName, setPortfolioName] = useState(''); const [assets, setAssets] = useState([ { ticker: '', target_ratio: 0 }, ]); const [currentHoldings, setCurrentHoldings] = useState([]); const [cash, setCash] = useState(0); const [portfolioId, setPortfolioId] = useState(null); const [recommendations, setRecommendations] = useState(null); const [loading, setLoading] = useState(false); const addAsset = () => { setAssets([...assets, { ticker: '', target_ratio: 0 }]); }; const removeAsset = (index: number) => { setAssets(assets.filter((_, i) => i !== index)); }; const updateAsset = (index: number, field: keyof PortfolioAsset, value: any) => { const newAssets = [...assets]; newAssets[index] = { ...newAssets[index], [field]: value }; setAssets(newAssets); }; const createPortfolio = async () => { try { setLoading(true); // 목표 비율 합계 검증 const totalRatio = assets.reduce((sum, asset) => sum + asset.target_ratio, 0); if (Math.abs(totalRatio - 100) > 0.01) { alert(`목표 비율의 합은 100%여야 합니다 (현재: ${totalRatio}%)`); return; } const response = await portfolioAPI.create({ name: portfolioName, description: '퇴직연금 포트폴리오', assets: assets, }); setPortfolioId(response.data.id); alert('포트폴리오가 생성되었습니다!'); // 현재 보유량 초기화 const initialHoldings = assets.map(asset => ({ ticker: asset.ticker, quantity: 0, })); setCurrentHoldings(initialHoldings); } catch (error: any) { alert(`포트폴리오 생성 오류: ${error.response?.data?.detail || error.message}`); } finally { setLoading(false); } }; const updateHolding = (index: number, field: keyof CurrentHolding, value: any) => { const newHoldings = [...currentHoldings]; newHoldings[index] = { ...newHoldings[index], [field]: value }; setCurrentHoldings(newHoldings); }; const calculateRebalancing = async () => { if (!portfolioId) { alert('먼저 포트폴리오를 생성하세요.'); return; } try { setLoading(true); const response = await rebalancingAPI.calculate({ portfolio_id: portfolioId, current_holdings: currentHoldings, cash: cash, }); setRecommendations(response.data); } catch (error: any) { alert(`리밸런싱 계산 오류: ${error.response?.data?.detail || error.message}`); } finally { setLoading(false); } }; const totalRatio = assets.reduce((sum, asset) => sum + asset.target_ratio, 0); return (

퇴직연금 리밸런싱

{/* 포트폴리오 생성 */} {!portfolioId ? (
setPortfolioName(e.target.value)} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500" placeholder="예: 내 퇴직연금 포트폴리오" />
합계: {totalRatio}%
{assets.map((asset, index) => (
updateAsset(index, 'ticker', e.target.value)} placeholder="종목코드 (예: 005930)" className="flex-1 rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500" /> updateAsset(index, 'target_ratio', parseFloat(e.target.value))} placeholder="비율 (%)" step="0.1" className="w-32 rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500" />
))}
) : (

포트폴리오 생성 완료: {portfolioName}

{/* 현재 보유량 입력 */}
{currentHoldings.map((holding, index) => (
updateHolding(index, 'quantity', parseFloat(e.target.value))} placeholder="보유 수량" step="1" className="w-32 rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500" />
))}
{/* 현금 */}
setCash(parseFloat(e.target.value))} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500" placeholder="0" step="10000" />
)}
{/* 리밸런싱 결과 */} {recommendations && (

리밸런싱 추천

총 자산: {recommendations.total_value.toLocaleString()}원

현금: {recommendations.cash.toLocaleString()}원

매수: {recommendations.summary.buy}건, 매도: {recommendations.summary.sell}건, 유지: {recommendations.summary.hold}건

{recommendations.recommendations.map((rec: any, index: number) => ( ))}
종목 현재 비율 목표 비율 수량 액션
{rec.ticker}
{rec.name}
{rec.current_ratio.toFixed(2)}% {rec.target_ratio.toFixed(2)}% {rec.delta_quantity}주 {rec.action === 'buy' ? '매수' : rec.action === 'sell' ? '매도' : '유지'}
)}
); }; export default RebalancingDashboard;