diff --git a/frontend/src/components/StrategyEvaluationPage.tsx b/frontend/src/components/StrategyEvaluationPage.tsx index 7f55329..169d7d0 100644 --- a/frontend/src/components/StrategyEvaluationPage.tsx +++ b/frontend/src/components/StrategyEvaluationPage.tsx @@ -94,6 +94,7 @@ import { } from '../utils/strategyTemplateList'; import type { TemplatePaletteRow } from './strategyEditor/TemplatePaletteTab'; import { buildStrategyEditorDefFromSettings } from '../utils/strategyEditorShared'; +import { isEvaluationTemplateStrategy } from '../utils/strategyEvaluationTemplateApply'; const LEFT_KEY = 'seval-left-width'; const LEFT_OPEN_KEY = 'seval-left-open'; @@ -261,6 +262,21 @@ function StrategyEvaluationPageInner({ theme = 'dark' }: Props) { [templateRows, templateSearch], ); + /** 전략 탭·일괄평가용 — `[템플릿]` 접두 전략 제외 (템플릿 탭 전용) */ + const listStrategies = useMemo( + () => strategies.filter(s => !isEvaluationTemplateStrategy(s.name)), + [strategies], + ); + + /** 차트 전략 선택 — 목록 제외, 현재 선택된 템플릿 평가 전략만 예외 표시 */ + const chartStrategies = useMemo(() => { + if (!selectedStrategy?.id || !isEvaluationTemplateStrategy(selectedStrategy.name)) { + return listStrategies; + } + if (listStrategies.some(s => s.id === selectedStrategy.id)) return listStrategies; + return [selectedStrategy, ...listStrategies]; + }, [listStrategies, selectedStrategy]); + const [leftWidth, setLeftWidth] = useState(() => readMinWidth(LEFT_KEY, LEFT_DEFAULT)); const [leftOpen, setLeftOpen] = useState(() => readStoredBool(LEFT_OPEN_KEY, true)); const [rightWidth, setRightWidth] = useState(() => readMinWidth(RIGHT_KEY, RIGHT_DEFAULT)); @@ -443,12 +459,12 @@ function StrategyEvaluationPageInner({ theme = 'dark' }: Props) { const filteredStrategies = useMemo(() => { const q = strategySearch.trim().toLowerCase(); - if (!q) return strategies; - return strategies.filter(s => + if (!q) return listStrategies; + return listStrategies.filter(s => (s.name ?? '').toLowerCase().includes(q) || String(s.id ?? '').includes(q), ); - }, [strategies, strategySearch]); + }, [listStrategies, strategySearch]); const sortedFilteredStrategies = useMemo( () => sortStrategies(filteredStrategies, strategyListSort), @@ -533,7 +549,7 @@ function StrategyEvaluationPageInner({ theme = 'dark' }: Props) { ]); const bulkEval = useStrategyBulkEvaluation({ - strategies, + strategies: listStrategies, market, timeframe: chartTimeframe, bars, @@ -547,7 +563,7 @@ function StrategyEvaluationPageInner({ theme = 'dark' }: Props) { const autoBulkEvalTriggerKey = useMemo(() => { const lastBarTime = bars[bars.length - 1]?.time ?? 0; - const strategyIds = strategies.map(s => s.id).filter(Boolean).join(','); + const strategyIds = listStrategies.map(s => s.id).filter(Boolean).join(','); return [ market, chartTimeframe, @@ -567,7 +583,7 @@ function StrategyEvaluationPageInner({ theme = 'dark' }: Props) { evalWindowMeta.evaluationBarCount, bars, paramsRevision, - strategies, + listStrategies, ]); const lastAutoBulkEvalKeyRef = useRef(null); @@ -581,7 +597,7 @@ function StrategyEvaluationPageInner({ theme = 'dark' }: Props) { useEffect(() => { const waitingForSelectedSignals = selectedStrategyId != null && signalScanRunning; if (chartLoading || waitingForSelectedSignals) return; - if (bars.length < 10 || !strategies.some(s => s.id != null)) return; + if (bars.length < 10 || !listStrategies.some(s => s.id != null)) return; const key = autoBulkEvalTriggerKey; if (lastAutoBulkEvalKeyRef.current === key) return; @@ -594,7 +610,7 @@ function StrategyEvaluationPageInner({ theme = 'dark' }: Props) { signalScanRunning, selectedStrategyId, bars.length, - strategies, + listStrategies, bulkEval.startBulkEvaluation, ]); @@ -1128,7 +1144,7 @@ function StrategyEvaluationPageInner({ theme = 'dark' }: Props) { timeframe={chartTimeframe} timeframeChoice={timeframeChoice} tfOptions={tfOptions} - strategies={strategies} + strategies={chartStrategies} selectedStrategyId={selectedStrategyId} onStrategySelect={handleSelectStrategyById} onMarketChange={setMarket} diff --git a/frontend/src/utils/strategyEvaluationTemplateApply.ts b/frontend/src/utils/strategyEvaluationTemplateApply.ts index 267a75e..4074f28 100644 --- a/frontend/src/utils/strategyEvaluationTemplateApply.ts +++ b/frontend/src/utils/strategyEvaluationTemplateApply.ts @@ -44,6 +44,12 @@ import { type StrategyFlowLayoutStore, } from './strategyEditorLayoutStorage'; +export const EVALUATION_TEMPLATE_STRATEGY_PREFIX = '[템플릿]'; + +export function isEvaluationTemplateStrategy(name: string | null | undefined): boolean { + return (name ?? '').trimStart().startsWith(EVALUATION_TEMPLATE_STRATEGY_PREFIX); +} + export type TemplateEvaluationPayload = { buy: EditorConditionState; sell: EditorConditionState; @@ -118,7 +124,7 @@ function payloadForSide( sellOrphans, flowLayout: buildStrategyFlowLayoutStore(buyLayout, sellLayout, buyOrphans, sellOrphans), preferredTab: signal, - strategyName: `[템플릿] ${row.label}`, + strategyName: `${EVALUATION_TEMPLATE_STRATEGY_PREFIX} ${row.label}`, strategyDescription: row.description?.trim() || `전략 템플릿: ${row.label}`, }; } @@ -168,7 +174,7 @@ export function suggestTemplateEvaluationStrategyName( baseLabel: string, existingNames: ReadonlySet, ): string { - const base = `[템플릿] ${baseLabel.trim() || '새 전략'}`; + const base = `${EVALUATION_TEMPLATE_STRATEGY_PREFIX} ${baseLabel.trim() || '새 전략'}`; if (!existingNames.has(base)) return base; let n = 2; while (existingNames.has(`${base} (${n})`)) n += 1;