From 674cb1ef287eb8f504bd7dbc819c9d2b6c73e587 Mon Sep 17 00:00:00 2001 From: YoungWooSeo Date: Fri, 6 Mar 2026 20:47:19 +0900 Subject: [PATCH] feat: Add strategy presets for quick configuration --- frontend/src/utils/strategyPresets.ts | 166 ++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 frontend/src/utils/strategyPresets.ts diff --git a/frontend/src/utils/strategyPresets.ts b/frontend/src/utils/strategyPresets.ts new file mode 100644 index 0000000..ba92dc6 --- /dev/null +++ b/frontend/src/utils/strategyPresets.ts @@ -0,0 +1,166 @@ +/** + * 전략 프리셋 정의 + * + * 사용자가 복잡한 파라미터를 직접 조정하지 않고 + * 미리 정의된 설정을 선택할 수 있도록 지원 + */ + +export interface PresetConfig { + name: string + description: string + emoji: string + risk: 'low' | 'medium' | 'high' + expectedReturn: string + params: Record +} + +export const STRATEGY_PRESETS: Record = { + conservative: { + name: '보수적 (안정 중시)', + description: '낮은 위험으로 꾸준한 수익 추구. 손절 타이트, 진입 까다로움', + emoji: '🛡️', + risk: 'low', + expectedReturn: '연 8-12%', + params: { + // 리스크 관리 + 'MEAN_REVERSION_STOP_LOSS_PCT': -5.0, // 손절 -5% + 'MEAN_REVERSION_TAKE_PROFIT_PCT': 8.0, // 익절 +8% + 'MAX_TOTAL_POSITIONS': 3, // 최대 3종목 + 'MDD_LIMIT_PCT': -15.0, // MDD 한도 -15% + + // 진입 필터 (까다롭게) + 'MEAN_REVERSION_RSI_THRESHOLD': 25, // RSI ≤ 25만 진입 + 'MIN_AVG_VOLUME_20D': 100000, // 최소 거래량 높게 + 'MIN_MARKET_CAP': 3000, // 시가총액 3천억 이상 + + // 지표 + 'RSI_PERIOD': 14, + 'BOLLINGER_PERIOD': 20, + 'ADX_THRESHOLD': 30, // 강추세 시 진입 차단 + + // 데이트레이딩 비활성화 + 'DAY_TRADE_ENABLED': false, + 'DAY_TRADE_MAX_POSITIONS': 0, + }, + }, + + balanced: { + name: '균형형 (기본 설정)', + description: '리스크와 수익의 균형. 대부분의 시장 환경에 적합', + emoji: '⚖️', + risk: 'medium', + expectedReturn: '연 12-18%', + params: { + // 리스크 관리 + 'MEAN_REVERSION_STOP_LOSS_PCT': -6.0, + 'MEAN_REVERSION_TAKE_PROFIT_PCT': 12.0, + 'MAX_TOTAL_POSITIONS': 5, + 'MDD_LIMIT_PCT': -20.0, + + // 진입 필터 (표준) + 'MEAN_REVERSION_RSI_THRESHOLD': 30, + 'MIN_AVG_VOLUME_20D': 50000, + 'MIN_MARKET_CAP': 1000, + + // 지표 + 'RSI_PERIOD': 14, + 'BOLLINGER_PERIOD': 20, + 'ADX_THRESHOLD': 30, + + // 데이트레이딩 제한적 활성화 + 'DAY_TRADE_ENABLED': true, + 'DAY_TRADE_MAX_POSITIONS': 2, + 'DAY_TRADE_MAX_CAPITAL_RATIO': 0.15, // 전체 자본의 15% + }, + }, + + aggressive: { + name: '공격적 (수익 극대화)', + description: '높은 수익 목표, 넓은 손절폭. 변동성 수용 필수', + emoji: '🚀', + risk: 'high', + expectedReturn: '연 20-30%', + params: { + // 리스크 관리 (여유롭게) + 'MEAN_REVERSION_STOP_LOSS_PCT': -8.0, // 손절 -8% + 'MEAN_REVERSION_TAKE_PROFIT_PCT': 20.0, // 익절 +20% + 'MAX_TOTAL_POSITIONS': 8, // 최대 8종목 + 'MDD_LIMIT_PCT': -25.0, + + // 진입 필터 (완화) + 'MEAN_REVERSION_RSI_THRESHOLD': 35, // RSI ≤ 35 + 'MIN_AVG_VOLUME_20D': 30000, + 'MIN_MARKET_CAP': 500, // 시가총액 500억 + + // 지표 + 'RSI_PERIOD': 10, // 더 민감하게 + 'BOLLINGER_PERIOD': 15, + 'ADX_THRESHOLD': 40, // 강추세도 일부 허용 + + // 데이트레이딩 적극 활용 + 'DAY_TRADE_ENABLED': true, + 'DAY_TRADE_MAX_POSITIONS': 3, + 'DAY_TRADE_MAX_CAPITAL_RATIO': 0.25, // 전체 자본의 25% + 'DAY_TRADE_ORB_STOP_LOSS_PCT': 0.025, // ORB 손절 -2.5% + }, + }, +} + +export const PRESET_ORDER = ['conservative', 'balanced', 'aggressive'] as const +export type PresetKey = typeof PRESET_ORDER[number] + +/** + * 현재 설정이 어떤 프리셋에 가장 가까운지 판단 + */ +export function detectCurrentPreset( + configs: Array<{ key: string; value: string }> +): PresetKey | 'custom' { + const configMap = new Map(configs.map(c => [c.key, c.value])) + + for (const presetKey of PRESET_ORDER) { + const preset = STRATEGY_PRESETS[presetKey] + let matchCount = 0 + let totalChecked = 0 + + for (const [key, expectedValue] of Object.entries(preset.params)) { + const currentValue = configMap.get(key) + if (currentValue !== undefined) { + totalChecked++ + if (String(currentValue) === String(expectedValue)) { + matchCount++ + } + } + } + + // 80% 이상 일치하면 해당 프리셋으로 간주 + if (totalChecked > 0 && matchCount / totalChecked >= 0.8) { + return presetKey as PresetKey + } + } + + return 'custom' +} + +/** + * 프리셋 적용 시 변경될 파라미터 목록 반환 + */ +export function getPresetDiff( + presetKey: PresetKey, + currentConfigs: Array<{ key: string; value: string }> +): Array<{ key: string; from: string; to: string }> { + const preset = STRATEGY_PRESETS[presetKey] + const diffs: Array<{ key: string; from: string; to: string }> = [] + + for (const [key, newValue] of Object.entries(preset.params)) { + const current = currentConfigs.find(c => c.key === key) + if (current && String(current.value) !== String(newValue)) { + diffs.push({ + key, + from: current.value, + to: String(newValue), + }) + } + } + + return diffs +}