/** * 전략 평가 — 좌측 패널 전략설정 탭 (지표 파라미터 테스트) */ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import type { StrategyDto } from '../../utils/backendApi'; import { getIndicatorDef } from '../../utils/indicatorRegistry'; import { getGlobalParamKeys, getPlotParamKeys } from '../../utils/indicatorSettingsLayout'; import { GlobalParamRow } from '../IndicatorSettingsSections'; import { collectStrategyRegistryTypes } from '../../utils/strategyToChartIndicators'; import { buildEvalParamsFromStrategy, hasEvalParamsDiff, type EvalIndicatorParams, } from '../../utils/strategyEvaluationParams'; import { repairUtf8Mojibake } from '../../utils/textEncoding'; interface Props { strategy: StrategyDto | null; getParams: (type: string) => Record; appliedParams: EvalIndicatorParams | null; onSave: (params: EvalIndicatorParams) => void; onReset: () => void; applying?: boolean; } function getEditableParamKeys( indicatorType: string, params: Record, ): string[] { const def = getIndicatorDef(indicatorType); const plots = def?.plots ?? []; const keys = new Set(); for (const k of getGlobalParamKeys(indicatorType, params, plots)) keys.add(k); plots.forEach((p, i) => { for (const k of getPlotParamKeys(indicatorType, p.id, i, plots, params)) keys.add(k); }); if (keys.size === 0) { Object.keys(params).forEach(k => { if (k !== 'symbolMode' && k !== 'refSymbol') keys.add(k); }); } return [...keys]; } const StrategyEvaluationSettingsTab: React.FC = ({ strategy, getParams, appliedParams, onSave, onReset, applying = false, }) => { const [draft, setDraft] = useState({}); const [expanded, setExpanded] = useState(null); const indicatorTypes = useMemo( () => collectStrategyRegistryTypes(strategy ?? undefined), [strategy], ); const resolveBaseline = useCallback((): EvalIndicatorParams => { if (!strategy) return {}; return appliedParams ?? buildEvalParamsFromStrategy(strategy, getParams); }, [strategy, appliedParams, getParams]); useEffect(() => { if (!strategy) { setDraft({}); setExpanded(null); return; } setDraft(JSON.parse(JSON.stringify(resolveBaseline())) as EvalIndicatorParams); setExpanded(indicatorTypes[0] ?? null); // eslint-disable-next-line react-hooks/exhaustive-deps }, [strategy?.id, appliedParams, indicatorTypes.join(',')]); const dirty = useMemo( () => hasEvalParamsDiff(draft, resolveBaseline()), [draft, resolveBaseline], ); const handleApply = useCallback(() => { onSave(JSON.parse(JSON.stringify(draft)) as EvalIndicatorParams); }, [draft, onSave]); const patchParam = useCallback((type: string, key: string, raw: string | boolean) => { setDraft(prev => { const current = prev[type] ?? {}; const oldVal = current[key]; const defVal = getIndicatorDef(type)?.defaultParams?.[key]; let nextVal: number | string | boolean; if (typeof oldVal === 'number' || typeof defVal === 'number') { const n = parseFloat(raw as string); const fallback = typeof oldVal === 'number' ? oldVal : (defVal as number); nextVal = Number.isNaN(n) ? fallback : n; } else if (typeof oldVal === 'boolean' || typeof defVal === 'boolean') { nextVal = raw as boolean; } else { nextVal = raw as string; } return { ...prev, [type]: { ...current, [key]: nextVal }, }; }); }, []); const resetType = useCallback((type: string) => { setDraft(prev => ({ ...prev, [type]: { ...getParams(type) }, })); }, [getParams]); if (!strategy) { return

전략을 먼저 선택하세요

; } if (indicatorTypes.length === 0) { return

선택한 전략에 설정 가능한 보조지표가 없습니다

; } const strategyName = repairUtf8Mojibake(strategy.name ?? `전략 #${strategy.id}`); return (

{strategyName}

지표 값을 변경한 뒤 적용을 누르면 차트·일치율이 갱신됩니다

{indicatorTypes.map(type => { const def = getIndicatorDef(type); const params = draft[type] ?? {}; const paramKeys = getEditableParamKeys(type, params); const isOpen = expanded === type; const label = def?.koreanName ?? def?.shortName ?? type; return (
{isOpen && (
{paramKeys.map(key => ( patchParam(type, k, raw)} /> ))}
)}
); })}
); }; export default StrategyEvaluationSettingsTab;