전략빌더 수정
This commit is contained in:
@@ -27,6 +27,7 @@ import {
|
||||
deleteStrategyFlowLayout,
|
||||
type SignalFlowLayoutSnapshot,
|
||||
type FlowLayoutChangePayload,
|
||||
type StrategyFlowLayoutStore,
|
||||
} from '../utils/strategyEditorLayoutStorage';
|
||||
import LogicExpressionPreview from './strategyEditor/LogicExpressionPreview';
|
||||
import StrategyEditorCanvas, { type FlowLayoutSeed } from './strategyEditor/StrategyEditorCanvas';
|
||||
@@ -42,6 +43,15 @@ import {
|
||||
simpleTemplateToNode,
|
||||
type StrategyTemplateDef,
|
||||
} from '../utils/strategyPresets';
|
||||
import {
|
||||
buildStrategyExportPayload,
|
||||
buildStrategyListExportPayload,
|
||||
downloadStrategyJson,
|
||||
listImportItemToStrategyDto,
|
||||
parseStrategyImportFile,
|
||||
pickJsonFile,
|
||||
strategyDtoToListExportItem,
|
||||
} from '../utils/strategyImportExport';
|
||||
import PaletteChip from './strategyEditor/PaletteChip';
|
||||
import { readStoredSize, storeSize, usePanelResize } from './strategyEditor/usePanelResize';
|
||||
import '../styles/strategyEditor.css';
|
||||
@@ -393,6 +403,113 @@ export default function StrategyEditorPage({ theme, activeIndicators = [] }: Pro
|
||||
showSnack('전략이 삭제되었습니다');
|
||||
};
|
||||
|
||||
const applyImportedFlowLayout = useCallback((layout: StrategyFlowLayoutStore | undefined, tab: 'buy' | 'sell' = signalTab) => {
|
||||
if (layout) {
|
||||
setBuyLayout({
|
||||
positions: layout.buy.positions ?? {},
|
||||
edgeHandles: layout.buy.edgeHandles ?? {},
|
||||
});
|
||||
setSellLayout({
|
||||
positions: layout.sell.positions ?? {},
|
||||
edgeHandles: layout.sell.edgeHandles ?? {},
|
||||
});
|
||||
setBuyOrphans(layout.buy.orphans ?? []);
|
||||
setSellOrphans(layout.sell.orphans ?? []);
|
||||
saveStrategyFlowLayout('draft', layout);
|
||||
} else {
|
||||
resetFlowLayout('draft', tab);
|
||||
}
|
||||
bumpLayoutSeed('draft', tab);
|
||||
}, [resetFlowLayout, bumpLayoutSeed, signalTab]);
|
||||
|
||||
const handleExport = useCallback(() => {
|
||||
if (!buyCondition && !sellCondition) {
|
||||
showSnack('내보낼 조건이 없습니다', false);
|
||||
return;
|
||||
}
|
||||
if (editorMode === 'graph') layoutFlushRef.current?.();
|
||||
const payload = buildStrategyExportPayload({
|
||||
name: stratName,
|
||||
description: stratDesc,
|
||||
buyCondition,
|
||||
sellCondition,
|
||||
flowLayout: {
|
||||
buy: { ...buyLayoutRef.current, orphans: buyOrphans },
|
||||
sell: { ...sellLayoutRef.current, orphans: sellOrphans },
|
||||
},
|
||||
editorMode,
|
||||
});
|
||||
const safeName = (stratName || '전략').replace(/[^\w\uAC00-\uD7A3-]+/g, '_');
|
||||
downloadStrategyJson(`${safeName}_${new Date().toISOString().slice(0, 10)}`, payload);
|
||||
showSnack('JSON으로 내보냈습니다');
|
||||
}, [buyCondition, sellCondition, stratName, stratDesc, buyOrphans, sellOrphans, editorMode]);
|
||||
|
||||
const handleImport = useCallback(async () => {
|
||||
const text = await pickJsonFile();
|
||||
if (!text) return;
|
||||
try {
|
||||
const result = parseStrategyImportFile(text);
|
||||
if (result.kind === 'list') {
|
||||
showSnack('단일 전략 파일이 아닙니다. 전체 가져오기(⬆)를 사용하세요', false);
|
||||
return;
|
||||
}
|
||||
if (editorMode === 'graph') layoutFlushRef.current?.();
|
||||
const { data } = result;
|
||||
setBuyCondition(data.buyCondition ?? null);
|
||||
setSellCondition(data.sellCondition ?? null);
|
||||
setStratName(data.name ?? '가져온 전략');
|
||||
setStratDesc(data.description ?? '');
|
||||
setSelectedId(null);
|
||||
setSelectedNodeId(null);
|
||||
applyImportedFlowLayout(data.flowLayout);
|
||||
if (data.editorMode === 'list' || data.editorMode === 'graph') {
|
||||
setEditorMode(data.editorMode);
|
||||
saveEditorMode(data.editorMode);
|
||||
}
|
||||
showSnack('전략을 가져왔습니다');
|
||||
} catch (e) {
|
||||
showSnack(e instanceof Error ? e.message : '파일 읽기 실패', false);
|
||||
}
|
||||
}, [editorMode, applyImportedFlowLayout]);
|
||||
|
||||
const handleExportAll = useCallback(() => {
|
||||
if (strategies.length === 0) {
|
||||
showSnack('내보낼 전략이 없습니다', false);
|
||||
return;
|
||||
}
|
||||
const items = strategies.map(s => strategyDtoToListExportItem(
|
||||
s,
|
||||
loadStrategyFlowLayout(String(s.id)),
|
||||
));
|
||||
downloadStrategyJson(
|
||||
`전략목록_${new Date().toISOString().slice(0, 10)}`,
|
||||
buildStrategyListExportPayload(items),
|
||||
);
|
||||
showSnack(`${strategies.length}개 전략을 내보냈습니다`);
|
||||
}, [strategies]);
|
||||
|
||||
const handleImportAll = useCallback(async () => {
|
||||
const text = await pickJsonFile();
|
||||
if (!text) return;
|
||||
try {
|
||||
const result = parseStrategyImportFile(text);
|
||||
if (result.kind !== 'list') {
|
||||
showSnack('전략 목록 형식 파일이 필요합니다', false);
|
||||
return;
|
||||
}
|
||||
const baseId = Date.now();
|
||||
const imported = result.data.strategies.map((item, index) => {
|
||||
const id = baseId + index;
|
||||
if (item.flowLayout) saveStrategyFlowLayout(String(id), item.flowLayout);
|
||||
return listImportItemToStrategyDto(item, id);
|
||||
});
|
||||
setStrategies(prev => [...prev, ...imported]);
|
||||
showSnack(`${imported.length}개 전략을 가져왔습니다`);
|
||||
} catch (e) {
|
||||
showSnack(e instanceof Error ? e.message : '파일 읽기 실패', false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const applyPalette = useCallback((type: string, value: string, label: string) => {
|
||||
const newNode = makeNode(type, value, signalTab, DEF);
|
||||
const root = currentRoot;
|
||||
@@ -404,21 +521,23 @@ export default function StrategyEditorPage({ theme, activeIndicators = [] }: Pro
|
||||
const templates = useMemo(() => getStrategyTemplates(DEF), [DEF]);
|
||||
|
||||
const handleTemplate = useCallback((tmpl: StrategyTemplateDef) => {
|
||||
if (tmpl.signal !== signalTab) switchSignalTab(tmpl.signal);
|
||||
const setRoot = tmpl.signal === 'buy' ? setBuyCondition : setSellCondition;
|
||||
const root = tmpl.signal === 'buy' ? buyCondition : sellCondition;
|
||||
if (editorMode === 'graph') layoutFlushRef.current?.();
|
||||
|
||||
if (tmpl.kind === 'composite') {
|
||||
setRoot(tmpl.build());
|
||||
showSnack(`"${tmpl.label}" 템플릿이 적용됐습니다`);
|
||||
return;
|
||||
}
|
||||
const targetTab = tmpl.signal;
|
||||
const nextRoot = tmpl.kind === 'composite' ? tmpl.build() : simpleTemplateToNode(tmpl);
|
||||
|
||||
const newNode = simpleTemplateToNode(tmpl);
|
||||
if (!root) setRoot(newNode);
|
||||
else setRoot(mergeAtRoot(root, newNode, false));
|
||||
showSnack(`"${tmpl.label}" 추가됨`);
|
||||
}, [signalTab, switchSignalTab, buyCondition, sellCondition, setBuyCondition, setSellCondition]);
|
||||
setBuyCondition(null);
|
||||
setSellCondition(null);
|
||||
setSelectedNodeId(null);
|
||||
|
||||
if (targetTab === 'buy') setBuyCondition(nextRoot);
|
||||
else setSellCondition(nextRoot);
|
||||
|
||||
resetFlowLayout(layoutStrategyKey, targetTab);
|
||||
if (targetTab !== signalTab) setSignalTab(targetTab);
|
||||
|
||||
showSnack(`"${tmpl.label}" 템플릿이 적용됐습니다`);
|
||||
}, [editorMode, layoutStrategyKey, resetFlowLayout, signalTab]);
|
||||
|
||||
const operators = [
|
||||
{ type: 'operator' as const, value: 'AND', label: 'AND', color: 'logic-and' },
|
||||
@@ -430,6 +549,7 @@ export default function StrategyEditorPage({ theme, activeIndicators = [] }: Pro
|
||||
{ type: 'indicator' as const, value: 'MA', label: 'MA', desc: '이동평균', color: 'band' },
|
||||
{ type: 'indicator' as const, value: 'EMA', label: 'EMA', desc: '지수이동평균', color: 'band' },
|
||||
{ type: 'indicator' as const, value: 'BOLLINGER', label: 'Bollinger', desc: '볼린저밴드', color: 'band' },
|
||||
{ type: 'indicator' as const, value: 'DONCHIAN', label: 'Donchian', desc: '돈치안 채널', color: 'band' },
|
||||
{ type: 'indicator' as const, value: 'ICHIMOKU', label: 'Ichimoku', desc: '일목균형표', color: 'band' },
|
||||
];
|
||||
|
||||
@@ -484,6 +604,40 @@ export default function StrategyEditorPage({ theme, activeIndicators = [] }: Pro
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" className="se-btn se-btn--ghost" onClick={handleNew}>+ 새 전략</button>
|
||||
<button
|
||||
type="button"
|
||||
className="se-btn se-btn--ghost se-btn--icon"
|
||||
title="현재 전략 JSON 내보내기"
|
||||
onClick={handleExport}
|
||||
disabled={!buyCondition && !sellCondition}
|
||||
>
|
||||
↓
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="se-btn se-btn--ghost se-btn--icon"
|
||||
title="JSON 파일에서 전략 가져오기"
|
||||
onClick={() => { void handleImport(); }}
|
||||
>
|
||||
↑
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="se-btn se-btn--ghost se-btn--icon"
|
||||
title="전체 전략 목록 JSON 내보내기"
|
||||
onClick={handleExportAll}
|
||||
disabled={strategies.length === 0}
|
||||
>
|
||||
⬇
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="se-btn se-btn--ghost se-btn--icon"
|
||||
title="전체 전략 목록 JSON 가져오기"
|
||||
onClick={() => { void handleImportAll(); }}
|
||||
>
|
||||
⬆
|
||||
</button>
|
||||
<button type="button" className="se-btn se-btn--gold" onClick={() => setSaveOpen(true)}>저장하기</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
Reference in New Issue
Block a user