전략편집기 수정, 보조지표 설정 수정

This commit is contained in:
Macbook
2026-05-27 23:59:30 +09:00
parent 8cc0d1c88c
commit 2713b2951d
17 changed files with 210 additions and 46 deletions
+22 -3
View File
@@ -52,18 +52,25 @@ const ColorInput: React.FC<ColorInputProps> = ({ value, onChange, disabled = fal
useEffect(() => {
if (!open) return;
const onDoc = (e: MouseEvent) => {
const onDocPointer = (e: Event) => {
const t = e.target as Node;
if (anchorRef.current?.contains(t) || popoverRef.current?.contains(t)) return;
setOpen(false);
};
document.addEventListener('mousedown', onDoc);
return () => document.removeEventListener('mousedown', onDoc);
// 설정 모달 등이 bubble 단계에서 stopPropagation 하므로 capture 사용
document.addEventListener('mousedown', onDocPointer, true);
document.addEventListener('pointerdown', onDocPointer, true);
return () => {
document.removeEventListener('mousedown', onDocPointer, true);
document.removeEventListener('pointerdown', onDocPointer, true);
};
}, [open]);
const patchHex = (h: string) => onChange(formatPlotColor(h, alpha));
const patchAlpha = (a: number) => onChange(formatPlotColor(hex6, a));
const closePopover = useCallback(() => setOpen(false), []);
const popover = open && typeof document !== 'undefined' ? (
<div
ref={popoverRef}
@@ -72,7 +79,19 @@ const ColorInput: React.FC<ColorInputProps> = ({ value, onChange, disabled = fal
role="dialog"
aria-label="색상 선택"
onMouseDown={e => e.stopPropagation()}
onPointerDown={e => e.stopPropagation()}
>
<div className="ism-color-popover-header">
<span className="ism-color-popover-title"> </span>
<button
type="button"
className="gc-popup-close ism-color-popover-close"
onClick={closePopover}
aria-label="색상 선택 닫기"
>
</button>
</div>
<div className="ism-color-popover-native">
<label className="ism-color-native-label">
<span className="ism-style-label"> </span>
+32 -7
View File
@@ -18,7 +18,7 @@ import {
genId,
type StrategyDto,
} from '../utils/strategyEditorShared';
import { syncLogicNodeWithGlobalDef } from '../utils/strategyDefSync';
import { syncLogicNodeWithGlobalDef, syncLogicRootWithGlobalDef } from '../utils/strategyDefSync';
import { useIndicatorSettings } from '../hooks/useIndicatorSettings';
import { findLogicNode, getIndicatorPeriodLabel } from '../utils/strategyFlowLayout';
import {
@@ -29,6 +29,7 @@ import {
hasMultipleStartSections,
normalizeStartCombineOp,
updateStartCombineOp,
updateStartCandleTypes,
type EditorConditionState,
} from '../utils/strategyConditionSerde';
import {
@@ -131,7 +132,7 @@ function normalizeTabLayoutState(snap: SignalFlowLayoutSnapshot) {
}
export default function StrategyEditorPage({ theme }: Props) {
const { getParams, getVisualConfig, settingsRevision } = useIndicatorSettings();
const { getParams, getVisualConfig, settingsRevision, settingsLoaded } = useIndicatorSettings();
const DEF = useMemo(
() => buildStrategyEditorDefFromSettings(getParams, getVisualConfig),
[getParams, getVisualConfig, settingsRevision],
@@ -391,6 +392,11 @@ export default function StrategyEditorPage({ theme }: Props) {
schedulePersistFlowLayout(layoutStrategyKey);
}, [setCurrentRoot, setCurrentLayout, layoutStrategyKey, schedulePersistFlowLayout]);
const handleStartCandleTypesChange = useCallback((startId: string, candleTypes: string[]) => {
const state = signalTab === 'buy' ? buyEditorState : sellEditorState;
handleEditorStateChange(updateStartCandleTypes(state, startId, candleTypes));
}, [signalTab, buyEditorState, sellEditorState, handleEditorStateChange]);
const handleStartCombineOpChange = useCallback((op: StartCombineOp) => {
handleEditorStateChange(updateStartCombineOp(currentEditorState, op));
}, [handleEditorStateChange, currentEditorState]);
@@ -491,13 +497,26 @@ export default function StrategyEditorPage({ theme }: Props) {
const sellDecoded = decodeConditionForEditor(s.sellCondition ?? null);
const stored = loadStrategyFlowLayout(String(s.id));
const buySynced = syncLogicRootWithGlobalDef(
buyDecoded.root,
stored?.buy.orphans ?? [],
DEF,
'buy',
);
const sellSynced = syncLogicRootWithGlobalDef(
sellDecoded.root,
stored?.sell.orphans ?? [],
DEF,
'sell',
);
setSelectedId(s.id);
setStratName(s.name);
setStratDesc(s.description ?? '');
setBuyCondition(buyDecoded.root);
setSellCondition(sellDecoded.root);
setBuyOrphans(stored?.buy.orphans ?? []);
setSellOrphans(stored?.sell.orphans ?? []);
setBuyCondition(buySynced.root);
setSellCondition(sellSynced.root);
setBuyOrphans(buySynced.orphans);
setSellOrphans(sellSynced.orphans);
setBuyLayout({
positions: stored?.buy.positions ?? {},
edgeHandles: stored?.buy.edgeHandles ?? {},
@@ -802,7 +821,12 @@ export default function StrategyEditorPage({ theme }: Props) {
<header className="se-header">
<div className="se-header-left">
<h1 className="se-title"> </h1>
<span className="se-subtitle">Strategy Builder</span>
<span className="se-subtitle">
Strategy Builder
{!settingsLoaded && (
<span className="se-subtitle-hint"> · </span>
)}
</span>
</div>
<div className="se-header-actions">
<div className="se-editor-mode" role="group" aria-label="편집 방식">
@@ -993,6 +1017,7 @@ export default function StrategyEditorPage({ theme }: Props) {
extraStartIds={currentLayout.extraStartIds}
extraRoots={currentLayout.extraRoots}
onStartMetaChange={handleStartMetaChange}
onStartCandleTypesChange={handleStartCandleTypesChange}
onExtraStartIdsChange={handleExtraStartIdsChange}
onExtraRootsChange={handleExtraRootsChange}
/>
@@ -1,6 +1,6 @@
import React, { useEffect, useRef } from 'react';
import type { ConditionDSL } from '../../utils/strategyTypes';
import type { DefType } from '../../utils/strategyEditorShared';
import { getDefaultConditionFields, type DefType } from '../../utils/strategyEditorShared';
import {
getCompositeLeftCandleType,
getCompositeRightCandleType,
@@ -27,13 +27,19 @@ import ComboNumberInput from './ComboNumberInput';
interface Props {
condition: ConditionDSL;
def: DefType;
signalType?: 'buy' | 'sell';
onChange: (c: ConditionDSL) => void;
onClose: () => void;
popoverClassName?: string;
}
export default function ConditionNodeSettings({
condition, def, onChange, onClose, popoverClassName = 'se-flow-settings-pop',
condition,
def,
signalType = 'buy',
onChange,
onClose,
popoverClassName = 'se-flow-settings-pop',
}: Props) {
const ref = useRef<HTMLDivElement>(null);
@@ -45,7 +51,8 @@ export default function ConditionNodeSettings({
return () => document.removeEventListener('mousedown', onDoc);
}, [onClose]);
const rightThreshold = getConditionThreshold(condition, 'right');
const inheritedThreshold = getDefaultConditionFields(condition.indicatorType, signalType, def).r;
const rightThreshold = getConditionThreshold(condition, 'right', inheritedThreshold);
const showThreshold = hasEditableThreshold(condition) && rightThreshold != null;
const periodPresets = getPeriodPresetOptions(condition.indicatorType, def);
const thresholdPresets = getThresholdPresetOptions(condition.indicatorType);
@@ -283,6 +283,7 @@ export const ConditionFlowNode = memo(function ConditionFlowNode({ id, data, sel
<ConditionNodeSettings
condition={cond}
def={d.def}
signalType={d.signalTab ?? 'buy'}
onChange={handleSettingsChange}
onClose={() => setSettingsOpen(false)}
/>
@@ -105,6 +105,8 @@ interface Props {
extraStartIds?: string[];
extraRoots?: Record<string, LogicNode | null>;
onStartMetaChange?: (meta: Record<string, StartNodeMeta>) => void;
/** START 체크박스 — startMeta + 조건 트리 분봉 동기화 */
onStartCandleTypesChange?: (startId: string, candleTypes: string[]) => void;
onExtraStartIdsChange?: (ids: string[]) => void;
onExtraRootsChange?: (roots: Record<string, LogicNode | null>) => void;
}
@@ -268,6 +270,7 @@ function StrategyEditorCanvasInner({
extraStartIds = [],
extraRoots = {},
onStartMetaChange,
onStartCandleTypesChange,
onExtraStartIdsChange,
onExtraRootsChange,
}: Props) {
@@ -372,13 +375,17 @@ function StrategyEditorCanvasInner({
}, [emitLayoutChange]);
const handleStartCandleTypesChange = useCallback((startId: string, candleTypes: string[]) => {
if (onStartCandleTypesChange) {
onStartCandleTypesChange(startId, candleTypes);
return;
}
const types = candleTypes.length > 0 ? candleTypes : [DEFAULT_START_CANDLE];
const normalized = types.map(normalizeStartCandleType);
onStartMetaChange?.({
...startMeta,
[startId]: { candleTypes: normalized, candleType: normalized[0] },
});
}, [startMeta, onStartMetaChange]);
}, [startMeta, onStartMetaChange, onStartCandleTypesChange]);
const handleDeleteStart = useCallback((startId: string) => {
if (startId === START_NODE_ID) return;
@@ -149,6 +149,7 @@ const TreeNodeComp: React.FC<TreeNodeProps> = ({
<ConditionNodeSettings
condition={node.condition}
def={def}
signalType={signalType}
onChange={handleCondChange}
onClose={() => setSettingsOpen(false)}
popoverClassName="sp-node-settings-pop"