전략편집기 수정
This commit is contained in:
@@ -19,8 +19,18 @@ import {
|
||||
type StrategyDto,
|
||||
} from '../utils/strategyEditorShared';
|
||||
import { findLogicNode, getIndicatorPeriodLabel } from '../utils/strategyFlowLayout';
|
||||
import {
|
||||
emptySignalFlowLayout,
|
||||
loadStrategyFlowLayout,
|
||||
migrateStrategyFlowLayout,
|
||||
saveStrategyFlowLayout,
|
||||
deleteStrategyFlowLayout,
|
||||
type SignalFlowLayoutSnapshot,
|
||||
type FlowLayoutChangePayload,
|
||||
} from '../utils/strategyEditorLayoutStorage';
|
||||
import LogicExpressionPreview from './strategyEditor/LogicExpressionPreview';
|
||||
import StrategyEditorCanvas from './strategyEditor/StrategyEditorCanvas';
|
||||
import StrategyEditorCanvas, { type FlowLayoutSeed } from './strategyEditor/StrategyEditorCanvas';
|
||||
import { layoutFlushRef } from './strategyEditor/strategyEditorCallbacks';
|
||||
import PaletteChip from './strategyEditor/PaletteChip';
|
||||
import { readStoredSize, storeSize, usePanelResize } from './strategyEditor/usePanelResize';
|
||||
import '../styles/strategyEditor.css';
|
||||
@@ -38,6 +48,17 @@ interface Props {
|
||||
activeIndicators?: IndicatorConfig[];
|
||||
}
|
||||
|
||||
function readTabLayout(strategyKey: string, tab: 'buy' | 'sell'): SignalFlowLayoutSnapshot {
|
||||
const stored = loadStrategyFlowLayout(strategyKey);
|
||||
const snap = stored?.[tab];
|
||||
if (!snap) return emptySignalFlowLayout();
|
||||
return {
|
||||
positions: snap.positions ?? {},
|
||||
edgeHandles: snap.edgeHandles ?? {},
|
||||
orphans: snap.orphans ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
export default function StrategyEditorPage({ theme, activeIndicators = [] }: Props) {
|
||||
const DEF = useMemo(() => buildDef(activeIndicators), [activeIndicators]);
|
||||
|
||||
@@ -55,17 +76,36 @@ export default function StrategyEditorPage({ theme, activeIndicators = [] }: Pro
|
||||
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||
const [deleteId, setDeleteId] = useState<number | null>(null);
|
||||
const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);
|
||||
const [buyOrphans, setBuyOrphans] = useState<LogicNode[]>([]);
|
||||
const [sellOrphans, setSellOrphans] = useState<LogicNode[]>([]);
|
||||
const initialDraftBuy = readTabLayout('draft', 'buy');
|
||||
const initialDraftSell = readTabLayout('draft', 'sell');
|
||||
const [buyOrphans, setBuyOrphans] = useState<LogicNode[]>(() => initialDraftBuy.orphans ?? []);
|
||||
const [sellOrphans, setSellOrphans] = useState<LogicNode[]>(() => initialDraftSell.orphans ?? []);
|
||||
const [snack, setSnack] = useState<{ msg: string; ok: boolean } | null>(null);
|
||||
const [saveToast, setSaveToast] = useState(false);
|
||||
const [leftWidth, setLeftWidth] = useState(() => readStoredSize('se-left-width', LEFT_PANEL_DEFAULT));
|
||||
const [terminalHeight, setTerminalHeight] = useState(() => readStoredSize('se-terminal-height', TERMINAL_DEFAULT));
|
||||
const leftWidthRef = useRef(leftWidth);
|
||||
const terminalHeightRef = useRef(terminalHeight);
|
||||
const [buyLayout, setBuyLayout] = useState(() => ({
|
||||
positions: initialDraftBuy.positions,
|
||||
edgeHandles: initialDraftBuy.edgeHandles,
|
||||
}));
|
||||
const [sellLayout, setSellLayout] = useState(() => ({
|
||||
positions: initialDraftSell.positions,
|
||||
edgeHandles: initialDraftSell.edgeHandles,
|
||||
}));
|
||||
const buyLayoutRef = useRef(buyLayout);
|
||||
const sellLayoutRef = useRef(sellLayout);
|
||||
buyLayoutRef.current = buyLayout;
|
||||
sellLayoutRef.current = sellLayout;
|
||||
const layoutRevisionRef = useRef(0);
|
||||
const persistLayoutTimerRef = useRef<number | null>(null);
|
||||
const layoutPersistReadyRef = useRef(false);
|
||||
leftWidthRef.current = leftWidth;
|
||||
terminalHeightRef.current = terminalHeight;
|
||||
|
||||
const [layoutSeedKey, setLayoutSeedKey] = useState('draft:buy:0');
|
||||
|
||||
const onLeftSplitter = usePanelResize(
|
||||
'vertical',
|
||||
setLeftWidth,
|
||||
@@ -106,6 +146,105 @@ export default function StrategyEditorPage({ theme, activeIndicators = [] }: Pro
|
||||
|
||||
const orphanTotal = buyOrphans.length + sellOrphans.length;
|
||||
|
||||
const layoutStrategyKey = selectedId != null ? String(selectedId) : 'draft';
|
||||
|
||||
const persistFlowLayout = useCallback((strategyKey: string) => {
|
||||
saveStrategyFlowLayout(strategyKey, {
|
||||
buy: { ...buyLayoutRef.current, orphans: buyOrphans },
|
||||
sell: { ...sellLayoutRef.current, orphans: sellOrphans },
|
||||
});
|
||||
}, [buyOrphans, sellOrphans]);
|
||||
|
||||
const schedulePersistFlowLayout = useCallback((strategyKey: string) => {
|
||||
if (persistLayoutTimerRef.current != null) window.clearTimeout(persistLayoutTimerRef.current);
|
||||
persistLayoutTimerRef.current = window.setTimeout(() => {
|
||||
persistLayoutTimerRef.current = null;
|
||||
persistFlowLayout(strategyKey);
|
||||
}, 150);
|
||||
}, [persistFlowLayout]);
|
||||
|
||||
const bumpLayoutSeed = useCallback((strategyKey: string, tab: 'buy' | 'sell') => {
|
||||
layoutRevisionRef.current += 1;
|
||||
setLayoutSeedKey(`${strategyKey}:${tab}:${layoutRevisionRef.current}`);
|
||||
}, []);
|
||||
|
||||
const resetFlowLayout = useCallback((strategyKey: string, tab: 'buy' | 'sell' = 'buy') => {
|
||||
const emptyBuy = emptySignalFlowLayout();
|
||||
const emptySell = emptySignalFlowLayout();
|
||||
setBuyLayout(emptyBuy);
|
||||
setSellLayout(emptySell);
|
||||
setBuyOrphans([]);
|
||||
setSellOrphans([]);
|
||||
saveStrategyFlowLayout(strategyKey, { buy: emptyBuy, sell: emptySell });
|
||||
bumpLayoutSeed(strategyKey, tab);
|
||||
}, [bumpLayoutSeed]);
|
||||
|
||||
const applyStoredFlowLayout = useCallback((strategyKey: string, tab: 'buy' | 'sell' = 'buy') => {
|
||||
const stored = loadStrategyFlowLayout(strategyKey);
|
||||
if (stored) {
|
||||
const nextBuy = {
|
||||
positions: stored.buy.positions ?? {},
|
||||
edgeHandles: stored.buy.edgeHandles ?? {},
|
||||
};
|
||||
const nextSell = {
|
||||
positions: stored.sell.positions ?? {},
|
||||
edgeHandles: stored.sell.edgeHandles ?? {},
|
||||
};
|
||||
setBuyLayout(nextBuy);
|
||||
setSellLayout(nextSell);
|
||||
setBuyOrphans(stored.buy.orphans ?? []);
|
||||
setSellOrphans(stored.sell.orphans ?? []);
|
||||
} else {
|
||||
const emptyBuy = emptySignalFlowLayout();
|
||||
const emptySell = emptySignalFlowLayout();
|
||||
setBuyLayout(emptyBuy);
|
||||
setSellLayout(emptySell);
|
||||
setBuyOrphans([]);
|
||||
setSellOrphans([]);
|
||||
}
|
||||
bumpLayoutSeed(strategyKey, tab);
|
||||
}, [bumpLayoutSeed]);
|
||||
|
||||
const handleLayoutChange = useCallback((snapshot: FlowLayoutChangePayload) => {
|
||||
const next = {
|
||||
positions: snapshot.positions,
|
||||
edgeHandles: snapshot.edgeHandles,
|
||||
};
|
||||
if (snapshot.tab === 'buy') setBuyLayout(next);
|
||||
else setSellLayout(next);
|
||||
schedulePersistFlowLayout(layoutStrategyKey);
|
||||
}, [layoutStrategyKey, schedulePersistFlowLayout]);
|
||||
|
||||
const switchSignalTab = useCallback((tab: 'buy' | 'sell') => {
|
||||
layoutFlushRef.current?.();
|
||||
persistFlowLayout(layoutStrategyKey);
|
||||
layoutRevisionRef.current += 1;
|
||||
setLayoutSeedKey(`${layoutStrategyKey}:${tab}:${layoutRevisionRef.current}`);
|
||||
setSignalTab(tab);
|
||||
setSelectedNodeId(null);
|
||||
}, [layoutStrategyKey, persistFlowLayout]);
|
||||
|
||||
const layoutSeed = useMemo((): FlowLayoutSeed => {
|
||||
const source = signalTab === 'buy' ? buyLayout : sellLayout;
|
||||
return {
|
||||
seedKey: layoutSeedKey,
|
||||
positions: source.positions,
|
||||
edgeHandles: source.edgeHandles,
|
||||
};
|
||||
}, [signalTab, layoutSeedKey, buyLayout, sellLayout]);
|
||||
|
||||
useEffect(() => () => {
|
||||
if (persistLayoutTimerRef.current != null) window.clearTimeout(persistLayoutTimerRef.current);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!layoutPersistReadyRef.current) {
|
||||
layoutPersistReadyRef.current = true;
|
||||
return;
|
||||
}
|
||||
schedulePersistFlowLayout(layoutStrategyKey);
|
||||
}, [buyOrphans, sellOrphans, layoutStrategyKey, schedulePersistFlowLayout]);
|
||||
|
||||
useEffect(() => { saveStratsLocal(strategies); }, [strategies]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -159,9 +298,8 @@ export default function StrategyEditorPage({ theme, activeIndicators = [] }: Pro
|
||||
setStratDesc(s.description ?? '');
|
||||
setBuyCondition(s.buyCondition ?? null);
|
||||
setSellCondition(s.sellCondition ?? null);
|
||||
setBuyOrphans([]);
|
||||
setSellOrphans([]);
|
||||
setSelectedNodeId(null);
|
||||
applyStoredFlowLayout(String(s.id), signalTab);
|
||||
};
|
||||
|
||||
const handleNew = () => {
|
||||
@@ -170,9 +308,8 @@ export default function StrategyEditorPage({ theme, activeIndicators = [] }: Pro
|
||||
setStratDesc('');
|
||||
setBuyCondition(null);
|
||||
setSellCondition(null);
|
||||
setBuyOrphans([]);
|
||||
setSellOrphans([]);
|
||||
setSelectedNodeId(null);
|
||||
resetFlowLayout('draft', signalTab);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
@@ -191,6 +328,7 @@ export default function StrategyEditorPage({ theme, activeIndicators = [] }: Pro
|
||||
const saved = await saveStrategy(payload);
|
||||
const now = new Date().toISOString();
|
||||
const dbId = saved?.id ?? selectedId ?? Date.now();
|
||||
const wasDraft = selectedId == null;
|
||||
setStrategies(prev => {
|
||||
const existing = prev.find(s => s.id === selectedId);
|
||||
if (existing && selectedId) {
|
||||
@@ -202,6 +340,11 @@ export default function StrategyEditorPage({ theme, activeIndicators = [] }: Pro
|
||||
return [...prev, { id: dbId, name: stratName, description: stratDesc, buyCondition, sellCondition, enabled: true, createdAt: saved?.createdAt ?? now, updatedAt: saved?.updatedAt ?? now }];
|
||||
});
|
||||
if (!selectedId) setSelectedId(dbId);
|
||||
if (wasDraft) {
|
||||
migrateStrategyFlowLayout('draft', String(dbId));
|
||||
}
|
||||
persistFlowLayout(String(dbId));
|
||||
bumpLayoutSeed(String(dbId), signalTab);
|
||||
setSaveOpen(false);
|
||||
setSaveToast(true);
|
||||
setTimeout(() => setSaveToast(false), 2500);
|
||||
@@ -216,6 +359,7 @@ export default function StrategyEditorPage({ theme, activeIndicators = [] }: Pro
|
||||
const handleDeleteConfirm = async () => {
|
||||
if (!deleteId) return;
|
||||
try { await deleteStrategy(deleteId); } catch { /* local */ }
|
||||
deleteStrategyFlowLayout(String(deleteId));
|
||||
setStrategies(prev => prev.filter(s => s.id !== deleteId));
|
||||
if (selectedId === deleteId) handleNew();
|
||||
setDeleteOpen(false);
|
||||
@@ -245,7 +389,7 @@ export default function StrategyEditorPage({ theme, activeIndicators = [] }: Pro
|
||||
id: genId(), type: 'CONDITION',
|
||||
condition: { indicatorType: tmpl.ind, conditionType: tmpl.cond, leftField: tmpl.l, rightField: tmpl.r, candleRange: 1 },
|
||||
};
|
||||
if (tmpl.signal !== signalTab) setSignalTab(tmpl.signal);
|
||||
if (tmpl.signal !== signalTab) switchSignalTab(tmpl.signal);
|
||||
const root = tmpl.signal === 'buy' ? buyCondition : sellCondition;
|
||||
const setRoot = tmpl.signal === 'buy' ? setBuyCondition : setSellCondition;
|
||||
if (!root) setRoot(newNode);
|
||||
@@ -380,14 +524,14 @@ export default function StrategyEditorPage({ theme, activeIndicators = [] }: Pro
|
||||
<button
|
||||
type="button"
|
||||
className={`se-signal-tab${signalTab === 'buy' ? ' se-signal-tab--buy-on' : ''}`}
|
||||
onClick={() => { setSignalTab('buy'); setSelectedNodeId(null); }}
|
||||
onClick={() => switchSignalTab('buy')}
|
||||
>
|
||||
매수 조건 (Entry)
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`se-signal-tab${signalTab === 'sell' ? ' se-signal-tab--sell-on' : ''}`}
|
||||
onClick={() => { setSignalTab('sell'); setSelectedNodeId(null); }}
|
||||
onClick={() => switchSignalTab('sell')}
|
||||
>
|
||||
매도 조건 (Exit)
|
||||
</button>
|
||||
@@ -406,6 +550,8 @@ export default function StrategyEditorPage({ theme, activeIndicators = [] }: Pro
|
||||
onChange={setCurrentRoot}
|
||||
selectedNodeId={selectedNodeId}
|
||||
onSelectNode={setSelectedNodeId}
|
||||
layoutSeed={layoutSeed}
|
||||
onLayoutChange={handleLayoutChange}
|
||||
/>
|
||||
</ReactFlowProvider>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user