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

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
@@ -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)
);
});
}