지표선택 목록 추가
This commit is contained in:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user