132 lines
4.8 KiB
TypeScript
132 lines
4.8 KiB
TypeScript
/** 신고가·신저가 — 단일(보조) 지표: 종가/고가 vs 직전 N봉 최고·최저 */
|
|
import type { ConditionDSL } from './strategyTypes';
|
|
import { parseDonchianPeriod } from './compositeIndicators';
|
|
|
|
export function isPriceExtremeIndicator(ind?: string): boolean {
|
|
return ind === 'NEW_HIGH' || ind === 'NEW_LOW';
|
|
}
|
|
|
|
export function nhPriorField(period: number): string {
|
|
return `NH_PRIOR_${Math.max(1, Math.round(period))}`;
|
|
}
|
|
|
|
export function nlPriorField(period: number): string {
|
|
return `NL_PRIOR_${Math.max(1, Math.round(period))}`;
|
|
}
|
|
|
|
export function parsePriorExtremePeriod(field?: string): number | null {
|
|
if (!field) return null;
|
|
const m = field.match(/^(?:NH|NL)_PRIOR_(\d+)$/);
|
|
if (m) {
|
|
const n = parseInt(m[1], 10);
|
|
return Number.isFinite(n) && n > 0 ? n : null;
|
|
}
|
|
return parseDonchianPeriod(field);
|
|
}
|
|
|
|
export function priorExtremeField(ind: string, period: number): string {
|
|
return ind === 'NEW_LOW' ? nlPriorField(period) : nhPriorField(period);
|
|
}
|
|
|
|
/** 단일 지표 — 기간에 맞춰 직전 N봉 신고가/신저가선 필드 동기화 */
|
|
export function syncPriceExtremeSimpleFields(cond: ConditionDSL): ConditionDSL {
|
|
if (!isPriceExtremeIndicator(cond.indicatorType)) return cond;
|
|
const period = cond.period
|
|
?? parsePriorExtremePeriod(cond.rightField)
|
|
?? parsePriorExtremePeriod(cond.leftField)
|
|
?? 9;
|
|
const p = Math.max(1, Math.min(500, Math.round(period)));
|
|
const band = priorExtremeField(cond.indicatorType, p);
|
|
const priceFields = new Set(['CLOSE_PRICE', 'HIGH_PRICE', 'LOW_PRICE', 'OPEN_PRICE']);
|
|
let leftField = cond.leftField;
|
|
let rightField = cond.rightField;
|
|
if (cond.indicatorType === 'NEW_HIGH') {
|
|
leftField = leftField && priceFields.has(leftField) ? leftField : 'CLOSE_PRICE';
|
|
rightField = band;
|
|
} else {
|
|
leftField = leftField && priceFields.has(leftField) ? leftField : 'CLOSE_PRICE';
|
|
rightField = band;
|
|
}
|
|
return {
|
|
...cond,
|
|
composite: false,
|
|
period: p,
|
|
leftPeriod: undefined,
|
|
rightPeriod: undefined,
|
|
leftCandleType: undefined,
|
|
rightCandleType: undefined,
|
|
leftField,
|
|
rightField,
|
|
};
|
|
}
|
|
|
|
/** 저장 DSL — 복합지표 승격·DC_UPPER_N 필드를 단일 신고가/신저가로 정규화 */
|
|
export function migratePriceExtremeCondition(cond: ConditionDSL): ConditionDSL {
|
|
const ind = cond.indicatorType;
|
|
if (!isPriceExtremeIndicator(ind)) return cond;
|
|
|
|
if (cond.composite && isPriceExtremeIndicator(ind)) {
|
|
const period = cond.leftPeriod ?? cond.rightPeriod ?? 9;
|
|
const priceFields = new Set(['CLOSE_PRICE', 'HIGH_PRICE', 'LOW_PRICE', 'OPEN_PRICE']);
|
|
const leftField = cond.leftField && priceFields.has(cond.leftField)
|
|
? cond.leftField
|
|
: 'CLOSE_PRICE';
|
|
const rightField = priorExtremeField(ind, period);
|
|
let conditionType = cond.conditionType;
|
|
if (conditionType === 'CROSS_UP' && ind === 'NEW_HIGH' && !rightField.startsWith('NH_PRIOR_')) {
|
|
conditionType = 'GTE';
|
|
} else if (conditionType === 'CROSS_DOWN' && ind === 'NEW_LOW' && !rightField.startsWith('NL_PRIOR_')) {
|
|
conditionType = 'LTE';
|
|
}
|
|
return syncPriceExtremeSimpleFields({
|
|
...cond,
|
|
composite: false,
|
|
leftField,
|
|
rightField,
|
|
period,
|
|
conditionType,
|
|
});
|
|
}
|
|
|
|
if (!isPriceExtremeIndicator(ind)) return cond;
|
|
|
|
const fromRight = parsePriorExtremePeriod(cond.rightField);
|
|
const fromLeft = parsePriorExtremePeriod(cond.leftField);
|
|
const period = cond.period ?? fromRight ?? fromLeft ?? 9;
|
|
let migrated = syncPriceExtremeSimpleFields({ ...cond, period });
|
|
|
|
if (migrated.rightField?.startsWith('DC_UPPER_') || migrated.rightField?.startsWith('DC_LOWER_')) {
|
|
migrated = syncPriceExtremeSimpleFields({
|
|
...migrated,
|
|
period: parseDonchianPeriod(migrated.rightField) ?? period,
|
|
});
|
|
}
|
|
|
|
if (migrated.conditionType === 'CROSS_UP' && ind === 'NEW_HIGH'
|
|
&& !migrated.rightField?.startsWith('NH_PRIOR_')) {
|
|
migrated = { ...migrated, conditionType: 'GTE' };
|
|
}
|
|
if (migrated.conditionType === 'CROSS_DOWN' && ind === 'NEW_LOW'
|
|
&& !migrated.rightField?.startsWith('NL_PRIOR_')) {
|
|
migrated = { ...migrated, conditionType: 'LTE' };
|
|
}
|
|
|
|
// 종가 vs NH/NL_PRIOR — GTE/LTE 유지형은 봉마다 시그널 → CROSS 돌파 1봉으로 정규화
|
|
if (ind === 'NEW_HIGH' && migrated.leftField === 'CLOSE_PRICE'
|
|
&& migrated.conditionType === 'GTE'
|
|
&& migrated.rightField?.startsWith('NH_PRIOR_')) {
|
|
migrated = { ...migrated, conditionType: 'CROSS_UP' };
|
|
}
|
|
if (ind === 'NEW_LOW' && migrated.leftField === 'CLOSE_PRICE'
|
|
&& migrated.conditionType === 'LTE'
|
|
&& migrated.rightField?.startsWith('NL_PRIOR_')) {
|
|
migrated = { ...migrated, conditionType: 'CROSS_DOWN' };
|
|
}
|
|
|
|
return migrated;
|
|
}
|
|
|
|
export function priceExtremePeriodLabel(ind: string, period: number): string {
|
|
return ind === 'NEW_LOW' ? `${period}일 신저가선` : `${period}일 신고가선`;
|
|
}
|