Files
goldenChart/frontend/src/components/strategyEvaluation/StrategyEvaluationSettingsTab.tsx
T
2026-06-12 19:12:47 +09:00

201 lines
6.7 KiB
TypeScript

/**
* 전략 평가 — 좌측 패널 전략설정 탭 (지표 파라미터 테스트)
*/
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<string, number | string | boolean>;
appliedParams: EvalIndicatorParams | null;
onSave: (params: EvalIndicatorParams) => void;
onReset: () => void;
applying?: boolean;
}
function getEditableParamKeys(
indicatorType: string,
params: Record<string, number | string | boolean>,
): string[] {
const def = getIndicatorDef(indicatorType);
const plots = def?.plots ?? [];
const keys = new Set<string>();
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<Props> = ({
strategy,
getParams,
appliedParams,
onSave,
onReset,
applying = false,
}) => {
const [draft, setDraft] = useState<EvalIndicatorParams>({});
const [expanded, setExpanded] = useState<string | null>(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 <p className="btd-sidebar-empty">전략을 먼저 선택하세요</p>;
}
if (indicatorTypes.length === 0) {
return <p className="btd-sidebar-empty">선택한 전략에 설정 가능한 보조지표가 없습니다</p>;
}
const strategyName = repairUtf8Mojibake(strategy.name ?? `전략 #${strategy.id}`);
return (
<div className="seval-settings-tab">
<div className="seval-settings-head">
<div className="seval-settings-head-row">
<p className="seval-settings-strategy">{strategyName}</p>
<button
type="button"
className="seval-settings-apply"
disabled={!dirty || applying}
onClick={handleApply}
title="변경된 지표 값을 차트·일치율에 반영"
>
{applying ? '적용 중…' : '적용'}
</button>
</div>
<p className="seval-settings-hint">지표 값을 변경한 적용을 누르면 차트·일치율이 갱신됩니다</p>
</div>
<div className="seval-settings-list">
{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 (
<div key={type} className={`seval-settings-card${isOpen ? ' seval-settings-card--open' : ''}`}>
<button
type="button"
className="seval-settings-card-head"
onClick={() => setExpanded(isOpen ? null : type)}
>
<span className="seval-settings-card-title">{label}</span>
<span className="seval-settings-card-type">{type}</span>
</button>
{isOpen && (
<div className="seval-settings-card-body">
{paramKeys.map(key => (
<GlobalParamRow
key={key}
indicatorType={type}
paramKey={key}
value={params[key] ?? getParams(type)[key]}
onChange={(k, raw) => patchParam(type, k, raw)}
/>
))}
<button
type="button"
className="seval-settings-reset-type"
onClick={() => resetType(type)}
>
지표 기본값
</button>
</div>
)}
</div>
);
})}
</div>
<div className="seval-settings-actions">
<button
type="button"
className="seval-settings-reset"
disabled={(!appliedParams && !dirty) || applying}
onClick={onReset}
>
초기화
</button>
</div>
</div>
);
};
export default StrategyEvaluationSettingsTab;