전략편집기 복합지표 기능 반영
This commit is contained in:
@@ -0,0 +1,219 @@
|
||||
/** 조건 노드별 지표 기간·임계값 — 차트 DEF 기본값 대비 노드 단위 오버라이드 */
|
||||
import type { ConditionDSL } from './strategyTypes';
|
||||
import { getCompositeDefaultPeriods, parsePeriodFromCompositeField, syncCompositeFields, type CompositePeriodDef } from './compositeIndicators';
|
||||
|
||||
export interface IndicatorPeriodDef {
|
||||
rsiPeriod: number;
|
||||
cciPeriod: number;
|
||||
adxPeriod: number;
|
||||
williamsR: number;
|
||||
trixPeriod: number;
|
||||
vrPeriod: number;
|
||||
newPsy: number;
|
||||
investPsy: number;
|
||||
}
|
||||
|
||||
const VALUE_FIELD_PREFIX: Record<string, string> = {
|
||||
RSI: 'RSI_VALUE',
|
||||
CCI: 'CCI_VALUE',
|
||||
ADX: 'ADX_VALUE',
|
||||
WILLIAMS_R: 'WILLIAMS_R_VALUE',
|
||||
TRIX: 'TRIX_VALUE',
|
||||
VR: 'VR_VALUE',
|
||||
PSYCHOLOGICAL: 'PSY_VALUE',
|
||||
NEW_PSYCHOLOGICAL: 'PSY_VALUE',
|
||||
INVEST_PSYCHOLOGICAL: 'INVEST_PSY_VALUE',
|
||||
};
|
||||
|
||||
export function getDefaultIndicatorPeriod(indicatorType: string, def: IndicatorPeriodDef): number {
|
||||
switch (indicatorType) {
|
||||
case 'RSI': return def.rsiPeriod;
|
||||
case 'CCI': return def.cciPeriod;
|
||||
case 'ADX': return def.adxPeriod;
|
||||
case 'WILLIAMS_R': return def.williamsR;
|
||||
case 'TRIX': return def.trixPeriod;
|
||||
case 'VR': return def.vrPeriod;
|
||||
case 'PSYCHOLOGICAL':
|
||||
case 'NEW_PSYCHOLOGICAL': return def.newPsy;
|
||||
case 'INVEST_PSYCHOLOGICAL': return def.investPsy;
|
||||
default: return 14;
|
||||
}
|
||||
}
|
||||
|
||||
function parseFieldPeriod(field?: string): number | null {
|
||||
if (!field) return null;
|
||||
const m = field.match(/_(\d+)$/);
|
||||
if (!m) return null;
|
||||
const n = parseInt(m[1], 10);
|
||||
return Number.isFinite(n) && n > 0 ? n : null;
|
||||
}
|
||||
|
||||
/** 조건 노드의 RSI 등 값(좌측) 기간 */
|
||||
export function getConditionValuePeriod(cond: ConditionDSL, def: IndicatorPeriodDef): number {
|
||||
if (cond.composite) {
|
||||
if (cond.leftPeriod && cond.leftPeriod > 0) return cond.leftPeriod;
|
||||
const fromField = parsePeriodFromCompositeField(cond.indicatorType, cond.leftField);
|
||||
if (fromField) return fromField;
|
||||
}
|
||||
if (cond.period && cond.period > 0) return cond.period;
|
||||
const fromField = parseFieldPeriod(cond.leftField);
|
||||
if (fromField) return fromField;
|
||||
return getDefaultIndicatorPeriod(cond.indicatorType, def);
|
||||
}
|
||||
|
||||
export function getConditionRightPeriod(cond: ConditionDSL, def: IndicatorPeriodDef): number {
|
||||
if (cond.composite) {
|
||||
if (cond.rightPeriod && cond.rightPeriod > 0) return cond.rightPeriod;
|
||||
const fromField = parsePeriodFromCompositeField(cond.indicatorType, cond.rightField);
|
||||
if (fromField) return fromField;
|
||||
}
|
||||
const fromField = parseFieldPeriod(cond.rightField);
|
||||
if (fromField) return fromField;
|
||||
return getDefaultIndicatorPeriod(cond.indicatorType, def);
|
||||
}
|
||||
|
||||
export function usesValuePeriodField(cond: ConditionDSL): boolean {
|
||||
if (cond.composite) return true;
|
||||
const prefix = VALUE_FIELD_PREFIX[cond.indicatorType];
|
||||
if (!prefix) return false;
|
||||
const lf = cond.leftField ?? '';
|
||||
return lf === prefix || lf.startsWith(`${prefix}_`);
|
||||
}
|
||||
|
||||
export function parseThresholdField(field?: string): number | null {
|
||||
if (!field?.startsWith('K_')) return null;
|
||||
const n = parseFloat(field.slice(2));
|
||||
return Number.isFinite(n) ? n : null;
|
||||
}
|
||||
|
||||
export function thresholdField(value: number): string {
|
||||
return `K_${value}`;
|
||||
}
|
||||
|
||||
export function hasEditableThreshold(cond: ConditionDSL): boolean {
|
||||
return parseThresholdField(cond.rightField) != null
|
||||
|| parseThresholdField(cond.leftField) != null;
|
||||
}
|
||||
|
||||
export function getConditionThreshold(cond: ConditionDSL, side: 'left' | 'right'): number | null {
|
||||
const field = side === 'left' ? cond.leftField : cond.rightField;
|
||||
return parseThresholdField(field);
|
||||
}
|
||||
|
||||
export function setConditionValuePeriod(
|
||||
cond: ConditionDSL,
|
||||
period: number,
|
||||
_def: IndicatorPeriodDef,
|
||||
): ConditionDSL {
|
||||
const p = Math.max(1, Math.min(500, Math.round(period)));
|
||||
if (cond.composite) {
|
||||
return syncCompositeFields({ ...cond, composite: true, leftPeriod: p });
|
||||
}
|
||||
const prefix = VALUE_FIELD_PREFIX[cond.indicatorType];
|
||||
const next: ConditionDSL = { ...cond, period: p };
|
||||
if (prefix && (cond.leftField === prefix || cond.leftField?.startsWith(`${prefix}_`))) {
|
||||
next.leftField = `${prefix}_${p}`;
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
export function setConditionRightPeriod(cond: ConditionDSL, period: number): ConditionDSL {
|
||||
const p = Math.max(1, Math.min(500, Math.round(period)));
|
||||
if (!cond.composite) return cond;
|
||||
return syncCompositeFields({ ...cond, composite: true, rightPeriod: p });
|
||||
}
|
||||
|
||||
export function setConditionThreshold(
|
||||
cond: ConditionDSL,
|
||||
side: 'left' | 'right',
|
||||
value: number,
|
||||
): ConditionDSL {
|
||||
const fieldKey = side === 'left' ? 'leftField' : 'rightField';
|
||||
const current = cond[fieldKey];
|
||||
if (!current?.startsWith('K_')) return cond;
|
||||
return {
|
||||
...cond,
|
||||
[fieldKey]: thresholdField(value),
|
||||
targetValue: value,
|
||||
};
|
||||
}
|
||||
|
||||
export function initConditionPeriods(
|
||||
indicatorType: string,
|
||||
condition: ConditionDSL,
|
||||
def: IndicatorPeriodDef,
|
||||
): ConditionDSL {
|
||||
if (condition.composite) return condition;
|
||||
const prefix = VALUE_FIELD_PREFIX[indicatorType];
|
||||
if (!prefix) return condition;
|
||||
const period = getDefaultIndicatorPeriod(indicatorType, def);
|
||||
const lf = condition.leftField ?? '';
|
||||
if (lf === prefix || lf.startsWith(`${prefix}_`)) {
|
||||
return { ...condition, period: condition.period ?? period, leftField: `${prefix}_${condition.period ?? period}` };
|
||||
}
|
||||
return { ...condition, period: condition.period ?? period };
|
||||
}
|
||||
|
||||
export function hasNodeSettings(cond: ConditionDSL): boolean {
|
||||
return usesValuePeriodField(cond)
|
||||
|| hasEditableThreshold(cond)
|
||||
|| !!cond.composite;
|
||||
}
|
||||
|
||||
const COMMON_PERIODS = [5, 7, 9, 10, 12, 13, 14, 20, 21, 26, 28, 50, 60, 120];
|
||||
|
||||
export function getPeriodPresetOptions(indicatorType: string, def: IndicatorPeriodDef): number[] {
|
||||
const base = getDefaultIndicatorPeriod(indicatorType, def);
|
||||
return [...new Set([base, ...COMMON_PERIODS])].sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
/** 복합지표 요소1(단기)·요소2(장기) 기간 프리셋 */
|
||||
export function getCompositePeriodPresetOptions(
|
||||
indicatorType: string,
|
||||
def: IndicatorPeriodDef,
|
||||
side: 'left' | 'right',
|
||||
): number[] {
|
||||
const { short, long } = getCompositeDefaultPeriods(indicatorType, def as CompositePeriodDef);
|
||||
const base = side === 'left' ? short : long;
|
||||
return [...new Set([base, short, long, ...COMMON_PERIODS])].sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
export function getThresholdPresetOptions(indicatorType: string): number[] {
|
||||
switch (indicatorType) {
|
||||
case 'RSI':
|
||||
case 'PSYCHOLOGICAL':
|
||||
case 'NEW_PSYCHOLOGICAL':
|
||||
case 'INVEST_PSYCHOLOGICAL':
|
||||
return [20, 25, 30, 50, 70, 75, 80];
|
||||
case 'STOCHASTIC':
|
||||
return [20, 50, 80];
|
||||
case 'CCI':
|
||||
return [-200, -100, -50, 0, 50, 100, 150, 200];
|
||||
case 'WILLIAMS_R':
|
||||
return [-80, -50, -20];
|
||||
case 'ADX':
|
||||
case 'DMI':
|
||||
return [20, 25, 30, 40, 50];
|
||||
case 'VR':
|
||||
return [70, 100, 150, 200, 250];
|
||||
default:
|
||||
return [-100, -50, 0, 50, 100];
|
||||
}
|
||||
}
|
||||
|
||||
export function getThresholdBounds(indicatorType: string): { min: number; max: number } {
|
||||
switch (indicatorType) {
|
||||
case 'RSI':
|
||||
case 'PSYCHOLOGICAL':
|
||||
case 'NEW_PSYCHOLOGICAL':
|
||||
case 'INVEST_PSYCHOLOGICAL':
|
||||
case 'STOCHASTIC':
|
||||
return { min: 0, max: 100 };
|
||||
case 'WILLIAMS_R':
|
||||
return { min: -100, max: 0 };
|
||||
case 'CCI':
|
||||
return { min: -500, max: 500 };
|
||||
default:
|
||||
return { min: -1000, max: 1000 };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user