지표조건 프리셋 적용
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
import React from 'react';
|
||||
import type { ConditionPresetGraph } from '../../utils/conditionPresets';
|
||||
|
||||
interface Props {
|
||||
type: ConditionPresetGraph;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
/** 조건 프리셋 미니 그래프 아이콘 */
|
||||
export default function ConditionPresetGraph({ type, size = 48 }: Props) {
|
||||
const w = size;
|
||||
const h = Math.round(size * 0.56);
|
||||
const stroke = 'currentColor';
|
||||
const thresh = '#6b7280';
|
||||
const line = '#a78bfa';
|
||||
const line2 = '#60a5fa';
|
||||
|
||||
return (
|
||||
<svg
|
||||
width={w}
|
||||
height={h}
|
||||
viewBox="0 0 48 28"
|
||||
className="se-cond-preset-graph"
|
||||
aria-hidden
|
||||
>
|
||||
{/* 임계/기준 수평선 */}
|
||||
{(type === 'cross_up' || type === 'cross_down' || type === 'above' || type === 'below' || type === 'between') && (
|
||||
<line x1="4" y1="14" x2="44" y2="14" stroke={thresh} strokeWidth="1.2" strokeDasharray="3 2" />
|
||||
)}
|
||||
|
||||
{type === 'cross_up' && (
|
||||
<path d="M4 22 Q16 22 22 14 T40 6" fill="none" stroke={line} strokeWidth="2" strokeLinecap="round" />
|
||||
)}
|
||||
{type === 'cross_down' && (
|
||||
<path d="M4 6 Q16 6 22 14 T40 22" fill="none" stroke={line} strokeWidth="2" strokeLinecap="round" />
|
||||
)}
|
||||
{type === 'above' && (
|
||||
<path d="M4 10 L14 8 L24 9 L34 7 L44 8" fill="none" stroke={line} strokeWidth="2" strokeLinecap="round" />
|
||||
)}
|
||||
{type === 'below' && (
|
||||
<path d="M4 18 L14 20 L24 19 L34 21 L44 20" fill="none" stroke={line} strokeWidth="2" strokeLinecap="round" />
|
||||
)}
|
||||
{type === 'slope_up' && (
|
||||
<path d="M6 24 L42 4" fill="none" stroke={line} strokeWidth="2" strokeLinecap="round" />
|
||||
)}
|
||||
{type === 'slope_down' && (
|
||||
<path d="M6 4 L42 24" fill="none" stroke={line} strokeWidth="2" strokeLinecap="round" />
|
||||
)}
|
||||
{type === 'dual_cross_up' && (
|
||||
<>
|
||||
<path d="M4 20 L44 12" fill="none" stroke={line2} strokeWidth="1.5" strokeLinecap="round" />
|
||||
<path d="M4 24 Q22 24 28 14 T44 8" fill="none" stroke={line} strokeWidth="2" strokeLinecap="round" />
|
||||
</>
|
||||
)}
|
||||
{type === 'dual_cross_down' && (
|
||||
<>
|
||||
<path d="M4 12 L44 20" fill="none" stroke={line2} strokeWidth="1.5" strokeLinecap="round" />
|
||||
<path d="M4 8 Q22 8 28 16 T44 22" fill="none" stroke={line} strokeWidth="2" strokeLinecap="round" />
|
||||
</>
|
||||
)}
|
||||
{type === 'between' && (
|
||||
<>
|
||||
<line x1="4" y1="8" x2="44" y2="8" stroke={thresh} strokeWidth="1" strokeDasharray="2 2" />
|
||||
<line x1="4" y1="20" x2="44" y2="20" stroke={thresh} strokeWidth="1" strokeDasharray="2 2" />
|
||||
<path d="M4 18 L44 16" fill="none" stroke={line} strokeWidth="2" strokeLinecap="round" />
|
||||
</>
|
||||
)}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import type { ConditionDSL } from '../../utils/strategyTypes';
|
||||
import type { DefType } from '../../utils/strategyEditorShared';
|
||||
import {
|
||||
applyConditionPreset,
|
||||
getConditionPresetsForIndicator,
|
||||
type ConditionPreset,
|
||||
} from '../../utils/conditionPresets';
|
||||
import ConditionPresetGraph from './ConditionPresetGraph';
|
||||
|
||||
interface Props {
|
||||
condition: ConditionDSL;
|
||||
def: DefType;
|
||||
signalType: 'buy' | 'sell';
|
||||
onChange: (next: ConditionDSL) => void;
|
||||
onClose: () => void;
|
||||
popoverClassName?: string;
|
||||
}
|
||||
|
||||
export default function ConditionPresetPicker({
|
||||
condition,
|
||||
def,
|
||||
signalType,
|
||||
onChange,
|
||||
onClose,
|
||||
popoverClassName = 'sp-node-preset-pop',
|
||||
}: Props) {
|
||||
const popRef = useRef<HTMLDivElement>(null);
|
||||
const presets = getConditionPresetsForIndicator(condition.indicatorType, def, signalType);
|
||||
|
||||
const sorted = [...presets].sort((a, b) => {
|
||||
const aMatch = a.tags?.includes(signalType) ? 0 : 1;
|
||||
const bMatch = b.tags?.includes(signalType) ? 0 : 1;
|
||||
return aMatch - bMatch;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const onDoc = (e: MouseEvent) => {
|
||||
if (popRef.current && !popRef.current.contains(e.target as Node)) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
const t = window.setTimeout(() => document.addEventListener('mousedown', onDoc), 0);
|
||||
return () => {
|
||||
window.clearTimeout(t);
|
||||
document.removeEventListener('mousedown', onDoc);
|
||||
};
|
||||
}, [onClose]);
|
||||
|
||||
const handlePick = (preset: ConditionPreset) => {
|
||||
onChange(applyConditionPreset(condition, preset, def));
|
||||
onClose();
|
||||
};
|
||||
|
||||
if (sorted.length === 0) {
|
||||
return (
|
||||
<div ref={popRef} className={popoverClassName} role="dialog" aria-label="조건 선택">
|
||||
<div className="se-cond-preset-head">
|
||||
<span>조건 선택</span>
|
||||
<button type="button" className="se-flow-settings-close" onClick={onClose} aria-label="닫기">×</button>
|
||||
</div>
|
||||
<p className="se-cond-preset-empty">이 지표에 등록된 조건 템플릿이 없습니다.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={popRef} className={popoverClassName} role="dialog" aria-label="조건 선택">
|
||||
<div className="se-cond-preset-head">
|
||||
<span>조건 선택</span>
|
||||
<button type="button" className="se-flow-settings-close" onClick={onClose} aria-label="닫기">×</button>
|
||||
</div>
|
||||
<p className="se-cond-preset-hint">그래프 패턴을 선택하면 캔들 범위·조건대상·조건이 자동 설정됩니다.</p>
|
||||
<ul className="se-cond-preset-list">
|
||||
{sorted.map(preset => (
|
||||
<li key={preset.id}>
|
||||
<button
|
||||
type="button"
|
||||
className="se-cond-preset-item"
|
||||
onClick={() => handlePick(preset)}
|
||||
>
|
||||
<span className="se-cond-preset-graph-wrap">
|
||||
<ConditionPresetGraph type={preset.graph} size={52} />
|
||||
</span>
|
||||
<span className="se-cond-preset-text">
|
||||
<span className="se-cond-preset-label">{preset.label}</span>
|
||||
<span className="se-cond-preset-desc">{preset.description}</span>
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
stochPairDisplayName,
|
||||
} from '../../utils/stochOverboughtPair';
|
||||
import ConditionNodeSettings from './ConditionNodeSettings';
|
||||
import ConditionPresetPicker from './ConditionPresetPicker';
|
||||
import StochPairNodeSettings from './StochPairNodeSettings';
|
||||
import {
|
||||
type EditorConditionState,
|
||||
@@ -85,8 +86,10 @@ const TreeNodeComp: React.FC<TreeNodeProps> = ({
|
||||
const color = isLogic ? NODE_COLORS[node.type] : IND_COLOR;
|
||||
const nodeDropKey = `${dropScope}:${node.id}`;
|
||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||
const [presetOpen, setPresetOpen] = useState(false);
|
||||
const isStochPair = isStochOverboughtPairRoot(node);
|
||||
const showSettings = node.type === 'CONDITION' && node.condition && hasNodeSettings(node.condition);
|
||||
const showPreset = node.type === 'CONDITION' && node.condition && !node.condition.composite;
|
||||
const showStochPairSettings = isStochPair;
|
||||
const label = node.type === 'CONDITION' && node.condition
|
||||
? nodeToText(node, def)
|
||||
@@ -170,6 +173,25 @@ const TreeNodeComp: React.FC<TreeNodeProps> = ({
|
||||
compact
|
||||
/>
|
||||
)}
|
||||
{showPreset && (
|
||||
<button
|
||||
type="button"
|
||||
className="sp-node-preset"
|
||||
title="조건 선택"
|
||||
aria-label="조건 선택"
|
||||
aria-expanded={presetOpen}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
setPresetOpen(v => !v);
|
||||
setSettingsOpen(false);
|
||||
}}
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M3 17l4-5 4 3 5-8 5 6" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<circle cx="18" cy="6" r="2" fill="currentColor" stroke="none" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
{(showSettings || showStochPairSettings) && (
|
||||
<button
|
||||
type="button"
|
||||
@@ -180,6 +202,7 @@ const TreeNodeComp: React.FC<TreeNodeProps> = ({
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
setSettingsOpen(v => !v);
|
||||
setPresetOpen(false);
|
||||
}}
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
@@ -190,6 +213,16 @@ const TreeNodeComp: React.FC<TreeNodeProps> = ({
|
||||
)}
|
||||
<button type="button" className="sp-node-del" title="삭제" onClick={onDelete}>✕</button>
|
||||
</div>
|
||||
{presetOpen && node.condition && (
|
||||
<ConditionPresetPicker
|
||||
condition={node.condition}
|
||||
def={def}
|
||||
signalType={signalType}
|
||||
onChange={handleCondChange}
|
||||
onClose={() => setPresetOpen(false)}
|
||||
popoverClassName="sp-node-preset-pop"
|
||||
/>
|
||||
)}
|
||||
{settingsOpen && node.condition && (
|
||||
<ConditionNodeSettings
|
||||
condition={node.condition}
|
||||
|
||||
Reference in New Issue
Block a user