지표조건 프리셋 적용

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
+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 };
// 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({