복합지표 추가

This commit is contained in:
Macbook
2026-06-11 22:53:19 +09:00
parent 280c187021
commit 05c15ec92b
17 changed files with 817 additions and 77 deletions
+84 -3
View File
@@ -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 && (
<div className="se-node-config-bar">
<span className="se-node-config-label">{selectedLogicNode.condition.indicatorType}</span>
<span className="se-node-config-label">
{stochPairEditForSelected && !stochPairEditForSelected.stochLocked
? 'Stoch 과열×보조'
: selectedLogicNode.condition.indicatorType}
</span>
<CondEditor
cond={selectedLogicNode.condition}
signalType={signalTab}
def={DEF}
stochPairEdit={stochPairEditForSelected}
onChange={c => {
if (!selectedNodeId) return;
const inOrphans = currentOrphans.some(o => o.id === selectedNodeId);
@@ -1382,6 +1451,18 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
<span className="se-sync-tip"> · </span>
</div>
)}
{selectedLogicNode && isStochOverboughtPairRoot(selectedLogicNode) && selectedLogicNode.stochPair && (
<div className="se-node-config-bar se-node-config-bar--stoch-pair">
<span className="se-node-config-label">Stoch ×</span>
<StochPairNodeSettings
variant="inline"
secondaryIndicator={selectedLogicNode.stochPair.secondaryIndicatorType}
onChange={next => applyStochPairSecondary(selectedLogicNode.id, next)}
onClose={() => {}}
/>
</div>
)}
</>
) : (
<StrategyListEditor