From 05c15ec92b502a3e7f8c45aeaced8d520a99d654 Mon Sep 17 00:00:00 2001 From: Macbook Date: Thu, 11 Jun 2026 22:53:19 +0900 Subject: [PATCH] =?UTF-8?q?=EB=B3=B5=ED=95=A9=EC=A7=80=ED=91=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/StrategyEditorPage.tsx | 87 +++++++- .../strategyEditor/ConditionNodeSettings.tsx | 14 +- .../components/strategyEditor/FlowNodes.tsx | 38 +++- .../strategyEditor/IndicatorPaletteTab.tsx | 10 +- .../components/strategyEditor/PaletteChip.tsx | 8 +- .../strategyEditor/PaletteItemModal.tsx | 73 +++++-- .../strategyEditor/StochPairNodeSettings.tsx | 97 +++++++++ .../strategyEditor/StrategyEditorCanvas.tsx | 40 +++- .../strategyEditor/StrategyListEditor.tsx | 81 ++++++-- frontend/src/styles/strategyEditor.css | 70 ++++++- frontend/src/utils/compositeIndicators.ts | 37 ++++ frontend/src/utils/stochOverboughtPair.ts | 194 ++++++++++++++++++ frontend/src/utils/strategyEditorShared.tsx | 106 ++++++++-- frontend/src/utils/strategyFlowLayout.ts | 9 +- frontend/src/utils/strategyNodeIds.ts | 3 + frontend/src/utils/strategyPaletteStorage.ts | 23 ++- frontend/src/utils/strategyTypes.ts | 4 + 17 files changed, 817 insertions(+), 77 deletions(-) create mode 100644 frontend/src/components/strategyEditor/StochPairNodeSettings.tsx create mode 100644 frontend/src/utils/stochOverboughtPair.ts create mode 100644 frontend/src/utils/strategyNodeIds.ts diff --git a/frontend/src/components/StrategyEditorPage.tsx b/frontend/src/components/StrategyEditorPage.tsx index 2454b55..f4439c8 100644 --- a/frontend/src/components/StrategyEditorPage.tsx +++ b/frontend/src/components/StrategyEditorPage.tsx @@ -49,6 +49,15 @@ import { loadPaletteItems, type PaletteItem, } from '../utils/strategyPaletteStorage'; +import { + buildStochOverboughtPairTree, + findStochPairRootInForest, + isStochOverboughtPairPaletteValue, + isStochOverboughtPairRoot, + patchStochPairInTree, + setStochPairSecondary, +} from '../utils/stochOverboughtPair'; +import StochPairNodeSettings from './strategyEditor/StochPairNodeSettings'; import { emptySignalFlowLayout, buildStrategyFlowLayoutStore, @@ -302,6 +311,15 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop ); }, [selectedNodeId, currentRoot, currentOrphans, currentLayout.extraRoots]); + const stochPairForest = useMemo( + () => [ + currentRoot, + ...currentOrphans, + ...Object.values(currentLayout.extraRoots ?? {}), + ], + [currentRoot, currentOrphans, currentLayout.extraRoots], + ); + const orphanTotal = buyOrphans.length + sellOrphans.length; const layoutStrategyKey = selectedId != null ? String(selectedId) : 'draft'; @@ -447,6 +465,49 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop scheduleStrategyPersist(); }, [setCurrentLayout, scheduleStrategyPersist]); + const applyStochPairSecondary = useCallback((orNodeId: string, secondary: string) => { + if (currentOrphans.some(o => o.id === orNodeId)) { + handleOrphansChange(currentOrphans.map(o => ( + o.id === orNodeId ? setStochPairSecondary(o, secondary, DEF) : o + ))); + return; + } + if (currentRoot) { + const patched = patchStochPairInTree(currentRoot, orNodeId, secondary, DEF); + if (patched && patched !== currentRoot) { + handleRootChange(patched); + return; + } + } + for (const [startId, branch] of Object.entries(currentLayout.extraRoots ?? {})) { + if (!branch) continue; + const patched = patchStochPairInTree(branch, orNodeId, secondary, DEF); + if (patched && patched !== branch) { + handleExtraRootsChange({ + ...(currentLayout.extraRoots ?? {}), + [startId]: patched, + }); + return; + } + } + }, [ + currentRoot, currentOrphans, currentLayout.extraRoots, DEF, + handleOrphansChange, handleRootChange, handleExtraRootsChange, + ]); + + const stochPairEditForSelected = useMemo(() => { + if (!selectedNodeId || selectedLogicNode?.type !== 'CONDITION' || !selectedLogicNode.condition) { + return undefined; + } + const pairRoot = findStochPairRootInForest(stochPairForest, selectedNodeId); + if (!pairRoot?.stochPair) return undefined; + return { + secondaryIndicator: pairRoot.stochPair.secondaryIndicatorType, + stochLocked: selectedLogicNode.condition.indicatorType === 'STOCHASTIC', + onSecondaryChange: (next: string) => applyStochPairSecondary(pairRoot.id, next), + }; + }, [selectedNodeId, selectedLogicNode, stochPairForest, applyStochPairSecondary]); + const handleEditorStateChange = useCallback((next: EditorConditionState) => { setCurrentRoot(next.root); setCurrentLayout(prev => ({ @@ -953,8 +1014,11 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop }, [signalTab, DEF, currentRoot, setCurrentRoot, handleAddStartSection, scheduleStrategyPersist]); const applyPaletteItem = useCallback((item: PaletteItem) => { - const composite = item.kind === 'composite'; - const newNode = makeNode('indicator', item.value, signalTab, DEF, composite ? { composite: true } : undefined); + const stochPair = isStochOverboughtPairPaletteValue(item.value); + const composite = item.kind === 'composite' && !stochPair; + const newNode = stochPair + ? buildStochOverboughtPairTree(item.secondaryIndicator ?? 'CCI', DEF) + : makeNode('indicator', item.value, signalTab, DEF, composite ? { composite: true } : undefined); const root = currentRoot; if (!root) setCurrentRoot(newNode); else setCurrentRoot(mergeAtRoot(root, newNode, false)); @@ -1348,11 +1412,16 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop {selectedLogicNode?.type === 'CONDITION' && selectedLogicNode.condition && (
- {selectedLogicNode.condition.indicatorType} + + {stochPairEditForSelected && !stochPairEditForSelected.stochLocked + ? 'Stoch 과열×보조' + : selectedLogicNode.condition.indicatorType} + { if (!selectedNodeId) return; const inOrphans = currentOrphans.some(o => o.id === selectedNodeId); @@ -1382,6 +1451,18 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop 전략 조건 전용 설정 · 차트 보조지표와 무관
)} + + {selectedLogicNode && isStochOverboughtPairRoot(selectedLogicNode) && selectedLogicNode.stochPair && ( +
+ Stoch 과열×보조 + applyStochPairSecondary(selectedLogicNode.id, next)} + onClose={() => {}} + /> +
+ )} ) : ( {condition.composite ? ( <> + - ) : ( + ) : isStochPairEdit ? null : ( <>