복합지표 전략 추가

This commit is contained in:
Macbook
2026-06-12 13:26:53 +09:00
parent 9208418c33
commit 52137cf1db
27 changed files with 2712 additions and 55 deletions
@@ -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,
]);