diff --git a/frontend/src/pages/StrategySettings.tsx b/frontend/src/pages/StrategySettings.tsx index 7d477ec..adb7188 100644 --- a/frontend/src/pages/StrategySettings.tsx +++ b/frontend/src/pages/StrategySettings.tsx @@ -2,9 +2,16 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import api from '../lib/api' import { useState, useMemo } from 'react' import HtsPage from '../components/HtsPage' -import { RotateCcw, Save, Power, PowerOff, ChevronDown, ChevronRight, RefreshCw } from 'lucide-react' +import { RotateCcw, Save, Power, PowerOff, ChevronDown, ChevronRight, RefreshCw, Zap } from 'lucide-react' +import { + STRATEGY_PRESETS, + PRESET_ORDER, + PresetKey, + detectCurrentPreset, + getPresetDiff, +} from '../utils/strategyPresets' -// ── 타입 ───────────────────────────────────────────────────────────────────── +// ── 타입 ────────────────────────────────────── interface StrategyConfig { id: number @@ -21,7 +28,7 @@ interface StrategyConfig { updated_by: string | null } -// ── 그룹 한글 레이블 ────────────────────────────────────────────────────────── +// ── 그룹 한글 레이블 ──────────────────────────── const GROUP_LABELS: Record = { INDICATOR: '기술 지표', @@ -34,7 +41,79 @@ const GROUP_LABELS: Record = { const GROUP_ORDER = ['GENERAL', 'RISK', 'FILTER', 'INDICATOR', 'MEAN_REVERSION', 'DAY_TRADE'] -// ── 개별 파라미터 행 컴포넌트 ───────────────────────────────────────────────── +const GROUP_ICONS: Record = { + GENERAL: '⚙️', + RISK: '🛡️', + FILTER: '🔍', + INDICATOR: '📊', + MEAN_REVERSION: '↩️', + DAY_TRADE: '⚡', +} + +// 핵심 설정 파라미터 (기본 확장) +const CORE_PARAMS = [ + 'MEAN_REVERSION_STOP_LOSS_PCT', + 'MEAN_REVERSION_TAKE_PROFIT_PCT', + 'MAX_TOTAL_POSITIONS', + 'MEAN_REVERSION_RSI_THRESHOLD', + 'DAY_TRADE_ENABLED', +] + +// ── 프리셋 카드 컴포넌트 ──────────────────────────── + +interface PresetCardProps { + presetKey: PresetKey + isSelected: boolean + onSelect: () => void +} + +function PresetCard({ presetKey, isSelected, onSelect }: PresetCardProps) { + const preset = STRATEGY_PRESETS[presetKey] + const riskColors = { + low: 'bg-green-500/10 text-green-400 border-green-500/30', + medium: 'bg-yellow-500/10 text-yellow-400 border-yellow-500/30', + high: 'bg-red-500/10 text-red-400 border-red-500/30', + } + + return ( + + ) +} + +// ── 개별 파라미터 행 컴포넌트 ──────────────────────── interface ConfigRowProps { cfg: StrategyConfig @@ -80,7 +159,7 @@ function ConfigRow({ cfg, onSave, onToggle, onReset, isSaving }: ConfigRowProps) ? 'border-blue-500/40 bg-blue-500/5' : 'border-slate-800 hover:border-slate-700' }`}> - {/* 상태 뱃지 */} + {/* 상태 배지 */} {(isModified || dirty) && ( = { - GENERAL: '⚙️', - RISK: '🛡️', - FILTER: '🔍', - INDICATOR: '📊', - MEAN_REVERSION: '↩️', - DAY_TRADE: '⚡', -} +// ── 그룹 섹션 컴포넌트 ────────────────────────────── interface GroupSectionProps { groupKey: string @@ -172,10 +242,11 @@ interface GroupSectionProps { onToggle: (key: string) => void onReset: (key: string) => void isSaving: boolean + defaultExpanded?: boolean } -function GroupSection({ groupKey, configs, onSave, onToggle, onReset, isSaving }: GroupSectionProps) { - const [collapsed, setCollapsed] = useState(false) +function GroupSection({ groupKey, configs, onSave, onToggle, onReset, isSaving, defaultExpanded = false }: GroupSectionProps) { + const [collapsed, setCollapsed] = useState(!defaultExpanded) const modifiedCount = configs.filter(c => c.value !== c.default_value).length return ( @@ -222,11 +293,12 @@ function GroupSection({ groupKey, configs, onSave, onToggle, onReset, isSaving } ) } -// ── 메인 페이지 ─────────────────────────────────────────────────────────────── +// ── 메인 페이지 ────────────────────────────────────── const StrategySettings = () => { const queryClient = useQueryClient() const [notification, setNotification] = useState<{ type: 'success' | 'error'; msg: string } | null>(null) + const [showPresets, setShowPresets] = useState(true) const showNotification = (type: 'success' | 'error', msg: string) => { setNotification({ type, msg }) @@ -243,6 +315,11 @@ const StrategySettings = () => { staleTime: 10_000, }) + // 현재 프리셋 감지 + const currentPreset = useMemo(() => { + return detectCurrentPreset(configs.map(c => ({ key: c.key, value: c.value }))) + }, [configs]) + // 단일 파라미터 수정 const updateMutation = useMutation({ mutationFn: async ({ key, value }: { key: string; value: string }) => { @@ -295,6 +372,28 @@ const StrategySettings = () => { }, }) + // 프리셋 적용 (일괄 변경) + const applyPresetMutation = useMutation({ + mutationFn: async (presetKey: PresetKey) => { + const preset = STRATEGY_PRESETS[presetKey] + const updates = Object.entries(preset.params).map(([key, value]) => ({ + key, + value: String(value), + })) + // 병렬 요청 + return Promise.all(updates.map(({ key, value }) => + api.put(`/strategy-config/${key}`, { value, updated_by: 'preset' }) + )) + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['strategy-config'] }) + showNotification('success', '프리셋 적용 완료') + }, + onError: (err: any) => { + showNotification('error', `프리셋 적용 실패: ${err.message}`) + }, + }) + // 그룹별 분류 const grouped = useMemo(() => { const map: Record = {} @@ -305,6 +404,10 @@ const StrategySettings = () => { return map }, [configs]) + // 핵심 설정 / 고급 설정 분리 + const coreConfigs = configs.filter(c => CORE_PARAMS.includes(c.key)) + const advancedGroups = GROUP_ORDER.filter(g => grouped[g]?.length) + // 수정된 파라미터 수 const totalModified = configs.filter(c => c.value !== c.default_value).length @@ -338,6 +441,13 @@ const StrategySettings = () => { 새로고침 +