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 ? (
<>
+