지표조건 프리셋 적용
This commit is contained in:
@@ -7594,6 +7594,112 @@ html.desktop-client .tmb-logo-version {
|
|||||||
.sp-node-settings:hover {
|
.sp-node-settings:hover {
|
||||||
background: rgba(230, 194, 0, 0.22);
|
background: rgba(230, 194, 0, 0.22);
|
||||||
}
|
}
|
||||||
|
.sp-node-preset {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: rgba(167, 139, 250, 0.15);
|
||||||
|
color: #a78bfa;
|
||||||
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.sp-node-preset:hover {
|
||||||
|
background: rgba(167, 139, 250, 0.28);
|
||||||
|
}
|
||||||
|
.sp-node-preset-pop {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 4px);
|
||||||
|
right: 8px;
|
||||||
|
z-index: 55;
|
||||||
|
width: min(320px, calc(100vw - 24px));
|
||||||
|
max-height: min(420px, 60vh);
|
||||||
|
overflow: auto;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid color-mix(in srgb, #a78bfa 45%, transparent);
|
||||||
|
background: var(--bg2);
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
|
||||||
|
}
|
||||||
|
.se-cond-preset-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 0.62rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #a78bfa;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
}
|
||||||
|
.se-cond-preset-hint {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
font-size: 0.65rem;
|
||||||
|
color: var(--text2);
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
.se-cond-preset-empty {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.68rem;
|
||||||
|
color: var(--text2);
|
||||||
|
}
|
||||||
|
.se-cond-preset-list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.se-cond-preset-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: color-mix(in srgb, var(--bg3, #1a1a2e) 80%, transparent);
|
||||||
|
color: var(--text1);
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.se-cond-preset-item:hover {
|
||||||
|
border-color: color-mix(in srgb, #a78bfa 50%, transparent);
|
||||||
|
background: rgba(167, 139, 250, 0.08);
|
||||||
|
}
|
||||||
|
.se-cond-preset-graph-wrap {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 56px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
color: #a78bfa;
|
||||||
|
}
|
||||||
|
.se-cond-preset-text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.se-cond-preset-label {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text1);
|
||||||
|
}
|
||||||
|
.se-cond-preset-desc {
|
||||||
|
font-size: 0.62rem;
|
||||||
|
color: var(--text2);
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
.sp-node-settings-pop {
|
.sp-node-settings-pop {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: calc(100% + 4px);
|
top: calc(100% + 4px);
|
||||||
|
|||||||
@@ -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,
|
stochPairDisplayName,
|
||||||
} from '../../utils/stochOverboughtPair';
|
} from '../../utils/stochOverboughtPair';
|
||||||
import ConditionNodeSettings from './ConditionNodeSettings';
|
import ConditionNodeSettings from './ConditionNodeSettings';
|
||||||
|
import ConditionPresetPicker from './ConditionPresetPicker';
|
||||||
import StochPairNodeSettings from './StochPairNodeSettings';
|
import StochPairNodeSettings from './StochPairNodeSettings';
|
||||||
import {
|
import {
|
||||||
type EditorConditionState,
|
type EditorConditionState,
|
||||||
@@ -85,8 +86,10 @@ const TreeNodeComp: React.FC<TreeNodeProps> = ({
|
|||||||
const color = isLogic ? NODE_COLORS[node.type] : IND_COLOR;
|
const color = isLogic ? NODE_COLORS[node.type] : IND_COLOR;
|
||||||
const nodeDropKey = `${dropScope}:${node.id}`;
|
const nodeDropKey = `${dropScope}:${node.id}`;
|
||||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||||
|
const [presetOpen, setPresetOpen] = useState(false);
|
||||||
const isStochPair = isStochOverboughtPairRoot(node);
|
const isStochPair = isStochOverboughtPairRoot(node);
|
||||||
const showSettings = node.type === 'CONDITION' && node.condition && hasNodeSettings(node.condition);
|
const showSettings = node.type === 'CONDITION' && node.condition && hasNodeSettings(node.condition);
|
||||||
|
const showPreset = node.type === 'CONDITION' && node.condition && !node.condition.composite;
|
||||||
const showStochPairSettings = isStochPair;
|
const showStochPairSettings = isStochPair;
|
||||||
const label = node.type === 'CONDITION' && node.condition
|
const label = node.type === 'CONDITION' && node.condition
|
||||||
? nodeToText(node, def)
|
? nodeToText(node, def)
|
||||||
@@ -170,6 +173,25 @@ const TreeNodeComp: React.FC<TreeNodeProps> = ({
|
|||||||
compact
|
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) && (
|
{(showSettings || showStochPairSettings) && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -180,6 +202,7 @@ const TreeNodeComp: React.FC<TreeNodeProps> = ({
|
|||||||
onClick={e => {
|
onClick={e => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setSettingsOpen(v => !v);
|
setSettingsOpen(v => !v);
|
||||||
|
setPresetOpen(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
<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>
|
<button type="button" className="sp-node-del" title="삭제" onClick={onDelete}>✕</button>
|
||||||
</div>
|
</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 && (
|
{settingsOpen && node.condition && (
|
||||||
<ConditionNodeSettings
|
<ConditionNodeSettings
|
||||||
condition={node.condition}
|
condition={node.condition}
|
||||||
|
|||||||
@@ -0,0 +1,305 @@
|
|||||||
|
/**
|
||||||
|
* 전략편집기 — 지표별 조건 프리셋 (그래프 템플릿 + 자동 필드 구성)
|
||||||
|
*/
|
||||||
|
import type { ConditionDSL } from './strategyTypes';
|
||||||
|
import type { DefType } from './strategyEditorShared';
|
||||||
|
import { applyCondTypeDefaults } from './strategyEditorShared';
|
||||||
|
import { initConditionPeriodsInherit } from './conditionPeriods';
|
||||||
|
import {
|
||||||
|
THRESHOLD_HL_MID,
|
||||||
|
THRESHOLD_HL_OVER,
|
||||||
|
THRESHOLD_HL_UNDER,
|
||||||
|
} from './thresholdSymbols';
|
||||||
|
import { nhPriorField, nlPriorField } from './priceExtremeIndicators';
|
||||||
|
|
||||||
|
export type ConditionPresetGraph =
|
||||||
|
| 'cross_up'
|
||||||
|
| 'cross_down'
|
||||||
|
| 'above'
|
||||||
|
| 'below'
|
||||||
|
| 'slope_up'
|
||||||
|
| 'slope_down'
|
||||||
|
| 'dual_cross_up'
|
||||||
|
| 'dual_cross_down'
|
||||||
|
| 'between';
|
||||||
|
|
||||||
|
export type ConditionPreset = {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
graph: ConditionPresetGraph;
|
||||||
|
patch: Partial<ConditionDSL>;
|
||||||
|
tags?: ('buy' | 'sell')[];
|
||||||
|
};
|
||||||
|
|
||||||
|
function crossUp(left: string, right: string, tags?: ('buy' | 'sell')[]): ConditionPreset {
|
||||||
|
return {
|
||||||
|
id: `cross_up_${left}_${right}`,
|
||||||
|
label: '상향 돌파',
|
||||||
|
description: '기준선을 아래에서 위로 돌파',
|
||||||
|
graph: 'cross_up',
|
||||||
|
patch: { conditionType: 'CROSS_UP', leftField: left, rightField: right, candleRangeMode: undefined, candleRange: 1 },
|
||||||
|
tags,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function crossDown(left: string, right: string, tags?: ('buy' | 'sell')[]): ConditionPreset {
|
||||||
|
return {
|
||||||
|
id: `cross_down_${left}_${right}`,
|
||||||
|
label: '하향 돌파',
|
||||||
|
description: '기준선을 위에서 아래로 이탈',
|
||||||
|
graph: 'cross_down',
|
||||||
|
patch: { conditionType: 'CROSS_DOWN', leftField: left, rightField: right, candleRangeMode: undefined, candleRange: 1 },
|
||||||
|
tags,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function compare(
|
||||||
|
condType: 'GT' | 'LT' | 'GTE' | 'LTE',
|
||||||
|
left: string,
|
||||||
|
right: string,
|
||||||
|
label: string,
|
||||||
|
desc: string,
|
||||||
|
graph: ConditionPresetGraph,
|
||||||
|
tags?: ('buy' | 'sell')[],
|
||||||
|
): ConditionPreset {
|
||||||
|
return {
|
||||||
|
id: `${condType}_${left}_${right}`,
|
||||||
|
label,
|
||||||
|
description: desc,
|
||||||
|
graph,
|
||||||
|
patch: { conditionType: condType, leftField: left, rightField: right, candleRangeMode: undefined, candleRange: 1 },
|
||||||
|
tags,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function slope(up: boolean, left: string, tags?: ('buy' | 'sell')[]): ConditionPreset {
|
||||||
|
return {
|
||||||
|
id: up ? 'slope_up' : 'slope_down',
|
||||||
|
label: up ? '상승 기울기' : '하락 기울기',
|
||||||
|
description: up ? '최근 구간 상승 추세' : '최근 구간 하락 추세',
|
||||||
|
graph: up ? 'slope_up' : 'slope_down',
|
||||||
|
patch: {
|
||||||
|
conditionType: up ? 'SLOPE_UP' : 'SLOPE_DOWN',
|
||||||
|
leftField: left,
|
||||||
|
rightField: 'NONE',
|
||||||
|
slopePeriod: 3,
|
||||||
|
candleRangeMode: undefined,
|
||||||
|
candleRange: 1,
|
||||||
|
},
|
||||||
|
tags,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function oscillatorPresets(
|
||||||
|
left: string,
|
||||||
|
signalField?: string,
|
||||||
|
): ConditionPreset[] {
|
||||||
|
const out: ConditionPreset[] = [
|
||||||
|
crossUp(left, THRESHOLD_HL_UNDER, ['buy']),
|
||||||
|
crossUp(left, THRESHOLD_HL_MID, ['buy']),
|
||||||
|
crossUp(left, THRESHOLD_HL_OVER, ['buy', 'sell']),
|
||||||
|
crossDown(left, THRESHOLD_HL_OVER, ['sell']),
|
||||||
|
crossDown(left, THRESHOLD_HL_MID, ['sell']),
|
||||||
|
crossDown(left, THRESHOLD_HL_UNDER, ['sell']),
|
||||||
|
compare('GTE', left, THRESHOLD_HL_OVER, '과열 구간', '과열선 이상 유지', 'above', ['sell']),
|
||||||
|
compare('LTE', left, THRESHOLD_HL_UNDER, '침체 구간', '침체선 이하 유지', 'below', ['buy']),
|
||||||
|
compare('GTE', left, THRESHOLD_HL_MID, '중앙선 위', '중앙선 이상', 'above', ['buy']),
|
||||||
|
compare('LTE', left, THRESHOLD_HL_MID, '중앙선 아래', '중앙선 이하', 'below', ['sell']),
|
||||||
|
slope(true, left, ['buy']),
|
||||||
|
slope(false, left, ['sell']),
|
||||||
|
];
|
||||||
|
if (signalField) {
|
||||||
|
out.unshift(
|
||||||
|
crossUp(left, signalField, ['buy']),
|
||||||
|
crossDown(left, signalField, ['sell']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PRESET_BUILDERS: Record<string, (def: DefType, signalType: 'buy' | 'sell') => ConditionPreset[]> = {
|
||||||
|
RSI: () => oscillatorPresets('RSI_VALUE', 'RSI_SIGNAL'),
|
||||||
|
CCI: () => oscillatorPresets('CCI_VALUE', 'CCI_SIGNAL'),
|
||||||
|
STOCHASTIC: () => [
|
||||||
|
{ ...crossUp('STOCH_K', 'STOCH_D', ['buy']), graph: 'dual_cross_up' as const },
|
||||||
|
{ ...crossDown('STOCH_K', 'STOCH_D', ['sell']), graph: 'dual_cross_down' as const },
|
||||||
|
...oscillatorPresets('STOCH_K'),
|
||||||
|
],
|
||||||
|
WILLIAMS_R: () => oscillatorPresets('WILLIAMS_R_VALUE'),
|
||||||
|
MACD: () => [
|
||||||
|
{ ...crossUp('MACD_LINE', 'SIGNAL_LINE', ['buy']), graph: 'dual_cross_up' as const },
|
||||||
|
{ ...crossDown('MACD_LINE', 'SIGNAL_LINE', ['sell']), graph: 'dual_cross_down' as const },
|
||||||
|
crossUp('MACD_LINE', THRESHOLD_HL_MID, ['buy']),
|
||||||
|
crossDown('MACD_LINE', THRESHOLD_HL_MID, ['sell']),
|
||||||
|
compare('GTE', 'MACD_LINE', 'SIGNAL_LINE', 'MACD > 시그널', 'MACD가 시그널 위', 'above', ['buy']),
|
||||||
|
compare('LTE', 'MACD_LINE', 'SIGNAL_LINE', 'MACD < 시그널', 'MACD가 시그널 아래', 'below', ['sell']),
|
||||||
|
slope(true, 'MACD_LINE', ['buy']),
|
||||||
|
slope(false, 'MACD_LINE', ['sell']),
|
||||||
|
],
|
||||||
|
ADX: () => [
|
||||||
|
compare('GTE', 'ADX_VALUE', THRESHOLD_HL_MID, '추세 강화', 'ADX 중앙선 이상', 'above', ['buy']),
|
||||||
|
compare('LT', 'ADX_VALUE', THRESHOLD_HL_MID, '추세 약화', 'ADX 중앙선 미만', 'below', ['sell']),
|
||||||
|
crossUp('ADX_VALUE', THRESHOLD_HL_MID, ['buy']),
|
||||||
|
crossDown('ADX_VALUE', THRESHOLD_HL_MID, ['sell']),
|
||||||
|
slope(true, 'ADX_VALUE', ['buy']),
|
||||||
|
],
|
||||||
|
DMI: () => [
|
||||||
|
crossUp('PDI', 'MDI', ['buy']),
|
||||||
|
crossDown('PDI', 'MDI', ['sell']),
|
||||||
|
compare('GT', 'PDI', 'MDI', '+DI > -DI', '매수 우위', 'above', ['buy']),
|
||||||
|
compare('LT', 'PDI', 'MDI', '+DI < -DI', '매도 우위', 'below', ['sell']),
|
||||||
|
],
|
||||||
|
TRIX: () => [
|
||||||
|
crossUp('TRIX_VALUE', 'TRIX_SIGNAL', ['buy']),
|
||||||
|
crossDown('TRIX_VALUE', 'TRIX_SIGNAL', ['sell']),
|
||||||
|
crossUp('TRIX_VALUE', THRESHOLD_HL_MID, ['buy']),
|
||||||
|
...oscillatorPresets('TRIX_VALUE', 'TRIX_SIGNAL').slice(2),
|
||||||
|
],
|
||||||
|
OBV: () => [
|
||||||
|
crossUp('OBV_LINE', 'OBV_SIGNAL', ['buy']),
|
||||||
|
crossDown('OBV_LINE', 'OBV_SIGNAL', ['sell']),
|
||||||
|
compare('GT', 'OBV_LINE', 'OBV_SIGNAL', 'OBV > 신호선', 'OBV가 신호선 위', 'above', ['buy']),
|
||||||
|
slope(true, 'OBV_LINE', ['buy']),
|
||||||
|
slope(false, 'OBV_LINE', ['sell']),
|
||||||
|
],
|
||||||
|
MA: (def) => {
|
||||||
|
const s = `MA${def.maLines[0] ?? 5}`;
|
||||||
|
const l = `MA${def.maLines[1] ?? 10}`;
|
||||||
|
return [
|
||||||
|
crossUp(s, l, ['buy']),
|
||||||
|
crossDown(s, l, ['sell']),
|
||||||
|
crossUp('CLOSE_PRICE', s, ['buy']),
|
||||||
|
crossDown('CLOSE_PRICE', s, ['sell']),
|
||||||
|
compare('GT', 'CLOSE_PRICE', s, '종가 > MA', '종가가 MA 위', 'above', ['buy']),
|
||||||
|
compare('LT', 'CLOSE_PRICE', s, '종가 < MA', '종가가 MA 아래', 'below', ['sell']),
|
||||||
|
slope(true, s, ['buy']),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
EMA: (def) => {
|
||||||
|
const s = `EMA${def.maLines[0] ?? 5}`;
|
||||||
|
const l = `EMA${def.maLines[1] ?? 10}`;
|
||||||
|
return [
|
||||||
|
crossUp(s, l, ['buy']),
|
||||||
|
crossDown(s, l, ['sell']),
|
||||||
|
crossUp('CLOSE_PRICE', s, ['buy']),
|
||||||
|
crossDown('CLOSE_PRICE', s, ['sell']),
|
||||||
|
compare('GT', 'CLOSE_PRICE', s, '종가 > EMA', '종가가 EMA 위', 'above', ['buy']),
|
||||||
|
slope(true, s, ['buy']),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
BOLLINGER: () => [
|
||||||
|
crossUp('CLOSE_PRICE', 'LOWER_BAND', ['buy']),
|
||||||
|
crossDown('CLOSE_PRICE', 'UPPER_BAND', ['sell']),
|
||||||
|
crossUp('CLOSE_PRICE', 'UPPER_BAND', ['buy', 'sell']),
|
||||||
|
compare('LT', 'CLOSE_PRICE', 'LOWER_BAND', '하단 이탈', '종가가 하단밴드 아래', 'below', ['buy']),
|
||||||
|
compare('GT', 'CLOSE_PRICE', 'UPPER_BAND', '상단 돌파', '종가가 상단밴드 위', 'above', ['sell']),
|
||||||
|
],
|
||||||
|
DONCHIAN: (_def, signalType) => [
|
||||||
|
crossUp('CLOSE_PRICE', signalType === 'buy' ? 'DC_UPPER_20' : 'DC_LOWER_20', [signalType]),
|
||||||
|
crossDown('CLOSE_PRICE', signalType === 'sell' ? 'DC_LOWER_20' : 'DC_UPPER_20', [signalType]),
|
||||||
|
compare('GT', 'CLOSE_PRICE', 'DC_UPPER_20', '상단 돌파', '20일 고가 위', 'above', ['buy']),
|
||||||
|
compare('LT', 'CLOSE_PRICE', 'DC_LOWER_20', '하단 이탈', '20일 저가 아래', 'below', ['sell']),
|
||||||
|
],
|
||||||
|
NEW_HIGH: () => [
|
||||||
|
crossUp('CLOSE_PRICE', nhPriorField(9), ['buy']),
|
||||||
|
crossUp('CLOSE_PRICE', nhPriorField(20), ['buy']),
|
||||||
|
compare('GTE', 'CLOSE_PRICE', nhPriorField(9), '신고가 근접', '9일 신고가 이상', 'above', ['buy']),
|
||||||
|
],
|
||||||
|
NEW_LOW: () => [
|
||||||
|
crossDown('CLOSE_PRICE', nlPriorField(9), ['sell']),
|
||||||
|
crossDown('CLOSE_PRICE', nlPriorField(20), ['sell']),
|
||||||
|
compare('LTE', 'CLOSE_PRICE', nlPriorField(9), '신저가 근접', '9일 신저가 이하', 'below', ['sell']),
|
||||||
|
],
|
||||||
|
ICHIMOKU: () => [
|
||||||
|
crossUp('CONVERSION_LINE', 'BASE_LINE', ['buy']),
|
||||||
|
crossDown('CONVERSION_LINE', 'BASE_LINE', ['sell']),
|
||||||
|
{
|
||||||
|
id: 'above_cloud',
|
||||||
|
label: '구름 위',
|
||||||
|
description: '가격이 구름대 위',
|
||||||
|
graph: 'above',
|
||||||
|
patch: { conditionType: 'ABOVE_CLOUD', leftField: 'NONE', rightField: 'NONE' },
|
||||||
|
tags: ['buy'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'below_cloud',
|
||||||
|
label: '구름 아래',
|
||||||
|
description: '가격이 구름대 아래',
|
||||||
|
graph: 'below',
|
||||||
|
patch: { conditionType: 'BELOW_CLOUD', leftField: 'NONE', rightField: 'NONE' },
|
||||||
|
tags: ['sell'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cloud_break_up',
|
||||||
|
label: '구름 상향 돌파',
|
||||||
|
description: '구름 상단 돌파',
|
||||||
|
graph: 'cross_up',
|
||||||
|
patch: { conditionType: 'CLOUD_BREAK_UP', leftField: 'NONE', rightField: 'NONE' },
|
||||||
|
tags: ['buy'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
PSYCHOLOGICAL: () => oscillatorPresets('PSY_VALUE'),
|
||||||
|
NEW_PSYCHOLOGICAL: () => oscillatorPresets('NEW_PSY_VALUE'),
|
||||||
|
INVEST_PSYCHOLOGICAL: () => oscillatorPresets('INVEST_PSY_VALUE'),
|
||||||
|
VR: () => oscillatorPresets('VR_VALUE'),
|
||||||
|
BWI: () => oscillatorPresets('BWI_VALUE'),
|
||||||
|
VOLUME_OSC: () => [
|
||||||
|
crossUp('VOLUME_OSC_VALUE', THRESHOLD_HL_MID, ['buy']),
|
||||||
|
crossDown('VOLUME_OSC_VALUE', THRESHOLD_HL_MID, ['sell']),
|
||||||
|
compare('GT', 'VOLUME_OSC_VALUE', THRESHOLD_HL_MID, '0선 위', '중앙선 위', 'above', ['buy']),
|
||||||
|
],
|
||||||
|
VOLUME: () => [
|
||||||
|
crossUp('VOLUME_VALUE', 'VOLUME_MA', ['buy']),
|
||||||
|
compare('GT', 'VOLUME_VALUE', 'VOLUME_MA', '거래량 > MA', '평균 거래량 초과', 'above', ['buy']),
|
||||||
|
],
|
||||||
|
DISPARITY: (def) => {
|
||||||
|
const u = `DISPARITY${def.dispUltra}`;
|
||||||
|
return [
|
||||||
|
crossUp(u, THRESHOLD_HL_MID, ['buy']),
|
||||||
|
crossDown(u, THRESHOLD_HL_MID, ['sell']),
|
||||||
|
compare('GT', u, THRESHOLD_HL_MID, '이격도 확대', '중앙선(100) 위', 'above', ['buy']),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getConditionPresetsForIndicator(
|
||||||
|
indicatorType: string,
|
||||||
|
def: DefType,
|
||||||
|
signalType: 'buy' | 'sell',
|
||||||
|
): ConditionPreset[] {
|
||||||
|
const builder = PRESET_BUILDERS[indicatorType];
|
||||||
|
if (!builder) return [];
|
||||||
|
const all = builder(def, signalType);
|
||||||
|
const seen = new Set<string>();
|
||||||
|
return all.filter(p => {
|
||||||
|
const key = `${p.patch.conditionType}:${p.patch.leftField}:${p.patch.rightField}:${p.label}`;
|
||||||
|
if (seen.has(key)) return false;
|
||||||
|
seen.add(key);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyConditionPreset(
|
||||||
|
cond: ConditionDSL,
|
||||||
|
preset: ConditionPreset,
|
||||||
|
def: DefType,
|
||||||
|
): ConditionDSL {
|
||||||
|
const condType = preset.patch.conditionType ?? cond.conditionType;
|
||||||
|
const merged: ConditionDSL = {
|
||||||
|
...cond,
|
||||||
|
...preset.patch,
|
||||||
|
conditionType: condType,
|
||||||
|
thresholdOverride: false,
|
||||||
|
valuePeriodOverride: false,
|
||||||
|
rightPeriodOverride: false,
|
||||||
|
};
|
||||||
|
if (preset.patch.candleRangeMode === undefined) {
|
||||||
|
delete merged.candleRangeMode;
|
||||||
|
merged.candleRange = 1;
|
||||||
|
}
|
||||||
|
delete merged.targetValue;
|
||||||
|
|
||||||
|
const inherited = initConditionPeriodsInherit(cond.indicatorType, merged, def);
|
||||||
|
return applyCondTypeDefaults(inherited, condType, def);
|
||||||
|
}
|
||||||
@@ -832,7 +832,7 @@ const getDefaultConditionFields = (ind: string, signalType: 'buy'|'sell', DEF: D
|
|||||||
export { getDefaultConditionFields };
|
export { getDefaultConditionFields };
|
||||||
|
|
||||||
// conditionType 변경 시 자동 필드 조정
|
// conditionType 변경 시 자동 필드 조정
|
||||||
const applyCondTypeDefaults = (cond: ConditionDSL, newCondType: string, DEF: DefType): ConditionDSL => {
|
export const applyCondTypeDefaults = (cond: ConditionDSL, newCondType: string, DEF: DefType): ConditionDSL => {
|
||||||
const updated = { ...cond, conditionType: newCondType };
|
const updated = { ...cond, conditionType: newCondType };
|
||||||
if (cond.composite) {
|
if (cond.composite) {
|
||||||
return syncCompositeFields({
|
return syncCompositeFields({
|
||||||
|
|||||||
Reference in New Issue
Block a user