전략평가 메뉴 추가
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
/**
|
||||
* 전략 평가 — 좌측 패널 전략설정 탭 (지표 파라미터 테스트)
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
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,
|
||||
}) => {
|
||||
const [draft, setDraft] = useState<EvalIndicatorParams>({});
|
||||
const [expanded, setExpanded] = useState<string | null>(null);
|
||||
|
||||
const indicatorTypes = useMemo(
|
||||
() => collectStrategyRegistryTypes(strategy ?? undefined),
|
||||
[strategy],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!strategy) {
|
||||
setDraft({});
|
||||
setExpanded(null);
|
||||
return;
|
||||
}
|
||||
const baseline = appliedParams ?? buildEvalParamsFromStrategy(strategy, getParams);
|
||||
setDraft(JSON.parse(JSON.stringify(baseline)) as EvalIndicatorParams);
|
||||
setExpanded(indicatorTypes[0] ?? null);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [strategy?.id, appliedParams, indicatorTypes.join(',')]);
|
||||
|
||||
const dirty = useMemo(
|
||||
() => hasEvalParamsDiff(draft, appliedParams),
|
||||
[draft, appliedParams],
|
||||
);
|
||||
|
||||
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">
|
||||
<p className="seval-settings-strategy">{strategyName}</p>
|
||||
<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-save"
|
||||
disabled={!dirty}
|
||||
onClick={() => onSave(JSON.parse(JSON.stringify(draft)) as EvalIndicatorParams)}
|
||||
>
|
||||
저장 · 재계산
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="seval-settings-reset"
|
||||
disabled={!appliedParams && !dirty}
|
||||
onClick={onReset}
|
||||
>
|
||||
초기화
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StrategyEvaluationSettingsTab;
|
||||
Reference in New Issue
Block a user