전략평가 화면 전략템플릿 목록탭 적용
This commit is contained in:
@@ -55,9 +55,9 @@ import {
|
||||
type PaletteItem,
|
||||
} from '../utils/strategyPaletteStorage';
|
||||
import {
|
||||
buildCompositePaletteTemplateRows,
|
||||
buildSidewaysPaletteTemplateRows,
|
||||
} from '../utils/strategyEditorTemplatePaletteRows';
|
||||
buildStrategyTemplatePaletteRows,
|
||||
filterStrategyTemplatePaletteRows,
|
||||
} from '../utils/strategyTemplateList';
|
||||
import {
|
||||
buildPriceExtremeBreakoutTree,
|
||||
isPriceExtremeBreakoutPairPaletteValue,
|
||||
@@ -125,8 +125,6 @@ import {
|
||||
type StrategyEditorMode,
|
||||
} from '../utils/strategyEditorModeStorage';
|
||||
import {
|
||||
getStrategyTemplates,
|
||||
getBuiltinTemplateId,
|
||||
simpleTemplateToNode,
|
||||
type StrategyTemplateDef,
|
||||
} from '../utils/strategyPresets';
|
||||
@@ -1398,55 +1396,21 @@ export default function StrategyEditorPage({
|
||||
scheduleStrategyPersist();
|
||||
}, [DEF, currentRoot, setCurrentRoot, scheduleStrategyPersist, sidewaysPalette]);
|
||||
|
||||
const templates = useMemo(() => getStrategyTemplates(DEF), [DEF]);
|
||||
|
||||
const templateRows = useMemo(() => {
|
||||
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 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)
|
||||
const templateRows = useMemo(
|
||||
() => buildStrategyTemplatePaletteRows({
|
||||
def: DEF,
|
||||
userTemplates,
|
||||
hiddenBuiltinTemplateIds,
|
||||
compositePalette,
|
||||
sidewaysPalette,
|
||||
}),
|
||||
[DEF, userTemplates, hiddenBuiltinTemplateIds, compositePalette, sidewaysPalette],
|
||||
);
|
||||
|
||||
const filteredTemplateRows = useMemo(
|
||||
() => filterStrategyTemplatePaletteRows(templateRows, templateSearch),
|
||||
[templateRows, templateSearch],
|
||||
);
|
||||
});
|
||||
}, [templateRows, templateSearch]);
|
||||
|
||||
const handleTemplate = useCallback((tmpl: StrategyTemplateDef) => {
|
||||
if (editorMode === 'graph') layoutFlushRef.current?.();
|
||||
|
||||
@@ -28,6 +28,7 @@ import StrategyEvaluationStrategyPanel from './strategyEvaluation/StrategyEvalua
|
||||
import StrategyEvaluationIndicatorPalettePanel from './strategyEvaluation/StrategyEvaluationIndicatorPalettePanel';
|
||||
import StrategyEvaluationSettingsTab from './strategyEvaluation/StrategyEvaluationSettingsTab';
|
||||
import StrategyEvaluationMarketTab from './strategyEvaluation/StrategyEvaluationMarketTab';
|
||||
import StrategyEvaluationTemplateTab from './strategyEvaluation/StrategyEvaluationTemplateTab';
|
||||
import { usePageRefresh } from '../utils/pageRefreshEvents';
|
||||
import StrategyEditorModal from './strategyEvaluation/StrategyEditorModal';
|
||||
import SePanelCollapseHandle from './strategyEditor/SePanelCollapseHandle';
|
||||
@@ -87,6 +88,12 @@ import {
|
||||
import type { StrategyEvaluationAiVerifyResponse } from '../utils/backendApi';
|
||||
import { useStrategyBulkEvaluation } from '../hooks/useStrategyBulkEvaluation';
|
||||
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_OPEN_KEY = 'seval-left-open';
|
||||
@@ -99,7 +106,7 @@ const STRATEGY_PANEL_MIN = 160;
|
||||
const STRATEGY_PANEL_MAX = 520;
|
||||
const STRATEGY_PANEL_DEFAULT = 280;
|
||||
|
||||
type LeftTab = 'strategy' | 'market' | 'settings';
|
||||
type LeftTab = 'strategy' | 'template' | 'market' | 'settings';
|
||||
type RightTab = 'signal' | 'indicators' | 'chart' | 'ai';
|
||||
|
||||
function readMinWidth(key: string, fallback: number): number {
|
||||
@@ -141,6 +148,8 @@ function StrategyEvaluationPageInner({ theme = 'dark' }: Props) {
|
||||
const [marketSearchOpen, setMarketSearchOpen] = useState(false);
|
||||
const [strategies, setStrategies] = useState<StrategyDto[]>([]);
|
||||
const [strategySearch, setStrategySearch] = useState('');
|
||||
const [templateSearch, setTemplateSearch] = useState('');
|
||||
const [templateApplyingKey, setTemplateApplyingKey] = useState<string | null>(null);
|
||||
const [strategyListSort, setStrategyListSort] = useState<StrategyListSort>(loadStrategyListSort);
|
||||
const [selectedStrategyId, setSelectedStrategyId] = useState<number | null>(null);
|
||||
const [selectedStrategy, setSelectedStrategy] = useState<StrategyDto | null>(null);
|
||||
@@ -237,6 +246,21 @@ function StrategyEvaluationPageInner({ theme = 'dark' }: Props) {
|
||||
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 [leftOpen, setLeftOpen] = useState(() => readStoredBool(LEFT_OPEN_KEY, true));
|
||||
const [rightWidth, setRightWidth] = useState(() => readMinWidth(RIGHT_KEY, RIGHT_DEFAULT));
|
||||
@@ -636,6 +660,15 @@ function StrategyEvaluationPageInner({ theme = 'dark' }: Props) {
|
||||
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 found = strategies.find(s => s.id === id);
|
||||
if (found) {
|
||||
@@ -933,6 +966,15 @@ function StrategyEvaluationPageInner({ theme = 'dark' }: Props) {
|
||||
>
|
||||
전략
|
||||
</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
|
||||
type="button"
|
||||
role="tab"
|
||||
@@ -1030,6 +1072,14 @@ function StrategyEvaluationPageInner({ theme = 'dark' }: Props) {
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : leftTab === 'template' ? (
|
||||
<StrategyEvaluationTemplateTab
|
||||
rows={filteredTemplateRows}
|
||||
searchQuery={templateSearch}
|
||||
onSearchChange={setTemplateSearch}
|
||||
applyingKey={templateApplyingKey}
|
||||
onApply={row => { void handleApplyTemplate(row); }}
|
||||
/>
|
||||
) : leftTab === 'market' ? (
|
||||
<StrategyEvaluationMarketTab
|
||||
activeMarket={market}
|
||||
@@ -1056,7 +1106,7 @@ function StrategyEvaluationPageInner({ theme = 'dark' }: Props) {
|
||||
side="left"
|
||||
open={leftOpen}
|
||||
onToggle={toggleLeftPanel}
|
||||
label={leftTab === 'market' ? '종목 패널' : '전략 패널'}
|
||||
label={leftTab === 'market' ? '종목 패널' : leftTab === 'template' ? '템플릿 패널' : '전략 패널'}
|
||||
/>
|
||||
</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 { IndicatorVisualConfig } from './useIndicatorSettings';
|
||||
import type { PaletteItem } from '../utils/strategyPaletteStorage';
|
||||
import type { TemplatePaletteRow } from '../components/strategyEditor/TemplatePaletteTab';
|
||||
import {
|
||||
buildTemplateEvaluationPayload,
|
||||
suggestTemplateEvaluationStrategyName,
|
||||
} from '../utils/strategyEvaluationTemplateApply';
|
||||
|
||||
const AUTOSAVE_MS = 400;
|
||||
|
||||
@@ -478,6 +483,38 @@ export function useStrategyEvaluationEditor({
|
||||
});
|
||||
}, [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') => {
|
||||
if (tab === signalTab) return;
|
||||
void flushPersist();
|
||||
@@ -497,6 +534,7 @@ export function useStrategyEvaluationEditor({
|
||||
applyPalette,
|
||||
applyPaletteItem,
|
||||
applySidewaysFilter,
|
||||
applyTemplateForEvaluation,
|
||||
saving,
|
||||
saveNow,
|
||||
openSaveDialog,
|
||||
|
||||
@@ -3292,3 +3292,41 @@ html.theme-light .seval-analyze-btn:hover:not(:disabled),
|
||||
border-color: rgba(25, 118, 210, 0.45);
|
||||
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)
|
||||
);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user