전략평가 화면 전략템플릿 목록탭 적용

This commit is contained in:
Macbook
2026-06-24 01:25:28 +09:00
parent 1ab5cef020
commit 0544971fd8
7 changed files with 495 additions and 55 deletions
+17 -53
View File
@@ -55,9 +55,9 @@ import {
type PaletteItem, type PaletteItem,
} from '../utils/strategyPaletteStorage'; } from '../utils/strategyPaletteStorage';
import { import {
buildCompositePaletteTemplateRows, buildStrategyTemplatePaletteRows,
buildSidewaysPaletteTemplateRows, filterStrategyTemplatePaletteRows,
} from '../utils/strategyEditorTemplatePaletteRows'; } from '../utils/strategyTemplateList';
import { import {
buildPriceExtremeBreakoutTree, buildPriceExtremeBreakoutTree,
isPriceExtremeBreakoutPairPaletteValue, isPriceExtremeBreakoutPairPaletteValue,
@@ -125,8 +125,6 @@ import {
type StrategyEditorMode, type StrategyEditorMode,
} from '../utils/strategyEditorModeStorage'; } from '../utils/strategyEditorModeStorage';
import { import {
getStrategyTemplates,
getBuiltinTemplateId,
simpleTemplateToNode, simpleTemplateToNode,
type StrategyTemplateDef, type StrategyTemplateDef,
} from '../utils/strategyPresets'; } from '../utils/strategyPresets';
@@ -1398,55 +1396,21 @@ export default function StrategyEditorPage({
scheduleStrategyPersist(); scheduleStrategyPersist();
}, [DEF, currentRoot, setCurrentRoot, scheduleStrategyPersist, sidewaysPalette]); }, [DEF, currentRoot, setCurrentRoot, scheduleStrategyPersist, sidewaysPalette]);
const templates = useMemo(() => getStrategyTemplates(DEF), [DEF]); const templateRows = useMemo(
() => buildStrategyTemplatePaletteRows({
def: DEF,
userTemplates,
hiddenBuiltinTemplateIds,
compositePalette,
sidewaysPalette,
}),
[DEF, userTemplates, hiddenBuiltinTemplateIds, compositePalette, sidewaysPalette],
);
const templateRows = useMemo(() => { const filteredTemplateRows = useMemo(
const userRows: TemplatePaletteRow[] = userTemplates.map(t => ({ () => filterStrategyTemplatePaletteRows(templateRows, templateSearch),
key: t.id, [templateRows, templateSearch],
source: 'user', );
label: t.label,
description: t.description,
signal: t.signal,
user: t,
}));
const hidden = new Set(hiddenBuiltinTemplateIds);
const builtinRows: TemplatePaletteRow[] = templates
.filter(t => !hidden.has(getBuiltinTemplateId(t)))
.map(t => {
const builtinId = getBuiltinTemplateId(t);
return {
key: builtinId,
source: 'builtin',
label: t.label,
description: 'description' in t ? t.description : undefined,
signal: t.signal,
builtinId,
builtin: t,
};
});
const compositeRows: TemplatePaletteRow[] = buildCompositePaletteTemplateRows(compositePalette, hidden);
const sidewaysRows: TemplatePaletteRow[] = buildSidewaysPaletteTemplateRows(sidewaysPalette, hidden);
return [...userRows, ...builtinRows, ...compositeRows, ...sidewaysRows];
}, [userTemplates, templates, hiddenBuiltinTemplateIds, compositePalette, sidewaysPalette]);
const filteredTemplateRows = useMemo(() => {
const q = templateSearch.trim().toLowerCase();
if (!q) return templateRows;
return templateRows.filter(row => {
const signalLabel = row.signal === 'buy' ? '매수' : '매도';
const badge = row.source === 'user' ? '내 템플릿'
: row.source === 'composite-palette' ? '복합지표'
: row.source === 'sideways-palette' ? '횡보지표'
: '';
return (
row.label.toLowerCase().includes(q)
|| row.description?.toLowerCase().includes(q)
|| signalLabel.includes(q)
|| row.signal.includes(q)
|| badge.includes(q)
);
});
}, [templateRows, templateSearch]);
const handleTemplate = useCallback((tmpl: StrategyTemplateDef) => { const handleTemplate = useCallback((tmpl: StrategyTemplateDef) => {
if (editorMode === 'graph') layoutFlushRef.current?.(); if (editorMode === 'graph') layoutFlushRef.current?.();
@@ -28,6 +28,7 @@ import StrategyEvaluationStrategyPanel from './strategyEvaluation/StrategyEvalua
import StrategyEvaluationIndicatorPalettePanel from './strategyEvaluation/StrategyEvaluationIndicatorPalettePanel'; import StrategyEvaluationIndicatorPalettePanel from './strategyEvaluation/StrategyEvaluationIndicatorPalettePanel';
import StrategyEvaluationSettingsTab from './strategyEvaluation/StrategyEvaluationSettingsTab'; import StrategyEvaluationSettingsTab from './strategyEvaluation/StrategyEvaluationSettingsTab';
import StrategyEvaluationMarketTab from './strategyEvaluation/StrategyEvaluationMarketTab'; import StrategyEvaluationMarketTab from './strategyEvaluation/StrategyEvaluationMarketTab';
import StrategyEvaluationTemplateTab from './strategyEvaluation/StrategyEvaluationTemplateTab';
import { usePageRefresh } from '../utils/pageRefreshEvents'; import { usePageRefresh } from '../utils/pageRefreshEvents';
import StrategyEditorModal from './strategyEvaluation/StrategyEditorModal'; import StrategyEditorModal from './strategyEvaluation/StrategyEditorModal';
import SePanelCollapseHandle from './strategyEditor/SePanelCollapseHandle'; import SePanelCollapseHandle from './strategyEditor/SePanelCollapseHandle';
@@ -87,6 +88,12 @@ import {
import type { StrategyEvaluationAiVerifyResponse } from '../utils/backendApi'; import type { StrategyEvaluationAiVerifyResponse } from '../utils/backendApi';
import { useStrategyBulkEvaluation } from '../hooks/useStrategyBulkEvaluation'; import { useStrategyBulkEvaluation } from '../hooks/useStrategyBulkEvaluation';
import StrategyBulkEvalBadge, { StrategyBulkEvalToolbar } from './strategyEvaluation/StrategyBulkEvalControls'; import StrategyBulkEvalBadge, { StrategyBulkEvalToolbar } from './strategyEvaluation/StrategyBulkEvalControls';
import {
buildStrategyTemplatePaletteRows,
filterStrategyTemplatePaletteRows,
} from '../utils/strategyTemplateList';
import type { TemplatePaletteRow } from './strategyEditor/TemplatePaletteTab';
import { buildStrategyEditorDefFromSettings } from '../utils/strategyEditorShared';
const LEFT_KEY = 'seval-left-width'; const LEFT_KEY = 'seval-left-width';
const LEFT_OPEN_KEY = 'seval-left-open'; const LEFT_OPEN_KEY = 'seval-left-open';
@@ -99,7 +106,7 @@ const STRATEGY_PANEL_MIN = 160;
const STRATEGY_PANEL_MAX = 520; const STRATEGY_PANEL_MAX = 520;
const STRATEGY_PANEL_DEFAULT = 280; const STRATEGY_PANEL_DEFAULT = 280;
type LeftTab = 'strategy' | 'market' | 'settings'; type LeftTab = 'strategy' | 'template' | 'market' | 'settings';
type RightTab = 'signal' | 'indicators' | 'chart' | 'ai'; type RightTab = 'signal' | 'indicators' | 'chart' | 'ai';
function readMinWidth(key: string, fallback: number): number { function readMinWidth(key: string, fallback: number): number {
@@ -141,6 +148,8 @@ function StrategyEvaluationPageInner({ theme = 'dark' }: Props) {
const [marketSearchOpen, setMarketSearchOpen] = useState(false); const [marketSearchOpen, setMarketSearchOpen] = useState(false);
const [strategies, setStrategies] = useState<StrategyDto[]>([]); const [strategies, setStrategies] = useState<StrategyDto[]>([]);
const [strategySearch, setStrategySearch] = useState(''); const [strategySearch, setStrategySearch] = useState('');
const [templateSearch, setTemplateSearch] = useState('');
const [templateApplyingKey, setTemplateApplyingKey] = useState<string | null>(null);
const [strategyListSort, setStrategyListSort] = useState<StrategyListSort>(loadStrategyListSort); const [strategyListSort, setStrategyListSort] = useState<StrategyListSort>(loadStrategyListSort);
const [selectedStrategyId, setSelectedStrategyId] = useState<number | null>(null); const [selectedStrategyId, setSelectedStrategyId] = useState<number | null>(null);
const [selectedStrategy, setSelectedStrategy] = useState<StrategyDto | null>(null); const [selectedStrategy, setSelectedStrategy] = useState<StrategyDto | null>(null);
@@ -237,6 +246,21 @@ function StrategyEvaluationPageInner({ theme = 'dark' }: Props) {
onStrategySaved: handleStrategyEditorSaved, onStrategySaved: handleStrategyEditorSaved,
}); });
const templateDef = useMemo(
() => buildStrategyEditorDefFromSettings(baseGetParams, getVisualConfig),
[baseGetParams, getVisualConfig, settingsRevision],
);
const templateRows = useMemo(
() => buildStrategyTemplatePaletteRows({ def: templateDef }),
[templateDef, settingsRevision],
);
const filteredTemplateRows = useMemo(
() => filterStrategyTemplatePaletteRows(templateRows, templateSearch),
[templateRows, templateSearch],
);
const [leftWidth, setLeftWidth] = useState(() => readMinWidth(LEFT_KEY, LEFT_DEFAULT)); const [leftWidth, setLeftWidth] = useState(() => readMinWidth(LEFT_KEY, LEFT_DEFAULT));
const [leftOpen, setLeftOpen] = useState(() => readStoredBool(LEFT_OPEN_KEY, true)); const [leftOpen, setLeftOpen] = useState(() => readStoredBool(LEFT_OPEN_KEY, true));
const [rightWidth, setRightWidth] = useState(() => readMinWidth(RIGHT_KEY, RIGHT_DEFAULT)); const [rightWidth, setRightWidth] = useState(() => readMinWidth(RIGHT_KEY, RIGHT_DEFAULT));
@@ -636,6 +660,15 @@ function StrategyEvaluationPageInner({ theme = 'dark' }: Props) {
setLeftTab('strategy'); setLeftTab('strategy');
}, []); }, []);
const handleApplyTemplate = useCallback(async (row: TemplatePaletteRow) => {
setTemplateApplyingKey(row.key);
try {
await strategyEditor.applyTemplateForEvaluation(row);
} finally {
setTemplateApplyingKey(null);
}
}, [strategyEditor]);
const handleSelectStrategyById = useCallback((id: number) => { const handleSelectStrategyById = useCallback((id: number) => {
const found = strategies.find(s => s.id === id); const found = strategies.find(s => s.id === id);
if (found) { if (found) {
@@ -933,6 +966,15 @@ function StrategyEvaluationPageInner({ theme = 'dark' }: Props) {
> >
</button> </button>
<button
type="button"
role="tab"
aria-selected={leftTab === 'template'}
className={`btd-exec-tab${leftTab === 'template' ? ' btd-exec-tab--on' : ''}`}
onClick={() => setLeftTab('template')}
>
릿
</button>
<button <button
type="button" type="button"
role="tab" role="tab"
@@ -1030,6 +1072,14 @@ function StrategyEvaluationPageInner({ theme = 'dark' }: Props) {
)} )}
</div> </div>
</> </>
) : leftTab === 'template' ? (
<StrategyEvaluationTemplateTab
rows={filteredTemplateRows}
searchQuery={templateSearch}
onSearchChange={setTemplateSearch}
applyingKey={templateApplyingKey}
onApply={row => { void handleApplyTemplate(row); }}
/>
) : leftTab === 'market' ? ( ) : leftTab === 'market' ? (
<StrategyEvaluationMarketTab <StrategyEvaluationMarketTab
activeMarket={market} activeMarket={market}
@@ -1056,7 +1106,7 @@ function StrategyEvaluationPageInner({ theme = 'dark' }: Props) {
side="left" side="left"
open={leftOpen} open={leftOpen}
onToggle={toggleLeftPanel} onToggle={toggleLeftPanel}
label={leftTab === 'market' ? '종목 패널' : '전략 패널'} label={leftTab === 'market' ? '종목 패널' : leftTab === 'template' ? '템플릿 패널' : '전략 패널'}
/> />
</div> </div>
@@ -0,0 +1,85 @@
/**
* 전략 평가 — 좌측 템플릿 탭 (전략편집기 템플릿 목록과 동일)
*/
import React from 'react';
import type { TemplatePaletteRow } from '../strategyEditor/TemplatePaletteTab';
import { templateBadgeForSource } from '../../utils/strategyEditorTemplatePaletteRows';
interface Props {
rows: TemplatePaletteRow[];
searchQuery: string;
onSearchChange: (query: string) => void;
applyingKey: string | null;
onApply: (row: TemplatePaletteRow) => void;
}
const StrategyEvaluationTemplateTab: React.FC<Props> = ({
rows,
searchQuery,
onSearchChange,
applyingKey,
onApply,
}) => (
<>
<div className="btd-exec-search">
<span className="btd-exec-search-icon" aria-hidden></span>
<input
className="btd-exec-search-input"
placeholder="템플릿 검색 (RSI, MA, 매수…)"
value={searchQuery}
onChange={e => onSearchChange(e.target.value)}
/>
{searchQuery && (
<button
type="button"
className="btd-exec-search-clear"
onClick={() => onSearchChange('')}
aria-label="검색어 지우기"
>
×
</button>
)}
</div>
<div className="btd-exec-scroll seval-template-scroll">
{rows.length === 0 ? (
<p className="btd-sidebar-empty">릿 </p>
) : (
rows.map(row => {
const sourceBadge = templateBadgeForSource(row.source);
const applying = applyingKey === row.key;
return (
<button
key={row.key}
type="button"
className={`btd-exec-item seval-template-item${applying ? ' seval-template-item--applying' : ''}`}
disabled={applyingKey != null}
onClick={() => onApply(row)}
>
<div className="btd-exec-item-top">
<span className="btd-exec-strategy seval-template-item-label">
{sourceBadge && (
<span className={`se-template-source-badge se-template-source-badge--${row.source}`}>
{sourceBadge}
</span>
)}
{row.label}
</span>
<span className={`se-template-signal se-template-signal--${row.signal}`}>
{row.signal === 'buy' ? '매수' : '매도'}
</span>
</div>
{row.description && (
<p className="btd-exec-meta seval-template-item-desc">{row.description}</p>
)}
{applying && (
<p className="btd-exec-meta seval-template-item-status"> </p>
)}
</button>
);
})
)}
</div>
</>
);
export default StrategyEvaluationTemplateTab;
@@ -58,6 +58,11 @@ import {
import type { PlotDef, HLineDef } from '../utils/indicatorRegistry'; import type { PlotDef, HLineDef } from '../utils/indicatorRegistry';
import type { IndicatorVisualConfig } from './useIndicatorSettings'; import type { IndicatorVisualConfig } from './useIndicatorSettings';
import type { PaletteItem } from '../utils/strategyPaletteStorage'; import type { PaletteItem } from '../utils/strategyPaletteStorage';
import type { TemplatePaletteRow } from '../components/strategyEditor/TemplatePaletteTab';
import {
buildTemplateEvaluationPayload,
suggestTemplateEvaluationStrategyName,
} from '../utils/strategyEvaluationTemplateApply';
const AUTOSAVE_MS = 400; const AUTOSAVE_MS = 400;
@@ -478,6 +483,38 @@ export function useStrategyEvaluationEditor({
}); });
}, [def, handleEditorStateChange]); }, [def, handleEditorStateChange]);
const applyTemplateForEvaluation = useCallback(async (row: TemplatePaletteRow): Promise<StrategyDto | null> => {
const payload = buildTemplateEvaluationPayload(row, def);
if (!payload) {
showSaveToast('템플릿을 적용할 수 없습니다');
return null;
}
setBuyEditorState(payload.buy);
setSellEditorState(payload.sell);
setBuyOrphans(payload.buyOrphans);
setSellOrphans(payload.sellOrphans);
setFlowLayout(payload.flowLayout);
setSignalTab(payload.preferredTab);
loadedStrategyIdRef.current = null;
buyEditorStateRef.current = payload.buy;
sellEditorStateRef.current = payload.sell;
buyOrphansRef.current = payload.buyOrphans;
sellOrphansRef.current = payload.sellOrphans;
flowLayoutRef.current = payload.flowLayout;
const existingNames = new Set(strategiesRef.current.map(s => s.name.trim()));
const name = suggestTemplateEvaluationStrategyName(row.label, existingNames);
return flushPersist({
asNew: true,
name,
description: payload.strategyDescription,
silent: false,
});
}, [def, flushPersist, showSaveToast]);
const switchSignalTab = useCallback((tab: 'buy' | 'sell') => { const switchSignalTab = useCallback((tab: 'buy' | 'sell') => {
if (tab === signalTab) return; if (tab === signalTab) return;
void flushPersist(); void flushPersist();
@@ -497,6 +534,7 @@ export function useStrategyEvaluationEditor({
applyPalette, applyPalette,
applyPaletteItem, applyPaletteItem,
applySidewaysFilter, applySidewaysFilter,
applyTemplateForEvaluation,
saving, saving,
saveNow, saveNow,
openSaveDialog, openSaveDialog,
@@ -3292,3 +3292,41 @@ html.theme-light .seval-analyze-btn:hover:not(:disabled),
border-color: rgba(25, 118, 210, 0.45); border-color: rgba(25, 118, 210, 0.45);
background: rgba(25, 118, 210, 0.08); background: rgba(25, 118, 210, 0.08);
} }
/* 좌측 — 전략템플릿 탭 */
.seval-template-scroll {
padding-top: 2px;
}
.seval-template-item .btd-exec-item-top {
align-items: flex-start;
gap: 8px;
}
.seval-template-item-label {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 4px 6px;
min-width: 0;
}
.seval-template-item-desc {
margin-top: 4px;
}
.seval-template-item-status {
margin-top: 4px;
color: var(--se-gold, #e6c200);
}
.seval-template-item--applying {
border-color: color-mix(in srgb, var(--se-gold) 45%, transparent);
box-shadow: 0 0 0 1px color-mix(in srgb, var(--se-gold) 25%, transparent);
}
.seval-template-item .se-template-source-badge {
font-size: 0.58rem;
padding: 1px 5px;
}
.seval-template-item .se-template-signal {
flex-shrink: 0;
font-size: 0.62rem;
font-weight: 700;
padding: 2px 6px;
border-radius: 999px;
}
@@ -0,0 +1,176 @@
/**
* 전략평가 — 템플릿 선택 시 편집 상태·저장용 payload 구성
*/
import type { TemplatePaletteRow } from '../components/strategyEditor/TemplatePaletteTab';
import type { LogicNode } from './strategyTypes';
import type { DefType } from './strategyEditorShared';
import { makeNode } from './strategyEditorShared';
import { simpleTemplateToNode } from './strategyPresets';
import {
buildStochOverboughtPairTree,
isStochOverboughtPairPaletteValue,
} from './stochOverboughtPair';
import {
buildPriceExtremeBreakoutTree,
isPriceExtremeBreakoutPairPaletteValue,
} from './priceExtremeBreakoutPair';
import {
buildInflection33Tree,
isInflection33PaletteValue,
} from './inflection33Strategy';
import {
buildIchimokuBbTree,
isIchimokuBbPaletteValue,
} from './ichimokuBbStrategy';
import {
buildStableStrategyTree,
isStableStrategyPaletteValue,
} from './stableStrategyPairs';
import {
buildSidewaysFilterNode,
loadSidewaysPaletteItems,
} from './sidewaysFilterPaletteStorage';
import type { PaletteItem } from './strategyPaletteStorage';
import {
decodeConditionForEditor,
emptyEditorConditionState,
normalizeStartCombineOp,
type EditorConditionState,
} from './strategyConditionSerde';
import {
buildStrategyFlowLayoutStore,
emptySignalFlowLayout,
emptyStrategyFlowLayout,
type StrategyFlowLayoutStore,
} from './strategyEditorLayoutStorage';
export type TemplateEvaluationPayload = {
buy: EditorConditionState;
sell: EditorConditionState;
buyOrphans: LogicNode[];
sellOrphans: LogicNode[];
flowLayout: StrategyFlowLayoutStore;
preferredTab: 'buy' | 'sell';
strategyName: string;
strategyDescription: string;
};
function buildLogicNodeFromPaletteItem(
item: PaletteItem,
signalTab: 'buy' | 'sell',
def: DefType,
): LogicNode | null {
const stochPair = isStochOverboughtPairPaletteValue(item.value);
const priceExtremeBreakout = isPriceExtremeBreakoutPairPaletteValue(item.value);
const inflection33 = isInflection33PaletteValue(item.value);
const ichimokuBb = isIchimokuBbPaletteValue(item.value);
const stableStrategy = isStableStrategyPaletteValue(item.value);
const composite = item.kind === 'composite'
&& !stochPair
&& !priceExtremeBreakout
&& !inflection33
&& !ichimokuBb
&& !stableStrategy;
if (stochPair) return buildStochOverboughtPairTree(item.secondaryIndicator ?? 'CCI', def);
if (priceExtremeBreakout) return buildPriceExtremeBreakoutTree(item.value, def);
if (inflection33) return buildInflection33Tree(item.value, def);
if (ichimokuBb) return buildIchimokuBbTree(item.value, def);
if (stableStrategy) return buildStableStrategyTree(item.value, def);
return makeNode('indicator', item.value, signalTab, def, composite ? { composite: true } : undefined);
}
function layoutFromEditorState(state: EditorConditionState, orphans: LogicNode[] = []) {
return {
positions: {},
edgeHandles: {},
orphans,
startMeta: state.startMeta,
extraStartIds: state.extraStartIds,
extraRoots: state.extraRoots,
startCombineOp: normalizeStartCombineOp(state.startCombineOp),
};
}
function payloadForSide(
signal: 'buy' | 'sell',
editorState: EditorConditionState,
orphans: LogicNode[],
row: TemplatePaletteRow,
): TemplateEvaluationPayload {
const emptyBuy = emptyEditorConditionState();
const emptySell = emptyEditorConditionState();
const buy = signal === 'buy' ? editorState : emptyBuy;
const sell = signal === 'sell' ? editorState : emptySell;
const buyOrphans = signal === 'buy' ? orphans : [];
const sellOrphans = signal === 'sell' ? orphans : [];
const buyLayout = signal === 'buy'
? layoutFromEditorState(editorState, buyOrphans)
: emptySignalFlowLayout();
const sellLayout = signal === 'sell'
? layoutFromEditorState(editorState, sellOrphans)
: emptySignalFlowLayout();
return {
buy,
sell,
buyOrphans,
sellOrphans,
flowLayout: buildStrategyFlowLayoutStore(buyLayout, sellLayout, buyOrphans, sellOrphans),
preferredTab: signal,
strategyName: `[템플릿] ${row.label}`,
strategyDescription: row.description?.trim() || `전략 템플릿: ${row.label}`,
};
}
export function buildTemplateEvaluationPayload(
row: TemplatePaletteRow,
def: DefType,
): TemplateEvaluationPayload | null {
if (row.source === 'user' && row.user) {
const t = row.user;
const editorState: EditorConditionState = {
root: t.editorState.root,
startMeta: t.editorState.startMeta,
extraStartIds: t.editorState.extraStartIds,
extraRoots: t.editorState.extraRoots ?? {},
startCombineOp: normalizeStartCombineOp(t.editorState.startCombineOp),
};
return payloadForSide(t.signal, editorState, t.orphans ?? [], row);
}
if (row.source === 'builtin' && row.builtin) {
const root = row.builtin.kind === 'composite'
? row.builtin.build()
: simpleTemplateToNode(row.builtin);
const editorState = decodeConditionForEditor(root);
return payloadForSide(row.builtin.signal, editorState, [], row);
}
if (row.source === 'composite-palette' && row.paletteItem) {
const root = buildLogicNodeFromPaletteItem(row.paletteItem, row.signal, def);
if (!root) return null;
const editorState = decodeConditionForEditor(root);
return payloadForSide(row.signal, editorState, [], row);
}
if (row.source === 'sideways-palette' && row.sidewaysFilterId) {
const root = buildSidewaysFilterNode(row.sidewaysFilterId, def, loadSidewaysPaletteItems());
if (!root) return null;
const editorState = decodeConditionForEditor(root);
return payloadForSide(row.signal, editorState, [], row);
}
return null;
}
export function suggestTemplateEvaluationStrategyName(
baseLabel: string,
existingNames: ReadonlySet<string>,
): string {
const base = `[템플릿] ${baseLabel.trim() || '새 전략'}`;
if (!existingNames.has(base)) return base;
let n = 2;
while (existingNames.has(`${base} (${n})`)) n += 1;
return `${base} (${n})`;
}
@@ -0,0 +1,89 @@
/**
* 전략편집기·전략평가 — 템플릿 팔레트 목록 (공통)
*/
import type { TemplatePaletteRow } from '../components/strategyEditor/TemplatePaletteTab';
import type { DefType } from './strategyEditorShared';
import {
getBuiltinTemplateId,
getStrategyTemplates,
} from './strategyPresets';
import {
buildCompositePaletteTemplateRows,
buildSidewaysPaletteTemplateRows,
} from './strategyEditorTemplatePaletteRows';
import {
loadHiddenBuiltinTemplateIds,
loadUserStrategyTemplates,
type UserStrategyTemplate,
} from './strategyTemplateStorage';
import { loadPaletteItems, type PaletteItem } from './strategyPaletteStorage';
import { loadSidewaysPaletteItems, type SidewaysFilterPaletteEntry } from './sidewaysFilterPaletteStorage';
export function buildStrategyTemplatePaletteRows(opts: {
def: DefType;
userTemplates?: UserStrategyTemplate[];
hiddenBuiltinTemplateIds?: string[];
compositePalette?: PaletteItem[];
sidewaysPalette?: SidewaysFilterPaletteEntry[];
}): TemplatePaletteRow[] {
const {
def,
userTemplates = loadUserStrategyTemplates(),
hiddenBuiltinTemplateIds = loadHiddenBuiltinTemplateIds(),
compositePalette = loadPaletteItems('composite'),
sidewaysPalette = loadSidewaysPaletteItems(),
} = opts;
const userRows: TemplatePaletteRow[] = userTemplates.map(t => ({
key: t.id,
source: 'user',
label: t.label,
description: t.description,
signal: t.signal,
user: t,
}));
const hidden = new Set(hiddenBuiltinTemplateIds);
const templates = getStrategyTemplates(def);
const builtinRows: TemplatePaletteRow[] = templates
.filter(t => !hidden.has(getBuiltinTemplateId(t)))
.map(t => {
const builtinId = getBuiltinTemplateId(t);
return {
key: builtinId,
source: 'builtin',
label: t.label,
description: 'description' in t ? t.description : undefined,
signal: t.signal,
builtinId,
builtin: t,
};
});
const compositeRows: TemplatePaletteRow[] = buildCompositePaletteTemplateRows(compositePalette, hidden);
const sidewaysRows: TemplatePaletteRow[] = buildSidewaysPaletteTemplateRows(sidewaysPalette, hidden);
return [...userRows, ...builtinRows, ...compositeRows, ...sidewaysRows];
}
export function filterStrategyTemplatePaletteRows(
rows: TemplatePaletteRow[],
searchQuery: string,
): TemplatePaletteRow[] {
const q = searchQuery.trim().toLowerCase();
if (!q) return rows;
return rows.filter(row => {
const signalLabel = row.signal === 'buy' ? '매수' : '매도';
const badge = row.source === 'user' ? '내 템플릿'
: row.source === 'composite-palette' ? '복합지표'
: row.source === 'sideways-palette' ? '횡보지표'
: '';
return (
row.label.toLowerCase().includes(q)
|| row.description?.toLowerCase().includes(q)
|| signalLabel.includes(q)
|| row.signal.includes(q)
|| badge.includes(q)
);
});
}