지표조건 프리셋 적용

This commit is contained in:
Macbook
2026-06-16 10:02:03 +09:00
parent f85c0bef20
commit 7440453bb3
6 changed files with 610 additions and 1 deletions
+106
View File
@@ -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}
+305
View File
@@ -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);
}
+1 -1
View File
@@ -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({