지표조건 프리셋 적용
This commit is contained in:
@@ -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