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