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 : ( <>