지표선택 목록 추가
This commit is contained in:
@@ -7643,6 +7643,16 @@ html.desktop-client .tmb-logo-version {
|
|||||||
color: var(--text2);
|
color: var(--text2);
|
||||||
line-height: 1.35;
|
line-height: 1.35;
|
||||||
}
|
}
|
||||||
|
.se-cond-preset-section {
|
||||||
|
margin: 10px 0 4px;
|
||||||
|
font-size: 0.65rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
color: #a78bfa;
|
||||||
|
}
|
||||||
|
.se-cond-preset-section:first-of-type {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
.se-cond-preset-empty {
|
.se-cond-preset-empty {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 0.68rem;
|
font-size: 0.68rem;
|
||||||
|
|||||||
@@ -65,6 +65,21 @@ export default function ConditionPresetGraph({ type, size = 48 }: Props) {
|
|||||||
<path d="M4 18 L44 16" fill="none" stroke={line} strokeWidth="2" strokeLinecap="round" />
|
<path d="M4 18 L44 16" fill="none" stroke={line} strokeWidth="2" strokeLinecap="round" />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{(type === 'path_first_cross' || type === 'path_pullback_recross' || type === 'path_deep_recovery') && (
|
||||||
|
<>
|
||||||
|
<line x1="4" y1="8" x2="44" y2="8" stroke={thresh} strokeWidth="1.2" strokeDasharray="3 2" />
|
||||||
|
<line x1="4" y1="20" x2="44" y2="20" stroke={thresh} strokeWidth="1.2" strokeDasharray="3 2" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{type === 'path_first_cross' && (
|
||||||
|
<path d="M4 16 Q14 16 20 12 T34 6" fill="none" stroke={line} strokeWidth="2" strokeLinecap="round" />
|
||||||
|
)}
|
||||||
|
{type === 'path_pullback_recross' && (
|
||||||
|
<path d="M4 10 L14 8 L22 12 L30 10 L38 14 L44 6" fill="none" stroke={line} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
|
)}
|
||||||
|
{type === 'path_deep_recovery' && (
|
||||||
|
<path d="M4 12 L12 10 L18 22 L26 18 L34 10 L44 6" fill="none" stroke={line} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
|
)}
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +1,84 @@
|
|||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
import type { ConditionDSL } from '../../utils/strategyTypes';
|
import type { ConditionDSL, LogicNode } from '../../utils/strategyTypes';
|
||||||
import type { DefType } from '../../utils/strategyEditorShared';
|
import type { DefType } from '../../utils/strategyEditorShared';
|
||||||
import {
|
import {
|
||||||
applyConditionPreset,
|
applyConditionPreset,
|
||||||
|
applyCompoundConditionPreset,
|
||||||
getConditionPresetsForIndicator,
|
getConditionPresetsForIndicator,
|
||||||
type ConditionPreset,
|
type ConditionPreset,
|
||||||
} from '../../utils/conditionPresets';
|
} from '../../utils/conditionPresets';
|
||||||
|
import { isCompoundConditionPreset } from '../../utils/conditionPathPresets';
|
||||||
import ConditionPresetGraph from './ConditionPresetGraph';
|
import ConditionPresetGraph from './ConditionPresetGraph';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
condition: ConditionDSL;
|
condition: ConditionDSL;
|
||||||
def: DefType;
|
def: DefType;
|
||||||
signalType: 'buy' | 'sell';
|
signalType: 'buy' | 'sell';
|
||||||
|
nodeId: string;
|
||||||
onChange: (next: ConditionDSL) => void;
|
onChange: (next: ConditionDSL) => void;
|
||||||
|
onReplaceNode: (node: LogicNode) => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
popoverClassName?: string;
|
popoverClassName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function PresetSection({
|
||||||
|
title,
|
||||||
|
presets,
|
||||||
|
onPick,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
presets: ConditionPreset[];
|
||||||
|
onPick: (preset: ConditionPreset) => void;
|
||||||
|
}) {
|
||||||
|
if (presets.length === 0) return null;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p className="se-cond-preset-section">{title}</p>
|
||||||
|
<ul className="se-cond-preset-list">
|
||||||
|
{presets.map(preset => (
|
||||||
|
<li key={preset.id}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="se-cond-preset-item"
|
||||||
|
onClick={() => onPick(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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function ConditionPresetPicker({
|
export default function ConditionPresetPicker({
|
||||||
condition,
|
condition,
|
||||||
def,
|
def,
|
||||||
signalType,
|
signalType,
|
||||||
|
nodeId,
|
||||||
onChange,
|
onChange,
|
||||||
|
onReplaceNode,
|
||||||
onClose,
|
onClose,
|
||||||
popoverClassName = 'sp-node-preset-pop',
|
popoverClassName = 'sp-node-preset-pop',
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const popRef = useRef<HTMLDivElement>(null);
|
const popRef = useRef<HTMLDivElement>(null);
|
||||||
const presets = getConditionPresetsForIndicator(condition.indicatorType, def, signalType);
|
const presets = getConditionPresetsForIndicator(condition.indicatorType, def, signalType);
|
||||||
|
|
||||||
const sorted = [...presets].sort((a, b) => {
|
const sortBySignal = (list: ConditionPreset[]) =>
|
||||||
const aMatch = a.tags?.includes(signalType) ? 0 : 1;
|
[...list].sort((a, b) => {
|
||||||
const bMatch = b.tags?.includes(signalType) ? 0 : 1;
|
const aMatch = a.tags?.includes(signalType) ? 0 : 1;
|
||||||
return aMatch - bMatch;
|
const bMatch = b.tags?.includes(signalType) ? 0 : 1;
|
||||||
});
|
return aMatch - bMatch;
|
||||||
|
});
|
||||||
|
|
||||||
|
const basicPresets = sortBySignal(presets.filter(p => p.category !== 'path'));
|
||||||
|
const pathPresets = sortBySignal(presets.filter(p => p.category === 'path'));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onDoc = (e: MouseEvent) => {
|
const onDoc = (e: MouseEvent) => {
|
||||||
@@ -48,11 +94,15 @@ export default function ConditionPresetPicker({
|
|||||||
}, [onClose]);
|
}, [onClose]);
|
||||||
|
|
||||||
const handlePick = (preset: ConditionPreset) => {
|
const handlePick = (preset: ConditionPreset) => {
|
||||||
onChange(applyConditionPreset(condition, preset, def));
|
if (isCompoundConditionPreset(preset)) {
|
||||||
|
onReplaceNode(applyCompoundConditionPreset(nodeId, condition, preset, def));
|
||||||
|
} else {
|
||||||
|
onChange(applyConditionPreset(condition, preset, def));
|
||||||
|
}
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
if (sorted.length === 0) {
|
if (presets.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div ref={popRef} className={popoverClassName} role="dialog" aria-label="조건 선택">
|
<div ref={popRef} className={popoverClassName} role="dialog" aria-label="조건 선택">
|
||||||
<div className="se-cond-preset-head">
|
<div className="se-cond-preset-head">
|
||||||
@@ -71,25 +121,8 @@ export default function ConditionPresetPicker({
|
|||||||
<button type="button" className="se-flow-settings-close" onClick={onClose} aria-label="닫기">×</button>
|
<button type="button" className="se-flow-settings-close" onClick={onClose} aria-label="닫기">×</button>
|
||||||
</div>
|
</div>
|
||||||
<p className="se-cond-preset-hint">그래프 패턴을 선택하면 캔들 범위·조건대상·조건이 자동 설정됩니다.</p>
|
<p className="se-cond-preset-hint">그래프 패턴을 선택하면 캔들 범위·조건대상·조건이 자동 설정됩니다.</p>
|
||||||
<ul className="se-cond-preset-list">
|
<PresetSection title="기본 조건" presets={basicPresets} onPick={handlePick} />
|
||||||
{sorted.map(preset => (
|
<PresetSection title="상향 돌파 경로 (복합 · AND)" presets={pathPresets} onPick={handlePick} />
|
||||||
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -218,7 +218,9 @@ const TreeNodeComp: React.FC<TreeNodeProps> = ({
|
|||||||
condition={node.condition}
|
condition={node.condition}
|
||||||
def={def}
|
def={def}
|
||||||
signalType={signalType}
|
signalType={signalType}
|
||||||
|
nodeId={node.id}
|
||||||
onChange={handleCondChange}
|
onChange={handleCondChange}
|
||||||
|
onReplaceNode={onUpdate}
|
||||||
onClose={() => setPresetOpen(false)}
|
onClose={() => setPresetOpen(false)}
|
||||||
popoverClassName="sp-node-preset-pop"
|
popoverClassName="sp-node-preset-pop"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
/**
|
||||||
|
* 상향 돌파 경로 판별 — 복합(AND) 조건 프리셋
|
||||||
|
* @see 상향돌파 케이스별 판별.md
|
||||||
|
*/
|
||||||
|
import type { ConditionDSL } from './strategyTypes';
|
||||||
|
import type { DefType } from './strategyEditorShared';
|
||||||
|
import { getFieldOpts } from './strategyEditorShared';
|
||||||
|
import {
|
||||||
|
THRESHOLD_HL_MID,
|
||||||
|
THRESHOLD_HL_OVER,
|
||||||
|
THRESHOLD_HL_UNDER,
|
||||||
|
} from './thresholdSymbols';
|
||||||
|
import { thresholdField } from './conditionPeriods';
|
||||||
|
import type { ConditionPreset, ConditionPresetGraph } from './conditionPresets';
|
||||||
|
|
||||||
|
const DEFAULT_PATH_LOOKBACK = 30;
|
||||||
|
|
||||||
|
/** 상향 돌파 경로 판별 지원 지표 */
|
||||||
|
const PATH_ELIGIBLE = new Set([
|
||||||
|
'RSI', 'CCI', 'STOCHASTIC', 'WILLIAMS_R',
|
||||||
|
'PSYCHOLOGICAL', 'NEW_PSYCHOLOGICAL', 'INVEST_PSYCHOLOGICAL',
|
||||||
|
'VR', 'BWI', 'TRIX',
|
||||||
|
]);
|
||||||
|
|
||||||
|
type PathLineConfig = {
|
||||||
|
valueField: string;
|
||||||
|
targetField: string;
|
||||||
|
floorField: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function pathLines(indicatorType: string): PathLineConfig | null {
|
||||||
|
switch (indicatorType) {
|
||||||
|
case 'RSI':
|
||||||
|
return { valueField: 'RSI_VALUE', targetField: THRESHOLD_HL_MID, floorField: THRESHOLD_HL_UNDER };
|
||||||
|
case 'CCI':
|
||||||
|
return { valueField: 'CCI_VALUE', targetField: thresholdField(50), floorField: THRESHOLD_HL_MID };
|
||||||
|
case 'STOCHASTIC':
|
||||||
|
return { valueField: 'STOCH_K', targetField: THRESHOLD_HL_MID, floorField: THRESHOLD_HL_UNDER };
|
||||||
|
case 'WILLIAMS_R':
|
||||||
|
return { valueField: 'WILLIAMS_R_VALUE', targetField: THRESHOLD_HL_MID, floorField: THRESHOLD_HL_UNDER };
|
||||||
|
case 'PSYCHOLOGICAL':
|
||||||
|
case 'INVEST_PSYCHOLOGICAL':
|
||||||
|
return { valueField: 'PSY_VALUE', targetField: THRESHOLD_HL_MID, floorField: THRESHOLD_HL_UNDER };
|
||||||
|
case 'NEW_PSYCHOLOGICAL':
|
||||||
|
return { valueField: 'NEW_PSY_VALUE', targetField: THRESHOLD_HL_MID, floorField: THRESHOLD_HL_UNDER };
|
||||||
|
case 'VR':
|
||||||
|
return { valueField: 'VR_VALUE', targetField: THRESHOLD_HL_MID, floorField: THRESHOLD_HL_UNDER };
|
||||||
|
case 'BWI':
|
||||||
|
return { valueField: 'BWI_VALUE', targetField: THRESHOLD_HL_MID, floorField: THRESHOLD_HL_UNDER };
|
||||||
|
case 'TRIX':
|
||||||
|
return { valueField: 'TRIX_VALUE', targetField: THRESHOLD_HL_MID, floorField: THRESHOLD_HL_UNDER };
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fieldLabel(
|
||||||
|
indicatorType: string,
|
||||||
|
field: string,
|
||||||
|
def: DefType,
|
||||||
|
signalType: 'buy' | 'sell',
|
||||||
|
mock: ConditionDSL,
|
||||||
|
): string {
|
||||||
|
return getFieldOpts(indicatorType, signalType, def, mock).find(o => o.value === field)?.label ?? field;
|
||||||
|
}
|
||||||
|
|
||||||
|
function withLookback(patch: Partial<ConditionDSL>, n = DEFAULT_PATH_LOOKBACK): Partial<ConditionDSL> {
|
||||||
|
return { ...patch, lookbackPeriod: n };
|
||||||
|
}
|
||||||
|
|
||||||
|
function withExistsIn(patch: Partial<ConditionDSL>, n = DEFAULT_PATH_LOOKBACK): Partial<ConditionDSL> {
|
||||||
|
return { ...patch, candleRangeMode: 'EXISTS_IN', candleRange: n };
|
||||||
|
}
|
||||||
|
|
||||||
|
function withNotExistsIn(patch: Partial<ConditionDSL>, n = DEFAULT_PATH_LOOKBACK): Partial<ConditionDSL> {
|
||||||
|
return { ...patch, candleRangeMode: 'NOT_EXISTS_IN', candleRange: n };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ① 0(바닥선) 위에서 목표선 첫 상향 돌파 */
|
||||||
|
function buildPathFirstCross(cfg: PathLineConfig): Partial<ConditionDSL>[] {
|
||||||
|
const { valueField, targetField, floorField } = cfg;
|
||||||
|
return [
|
||||||
|
{ conditionType: 'CROSS_UP', leftField: valueField, rightField: targetField },
|
||||||
|
withLookback({ conditionType: 'LOWEST_GTE', leftField: valueField, rightField: floorField }),
|
||||||
|
withNotExistsIn({ conditionType: 'GT', leftField: valueField, rightField: targetField }),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ② 목표선 위→아래 조정(바닥선 유지) 후 목표선 재상향 돌파 */
|
||||||
|
function buildPathPullbackRecross(cfg: PathLineConfig): Partial<ConditionDSL>[] {
|
||||||
|
const { valueField, targetField, floorField } = cfg;
|
||||||
|
return [
|
||||||
|
{ conditionType: 'CROSS_UP', leftField: valueField, rightField: targetField },
|
||||||
|
withLookback({ conditionType: 'LOWEST_GTE', leftField: valueField, rightField: floorField }),
|
||||||
|
withExistsIn({ conditionType: 'GT', leftField: valueField, rightField: targetField }),
|
||||||
|
withExistsIn({ conditionType: 'LT', leftField: valueField, rightField: targetField }),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ③ 바닥선 이탈(침체) 후 변곡·목표선 상향 돌파 */
|
||||||
|
function buildPathDeepRecovery(cfg: PathLineConfig): Partial<ConditionDSL>[] {
|
||||||
|
const { valueField, targetField, floorField } = cfg;
|
||||||
|
return [
|
||||||
|
{ conditionType: 'CROSS_UP', leftField: valueField, rightField: targetField },
|
||||||
|
withLookback({ conditionType: 'LOWEST_LTE', leftField: valueField, rightField: floorField }),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function enrichPathPreset(
|
||||||
|
preset: ConditionPreset,
|
||||||
|
indicatorType: string,
|
||||||
|
def: DefType,
|
||||||
|
signalType: 'buy' | 'sell',
|
||||||
|
cfg: PathLineConfig,
|
||||||
|
): ConditionPreset {
|
||||||
|
const mockCross: ConditionDSL = {
|
||||||
|
indicatorType,
|
||||||
|
conditionType: 'CROSS_UP',
|
||||||
|
leftField: cfg.valueField,
|
||||||
|
rightField: cfg.targetField,
|
||||||
|
};
|
||||||
|
const left = fieldLabel(indicatorType, cfg.valueField, def, signalType, mockCross);
|
||||||
|
const target = fieldLabel(indicatorType, cfg.targetField, def, signalType, mockCross);
|
||||||
|
const floor = fieldLabel(indicatorType, cfg.floorField, def, signalType, {
|
||||||
|
...mockCross,
|
||||||
|
conditionType: 'LOWEST_GTE',
|
||||||
|
rightField: cfg.floorField,
|
||||||
|
});
|
||||||
|
const n = DEFAULT_PATH_LOOKBACK;
|
||||||
|
|
||||||
|
const descById: Record<string, { label: string; description: string }> = {
|
||||||
|
path_first_cross: {
|
||||||
|
label: `① ${target} 첫 상향 돌파 (${floor} 이상 유지)`,
|
||||||
|
description: `${left}이(가) 최근 ${n}봉 동안 ${floor} 아래로 내려간 적 없고, ${target} 위 체류 이력도 없을 때 ${target}을(를) 처음 상향 돌파 (AND ${preset.compound?.length ?? 3}조건)`,
|
||||||
|
},
|
||||||
|
path_pullback_recross: {
|
||||||
|
label: `② ${target} 되돌림 후 재상향 돌파`,
|
||||||
|
description: `${left}이(가) ${floor} 위를 유지한 채 ${target} 위·아래를 오간 뒤, 다시 ${target}을(를) 상향 돌파 (AND ${preset.compound?.length ?? 4}조건)`,
|
||||||
|
},
|
||||||
|
path_deep_recovery: {
|
||||||
|
label: `③ ${floor} 침체 후 ${target} 상향 돌파`,
|
||||||
|
description: `최근 ${n}봉 내 ${left}이(가) ${floor} 이하(침체) 구간을 거친 뒤, ${target}을(를) 상향 돌파하는 V자 반등 (AND ${preset.compound?.length ?? 2}조건)`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const extra = descById[preset.id];
|
||||||
|
if (!extra) return preset;
|
||||||
|
return { ...preset, label: extra.label, description: extra.description };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPathBreakoutPresets(
|
||||||
|
indicatorType: string,
|
||||||
|
def: DefType,
|
||||||
|
signalType: 'buy' | 'sell',
|
||||||
|
): ConditionPreset[] {
|
||||||
|
if (!PATH_ELIGIBLE.has(indicatorType)) return [];
|
||||||
|
const cfg = pathLines(indicatorType);
|
||||||
|
if (!cfg) return [];
|
||||||
|
|
||||||
|
const base = (id: string, graph: ConditionPresetGraph, compound: Partial<ConditionDSL>[]): ConditionPreset => ({
|
||||||
|
id,
|
||||||
|
label: id,
|
||||||
|
description: '',
|
||||||
|
graph,
|
||||||
|
patch: {},
|
||||||
|
category: 'path',
|
||||||
|
compound,
|
||||||
|
tags: ['buy'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const presets: ConditionPreset[] = [
|
||||||
|
base('path_first_cross', 'path_first_cross', buildPathFirstCross(cfg)),
|
||||||
|
base('path_pullback_recross', 'path_pullback_recross', buildPathPullbackRecross(cfg)),
|
||||||
|
base('path_deep_recovery', 'path_deep_recovery', buildPathDeepRecovery(cfg)),
|
||||||
|
];
|
||||||
|
|
||||||
|
return presets.map(p => enrichPathPreset(p, indicatorType, def, signalType, cfg));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isCompoundConditionPreset(preset: ConditionPreset): boolean {
|
||||||
|
return !!preset.compound && preset.compound.length > 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
/**
|
||||||
|
* 조건 프리셋 — 지표·기준선 값 반영 라벨/설명 생성
|
||||||
|
*/
|
||||||
|
import type { ConditionDSL } from './strategyTypes';
|
||||||
|
import { CONDITION_LABEL } from './strategyTypes';
|
||||||
|
import type { DefType } from './strategyEditorShared';
|
||||||
|
import { getFieldOpts } from './strategyEditorShared';
|
||||||
|
import type { ConditionPresetGraph } from './conditionPresets';
|
||||||
|
|
||||||
|
type PresetLike = {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
graph: ConditionPresetGraph;
|
||||||
|
patch: Partial<ConditionDSL>;
|
||||||
|
tags?: ('buy' | 'sell')[];
|
||||||
|
};
|
||||||
|
|
||||||
|
function resolveFieldLabel(
|
||||||
|
indicatorType: string,
|
||||||
|
field: string | undefined,
|
||||||
|
def: DefType,
|
||||||
|
signalType: 'buy' | 'sell',
|
||||||
|
mockCond: ConditionDSL,
|
||||||
|
): string {
|
||||||
|
if (!field || field === 'NONE') return '';
|
||||||
|
const opts = getFieldOpts(indicatorType, signalType, def, mockCond);
|
||||||
|
return opts.find(o => o.value === field)?.label ?? field;
|
||||||
|
}
|
||||||
|
|
||||||
|
function condVerb(condType: string): string {
|
||||||
|
switch (condType) {
|
||||||
|
case 'CROSS_UP': return '상향 돌파';
|
||||||
|
case 'CROSS_DOWN': return '하향 돌파';
|
||||||
|
case 'GT': return '초과';
|
||||||
|
case 'LT': return '미만';
|
||||||
|
case 'GTE': return '이상';
|
||||||
|
case 'LTE': return '이하';
|
||||||
|
case 'EQ': return '같음';
|
||||||
|
case 'NEQ': return '다름';
|
||||||
|
case 'SLOPE_UP': return '상승 기울기';
|
||||||
|
case 'SLOPE_DOWN': return '하락 기울기';
|
||||||
|
default: return CONDITION_LABEL[condType] ?? condType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildDescription(
|
||||||
|
left: string,
|
||||||
|
right: string,
|
||||||
|
condType: string,
|
||||||
|
slopePeriod?: number,
|
||||||
|
): string {
|
||||||
|
const sp = slopePeriod ?? 3;
|
||||||
|
switch (condType) {
|
||||||
|
case 'CROSS_UP':
|
||||||
|
if (left && right) {
|
||||||
|
return `${left}이(가) ${right}을(를) 아래에서 위로 돌파하는 순간 (직전 봉 ≤ 기준, 현재 봉 > 기준)`;
|
||||||
|
}
|
||||||
|
return '기준선을 아래에서 위로 돌파하는 순간';
|
||||||
|
case 'CROSS_DOWN':
|
||||||
|
if (left && right) {
|
||||||
|
return `${left}이(가) ${right}을(를) 위에서 아래로 이탈하는 순간 (직전 봉 ≥ 기준, 현재 봉 < 기준)`;
|
||||||
|
}
|
||||||
|
return '기준선을 위에서 아래로 이탈하는 순간';
|
||||||
|
case 'GT':
|
||||||
|
return left && right
|
||||||
|
? `${left}이(가) ${right}보다 큰 상태`
|
||||||
|
: '비교 대상보다 큰 상태';
|
||||||
|
case 'LT':
|
||||||
|
return left && right
|
||||||
|
? `${left}이(가) ${right}보다 작은 상태`
|
||||||
|
: '비교 대상보다 작은 상태';
|
||||||
|
case 'GTE':
|
||||||
|
return left && right
|
||||||
|
? `${left}이(가) ${right} 이상인 상태`
|
||||||
|
: '기준선 이상 유지';
|
||||||
|
case 'LTE':
|
||||||
|
return left && right
|
||||||
|
? `${left}이(가) ${right} 이하인 상태`
|
||||||
|
: '기준선 이하 유지';
|
||||||
|
case 'SLOPE_UP':
|
||||||
|
return left
|
||||||
|
? `${left}이(가) 최근 ${sp}봉 구간에서 상승 추세인 상태`
|
||||||
|
: `최근 ${sp}봉 구간 상승 추세`;
|
||||||
|
case 'SLOPE_DOWN':
|
||||||
|
return left
|
||||||
|
? `${left}이(가) 최근 ${sp}봉 구간에서 하락 추세인 상태`
|
||||||
|
: `최근 ${sp}봉 구간 하락 추세`;
|
||||||
|
case 'ABOVE_CLOUD':
|
||||||
|
return '종가가 일목균형표 구름대 위에 있는 상태';
|
||||||
|
case 'BELOW_CLOUD':
|
||||||
|
return '종가가 일목균형표 구름대 아래에 있는 상태';
|
||||||
|
case 'CLOUD_BREAK_UP':
|
||||||
|
return '종가가 구름 상단을 상향 돌파하는 순간';
|
||||||
|
case 'CLOUD_BREAK_DOWN':
|
||||||
|
return '종가가 구름 하단을 하향 이탈하는 순간';
|
||||||
|
default:
|
||||||
|
if (left && right) {
|
||||||
|
return `${left} — ${condVerb(condType)} — ${right}`;
|
||||||
|
}
|
||||||
|
return condVerb(condType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTitle(left: string, right: string, condType: string): string {
|
||||||
|
const verb = condVerb(condType);
|
||||||
|
if (condType === 'SLOPE_UP' || condType === 'SLOPE_DOWN') {
|
||||||
|
return left ? `${left} ${verb}` : verb;
|
||||||
|
}
|
||||||
|
if (['ABOVE_CLOUD', 'BELOW_CLOUD', 'CLOUD_BREAK_UP', 'CLOUD_BREAK_DOWN'].includes(condType)) {
|
||||||
|
return CONDITION_LABEL[condType] ?? verb;
|
||||||
|
}
|
||||||
|
if (left && right) {
|
||||||
|
return `${left} ${verb} ${right}`;
|
||||||
|
}
|
||||||
|
if (left) return `${left} ${verb}`;
|
||||||
|
if (right) return `${verb} ${right}`;
|
||||||
|
return verb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 프리셋 patch + DEF 기준으로 목록 제목·설명을 구체화 */
|
||||||
|
export function enrichConditionPresetLabels(
|
||||||
|
preset: PresetLike,
|
||||||
|
indicatorType: string,
|
||||||
|
def: DefType,
|
||||||
|
signalType: 'buy' | 'sell',
|
||||||
|
): PresetLike {
|
||||||
|
const patch = preset.patch;
|
||||||
|
const condType = patch.conditionType ?? 'GT';
|
||||||
|
|
||||||
|
if (['ABOVE_CLOUD', 'BELOW_CLOUD', 'CLOUD_BREAK_UP', 'CLOUD_BREAK_DOWN', 'IN_CLOUD'].includes(condType)) {
|
||||||
|
const label = CONDITION_LABEL[condType] ?? preset.label;
|
||||||
|
const description = buildDescription('', '', condType);
|
||||||
|
return { ...preset, label, description };
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockCond: ConditionDSL = {
|
||||||
|
indicatorType,
|
||||||
|
conditionType: condType,
|
||||||
|
leftField: patch.leftField,
|
||||||
|
rightField: patch.rightField,
|
||||||
|
};
|
||||||
|
|
||||||
|
const left = patch.leftField && patch.leftField !== 'NONE'
|
||||||
|
? resolveFieldLabel(indicatorType, patch.leftField, def, signalType, mockCond)
|
||||||
|
: '';
|
||||||
|
const right = patch.rightField && patch.rightField !== 'NONE'
|
||||||
|
? resolveFieldLabel(indicatorType, patch.rightField, def, signalType, mockCond)
|
||||||
|
: '';
|
||||||
|
|
||||||
|
if (!left && !right) {
|
||||||
|
return preset;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...preset,
|
||||||
|
label: buildTitle(left, right, condType),
|
||||||
|
description: buildDescription(left, right, condType, patch.slopePeriod),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -11,6 +11,10 @@ import {
|
|||||||
THRESHOLD_HL_UNDER,
|
THRESHOLD_HL_UNDER,
|
||||||
} from './thresholdSymbols';
|
} from './thresholdSymbols';
|
||||||
import { nhPriorField, nlPriorField } from './priceExtremeIndicators';
|
import { nhPriorField, nlPriorField } from './priceExtremeIndicators';
|
||||||
|
import { enrichConditionPresetLabels } from './conditionPresetLabels';
|
||||||
|
import { getPathBreakoutPresets, isCompoundConditionPreset } from './conditionPathPresets';
|
||||||
|
import { genId } from './strategyEditorShared';
|
||||||
|
import type { LogicNode } from './strategyTypes';
|
||||||
|
|
||||||
export type ConditionPresetGraph =
|
export type ConditionPresetGraph =
|
||||||
| 'cross_up'
|
| 'cross_up'
|
||||||
@@ -21,7 +25,12 @@ export type ConditionPresetGraph =
|
|||||||
| 'slope_down'
|
| 'slope_down'
|
||||||
| 'dual_cross_up'
|
| 'dual_cross_up'
|
||||||
| 'dual_cross_down'
|
| 'dual_cross_down'
|
||||||
| 'between';
|
| 'between'
|
||||||
|
| 'path_first_cross'
|
||||||
|
| 'path_pullback_recross'
|
||||||
|
| 'path_deep_recovery';
|
||||||
|
|
||||||
|
export type ConditionPresetCategory = 'basic' | 'path';
|
||||||
|
|
||||||
export type ConditionPreset = {
|
export type ConditionPreset = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -29,6 +38,9 @@ export type ConditionPreset = {
|
|||||||
description: string;
|
description: string;
|
||||||
graph: ConditionPresetGraph;
|
graph: ConditionPresetGraph;
|
||||||
patch: Partial<ConditionDSL>;
|
patch: Partial<ConditionDSL>;
|
||||||
|
/** 복합(AND) — 여러 조건을 한 번에 구성 */
|
||||||
|
compound?: Partial<ConditionDSL>[];
|
||||||
|
category?: ConditionPresetCategory;
|
||||||
tags?: ('buy' | 'sell')[];
|
tags?: ('buy' | 'sell')[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -75,7 +87,7 @@ function compare(
|
|||||||
|
|
||||||
function slope(up: boolean, left: string, tags?: ('buy' | 'sell')[]): ConditionPreset {
|
function slope(up: boolean, left: string, tags?: ('buy' | 'sell')[]): ConditionPreset {
|
||||||
return {
|
return {
|
||||||
id: up ? 'slope_up' : 'slope_down',
|
id: up ? `slope_up_${left}` : `slope_down_${left}`,
|
||||||
label: up ? '상승 기울기' : '하락 기울기',
|
label: up ? '상승 기울기' : '하락 기울기',
|
||||||
description: up ? '최근 구간 상승 추세' : '최근 구간 하락 추세',
|
description: up ? '최근 구간 상승 추세' : '최근 구간 하락 추세',
|
||||||
graph: up ? 'slope_up' : 'slope_down',
|
graph: up ? 'slope_up' : 'slope_down',
|
||||||
@@ -269,15 +281,25 @@ export function getConditionPresetsForIndicator(
|
|||||||
signalType: 'buy' | 'sell',
|
signalType: 'buy' | 'sell',
|
||||||
): ConditionPreset[] {
|
): ConditionPreset[] {
|
||||||
const builder = PRESET_BUILDERS[indicatorType];
|
const builder = PRESET_BUILDERS[indicatorType];
|
||||||
if (!builder) return [];
|
if (!builder) return getPathBreakoutPresets(indicatorType, def, signalType);
|
||||||
const all = builder(def, signalType);
|
const basic = builder(def, signalType);
|
||||||
|
const path = getPathBreakoutPresets(indicatorType, def, signalType);
|
||||||
|
const all = [...basic, ...path];
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
return all.filter(p => {
|
return all
|
||||||
const key = `${p.patch.conditionType}:${p.patch.leftField}:${p.patch.rightField}:${p.label}`;
|
.filter(p => {
|
||||||
if (seen.has(key)) return false;
|
if (isCompoundConditionPreset(p)) {
|
||||||
seen.add(key);
|
return !seen.has(p.id);
|
||||||
return true;
|
}
|
||||||
});
|
const key = `${p.patch.conditionType}:${p.patch.leftField}:${p.patch.rightField}`;
|
||||||
|
if (seen.has(key)) return false;
|
||||||
|
seen.add(key);
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.map(p => {
|
||||||
|
if (isCompoundConditionPreset(p)) return p;
|
||||||
|
return enrichConditionPresetLabels(p, indicatorType, def, signalType);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applyConditionPreset(
|
export function applyConditionPreset(
|
||||||
@@ -303,3 +325,32 @@ export function applyConditionPreset(
|
|||||||
const inherited = initConditionPeriodsInherit(cond.indicatorType, merged, def);
|
const inherited = initConditionPeriodsInherit(cond.indicatorType, merged, def);
|
||||||
return applyCondTypeDefaults(inherited, condType, def);
|
return applyCondTypeDefaults(inherited, condType, def);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 복합 프리셋 → AND 노드 (동일 id 유지) */
|
||||||
|
export function applyCompoundConditionPreset(
|
||||||
|
nodeId: string,
|
||||||
|
cond: ConditionDSL,
|
||||||
|
preset: ConditionPreset,
|
||||||
|
def: DefType,
|
||||||
|
): LogicNode {
|
||||||
|
const patches = preset.compound ?? [];
|
||||||
|
const children: LogicNode[] = patches.map(patch => {
|
||||||
|
const single: ConditionPreset = {
|
||||||
|
id: `${preset.id}_part`,
|
||||||
|
label: '',
|
||||||
|
description: '',
|
||||||
|
graph: preset.graph,
|
||||||
|
patch,
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
id: genId(),
|
||||||
|
type: 'CONDITION',
|
||||||
|
condition: applyConditionPreset(cond, single, def),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
id: nodeId,
|
||||||
|
type: 'AND',
|
||||||
|
children,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user