다중 시간봉 전략 설정기능 추가
This commit is contained in:
@@ -19,6 +19,19 @@ import {
|
||||
type StrategyDto,
|
||||
} from '../utils/strategyEditorShared';
|
||||
import { findLogicNode, getIndicatorPeriodLabel } from '../utils/strategyFlowLayout';
|
||||
import {
|
||||
decodeConditionForEditor,
|
||||
encodeConditionForSave,
|
||||
addExtraStartSection,
|
||||
hasMultipleStartSections,
|
||||
normalizeStartCombineOp,
|
||||
updateStartCombineOp,
|
||||
type EditorConditionState,
|
||||
} from '../utils/strategyConditionSerde';
|
||||
import {
|
||||
defaultStartMeta,
|
||||
type StartCombineOp,
|
||||
} from '../utils/strategyStartNodes';
|
||||
import {
|
||||
COMPOSITE_INDICATOR_ITEMS,
|
||||
compositePeriodLabel,
|
||||
@@ -36,6 +49,7 @@ import {
|
||||
import LogicExpressionPreview from './strategyEditor/LogicExpressionPreview';
|
||||
import StrategyEditorCanvas, { type FlowLayoutSeed } from './strategyEditor/StrategyEditorCanvas';
|
||||
import StrategyListEditor from './strategyEditor/StrategyListEditor';
|
||||
import StartCombineOpControl from './strategyEditor/StartCombineOpControl';
|
||||
import { layoutFlushRef } from './strategyEditor/strategyEditorCallbacks';
|
||||
import {
|
||||
loadEditorMode,
|
||||
@@ -80,6 +94,34 @@ function readTabLayout(strategyKey: string, tab: 'buy' | 'sell'): SignalFlowLayo
|
||||
positions: snap.positions ?? {},
|
||||
edgeHandles: snap.edgeHandles ?? {},
|
||||
orphans: snap.orphans ?? [],
|
||||
startMeta: snap.startMeta ?? defaultStartMeta(),
|
||||
extraStartIds: snap.extraStartIds ?? [],
|
||||
extraRoots: snap.extraRoots ?? {},
|
||||
startCombineOp: normalizeStartCombineOp(snap.startCombineOp),
|
||||
};
|
||||
}
|
||||
|
||||
function toEditorState(
|
||||
root: LogicNode | null,
|
||||
layout: Pick<SignalFlowLayoutSnapshot, 'startMeta' | 'extraStartIds' | 'extraRoots' | 'startCombineOp'>,
|
||||
): EditorConditionState {
|
||||
return {
|
||||
root,
|
||||
startMeta: layout.startMeta ?? defaultStartMeta(),
|
||||
extraStartIds: layout.extraStartIds ?? [],
|
||||
extraRoots: layout.extraRoots ?? {},
|
||||
startCombineOp: normalizeStartCombineOp(layout.startCombineOp),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeTabLayoutState(snap: SignalFlowLayoutSnapshot) {
|
||||
return {
|
||||
positions: snap.positions ?? {},
|
||||
edgeHandles: snap.edgeHandles ?? {},
|
||||
startMeta: snap.startMeta ?? defaultStartMeta(),
|
||||
extraStartIds: snap.extraStartIds ?? [],
|
||||
extraRoots: snap.extraRoots ?? {},
|
||||
startCombineOp: normalizeStartCombineOp(snap.startCombineOp),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -114,10 +156,18 @@ export default function StrategyEditorPage({ theme }: Props) {
|
||||
const [buyLayout, setBuyLayout] = useState(() => ({
|
||||
positions: initialDraftBuy.positions,
|
||||
edgeHandles: initialDraftBuy.edgeHandles,
|
||||
startMeta: initialDraftBuy.startMeta ?? defaultStartMeta(),
|
||||
extraStartIds: initialDraftBuy.extraStartIds ?? [],
|
||||
extraRoots: initialDraftBuy.extraRoots ?? {},
|
||||
startCombineOp: normalizeStartCombineOp(initialDraftBuy.startCombineOp),
|
||||
}));
|
||||
const [sellLayout, setSellLayout] = useState(() => ({
|
||||
positions: initialDraftSell.positions,
|
||||
edgeHandles: initialDraftSell.edgeHandles,
|
||||
startMeta: initialDraftSell.startMeta ?? defaultStartMeta(),
|
||||
extraStartIds: initialDraftSell.extraStartIds ?? [],
|
||||
extraRoots: initialDraftSell.extraRoots ?? {},
|
||||
startCombineOp: normalizeStartCombineOp(initialDraftSell.startCombineOp),
|
||||
}));
|
||||
const buyLayoutRef = useRef(buyLayout);
|
||||
const sellLayoutRef = useRef(sellLayout);
|
||||
@@ -165,10 +215,28 @@ export default function StrategyEditorPage({ theme }: Props) {
|
||||
const currentOrphans = signalTab === 'buy' ? buyOrphans : sellOrphans;
|
||||
const setCurrentOrphans = signalTab === 'buy' ? setBuyOrphans : setSellOrphans;
|
||||
|
||||
const currentLayout = signalTab === 'buy' ? buyLayout : sellLayout;
|
||||
const setCurrentLayout = signalTab === 'buy' ? setBuyLayout : setSellLayout;
|
||||
|
||||
const buyEditorState = useMemo(
|
||||
() => toEditorState(buyCondition, buyLayout),
|
||||
[buyCondition, buyLayout],
|
||||
);
|
||||
const sellEditorState = useMemo(
|
||||
() => toEditorState(sellCondition, sellLayout),
|
||||
[sellCondition, sellLayout],
|
||||
);
|
||||
const currentEditorState = signalTab === 'buy' ? buyEditorState : sellEditorState;
|
||||
|
||||
const selectedLogicNode = useMemo(() => {
|
||||
if (!selectedNodeId) return null;
|
||||
return findLogicNode(currentRoot, currentOrphans, selectedNodeId);
|
||||
}, [selectedNodeId, currentRoot, currentOrphans]);
|
||||
return findLogicNode(
|
||||
currentRoot,
|
||||
currentOrphans,
|
||||
selectedNodeId,
|
||||
currentLayout.extraRoots ?? {},
|
||||
);
|
||||
}, [selectedNodeId, currentRoot, currentOrphans, currentLayout.extraRoots]);
|
||||
|
||||
const orphanTotal = buyOrphans.length + sellOrphans.length;
|
||||
|
||||
@@ -197,8 +265,8 @@ export default function StrategyEditorPage({ theme }: Props) {
|
||||
const resetFlowLayout = useCallback((strategyKey: string, tab: 'buy' | 'sell' = 'buy') => {
|
||||
const emptyBuy = emptySignalFlowLayout();
|
||||
const emptySell = emptySignalFlowLayout();
|
||||
setBuyLayout(emptyBuy);
|
||||
setSellLayout(emptySell);
|
||||
setBuyLayout(normalizeTabLayoutState(emptyBuy));
|
||||
setSellLayout(normalizeTabLayoutState(emptySell));
|
||||
setBuyOrphans([]);
|
||||
setSellOrphans([]);
|
||||
saveStrategyFlowLayout(strategyKey, { buy: emptyBuy, sell: emptySell });
|
||||
@@ -211,10 +279,18 @@ export default function StrategyEditorPage({ theme }: Props) {
|
||||
const nextBuy = {
|
||||
positions: stored.buy.positions ?? {},
|
||||
edgeHandles: stored.buy.edgeHandles ?? {},
|
||||
startMeta: stored.buy.startMeta ?? defaultStartMeta(),
|
||||
extraStartIds: stored.buy.extraStartIds ?? [],
|
||||
extraRoots: stored.buy.extraRoots ?? {},
|
||||
startCombineOp: normalizeStartCombineOp(stored.buy.startCombineOp),
|
||||
};
|
||||
const nextSell = {
|
||||
positions: stored.sell.positions ?? {},
|
||||
edgeHandles: stored.sell.edgeHandles ?? {},
|
||||
startMeta: stored.sell.startMeta ?? defaultStartMeta(),
|
||||
extraStartIds: stored.sell.extraStartIds ?? [],
|
||||
extraRoots: stored.sell.extraRoots ?? {},
|
||||
startCombineOp: normalizeStartCombineOp(stored.sell.startCombineOp),
|
||||
};
|
||||
setBuyLayout(nextBuy);
|
||||
setSellLayout(nextSell);
|
||||
@@ -223,8 +299,8 @@ export default function StrategyEditorPage({ theme }: Props) {
|
||||
} else {
|
||||
const emptyBuy = emptySignalFlowLayout();
|
||||
const emptySell = emptySignalFlowLayout();
|
||||
setBuyLayout(emptyBuy);
|
||||
setSellLayout(emptySell);
|
||||
setBuyLayout(normalizeTabLayoutState(emptyBuy));
|
||||
setSellLayout(normalizeTabLayoutState(emptySell));
|
||||
setBuyOrphans([]);
|
||||
setSellOrphans([]);
|
||||
}
|
||||
@@ -232,15 +308,47 @@ export default function StrategyEditorPage({ theme }: Props) {
|
||||
}, [bumpLayoutSeed]);
|
||||
|
||||
const handleLayoutChange = useCallback((snapshot: FlowLayoutChangePayload) => {
|
||||
const next = {
|
||||
const patch = {
|
||||
positions: snapshot.positions,
|
||||
edgeHandles: snapshot.edgeHandles,
|
||||
};
|
||||
if (snapshot.tab === 'buy') setBuyLayout(next);
|
||||
else setSellLayout(next);
|
||||
if (snapshot.tab === 'buy') setBuyLayout(prev => ({ ...prev, ...patch }));
|
||||
else setSellLayout(prev => ({ ...prev, ...patch }));
|
||||
schedulePersistFlowLayout(layoutStrategyKey);
|
||||
}, [layoutStrategyKey, schedulePersistFlowLayout]);
|
||||
|
||||
const handleStartMetaChange = useCallback((meta: Record<string, { candleType: string }>) => {
|
||||
setCurrentLayout(prev => ({ ...prev, startMeta: meta }));
|
||||
schedulePersistFlowLayout(layoutStrategyKey);
|
||||
}, [setCurrentLayout, layoutStrategyKey, schedulePersistFlowLayout]);
|
||||
|
||||
const handleExtraStartIdsChange = useCallback((ids: string[]) => {
|
||||
setCurrentLayout(prev => ({ ...prev, extraStartIds: ids }));
|
||||
schedulePersistFlowLayout(layoutStrategyKey);
|
||||
bumpLayoutSeed(layoutStrategyKey, signalTab);
|
||||
}, [setCurrentLayout, layoutStrategyKey, schedulePersistFlowLayout, bumpLayoutSeed, signalTab]);
|
||||
|
||||
const handleExtraRootsChange = useCallback((roots: Record<string, LogicNode | null>) => {
|
||||
setCurrentLayout(prev => ({ ...prev, extraRoots: roots }));
|
||||
schedulePersistFlowLayout(layoutStrategyKey);
|
||||
}, [setCurrentLayout, layoutStrategyKey, schedulePersistFlowLayout]);
|
||||
|
||||
const handleEditorStateChange = useCallback((next: EditorConditionState) => {
|
||||
setCurrentRoot(next.root);
|
||||
setCurrentLayout(prev => ({
|
||||
...prev,
|
||||
startMeta: next.startMeta,
|
||||
extraStartIds: next.extraStartIds,
|
||||
extraRoots: next.extraRoots,
|
||||
startCombineOp: normalizeStartCombineOp(next.startCombineOp),
|
||||
}));
|
||||
schedulePersistFlowLayout(layoutStrategyKey);
|
||||
}, [setCurrentRoot, setCurrentLayout, layoutStrategyKey, schedulePersistFlowLayout]);
|
||||
|
||||
const handleStartCombineOpChange = useCallback((op: StartCombineOp) => {
|
||||
handleEditorStateChange(updateStartCombineOp(currentEditorState, op));
|
||||
}, [handleEditorStateChange, currentEditorState]);
|
||||
|
||||
const switchSignalTab = useCallback((tab: 'buy' | 'sell') => {
|
||||
if (editorMode === 'graph') layoutFlushRef.current?.();
|
||||
persistFlowLayout(layoutStrategyKey);
|
||||
@@ -333,13 +441,39 @@ export default function StrategyEditorPage({ theme }: Props) {
|
||||
}, []);
|
||||
|
||||
const handleSelectStrategy = (s: StrategyDto) => {
|
||||
const buyDecoded = decodeConditionForEditor(s.buyCondition ?? null);
|
||||
const sellDecoded = decodeConditionForEditor(s.sellCondition ?? null);
|
||||
const stored = loadStrategyFlowLayout(String(s.id));
|
||||
|
||||
setSelectedId(s.id);
|
||||
setStratName(s.name);
|
||||
setStratDesc(s.description ?? '');
|
||||
setBuyCondition(s.buyCondition ?? null);
|
||||
setSellCondition(s.sellCondition ?? null);
|
||||
setBuyCondition(buyDecoded.root);
|
||||
setSellCondition(sellDecoded.root);
|
||||
setBuyOrphans(stored?.buy.orphans ?? []);
|
||||
setSellOrphans(stored?.sell.orphans ?? []);
|
||||
setBuyLayout({
|
||||
positions: stored?.buy.positions ?? {},
|
||||
edgeHandles: stored?.buy.edgeHandles ?? {},
|
||||
startMeta: stored?.buy.startMeta ?? buyDecoded.startMeta,
|
||||
extraStartIds: stored?.buy.extraStartIds?.length ? stored.buy.extraStartIds : buyDecoded.extraStartIds,
|
||||
extraRoots: stored?.buy.extraRoots && Object.keys(stored.buy.extraRoots).length
|
||||
? stored.buy.extraRoots
|
||||
: buyDecoded.extraRoots,
|
||||
startCombineOp: normalizeStartCombineOp(stored?.buy.startCombineOp ?? buyDecoded.startCombineOp),
|
||||
});
|
||||
setSellLayout({
|
||||
positions: stored?.sell.positions ?? {},
|
||||
edgeHandles: stored?.sell.edgeHandles ?? {},
|
||||
startMeta: stored?.sell.startMeta ?? sellDecoded.startMeta,
|
||||
extraStartIds: stored?.sell.extraStartIds?.length ? stored.sell.extraStartIds : sellDecoded.extraStartIds,
|
||||
extraRoots: stored?.sell.extraRoots && Object.keys(stored.sell.extraRoots).length
|
||||
? stored.sell.extraRoots
|
||||
: sellDecoded.extraRoots,
|
||||
startCombineOp: normalizeStartCombineOp(stored?.sell.startCombineOp ?? sellDecoded.startCombineOp),
|
||||
});
|
||||
setSelectedNodeId(null);
|
||||
applyStoredFlowLayout(String(s.id), signalTab);
|
||||
bumpLayoutSeed(String(s.id), signalTab);
|
||||
};
|
||||
|
||||
const handleNew = () => {
|
||||
@@ -354,15 +488,17 @@ export default function StrategyEditorPage({ theme }: Props) {
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!stratName.trim()) { showSnack('전략 이름을 입력하세요', false); return; }
|
||||
if (!buyCondition && !sellCondition) { showSnack('조건을 최소 1개 추가하세요', false); return; }
|
||||
const encodedBuy = encodeConditionForSave(buyEditorState);
|
||||
const encodedSell = encodeConditionForSave(sellEditorState);
|
||||
if (!encodedBuy && !encodedSell) { showSnack('조건을 최소 1개 추가하세요', false); return; }
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const payload: ApiStrategyDto = {
|
||||
id: selectedId ?? undefined,
|
||||
name: stratName,
|
||||
description: stratDesc,
|
||||
buyCondition,
|
||||
sellCondition,
|
||||
buyCondition: encodedBuy,
|
||||
sellCondition: encodedSell,
|
||||
enabled: true,
|
||||
};
|
||||
const saved = await saveStrategy(payload);
|
||||
@@ -373,11 +509,11 @@ export default function StrategyEditorPage({ theme }: Props) {
|
||||
const existing = prev.find(s => s.id === selectedId);
|
||||
if (existing && selectedId) {
|
||||
return prev.map(s => s.id === selectedId
|
||||
? { ...s, id: dbId, name: stratName, description: stratDesc, buyCondition, sellCondition, updatedAt: saved?.updatedAt ?? now }
|
||||
? { ...s, id: dbId, name: stratName, description: stratDesc, buyCondition: encodedBuy, sellCondition: encodedSell, updatedAt: saved?.updatedAt ?? now }
|
||||
: s);
|
||||
}
|
||||
setSelectedId(dbId);
|
||||
return [...prev, { id: dbId, name: stratName, description: stratDesc, buyCondition, sellCondition, enabled: true, createdAt: saved?.createdAt ?? now, updatedAt: saved?.updatedAt ?? now }];
|
||||
return [...prev, { id: dbId, name: stratName, description: stratDesc, buyCondition: encodedBuy, sellCondition: encodedSell, enabled: true, createdAt: saved?.createdAt ?? now, updatedAt: saved?.updatedAt ?? now }];
|
||||
});
|
||||
if (!selectedId) setSelectedId(dbId);
|
||||
if (wasDraft) {
|
||||
@@ -412,10 +548,18 @@ export default function StrategyEditorPage({ theme }: Props) {
|
||||
setBuyLayout({
|
||||
positions: layout.buy.positions ?? {},
|
||||
edgeHandles: layout.buy.edgeHandles ?? {},
|
||||
startMeta: layout.buy.startMeta ?? defaultStartMeta(),
|
||||
extraStartIds: layout.buy.extraStartIds ?? [],
|
||||
extraRoots: layout.buy.extraRoots ?? {},
|
||||
startCombineOp: normalizeStartCombineOp(layout.buy.startCombineOp),
|
||||
});
|
||||
setSellLayout({
|
||||
positions: layout.sell.positions ?? {},
|
||||
edgeHandles: layout.sell.edgeHandles ?? {},
|
||||
startMeta: layout.sell.startMeta ?? defaultStartMeta(),
|
||||
extraStartIds: layout.sell.extraStartIds ?? [],
|
||||
extraRoots: layout.sell.extraRoots ?? {},
|
||||
startCombineOp: normalizeStartCombineOp(layout.sell.startCombineOp),
|
||||
});
|
||||
setBuyOrphans(layout.buy.orphans ?? []);
|
||||
setSellOrphans(layout.sell.orphans ?? []);
|
||||
@@ -427,7 +571,9 @@ export default function StrategyEditorPage({ theme }: Props) {
|
||||
}, [resetFlowLayout, bumpLayoutSeed, signalTab]);
|
||||
|
||||
const handleExport = useCallback(() => {
|
||||
if (!buyCondition && !sellCondition) {
|
||||
const encodedBuy = encodeConditionForSave(buyEditorState);
|
||||
const encodedSell = encodeConditionForSave(sellEditorState);
|
||||
if (!encodedBuy && !encodedSell) {
|
||||
showSnack('내보낼 조건이 없습니다', false);
|
||||
return;
|
||||
}
|
||||
@@ -435,8 +581,8 @@ export default function StrategyEditorPage({ theme }: Props) {
|
||||
const payload = buildStrategyExportPayload({
|
||||
name: stratName,
|
||||
description: stratDesc,
|
||||
buyCondition,
|
||||
sellCondition,
|
||||
buyCondition: encodedBuy,
|
||||
sellCondition: encodedSell,
|
||||
flowLayout: {
|
||||
buy: { ...buyLayoutRef.current, orphans: buyOrphans },
|
||||
sell: { ...sellLayoutRef.current, orphans: sellOrphans },
|
||||
@@ -446,7 +592,7 @@ export default function StrategyEditorPage({ theme }: Props) {
|
||||
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]);
|
||||
}, [buyEditorState, sellEditorState, stratName, stratDesc, buyOrphans, sellOrphans, editorMode]);
|
||||
|
||||
const handleImport = useCallback(async () => {
|
||||
const text = await pickJsonFile();
|
||||
@@ -459,13 +605,23 @@ export default function StrategyEditorPage({ theme }: Props) {
|
||||
}
|
||||
if (editorMode === 'graph') layoutFlushRef.current?.();
|
||||
const { data } = result;
|
||||
setBuyCondition(data.buyCondition ?? null);
|
||||
setSellCondition(data.sellCondition ?? null);
|
||||
const buyDecoded = decodeConditionForEditor(data.buyCondition ?? null);
|
||||
const sellDecoded = decodeConditionForEditor(data.sellCondition ?? null);
|
||||
setBuyCondition(buyDecoded.root);
|
||||
setSellCondition(sellDecoded.root);
|
||||
setStratName(data.name ?? '가져온 전략');
|
||||
setStratDesc(data.description ?? '');
|
||||
setSelectedId(null);
|
||||
setSelectedNodeId(null);
|
||||
applyImportedFlowLayout(data.flowLayout);
|
||||
setBuyLayout(prev => ({
|
||||
...prev,
|
||||
startCombineOp: normalizeStartCombineOp(data.flowLayout?.buy?.startCombineOp ?? buyDecoded.startCombineOp),
|
||||
}));
|
||||
setSellLayout(prev => ({
|
||||
...prev,
|
||||
startCombineOp: normalizeStartCombineOp(data.flowLayout?.sell?.startCombineOp ?? sellDecoded.startCombineOp),
|
||||
}));
|
||||
if (data.editorMode === 'list' || data.editorMode === 'graph') {
|
||||
setEditorMode(data.editorMode);
|
||||
saveEditorMode(data.editorMode);
|
||||
@@ -514,13 +670,22 @@ export default function StrategyEditorPage({ theme }: Props) {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleAddStartSection = useCallback(() => {
|
||||
handleEditorStateChange(addExtraStartSection(currentEditorState));
|
||||
bumpLayoutSeed(layoutStrategyKey, signalTab);
|
||||
}, [handleEditorStateChange, currentEditorState, bumpLayoutSeed, layoutStrategyKey, signalTab]);
|
||||
|
||||
const applyPalette = useCallback((type: string, value: string, _label: string, composite = false) => {
|
||||
if (type === 'start') {
|
||||
handleAddStartSection();
|
||||
return;
|
||||
}
|
||||
const newNode = makeNode(type, value, signalTab, DEF, composite ? { composite: true } : undefined);
|
||||
const root = currentRoot;
|
||||
if (!root) setCurrentRoot(newNode);
|
||||
else if (type === 'operator') setCurrentRoot(mergeAtRoot(root, newNode, true));
|
||||
else setCurrentRoot(mergeAtRoot(root, newNode, false));
|
||||
}, [signalTab, DEF, currentRoot, setCurrentRoot]);
|
||||
}, [signalTab, DEF, currentRoot, setCurrentRoot, handleAddStartSection]);
|
||||
|
||||
const templates = useMemo(() => getStrategyTemplates(DEF), [DEF]);
|
||||
|
||||
@@ -725,7 +890,7 @@ export default function StrategyEditorPage({ theme }: Props) {
|
||||
<div className="se-main-row">
|
||||
<main className="se-center">
|
||||
<div className="se-center-panel">
|
||||
<div className="se-center-work">
|
||||
<div className={`se-center-work${editorMode === 'list' ? ' se-center-work--list' : ''}`}>
|
||||
<div className="se-center-head">
|
||||
<div className="se-signal-tabs">
|
||||
<button
|
||||
@@ -744,6 +909,12 @@ export default function StrategyEditorPage({ theme }: Props) {
|
||||
</button>
|
||||
</div>
|
||||
{stratName && <span className="se-editing-name">{stratName}</span>}
|
||||
{hasMultipleStartSections(currentEditorState) && (
|
||||
<StartCombineOpControl
|
||||
value={normalizeStartCombineOp(currentEditorState.startCombineOp)}
|
||||
onChange={handleStartCombineOpChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{editorMode === 'graph' ? (
|
||||
@@ -761,6 +932,12 @@ export default function StrategyEditorPage({ theme }: Props) {
|
||||
onSelectNode={setSelectedNodeId}
|
||||
layoutSeed={layoutSeed}
|
||||
onLayoutChange={handleLayoutChange}
|
||||
startMeta={currentLayout.startMeta}
|
||||
extraStartIds={currentLayout.extraStartIds}
|
||||
extraRoots={currentLayout.extraRoots}
|
||||
onStartMetaChange={handleStartMetaChange}
|
||||
onExtraStartIdsChange={handleExtraStartIdsChange}
|
||||
onExtraRootsChange={handleExtraRootsChange}
|
||||
/>
|
||||
</ReactFlowProvider>
|
||||
|
||||
@@ -778,8 +955,22 @@ export default function StrategyEditorPage({ theme }: Props) {
|
||||
setCurrentOrphans(currentOrphans.map(o => (
|
||||
o.id === selectedNodeId ? updateNode(o, selectedNodeId, n => ({ ...n, condition: c })) : o
|
||||
)));
|
||||
} else if (currentRoot) {
|
||||
setCurrentRoot(updateNode(currentRoot, selectedNodeId, n => ({ ...n, condition: c })));
|
||||
return;
|
||||
}
|
||||
if (currentRoot && findLogicNode(currentRoot, currentOrphans, selectedNodeId, currentLayout.extraRoots ?? {})) {
|
||||
if (findLogicNode(currentRoot, [], selectedNodeId)) {
|
||||
setCurrentRoot(updateNode(currentRoot, selectedNodeId, n => ({ ...n, condition: c })));
|
||||
return;
|
||||
}
|
||||
for (const [startId, branch] of Object.entries(currentLayout.extraRoots ?? {})) {
|
||||
if (branch && findLogicNode(branch, [], selectedNodeId)) {
|
||||
handleExtraRootsChange({
|
||||
...(currentLayout.extraRoots ?? {}),
|
||||
[startId]: updateNode(branch, selectedNodeId, n => ({ ...n, condition: c })),
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@@ -789,10 +980,13 @@ export default function StrategyEditorPage({ theme }: Props) {
|
||||
</>
|
||||
) : (
|
||||
<StrategyListEditor
|
||||
root={currentRoot}
|
||||
editorState={currentEditorState}
|
||||
signalTab={signalTab}
|
||||
def={DEF}
|
||||
onChange={setCurrentRoot}
|
||||
onEditorStateChange={handleEditorStateChange}
|
||||
onAddStart={handleAddStartSection}
|
||||
orphans={currentOrphans}
|
||||
onOrphansChange={setCurrentOrphans}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -811,6 +1005,8 @@ export default function StrategyEditorPage({ theme }: Props) {
|
||||
<LogicExpressionPreview
|
||||
buyCondition={buyCondition}
|
||||
sellCondition={sellCondition}
|
||||
buyEditorState={buyEditorState}
|
||||
sellEditorState={sellEditorState}
|
||||
orphanCount={orphanTotal}
|
||||
def={DEF}
|
||||
/>
|
||||
@@ -834,8 +1030,18 @@ export default function StrategyEditorPage({ theme }: Props) {
|
||||
onChange={e => setPaletteSearch(e.target.value)}
|
||||
/>
|
||||
<div className="se-palette-section se-palette-section--logic">
|
||||
<h3>논리 연산</h3>
|
||||
<h3>시작 · 논리</h3>
|
||||
<div className="se-palette-grid se-palette-grid--3">
|
||||
<PaletteChip
|
||||
type="start"
|
||||
value="START"
|
||||
label="START"
|
||||
desc="시간봉 시작점"
|
||||
color="logic-start"
|
||||
selected={selectedPaletteKey === 'start:START'}
|
||||
onSelect={() => setSelectedPaletteKey('start:START')}
|
||||
onAdd={() => applyPalette('start', 'START', 'START')}
|
||||
/>
|
||||
{operators.map(op => (
|
||||
<PaletteChip
|
||||
key={op.value}
|
||||
|
||||
Reference in New Issue
Block a user