전략평가 화면 전략템플릿 목록탭 적용
This commit is contained in:
@@ -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)
|
||||
);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user