복합지표 전략 추가
This commit is contained in:
@@ -51,6 +51,35 @@ import {
|
||||
loadPaletteItems,
|
||||
type PaletteItem,
|
||||
} from '../utils/strategyPaletteStorage';
|
||||
import {
|
||||
buildPriceExtremeBreakoutTree,
|
||||
isPriceExtremeBreakoutPairPaletteValue,
|
||||
isPriceExtremeBreakoutPairRoot,
|
||||
findPriceExtremePairRootInForest,
|
||||
patchPriceExtremePairInTree,
|
||||
setPriceExtremePairFilterLevel,
|
||||
inferPriceExtremeFilterLevel,
|
||||
type PriceExtremeFilterLevel,
|
||||
} from '../utils/priceExtremeBreakoutPair';
|
||||
import {
|
||||
buildInflection33Tree,
|
||||
isInflection33PaletteValue,
|
||||
isInflection33PairRoot,
|
||||
setInflection33PairFilterLevel,
|
||||
patchInflection33PairInTree,
|
||||
inferInflection33FilterLevel,
|
||||
type Inflection33FilterLevel,
|
||||
} from '../utils/inflection33Strategy';
|
||||
import {
|
||||
buildStableStrategyTree,
|
||||
isStableStrategyPaletteValue,
|
||||
isStableStrategyPairRoot,
|
||||
setStableStrategyPairFilterLevel,
|
||||
patchStableStrategyPairInTree,
|
||||
inferStableStrategyFilterLevel,
|
||||
type StableStrategyFilterLevel,
|
||||
type StableStrategyId,
|
||||
} from '../utils/stableStrategyPairs';
|
||||
import {
|
||||
buildStochOverboughtPairTree,
|
||||
findStochPairRootInForest,
|
||||
@@ -60,6 +89,9 @@ import {
|
||||
setStochPairSecondary,
|
||||
} from '../utils/stochOverboughtPair';
|
||||
import StochPairNodeSettings from './strategyEditor/StochPairNodeSettings';
|
||||
import PriceExtremePairNodeSettings from './strategyEditor/PriceExtremePairNodeSettings';
|
||||
import Inflection33PairNodeSettings from './strategyEditor/Inflection33PairNodeSettings';
|
||||
import StableStrategyPairNodeSettings from './strategyEditor/StableStrategyPairNodeSettings';
|
||||
import {
|
||||
emptySignalFlowLayout,
|
||||
buildStrategyFlowLayoutStore,
|
||||
@@ -568,6 +600,96 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
|
||||
handleOrphansChange, handleRootChange, handleExtraRootsChange,
|
||||
]);
|
||||
|
||||
const applyPriceExtremeFilterLevel = useCallback((andNodeId: string, filterLevel: PriceExtremeFilterLevel) => {
|
||||
if (currentOrphans.some(o => o.id === andNodeId)) {
|
||||
handleOrphansChange(currentOrphans.map(o => (
|
||||
o.id === andNodeId ? setPriceExtremePairFilterLevel(o, filterLevel, DEF) : o
|
||||
)));
|
||||
return;
|
||||
}
|
||||
if (currentRoot) {
|
||||
const patched = patchPriceExtremePairInTree(currentRoot, andNodeId, filterLevel, DEF);
|
||||
if (patched && patched !== currentRoot) {
|
||||
handleRootChange(patched);
|
||||
return;
|
||||
}
|
||||
}
|
||||
for (const [startId, branch] of Object.entries(currentLayout.extraRoots ?? {})) {
|
||||
if (!branch) continue;
|
||||
const patched = patchPriceExtremePairInTree(branch, andNodeId, filterLevel, DEF);
|
||||
if (patched && patched !== branch) {
|
||||
handleExtraRootsChange({
|
||||
...(currentLayout.extraRoots ?? {}),
|
||||
[startId]: patched,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, [
|
||||
currentRoot, currentOrphans, currentLayout.extraRoots, DEF,
|
||||
handleOrphansChange, handleRootChange, handleExtraRootsChange,
|
||||
]);
|
||||
|
||||
const applyInflection33FilterLevel = useCallback((pairNodeId: string, filterLevel: Inflection33FilterLevel) => {
|
||||
if (currentOrphans.some(o => o.id === pairNodeId)) {
|
||||
handleOrphansChange(currentOrphans.map(o => (
|
||||
o.id === pairNodeId ? setInflection33PairFilterLevel(o, filterLevel, DEF) : o
|
||||
)));
|
||||
return;
|
||||
}
|
||||
if (currentRoot) {
|
||||
const patched = patchInflection33PairInTree(currentRoot, pairNodeId, filterLevel, DEF);
|
||||
if (patched && patched !== currentRoot) {
|
||||
handleRootChange(patched);
|
||||
return;
|
||||
}
|
||||
}
|
||||
for (const [startId, branch] of Object.entries(currentLayout.extraRoots ?? {})) {
|
||||
if (!branch) continue;
|
||||
const patched = patchInflection33PairInTree(branch, pairNodeId, filterLevel, DEF);
|
||||
if (patched && patched !== branch) {
|
||||
handleExtraRootsChange({
|
||||
...(currentLayout.extraRoots ?? {}),
|
||||
[startId]: patched,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, [
|
||||
currentRoot, currentOrphans, currentLayout.extraRoots, DEF,
|
||||
handleOrphansChange, handleRootChange, handleExtraRootsChange,
|
||||
]);
|
||||
|
||||
const applyStableStrategyFilterLevel = useCallback((pairNodeId: string, filterLevel: StableStrategyFilterLevel) => {
|
||||
if (currentOrphans.some(o => o.id === pairNodeId)) {
|
||||
handleOrphansChange(currentOrphans.map(o => (
|
||||
o.id === pairNodeId ? setStableStrategyPairFilterLevel(o, filterLevel, DEF) : o
|
||||
)));
|
||||
return;
|
||||
}
|
||||
if (currentRoot) {
|
||||
const patched = patchStableStrategyPairInTree(currentRoot, pairNodeId, filterLevel, DEF);
|
||||
if (patched && patched !== currentRoot) {
|
||||
handleRootChange(patched);
|
||||
return;
|
||||
}
|
||||
}
|
||||
for (const [startId, branch] of Object.entries(currentLayout.extraRoots ?? {})) {
|
||||
if (!branch) continue;
|
||||
const patched = patchStableStrategyPairInTree(branch, pairNodeId, filterLevel, 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;
|
||||
@@ -1147,10 +1269,20 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
|
||||
|
||||
const applyPaletteItem = useCallback((item: PaletteItem) => {
|
||||
const stochPair = isStochOverboughtPairPaletteValue(item.value);
|
||||
const composite = item.kind === 'composite' && !stochPair;
|
||||
const priceExtremeBreakout = isPriceExtremeBreakoutPairPaletteValue(item.value);
|
||||
const inflection33 = isInflection33PaletteValue(item.value);
|
||||
const stableStrategy = isStableStrategyPaletteValue(item.value);
|
||||
const composite = item.kind === 'composite' && !stochPair && !priceExtremeBreakout && !inflection33 && !stableStrategy;
|
||||
const newNode = stochPair
|
||||
? buildStochOverboughtPairTree(item.secondaryIndicator ?? 'CCI', DEF)
|
||||
: makeNode('indicator', item.value, signalTab, DEF, composite ? { composite: true } : undefined);
|
||||
: priceExtremeBreakout
|
||||
? buildPriceExtremeBreakoutTree(item.value, DEF)
|
||||
: inflection33
|
||||
? buildInflection33Tree(item.value, DEF)
|
||||
: stableStrategy
|
||||
? buildStableStrategyTree(item.value, DEF)
|
||||
: makeNode('indicator', item.value, signalTab, DEF, composite ? { composite: true } : undefined);
|
||||
if (!newNode) return;
|
||||
const root = currentRoot;
|
||||
if (!root) setCurrentRoot(newNode);
|
||||
else setCurrentRoot(mergeAtRoot(root, newNode, false));
|
||||
@@ -1774,6 +1906,48 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedLogicNode && isPriceExtremeBreakoutPairRoot(selectedLogicNode) && selectedLogicNode.priceExtremePair && (
|
||||
<div className="se-node-config-bar se-node-config-bar--price-extreme">
|
||||
<span className="se-node-config-label">9·20 신고가/신저가 필터</span>
|
||||
<PriceExtremePairNodeSettings
|
||||
variant="inline"
|
||||
mode={selectedLogicNode.priceExtremePair.mode}
|
||||
shortPeriod={selectedLogicNode.priceExtremePair.shortPeriod}
|
||||
longPeriod={selectedLogicNode.priceExtremePair.longPeriod}
|
||||
filterLevel={inferPriceExtremeFilterLevel(selectedLogicNode)}
|
||||
onChange={next => applyPriceExtremeFilterLevel(selectedLogicNode.id, next)}
|
||||
onClose={() => {}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{selectedLogicNode && isInflection33PairRoot(selectedLogicNode) && selectedLogicNode.inflection33Pair && (
|
||||
<div className="se-node-config-bar se-node-config-bar--inflection33">
|
||||
<span className="se-node-config-label">33변곡 필터</span>
|
||||
<Inflection33PairNodeSettings
|
||||
variant="inline"
|
||||
mode={selectedLogicNode.inflection33Pair.mode}
|
||||
period={selectedLogicNode.inflection33Pair.period}
|
||||
filterLevel={inferInflection33FilterLevel(selectedLogicNode)}
|
||||
onChange={next => applyInflection33FilterLevel(selectedLogicNode.id, next)}
|
||||
onClose={() => {}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{selectedLogicNode && isStableStrategyPairRoot(selectedLogicNode) && selectedLogicNode.stableStrategyPair && (
|
||||
<div className="se-node-config-bar se-node-config-bar--stable-strategy">
|
||||
<span className="se-node-config-label">추천 전략 필터</span>
|
||||
<StableStrategyPairNodeSettings
|
||||
variant="inline"
|
||||
strategyId={selectedLogicNode.stableStrategyPair.strategyId as StableStrategyId}
|
||||
mode={selectedLogicNode.stableStrategyPair.mode}
|
||||
filterLevel={inferStableStrategyFilterLevel(selectedLogicNode)}
|
||||
def={DEF}
|
||||
onChange={next => applyStableStrategyFilterLevel(selectedLogicNode.id, next)}
|
||||
onClose={() => {}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<StrategyListEditor
|
||||
|
||||
@@ -10,12 +10,31 @@ import StartTimeframeCheckboxes from './StartTimeframeCheckboxes';
|
||||
import LogicGateOpToggle from './LogicGateOpToggle';
|
||||
import { hasNodeSettings } from '../../utils/conditionPeriods';
|
||||
import { getStrategyIndicatorDisplayName } from '../../utils/strategyPaletteStorage';
|
||||
import {
|
||||
isPriceExtremeBreakoutPairRoot,
|
||||
priceExtremePairDisplayName,
|
||||
inferPriceExtremeFilterLevel,
|
||||
} from '../../utils/priceExtremeBreakoutPair';
|
||||
import {
|
||||
isInflection33PairRoot,
|
||||
inflection33DisplayName,
|
||||
inferInflection33FilterLevel,
|
||||
} from '../../utils/inflection33Strategy';
|
||||
import Inflection33PairNodeSettings from './Inflection33PairNodeSettings';
|
||||
import {
|
||||
isStableStrategyPairRoot,
|
||||
stableStrategyDisplayName,
|
||||
inferStableStrategyFilterLevel,
|
||||
type StableStrategyId,
|
||||
} from '../../utils/stableStrategyPairs';
|
||||
import StableStrategyPairNodeSettings from './StableStrategyPairNodeSettings';
|
||||
import {
|
||||
isStochOverboughtPairRoot,
|
||||
stochPairDisplayName,
|
||||
} from '../../utils/stochOverboughtPair';
|
||||
import ConditionNodeSettings from './ConditionNodeSettings';
|
||||
import StochPairNodeSettings from './StochPairNodeSettings';
|
||||
import PriceExtremePairNodeSettings from './PriceExtremePairNodeSettings';
|
||||
|
||||
const LOGIC_COLORS: Record<string, string> = {
|
||||
AND: '#00aaff',
|
||||
@@ -196,12 +215,15 @@ export const LogicGateNode = memo(function LogicGateNode({ id, data, selected }:
|
||||
const color = LOGIC_COLORS[node.type] ?? '#00aaff';
|
||||
const isSell = d.signalTab === 'sell';
|
||||
const isStochPair = isStochOverboughtPairRoot(node);
|
||||
const isPriceExtremePair = isPriceExtremeBreakoutPairRoot(node);
|
||||
const isInflection33Pair = isInflection33PairRoot(node);
|
||||
const isStableStrategyPair = isStableStrategyPairRoot(node);
|
||||
const { onDragOver, onDragLeave, onDrop } = usePaletteDropHandlers(id, d);
|
||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`se-flow-node se-flow-node--logic${isStochPair ? ' se-flow-node--stoch-pair' : ''}${isSell ? ' se-flow-node--sell-mode' : ''}${d.isOrphan ? ' se-flow-node--orphan' : ''} ${selected ? 'se-flow-node--selected' : ''} ${d.dragOver ? 'se-flow-node--drop' : ''}${settingsOpen ? ' se-flow-node--settings-open' : ''}`}
|
||||
className={`se-flow-node se-flow-node--logic${isStochPair ? ' se-flow-node--stoch-pair' : ''}${isPriceExtremePair ? ' se-flow-node--price-extreme-pair' : ''}${isInflection33Pair ? ' se-flow-node--inflection33-pair' : ''}${isStableStrategyPair ? ' se-flow-node--stable-strategy-pair' : ''}${isSell ? ' se-flow-node--sell-mode' : ''}${d.isOrphan ? ' se-flow-node--orphan' : ''} ${selected ? 'se-flow-node--selected' : ''} ${d.dragOver ? ' se-flow-node--drop' : ''}${settingsOpen ? ' se-flow-node--settings-open' : ''}`}
|
||||
style={{ '--se-gate-color': color } as React.CSSProperties}
|
||||
onDragOver={onDragOver}
|
||||
onDragLeave={onDragLeave}
|
||||
@@ -224,6 +246,79 @@ export const LogicGateNode = memo(function LogicGateNode({ id, data, selected }:
|
||||
<span className="se-flow-gate-badge">[{node.type}]</span>
|
||||
)}
|
||||
</div>
|
||||
{isStableStrategyPair && node.stableStrategyPair && (
|
||||
<div className="se-flow-gate-stoch-pair">
|
||||
<span className="se-flow-gate-stoch-pair-title">
|
||||
{stableStrategyDisplayName(
|
||||
node.stableStrategyPair.strategyId as StableStrategyId,
|
||||
node.stableStrategyPair.mode,
|
||||
)}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="se-flow-settings"
|
||||
title="필터 강도 설정"
|
||||
aria-label="필터 강도 설정"
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
setSettingsOpen(v => !v);
|
||||
}}
|
||||
>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
<path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{isInflection33Pair && node.inflection33Pair && (
|
||||
<div className="se-flow-gate-stoch-pair">
|
||||
<span className="se-flow-gate-stoch-pair-title">
|
||||
{inflection33DisplayName(node.inflection33Pair.mode, node.inflection33Pair.period)}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="se-flow-settings"
|
||||
title="필터 강도 설정"
|
||||
aria-label="필터 강도 설정"
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
setSettingsOpen(v => !v);
|
||||
}}
|
||||
>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
<path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{isPriceExtremePair && node.priceExtremePair && (
|
||||
<div className="se-flow-gate-stoch-pair">
|
||||
<span className="se-flow-gate-stoch-pair-title">
|
||||
{priceExtremePairDisplayName(
|
||||
node.priceExtremePair.mode,
|
||||
node.priceExtremePair.shortPeriod,
|
||||
node.priceExtremePair.longPeriod,
|
||||
)}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="se-flow-settings"
|
||||
title="필터 강도 설정"
|
||||
aria-label="필터 강도 설정"
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
setSettingsOpen(v => !v);
|
||||
}}
|
||||
>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
<path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{isStochPair && node.stochPair && (
|
||||
<div className="se-flow-gate-stoch-pair">
|
||||
<span className="se-flow-gate-stoch-pair-title">
|
||||
@@ -257,6 +352,35 @@ export const LogicGateNode = memo(function LogicGateNode({ id, data, selected }:
|
||||
onClose={() => setSettingsOpen(false)}
|
||||
/>
|
||||
)}
|
||||
{settingsOpen && isStableStrategyPair && node.stableStrategyPair && (
|
||||
<StableStrategyPairNodeSettings
|
||||
strategyId={node.stableStrategyPair.strategyId as StableStrategyId}
|
||||
mode={node.stableStrategyPair.mode}
|
||||
filterLevel={inferStableStrategyFilterLevel(node)}
|
||||
def={d.def!}
|
||||
onChange={level => d.onUpdateStableStrategyFilterLevel?.(id, level)}
|
||||
onClose={() => setSettingsOpen(false)}
|
||||
/>
|
||||
)}
|
||||
{settingsOpen && isInflection33Pair && node.inflection33Pair && (
|
||||
<Inflection33PairNodeSettings
|
||||
mode={node.inflection33Pair.mode}
|
||||
period={node.inflection33Pair.period}
|
||||
filterLevel={inferInflection33FilterLevel(node)}
|
||||
onChange={level => d.onUpdateInflection33FilterLevel?.(id, level)}
|
||||
onClose={() => setSettingsOpen(false)}
|
||||
/>
|
||||
)}
|
||||
{settingsOpen && isPriceExtremePair && node.priceExtremePair && (
|
||||
<PriceExtremePairNodeSettings
|
||||
mode={node.priceExtremePair.mode}
|
||||
shortPeriod={node.priceExtremePair.shortPeriod}
|
||||
longPeriod={node.priceExtremePair.longPeriod}
|
||||
filterLevel={inferPriceExtremeFilterLevel(node)}
|
||||
onChange={level => d.onUpdatePriceExtremeFilterLevel?.(id, level)}
|
||||
onClose={() => setSettingsOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -4,6 +4,9 @@ import PaletteItemModal from './PaletteItemModal';
|
||||
import type { DefType } from '../../utils/strategyEditorShared';
|
||||
import { getCompositeDefaultPeriods } from '../../utils/compositeIndicators';
|
||||
import { isStochOverboughtPairPaletteValue } from '../../utils/stochOverboughtPair';
|
||||
import { isPriceExtremeBreakoutPairPaletteValue } from '../../utils/priceExtremeBreakoutPair';
|
||||
import { isInflection33PaletteValue } from '../../utils/inflection33Strategy';
|
||||
import { isStableStrategyPaletteValue, STABLE_STRATEGY_PALETTE_ITEMS } from '../../utils/stableStrategyPairs';
|
||||
import { getDefaultIndicatorPeriod } from '../../utils/conditionPeriods';
|
||||
import { getStrategyIndicatorDisplayName } from '../../utils/strategyPaletteStorage';
|
||||
import { getIndicatorPeriodLabel } from '../../utils/strategyFlowLayout';
|
||||
@@ -20,6 +23,18 @@ function periodLabel(item: PaletteItem, def: DefType): string {
|
||||
const sec = item.secondaryIndicator ?? 'CCI';
|
||||
return `Stoch + ${getStrategyIndicatorDisplayName(sec)}`;
|
||||
}
|
||||
if (item.kind === 'composite' && isPriceExtremeBreakoutPairPaletteValue(item.value)) {
|
||||
const s = item.shortPeriod ?? 9;
|
||||
const l = item.longPeriod ?? 20;
|
||||
return `${s} / ${l}일`;
|
||||
}
|
||||
if (item.kind === 'composite' && isInflection33PaletteValue(item.value)) {
|
||||
return `${item.period ?? 33}일`;
|
||||
}
|
||||
if (item.kind === 'composite' && isStableStrategyPaletteValue(item.value)) {
|
||||
const meta = STABLE_STRATEGY_PALETTE_ITEMS.find(i => i.value === item.value);
|
||||
return meta?.periodHint ?? '';
|
||||
}
|
||||
if (item.kind === 'composite') {
|
||||
const d = getCompositeDefaultPeriods(item.value, def);
|
||||
return `${d.short} / ${d.long}`;
|
||||
@@ -170,8 +185,15 @@ export default function IndicatorPaletteTab({
|
||||
label={item.label}
|
||||
desc={item.desc}
|
||||
color={kind === 'composite' ? 'composite' : 'ind'}
|
||||
composite={kind === 'composite' && !isStochOverboughtPairPaletteValue(item.value)}
|
||||
composite={kind === 'composite'
|
||||
&& !isStochOverboughtPairPaletteValue(item.value)
|
||||
&& !isPriceExtremeBreakoutPairPaletteValue(item.value)
|
||||
&& !isInflection33PaletteValue(item.value)
|
||||
&& !isStableStrategyPaletteValue(item.value)}
|
||||
stochPair={isStochOverboughtPairPaletteValue(item.value)}
|
||||
priceExtremeBreakout={isPriceExtremeBreakoutPairPaletteValue(item.value)}
|
||||
inflection33={isInflection33PaletteValue(item.value)}
|
||||
stableStrategy={isStableStrategyPaletteValue(item.value)}
|
||||
secondaryIndicator={item.secondaryIndicator}
|
||||
period={periodLabel(item, def)}
|
||||
periodValue={item.period}
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import {
|
||||
INFLECTION_33_FILTER_OPTIONS,
|
||||
inflection33FilterSummary,
|
||||
inflection33SummaryText,
|
||||
type Inflection33FilterLevel,
|
||||
} from '../../utils/inflection33Strategy';
|
||||
|
||||
interface Props {
|
||||
mode: 'buy' | 'sell';
|
||||
period: number;
|
||||
filterLevel: Inflection33FilterLevel;
|
||||
onChange: (filterLevel: Inflection33FilterLevel) => void;
|
||||
onClose: () => void;
|
||||
popoverClassName?: string;
|
||||
variant?: 'popover' | 'inline';
|
||||
}
|
||||
|
||||
export default function Inflection33PairNodeSettings({
|
||||
mode,
|
||||
period,
|
||||
filterLevel,
|
||||
onChange,
|
||||
onClose,
|
||||
popoverClassName = 'se-flow-settings-pop',
|
||||
variant = 'popover',
|
||||
}: Props) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (variant === 'inline') return;
|
||||
const onDoc = (e: MouseEvent) => {
|
||||
if (ref.current && !ref.current.contains(e.target as Node)) onClose();
|
||||
};
|
||||
document.addEventListener('mousedown', onDoc);
|
||||
return () => document.removeEventListener('mousedown', onDoc);
|
||||
}, [onClose, variant]);
|
||||
|
||||
const body = (
|
||||
<>
|
||||
<label className="se-flow-settings-field se-flow-settings-field--readonly">
|
||||
<span>변곡 기간</span>
|
||||
<output className="se-flow-settings-readout">
|
||||
EMA{period} · {period}봉 종가 채널
|
||||
</output>
|
||||
</label>
|
||||
|
||||
<label className="se-flow-settings-field">
|
||||
<span>필터 강도</span>
|
||||
<select
|
||||
className="se-combo-num-select sp-cond-sel"
|
||||
value={filterLevel}
|
||||
onChange={e => onChange(e.target.value as Inflection33FilterLevel)}
|
||||
>
|
||||
{INFLECTION_33_FILTER_OPTIONS.map(o => (
|
||||
<option key={o.value} value={o.value}>{o.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<p className="se-flow-settings-hint">
|
||||
{INFLECTION_33_FILTER_OPTIONS.find(o => o.value === filterLevel)?.desc}
|
||||
</p>
|
||||
<p className="se-flow-settings-hint se-flow-settings-hint--muted">
|
||||
{inflection33FilterSummary(mode, filterLevel)}
|
||||
</p>
|
||||
<p className="se-flow-settings-hint se-flow-settings-hint--muted">
|
||||
{inflection33SummaryText(mode, period, filterLevel)}
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
|
||||
if (variant === 'inline') {
|
||||
return (
|
||||
<div className="se-inflection33-inline-settings" onClick={e => e.stopPropagation()}>
|
||||
{body}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={popoverClassName}
|
||||
role="dialog"
|
||||
aria-label="33변곡 복합 필터 설정"
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<div className="se-flow-settings-head">
|
||||
<span>필터 강도</span>
|
||||
<button
|
||||
type="button"
|
||||
className="se-flow-settings-close"
|
||||
aria-label="닫기"
|
||||
title="닫기"
|
||||
onClick={onClose}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
{body}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -6,6 +6,9 @@ export interface PaletteDragPayload {
|
||||
label: string;
|
||||
composite?: boolean;
|
||||
stochPair?: boolean;
|
||||
priceExtremeBreakout?: boolean;
|
||||
inflection33?: boolean;
|
||||
stableStrategy?: boolean;
|
||||
secondaryIndicator?: string;
|
||||
period?: number;
|
||||
shortPeriod?: number;
|
||||
@@ -24,6 +27,9 @@ interface Props {
|
||||
longPeriod?: number;
|
||||
composite?: boolean;
|
||||
stochPair?: boolean;
|
||||
priceExtremeBreakout?: boolean;
|
||||
inflection33?: boolean;
|
||||
stableStrategy?: boolean;
|
||||
secondaryIndicator?: string;
|
||||
selected?: boolean;
|
||||
onSelect?: () => void;
|
||||
@@ -32,13 +38,16 @@ interface Props {
|
||||
|
||||
export default function PaletteChip({
|
||||
type, value, label, desc, color, period, periodValue, shortPeriod, longPeriod,
|
||||
composite = false, stochPair = false, secondaryIndicator, selected = false, onSelect, onAdd,
|
||||
composite = false, stochPair = false, priceExtremeBreakout = false, inflection33 = false, stableStrategy = false, secondaryIndicator, selected = false, onSelect, onAdd,
|
||||
}: Props) {
|
||||
const onDragStart = (e: React.DragEvent) => {
|
||||
const payload: PaletteDragPayload = {
|
||||
type, value, label,
|
||||
composite: composite || undefined,
|
||||
stochPair: stochPair || undefined,
|
||||
priceExtremeBreakout: priceExtremeBreakout || undefined,
|
||||
inflection33: inflection33 || undefined,
|
||||
stableStrategy: stableStrategy || undefined,
|
||||
secondaryIndicator,
|
||||
period: periodValue,
|
||||
shortPeriod,
|
||||
|
||||
@@ -10,6 +10,9 @@ import {
|
||||
type PaletteItem,
|
||||
type PaletteItemKind,
|
||||
} from '../../utils/strategyPaletteStorage';
|
||||
import { isPriceExtremeBreakoutPairPaletteValue } from '../../utils/priceExtremeBreakoutPair';
|
||||
import { isInflection33PaletteValue } from '../../utils/inflection33Strategy';
|
||||
import { isStableStrategyPaletteValue } from '../../utils/stableStrategyPairs';
|
||||
import {
|
||||
isStochOverboughtPairPaletteValue,
|
||||
STOCH_PAIR_SECONDARY_OPTIONS,
|
||||
@@ -39,6 +42,10 @@ export default function PaletteItemModal({
|
||||
const [secondaryIndicator, setSecondaryIndicator] = useState('CCI');
|
||||
|
||||
const isStochPairEdit = kind === 'composite' && !!initial && isStochOverboughtPairPaletteValue(initial.value);
|
||||
const isPriceExtremePairEdit = kind === 'composite' && !!initial && isPriceExtremeBreakoutPairPaletteValue(initial.value);
|
||||
const isInflection33PairEdit = kind === 'composite' && !!initial && isInflection33PaletteValue(initial.value);
|
||||
const isStableStrategyPairEdit = kind === 'composite' && !!initial && isStableStrategyPaletteValue(initial.value);
|
||||
const isBuiltInCompositePair = isStochPairEdit || isPriceExtremePairEdit || isInflection33PairEdit || isStableStrategyPairEdit;
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
@@ -121,7 +128,7 @@ export default function PaletteItemModal({
|
||||
return (
|
||||
<DraggableModalFrame onClose={onClose} title={title}>
|
||||
<div className="se-modal-body se-palette-item-modal">
|
||||
{!isStochPairEdit && (
|
||||
{!isBuiltInCompositePair && (
|
||||
<>
|
||||
<label className="se-field-lbl">지표 종류</label>
|
||||
<select
|
||||
@@ -185,7 +192,7 @@ export default function PaletteItemModal({
|
||||
onChange={setPeriod}
|
||||
/>
|
||||
</label>
|
||||
) : isStochPairEdit ? null : (
|
||||
) : isBuiltInCompositePair ? null : (
|
||||
<>
|
||||
<label className="se-field-lbl">
|
||||
단기 기간 (일)
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import {
|
||||
PRICE_EXTREME_FILTER_OPTIONS,
|
||||
priceExtremeFilterSummary,
|
||||
priceExtremePairSummaryText,
|
||||
type PriceExtremeFilterLevel,
|
||||
} from '../../utils/priceExtremeBreakoutPair';
|
||||
|
||||
interface Props {
|
||||
mode: 'buy' | 'sell';
|
||||
shortPeriod: number;
|
||||
longPeriod: number;
|
||||
filterLevel: PriceExtremeFilterLevel;
|
||||
onChange: (filterLevel: PriceExtremeFilterLevel) => void;
|
||||
onClose: () => void;
|
||||
popoverClassName?: string;
|
||||
variant?: 'popover' | 'inline';
|
||||
}
|
||||
|
||||
export default function PriceExtremePairNodeSettings({
|
||||
mode,
|
||||
shortPeriod,
|
||||
longPeriod,
|
||||
filterLevel,
|
||||
onChange,
|
||||
onClose,
|
||||
popoverClassName = 'se-flow-settings-pop',
|
||||
variant = 'popover',
|
||||
}: Props) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (variant === 'inline') return;
|
||||
const onDoc = (e: MouseEvent) => {
|
||||
if (ref.current && !ref.current.contains(e.target as Node)) onClose();
|
||||
};
|
||||
document.addEventListener('mousedown', onDoc);
|
||||
return () => document.removeEventListener('mousedown', onDoc);
|
||||
}, [onClose, variant]);
|
||||
|
||||
const body = (
|
||||
<>
|
||||
<label className="se-flow-settings-field se-flow-settings-field--readonly">
|
||||
<span>가격 조건</span>
|
||||
<output className="se-flow-settings-readout">
|
||||
{shortPeriod}·{longPeriod}일 {mode === 'buy' ? '신고가' : '신저가'} 돌파
|
||||
</output>
|
||||
</label>
|
||||
|
||||
<label className="se-flow-settings-field">
|
||||
<span>필터 강도</span>
|
||||
<select
|
||||
className="se-combo-num-select sp-cond-sel"
|
||||
value={filterLevel}
|
||||
onChange={e => onChange(e.target.value as PriceExtremeFilterLevel)}
|
||||
>
|
||||
{PRICE_EXTREME_FILTER_OPTIONS.map(o => (
|
||||
<option key={o.value} value={o.value}>{o.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<p className="se-flow-settings-hint">
|
||||
{PRICE_EXTREME_FILTER_OPTIONS.find(o => o.value === filterLevel)?.desc}
|
||||
</p>
|
||||
<p className="se-flow-settings-hint se-flow-settings-hint--muted">
|
||||
{priceExtremeFilterSummary(mode, filterLevel)}
|
||||
</p>
|
||||
<p className="se-flow-settings-hint se-flow-settings-hint--muted">
|
||||
{priceExtremePairSummaryText(mode, shortPeriod, longPeriod, filterLevel)}
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
|
||||
if (variant === 'inline') {
|
||||
return (
|
||||
<div className="se-price-extreme-inline-settings" onClick={e => e.stopPropagation()}>
|
||||
{body}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={popoverClassName}
|
||||
role="dialog"
|
||||
aria-label="신고가·신저가 복합 필터 설정"
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<div className="se-flow-settings-head">
|
||||
<span>필터 강도</span>
|
||||
<button
|
||||
type="button"
|
||||
className="se-flow-settings-close"
|
||||
aria-label="닫기"
|
||||
title="닫기"
|
||||
onClick={onClose}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
{body}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import {
|
||||
STABLE_STRATEGY_FILTER_OPTIONS,
|
||||
stableStrategyDisplayName,
|
||||
stableStrategyFilterSummary,
|
||||
stableStrategySummaryText,
|
||||
type StableStrategyFilterLevel,
|
||||
type StableStrategyId,
|
||||
} from '../../utils/stableStrategyPairs';
|
||||
import type { DefType } from '../../utils/strategyEditorShared';
|
||||
|
||||
interface Props {
|
||||
strategyId: StableStrategyId;
|
||||
mode: 'buy' | 'sell';
|
||||
filterLevel: StableStrategyFilterLevel;
|
||||
onChange: (filterLevel: StableStrategyFilterLevel) => void;
|
||||
onClose: () => void;
|
||||
def: DefType;
|
||||
popoverClassName?: string;
|
||||
variant?: 'popover' | 'inline';
|
||||
}
|
||||
|
||||
export default function StableStrategyPairNodeSettings({
|
||||
strategyId,
|
||||
mode,
|
||||
filterLevel,
|
||||
onChange,
|
||||
onClose,
|
||||
def,
|
||||
popoverClassName = 'se-flow-settings-pop',
|
||||
variant = 'popover',
|
||||
}: Props) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (variant === 'inline') return;
|
||||
const onDoc = (e: MouseEvent) => {
|
||||
if (ref.current && !ref.current.contains(e.target as Node)) onClose();
|
||||
};
|
||||
document.addEventListener('mousedown', onDoc);
|
||||
return () => document.removeEventListener('mousedown', onDoc);
|
||||
}, [onClose, variant]);
|
||||
|
||||
const body = (
|
||||
<>
|
||||
<label className="se-flow-settings-field se-flow-settings-field--readonly">
|
||||
<span>전략</span>
|
||||
<output className="se-flow-settings-readout">
|
||||
{stableStrategyDisplayName(strategyId, mode)}
|
||||
</output>
|
||||
</label>
|
||||
|
||||
<label className="se-flow-settings-field">
|
||||
<span>필터 강도</span>
|
||||
<select
|
||||
className="se-combo-num-select sp-cond-sel"
|
||||
value={filterLevel}
|
||||
onChange={e => onChange(e.target.value as StableStrategyFilterLevel)}
|
||||
>
|
||||
{STABLE_STRATEGY_FILTER_OPTIONS.map(o => (
|
||||
<option key={o.value} value={o.value}>{o.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<p className="se-flow-settings-hint">
|
||||
{STABLE_STRATEGY_FILTER_OPTIONS.find(o => o.value === filterLevel)?.desc}
|
||||
</p>
|
||||
<p className="se-flow-settings-hint se-flow-settings-hint--muted">
|
||||
{stableStrategyFilterSummary(strategyId, filterLevel)}
|
||||
</p>
|
||||
<p className="se-flow-settings-hint se-flow-settings-hint--muted">
|
||||
{stableStrategySummaryText(strategyId, mode, filterLevel, def)}
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
|
||||
if (variant === 'inline') {
|
||||
return (
|
||||
<div className="se-stable-strategy-inline-settings" onClick={e => e.stopPropagation()}>
|
||||
{body}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={popoverClassName}
|
||||
role="dialog"
|
||||
aria-label="추천 전략 필터 설정"
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<div className="se-flow-settings-head">
|
||||
<span>필터 강도</span>
|
||||
<button
|
||||
type="button"
|
||||
className="se-flow-settings-close"
|
||||
aria-label="닫기"
|
||||
title="닫기"
|
||||
onClick={onClose}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
{body}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -30,6 +30,18 @@ import {
|
||||
} from '../../utils/strategyEditorShared';
|
||||
import { buildSidewaysFilterNode } from '../../utils/sidewaysFilterPaletteStorage';
|
||||
import { setStochPairSecondary } from '../../utils/stochOverboughtPair';
|
||||
import {
|
||||
setPriceExtremePairFilterLevel,
|
||||
type PriceExtremeFilterLevel,
|
||||
} from '../../utils/priceExtremeBreakoutPair';
|
||||
import {
|
||||
setInflection33PairFilterLevel,
|
||||
type Inflection33FilterLevel,
|
||||
} from '../../utils/inflection33Strategy';
|
||||
import {
|
||||
setStableStrategyPairFilterLevel,
|
||||
type StableStrategyFilterLevel,
|
||||
} from '../../utils/stableStrategyPairs';
|
||||
import {
|
||||
START_NODE_ID,
|
||||
applyEdgeHandles,
|
||||
@@ -700,7 +712,22 @@ function StrategyEditorCanvasInner({
|
||||
const handleChangeLogicGateType = useCallback((id: string, gateType: 'AND' | 'OR') => {
|
||||
const patchGate = (n: LogicNode) => (
|
||||
n.type === 'AND' || n.type === 'OR'
|
||||
? { ...n, type: gateType, stochPair: gateType === 'OR' ? n.stochPair : undefined }
|
||||
? {
|
||||
...n,
|
||||
type: gateType,
|
||||
stochPair: gateType === 'OR' ? n.stochPair : undefined,
|
||||
priceExtremePair: gateType === 'AND' ? n.priceExtremePair : undefined,
|
||||
inflection33Pair: n.inflection33Pair?.mode === 'buy'
|
||||
? (gateType === 'AND' ? n.inflection33Pair : undefined)
|
||||
: n.inflection33Pair?.mode === 'sell'
|
||||
? (gateType === 'OR' ? n.inflection33Pair : undefined)
|
||||
: undefined,
|
||||
stableStrategyPair: n.stableStrategyPair?.mode === 'buy'
|
||||
? (gateType === 'AND' ? n.stableStrategyPair : undefined)
|
||||
: n.stableStrategyPair?.mode === 'sell'
|
||||
? (gateType === 'OR' ? n.stableStrategyPair : undefined)
|
||||
: undefined,
|
||||
}
|
||||
: n
|
||||
);
|
||||
if (isOrphanNode(orphans, id)) {
|
||||
@@ -747,10 +774,76 @@ function StrategyEditorCanvasInner({
|
||||
}
|
||||
}, [root, extraRoots, orphans, def, onChange, onOrphansChange, onExtraRootsChange]);
|
||||
|
||||
const handleUpdatePriceExtremeFilterLevel = useCallback((id: string, filterLevel: PriceExtremeFilterLevel) => {
|
||||
const patchPair = (n: LogicNode) => setPriceExtremePairFilterLevel(n, filterLevel, def);
|
||||
if (isOrphanNode(orphans, id)) {
|
||||
onOrphansChange(orphans.map(o => (o.id === id ? patchPair(o) : o)));
|
||||
return;
|
||||
}
|
||||
if (root && findNodeInTree(root, id)) {
|
||||
onChange(updateNode(root, id, patchPair));
|
||||
return;
|
||||
}
|
||||
for (const [sid, branch] of Object.entries(extraRoots)) {
|
||||
if (branch && findNodeInTree(branch, id)) {
|
||||
onExtraRootsChange?.({
|
||||
...extraRoots,
|
||||
[sid]: updateNode(branch, id, patchPair),
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, [root, extraRoots, orphans, def, onChange, onOrphansChange, onExtraRootsChange]);
|
||||
|
||||
const handleUpdateInflection33FilterLevel = useCallback((id: string, filterLevel: Inflection33FilterLevel) => {
|
||||
const patchPair = (n: LogicNode) => setInflection33PairFilterLevel(n, filterLevel, def);
|
||||
if (isOrphanNode(orphans, id)) {
|
||||
onOrphansChange(orphans.map(o => (o.id === id ? patchPair(o) : o)));
|
||||
return;
|
||||
}
|
||||
if (root && findNodeInTree(root, id)) {
|
||||
onChange(updateNode(root, id, patchPair));
|
||||
return;
|
||||
}
|
||||
for (const [sid, branch] of Object.entries(extraRoots)) {
|
||||
if (branch && findNodeInTree(branch, id)) {
|
||||
onExtraRootsChange?.({
|
||||
...extraRoots,
|
||||
[sid]: updateNode(branch, id, patchPair),
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, [root, extraRoots, orphans, def, onChange, onOrphansChange, onExtraRootsChange]);
|
||||
|
||||
const handleUpdateStableStrategyFilterLevel = useCallback((id: string, filterLevel: StableStrategyFilterLevel) => {
|
||||
const patchPair = (n: LogicNode) => setStableStrategyPairFilterLevel(n, filterLevel, def);
|
||||
if (isOrphanNode(orphans, id)) {
|
||||
onOrphansChange(orphans.map(o => (o.id === id ? patchPair(o) : o)));
|
||||
return;
|
||||
}
|
||||
if (root && findNodeInTree(root, id)) {
|
||||
onChange(updateNode(root, id, patchPair));
|
||||
return;
|
||||
}
|
||||
for (const [sid, branch] of Object.entries(extraRoots)) {
|
||||
if (branch && findNodeInTree(branch, id)) {
|
||||
onExtraRootsChange?.({
|
||||
...extraRoots,
|
||||
[sid]: updateNode(branch, id, patchPair),
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, [root, extraRoots, orphans, def, onChange, onOrphansChange, onExtraRootsChange]);
|
||||
|
||||
const flowCallbacks = useMemo(() => ({
|
||||
onDelete: handleDelete,
|
||||
onUpdateCondition: handleUpdateCondition,
|
||||
onUpdateStochPairSecondary: handleUpdateStochPairSecondary,
|
||||
onUpdatePriceExtremeFilterLevel: handleUpdatePriceExtremeFilterLevel,
|
||||
onUpdateInflection33FilterLevel: handleUpdateInflection33FilterLevel,
|
||||
onUpdateStableStrategyFilterLevel: handleUpdateStableStrategyFilterLevel,
|
||||
onChangeLogicGateType: handleChangeLogicGateType,
|
||||
onDropTarget: handleDropTarget,
|
||||
onDragOverTarget: handleDragOverTarget,
|
||||
@@ -758,7 +851,7 @@ function StrategyEditorCanvasInner({
|
||||
onStartCandleTypesChange: handleStartCandleTypesChange,
|
||||
onDeleteStart: handleDeleteStart,
|
||||
}), [
|
||||
handleDelete, handleUpdateCondition, handleUpdateStochPairSecondary, handleChangeLogicGateType,
|
||||
handleDelete, handleUpdateCondition, handleUpdateStochPairSecondary, handleUpdatePriceExtremeFilterLevel, handleUpdateInflection33FilterLevel, handleUpdateStableStrategyFilterLevel, handleChangeLogicGateType,
|
||||
handleDropTarget, handleDragOverTarget, handleDragLeaveTarget,
|
||||
handleStartCandleTypesChange, handleDeleteStart,
|
||||
]);
|
||||
|
||||
Reference in New Issue
Block a user