feat: add strategy pages (multi-factor, quality, value-momentum)
- Multi-factor strategy page with weight configuration - Quality strategy page with F-Score filtering - Value-momentum strategy page Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
475a056bc8
commit
aa2047a922
250
frontend/src/app/strategy/multi-factor/page.tsx
Normal file
250
frontend/src/app/strategy/multi-factor/page.tsx
Normal file
@ -0,0 +1,250 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Sidebar from '@/components/layout/Sidebar';
|
||||
import Header from '@/components/layout/Header';
|
||||
import { api } from '@/lib/api';
|
||||
|
||||
interface User {
|
||||
id: number;
|
||||
username: string;
|
||||
}
|
||||
|
||||
interface StockFactor {
|
||||
ticker: string;
|
||||
name: string;
|
||||
market: string;
|
||||
sector_name: string | null;
|
||||
market_cap: number | null;
|
||||
close_price: number | null;
|
||||
per: number | null;
|
||||
pbr: number | null;
|
||||
value_score: number | null;
|
||||
quality_score: number | null;
|
||||
momentum_score: number | null;
|
||||
total_score: number | null;
|
||||
rank: number | null;
|
||||
}
|
||||
|
||||
interface StrategyResult {
|
||||
strategy_name: string;
|
||||
base_date: string;
|
||||
universe_count: number;
|
||||
result_count: number;
|
||||
stocks: StockFactor[];
|
||||
}
|
||||
|
||||
export default function MultiFactorPage() {
|
||||
const router = useRouter();
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [initialLoading, setInitialLoading] = useState(true);
|
||||
const [result, setResult] = useState<StrategyResult | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Weights
|
||||
const [valueWeight, setValueWeight] = useState(0.25);
|
||||
const [qualityWeight, setQualityWeight] = useState(0.25);
|
||||
const [momentumWeight, setMomentumWeight] = useState(0.25);
|
||||
const [topN, setTopN] = useState(30);
|
||||
|
||||
useEffect(() => {
|
||||
const init = async () => {
|
||||
try {
|
||||
const userData = await api.getCurrentUser() as User;
|
||||
setUser(userData);
|
||||
} catch {
|
||||
router.push('/login');
|
||||
} finally {
|
||||
setInitialLoading(false);
|
||||
}
|
||||
};
|
||||
init();
|
||||
}, [router]);
|
||||
|
||||
const runStrategy = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const data = await api.post<StrategyResult>('/api/strategy/multi-factor', {
|
||||
universe: {
|
||||
markets: ['KOSPI', 'KOSDAQ'],
|
||||
exclude_stock_types: ['spac', 'preferred', 'reit'],
|
||||
},
|
||||
top_n: topN,
|
||||
weights: {
|
||||
value: valueWeight,
|
||||
quality: qualityWeight,
|
||||
momentum: momentumWeight,
|
||||
low_vol: 1 - valueWeight - qualityWeight - momentumWeight,
|
||||
},
|
||||
});
|
||||
setResult(data);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Strategy execution failed');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const formatNumber = (value: number | null, decimals: number = 2) => {
|
||||
if (value === null) return '-';
|
||||
return value.toFixed(decimals);
|
||||
};
|
||||
|
||||
const formatCurrency = (value: number | null) => {
|
||||
if (value === null) return '-';
|
||||
return new Intl.NumberFormat('ko-KR').format(value);
|
||||
};
|
||||
|
||||
if (initialLoading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="text-gray-500">Loading...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen">
|
||||
<Sidebar />
|
||||
<div className="flex-1">
|
||||
<Header username={user?.username} />
|
||||
<main className="p-6">
|
||||
<h1 className="text-2xl font-bold text-gray-800 mb-6">멀티 팩터 전략</h1>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Settings */}
|
||||
<div className="bg-white rounded-lg shadow p-6 mb-6">
|
||||
<h2 className="text-lg font-semibold mb-4">전략 설정</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-4">
|
||||
<div>
|
||||
<label htmlFor="value-weight" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
밸류 가중치
|
||||
</label>
|
||||
<input
|
||||
id="value-weight"
|
||||
type="number"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.05"
|
||||
value={valueWeight}
|
||||
onChange={(e) => setValueWeight(parseFloat(e.target.value))}
|
||||
className="w-full px-3 py-2 border rounded"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="quality-weight" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
퀄리티 가중치
|
||||
</label>
|
||||
<input
|
||||
id="quality-weight"
|
||||
type="number"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.05"
|
||||
value={qualityWeight}
|
||||
onChange={(e) => setQualityWeight(parseFloat(e.target.value))}
|
||||
className="w-full px-3 py-2 border rounded"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="momentum-weight" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
모멘텀 가중치
|
||||
</label>
|
||||
<input
|
||||
id="momentum-weight"
|
||||
type="number"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.05"
|
||||
value={momentumWeight}
|
||||
onChange={(e) => setMomentumWeight(parseFloat(e.target.value))}
|
||||
className="w-full px-3 py-2 border rounded"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="top-n" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
상위 종목 수
|
||||
</label>
|
||||
<input
|
||||
id="top-n"
|
||||
type="number"
|
||||
min="1"
|
||||
max="100"
|
||||
value={topN}
|
||||
onChange={(e) => setTopN(parseInt(e.target.value))}
|
||||
className="w-full px-3 py-2 border rounded"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={runStrategy}
|
||||
disabled={loading}
|
||||
className="px-6 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:bg-blue-400"
|
||||
>
|
||||
{loading ? '실행 중...' : '전략 실행'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Results */}
|
||||
{result && (
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="p-4 border-b">
|
||||
<h2 className="text-lg font-semibold">
|
||||
결과 ({result.result_count}/{result.universe_count} 종목)
|
||||
</h2>
|
||||
<p className="text-sm text-gray-500">기준일: {result.base_date}</p>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th scope="col" className="px-4 py-3 text-left text-sm font-medium text-gray-600">순위</th>
|
||||
<th scope="col" className="px-4 py-3 text-left text-sm font-medium text-gray-600">종목</th>
|
||||
<th scope="col" className="px-4 py-3 text-left text-sm font-medium text-gray-600">섹터</th>
|
||||
<th scope="col" className="px-4 py-3 text-right text-sm font-medium text-gray-600">시가총액(억)</th>
|
||||
<th scope="col" className="px-4 py-3 text-right text-sm font-medium text-gray-600">현재가</th>
|
||||
<th scope="col" className="px-4 py-3 text-right text-sm font-medium text-gray-600">PER</th>
|
||||
<th scope="col" className="px-4 py-3 text-right text-sm font-medium text-gray-600">PBR</th>
|
||||
<th scope="col" className="px-4 py-3 text-right text-sm font-medium text-gray-600">밸류</th>
|
||||
<th scope="col" className="px-4 py-3 text-right text-sm font-medium text-gray-600">퀄리티</th>
|
||||
<th scope="col" className="px-4 py-3 text-right text-sm font-medium text-gray-600">모멘텀</th>
|
||||
<th scope="col" className="px-4 py-3 text-right text-sm font-medium text-gray-600">종합</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y">
|
||||
{result.stocks.map((stock) => (
|
||||
<tr key={stock.ticker} className="hover:bg-gray-50">
|
||||
<td className="px-4 py-3 text-sm font-medium">{stock.rank}</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="font-medium">{stock.ticker}</div>
|
||||
<div className="text-xs text-gray-500">{stock.name}</div>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm">{stock.sector_name || '-'}</td>
|
||||
<td className="px-4 py-3 text-sm text-right">{formatCurrency(stock.market_cap)}</td>
|
||||
<td className="px-4 py-3 text-sm text-right">{formatCurrency(stock.close_price)}</td>
|
||||
<td className="px-4 py-3 text-sm text-right">{formatNumber(stock.per)}</td>
|
||||
<td className="px-4 py-3 text-sm text-right">{formatNumber(stock.pbr)}</td>
|
||||
<td className="px-4 py-3 text-sm text-right">{formatNumber(stock.value_score)}</td>
|
||||
<td className="px-4 py-3 text-sm text-right">{formatNumber(stock.quality_score)}</td>
|
||||
<td className="px-4 py-3 text-sm text-right">{formatNumber(stock.momentum_score)}</td>
|
||||
<td className="px-4 py-3 text-sm text-right font-medium">{formatNumber(stock.total_score)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
216
frontend/src/app/strategy/quality/page.tsx
Normal file
216
frontend/src/app/strategy/quality/page.tsx
Normal file
@ -0,0 +1,216 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Sidebar from '@/components/layout/Sidebar';
|
||||
import Header from '@/components/layout/Header';
|
||||
import { api } from '@/lib/api';
|
||||
|
||||
interface User {
|
||||
id: number;
|
||||
username: string;
|
||||
}
|
||||
|
||||
interface StockFactor {
|
||||
ticker: string;
|
||||
name: string;
|
||||
market: string;
|
||||
sector_name: string | null;
|
||||
market_cap: number | null;
|
||||
close_price: number | null;
|
||||
per: number | null;
|
||||
pbr: number | null;
|
||||
dividend_yield: number | null;
|
||||
quality_score: number | null;
|
||||
fscore: number | null;
|
||||
rank: number | null;
|
||||
}
|
||||
|
||||
interface StrategyResult {
|
||||
strategy_name: string;
|
||||
base_date: string;
|
||||
universe_count: number;
|
||||
result_count: number;
|
||||
stocks: StockFactor[];
|
||||
}
|
||||
|
||||
export default function QualityStrategyPage() {
|
||||
const router = useRouter();
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [initialLoading, setInitialLoading] = useState(true);
|
||||
const [result, setResult] = useState<StrategyResult | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const [minFscore, setMinFscore] = useState(7);
|
||||
const [topN, setTopN] = useState(30);
|
||||
|
||||
useEffect(() => {
|
||||
const init = async () => {
|
||||
try {
|
||||
const userData = await api.getCurrentUser() as User;
|
||||
setUser(userData);
|
||||
} catch {
|
||||
router.push('/login');
|
||||
} finally {
|
||||
setInitialLoading(false);
|
||||
}
|
||||
};
|
||||
init();
|
||||
}, [router]);
|
||||
|
||||
const runStrategy = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const data = await api.post<StrategyResult>('/api/strategy/quality', {
|
||||
universe: {
|
||||
markets: ['KOSPI', 'KOSDAQ'],
|
||||
exclude_stock_types: ['spac', 'preferred', 'reit'],
|
||||
},
|
||||
top_n: topN,
|
||||
min_fscore: minFscore,
|
||||
});
|
||||
setResult(data);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Strategy execution failed');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const formatNumber = (value: number | null, decimals: number = 2) => {
|
||||
if (value === null) return '-';
|
||||
return value.toFixed(decimals);
|
||||
};
|
||||
|
||||
const formatCurrency = (value: number | null) => {
|
||||
if (value === null) return '-';
|
||||
return new Intl.NumberFormat('ko-KR').format(value);
|
||||
};
|
||||
|
||||
if (initialLoading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="text-gray-500">Loading...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen">
|
||||
<Sidebar />
|
||||
<div className="flex-1">
|
||||
<Header username={user?.username} />
|
||||
<main className="p-6">
|
||||
<h1 className="text-2xl font-bold text-gray-800 mb-6">슈퍼 퀄리티 전략</h1>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Settings */}
|
||||
<div className="bg-white rounded-lg shadow p-6 mb-6">
|
||||
<h2 className="text-lg font-semibold mb-4">전략 설정</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<label htmlFor="min-fscore" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
최소 F-Score
|
||||
</label>
|
||||
<input
|
||||
id="min-fscore"
|
||||
type="number"
|
||||
min="0"
|
||||
max="9"
|
||||
value={minFscore}
|
||||
onChange={(e) => setMinFscore(parseInt(e.target.value))}
|
||||
className="w-full px-3 py-2 border rounded"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="top-n" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
상위 종목 수
|
||||
</label>
|
||||
<input
|
||||
id="top-n"
|
||||
type="number"
|
||||
min="1"
|
||||
max="100"
|
||||
value={topN}
|
||||
onChange={(e) => setTopN(parseInt(e.target.value))}
|
||||
className="w-full px-3 py-2 border rounded"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={runStrategy}
|
||||
disabled={loading}
|
||||
className="px-6 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:bg-blue-400"
|
||||
>
|
||||
{loading ? '실행 중...' : '전략 실행'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Results */}
|
||||
{result && (
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="p-4 border-b">
|
||||
<h2 className="text-lg font-semibold">
|
||||
결과 ({result.result_count}/{result.universe_count} 종목)
|
||||
</h2>
|
||||
<p className="text-sm text-gray-500">기준일: {result.base_date}</p>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th scope="col" className="px-4 py-3 text-left text-sm font-medium text-gray-600">순위</th>
|
||||
<th scope="col" className="px-4 py-3 text-left text-sm font-medium text-gray-600">종목</th>
|
||||
<th scope="col" className="px-4 py-3 text-left text-sm font-medium text-gray-600">섹터</th>
|
||||
<th scope="col" className="px-4 py-3 text-right text-sm font-medium text-gray-600">시가총액(억)</th>
|
||||
<th scope="col" className="px-4 py-3 text-right text-sm font-medium text-gray-600">현재가</th>
|
||||
<th scope="col" className="px-4 py-3 text-right text-sm font-medium text-gray-600">PER</th>
|
||||
<th scope="col" className="px-4 py-3 text-right text-sm font-medium text-gray-600">PBR</th>
|
||||
<th scope="col" className="px-4 py-3 text-right text-sm font-medium text-gray-600">배당률</th>
|
||||
<th scope="col" className="px-4 py-3 text-center text-sm font-medium text-gray-600">F-Score</th>
|
||||
<th scope="col" className="px-4 py-3 text-right text-sm font-medium text-gray-600">퀄리티</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y">
|
||||
{result.stocks.map((stock) => (
|
||||
<tr key={stock.ticker} className="hover:bg-gray-50">
|
||||
<td className="px-4 py-3 text-sm font-medium">{stock.rank}</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="font-medium">{stock.ticker}</div>
|
||||
<div className="text-xs text-gray-500">{stock.name}</div>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm">{stock.sector_name || '-'}</td>
|
||||
<td className="px-4 py-3 text-sm text-right">{formatCurrency(stock.market_cap)}</td>
|
||||
<td className="px-4 py-3 text-sm text-right">{formatCurrency(stock.close_price)}</td>
|
||||
<td className="px-4 py-3 text-sm text-right">{formatNumber(stock.per)}</td>
|
||||
<td className="px-4 py-3 text-sm text-right">{formatNumber(stock.pbr)}</td>
|
||||
<td className="px-4 py-3 text-sm text-right">{formatNumber(stock.dividend_yield)}%</td>
|
||||
<td className="px-4 py-3 text-center">
|
||||
<span className={`px-2 py-1 rounded text-xs ${
|
||||
(stock.fscore ?? 0) >= 8 ? 'bg-green-100 text-green-800' :
|
||||
(stock.fscore ?? 0) >= 6 ? 'bg-yellow-100 text-yellow-800' :
|
||||
'bg-gray-100 text-gray-800'
|
||||
}`}>
|
||||
{stock.fscore}/9
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-right font-medium">{formatNumber(stock.quality_score)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
227
frontend/src/app/strategy/value-momentum/page.tsx
Normal file
227
frontend/src/app/strategy/value-momentum/page.tsx
Normal file
@ -0,0 +1,227 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Sidebar from '@/components/layout/Sidebar';
|
||||
import Header from '@/components/layout/Header';
|
||||
import { api } from '@/lib/api';
|
||||
|
||||
interface User {
|
||||
id: number;
|
||||
username: string;
|
||||
}
|
||||
|
||||
interface StockFactor {
|
||||
ticker: string;
|
||||
name: string;
|
||||
market: string;
|
||||
sector_name: string | null;
|
||||
market_cap: number | null;
|
||||
close_price: number | null;
|
||||
per: number | null;
|
||||
pbr: number | null;
|
||||
dividend_yield: number | null;
|
||||
value_score: number | null;
|
||||
momentum_score: number | null;
|
||||
total_score: number | null;
|
||||
rank: number | null;
|
||||
}
|
||||
|
||||
interface StrategyResult {
|
||||
strategy_name: string;
|
||||
base_date: string;
|
||||
universe_count: number;
|
||||
result_count: number;
|
||||
stocks: StockFactor[];
|
||||
}
|
||||
|
||||
export default function ValueMomentumPage() {
|
||||
const router = useRouter();
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [initialLoading, setInitialLoading] = useState(true);
|
||||
const [result, setResult] = useState<StrategyResult | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const [valueWeight, setValueWeight] = useState(0.5);
|
||||
const [momentumWeight, setMomentumWeight] = useState(0.5);
|
||||
const [topN, setTopN] = useState(30);
|
||||
|
||||
useEffect(() => {
|
||||
const init = async () => {
|
||||
try {
|
||||
const userData = await api.getCurrentUser() as User;
|
||||
setUser(userData);
|
||||
} catch {
|
||||
router.push('/login');
|
||||
} finally {
|
||||
setInitialLoading(false);
|
||||
}
|
||||
};
|
||||
init();
|
||||
}, [router]);
|
||||
|
||||
const runStrategy = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const data = await api.post<StrategyResult>('/api/strategy/value-momentum', {
|
||||
universe: {
|
||||
markets: ['KOSPI', 'KOSDAQ'],
|
||||
exclude_stock_types: ['spac', 'preferred', 'reit'],
|
||||
},
|
||||
top_n: topN,
|
||||
value_weight: valueWeight,
|
||||
momentum_weight: momentumWeight,
|
||||
});
|
||||
setResult(data);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Strategy execution failed');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const formatNumber = (value: number | null, decimals: number = 2) => {
|
||||
if (value === null) return '-';
|
||||
return value.toFixed(decimals);
|
||||
};
|
||||
|
||||
const formatCurrency = (value: number | null) => {
|
||||
if (value === null) return '-';
|
||||
return new Intl.NumberFormat('ko-KR').format(value);
|
||||
};
|
||||
|
||||
if (initialLoading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="text-gray-500">Loading...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen">
|
||||
<Sidebar />
|
||||
<div className="flex-1">
|
||||
<Header username={user?.username} />
|
||||
<main className="p-6">
|
||||
<h1 className="text-2xl font-bold text-gray-800 mb-6">밸류 모멘텀 전략</h1>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Settings */}
|
||||
<div className="bg-white rounded-lg shadow p-6 mb-6">
|
||||
<h2 className="text-lg font-semibold mb-4">전략 설정</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
|
||||
<div>
|
||||
<label htmlFor="value-weight" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
밸류 가중치
|
||||
</label>
|
||||
<input
|
||||
id="value-weight"
|
||||
type="number"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.1"
|
||||
value={valueWeight}
|
||||
onChange={(e) => setValueWeight(parseFloat(e.target.value))}
|
||||
className="w-full px-3 py-2 border rounded"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="momentum-weight" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
모멘텀 가중치
|
||||
</label>
|
||||
<input
|
||||
id="momentum-weight"
|
||||
type="number"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.1"
|
||||
value={momentumWeight}
|
||||
onChange={(e) => setMomentumWeight(parseFloat(e.target.value))}
|
||||
className="w-full px-3 py-2 border rounded"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="top-n" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
상위 종목 수
|
||||
</label>
|
||||
<input
|
||||
id="top-n"
|
||||
type="number"
|
||||
min="1"
|
||||
max="100"
|
||||
value={topN}
|
||||
onChange={(e) => setTopN(parseInt(e.target.value))}
|
||||
className="w-full px-3 py-2 border rounded"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={runStrategy}
|
||||
disabled={loading}
|
||||
className="px-6 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:bg-blue-400"
|
||||
>
|
||||
{loading ? '실행 중...' : '전략 실행'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Results */}
|
||||
{result && (
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="p-4 border-b">
|
||||
<h2 className="text-lg font-semibold">
|
||||
결과 ({result.result_count}/{result.universe_count} 종목)
|
||||
</h2>
|
||||
<p className="text-sm text-gray-500">기준일: {result.base_date}</p>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th scope="col" className="px-4 py-3 text-left text-sm font-medium text-gray-600">순위</th>
|
||||
<th scope="col" className="px-4 py-3 text-left text-sm font-medium text-gray-600">종목</th>
|
||||
<th scope="col" className="px-4 py-3 text-left text-sm font-medium text-gray-600">섹터</th>
|
||||
<th scope="col" className="px-4 py-3 text-right text-sm font-medium text-gray-600">시가총액(억)</th>
|
||||
<th scope="col" className="px-4 py-3 text-right text-sm font-medium text-gray-600">현재가</th>
|
||||
<th scope="col" className="px-4 py-3 text-right text-sm font-medium text-gray-600">PER</th>
|
||||
<th scope="col" className="px-4 py-3 text-right text-sm font-medium text-gray-600">PBR</th>
|
||||
<th scope="col" className="px-4 py-3 text-right text-sm font-medium text-gray-600">밸류</th>
|
||||
<th scope="col" className="px-4 py-3 text-right text-sm font-medium text-gray-600">모멘텀</th>
|
||||
<th scope="col" className="px-4 py-3 text-right text-sm font-medium text-gray-600">종합</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y">
|
||||
{result.stocks.map((stock) => (
|
||||
<tr key={stock.ticker} className="hover:bg-gray-50">
|
||||
<td className="px-4 py-3 text-sm font-medium">{stock.rank}</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="font-medium">{stock.ticker}</div>
|
||||
<div className="text-xs text-gray-500">{stock.name}</div>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm">{stock.sector_name || '-'}</td>
|
||||
<td className="px-4 py-3 text-sm text-right">{formatCurrency(stock.market_cap)}</td>
|
||||
<td className="px-4 py-3 text-sm text-right">{formatCurrency(stock.close_price)}</td>
|
||||
<td className="px-4 py-3 text-sm text-right">{formatNumber(stock.per)}</td>
|
||||
<td className="px-4 py-3 text-sm text-right">{formatNumber(stock.pbr)}</td>
|
||||
<td className="px-4 py-3 text-sm text-right">{formatNumber(stock.value_score)}</td>
|
||||
<td className="px-4 py-3 text-sm text-right">{formatNumber(stock.momentum_score)}</td>
|
||||
<td className="px-4 py-3 text-sm text-right font-medium">{formatNumber(stock.total_score)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user