지표조건 프리셋 적용
This commit is contained in:
@@ -7594,6 +7594,112 @@ html.desktop-client .tmb-logo-version {
|
||||
.sp-node-settings:hover {
|
||||
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 {
|
||||
position: absolute;
|
||||
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,
|
||||
} 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}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
// 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 };
|
||||
if (cond.composite) {
|
||||
return syncCompositeFields({
|
||||
|
||||
Reference in New Issue
Block a user