전략편집기 복합지표 기능 반영
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
/** 복합지표 — 동일 지표 2개(서로 다른 기간) 간 교차·비교 조건 */
|
||||
import type { ConditionDSL } from './strategyTypes';
|
||||
|
||||
export interface CompositePeriodDef {
|
||||
rsiPeriod: number;
|
||||
cciPeriod: number;
|
||||
adxPeriod: number;
|
||||
williamsR: number;
|
||||
trixPeriod: number;
|
||||
vrPeriod: number;
|
||||
newPsy: number;
|
||||
investPsy: number;
|
||||
dispShort: number;
|
||||
dispMid: number;
|
||||
}
|
||||
|
||||
export interface CompositeIndicatorItem {
|
||||
value: string;
|
||||
label: string;
|
||||
desc: string;
|
||||
}
|
||||
|
||||
/** 복합지표 팔레트 — 동일 지표 타입 2인스턴스 교차 */
|
||||
export const COMPOSITE_INDICATOR_ITEMS: CompositeIndicatorItem[] = [
|
||||
{ value: 'RSI', label: 'RSI', desc: 'RSI 기간 교차' },
|
||||
{ value: 'CCI', label: 'CCI', desc: 'CCI 기간 교차' },
|
||||
{ value: 'ADX', label: 'ADX', desc: 'ADX 기간 교차' },
|
||||
{ value: 'WILLIAMS_R', label: 'Williams %R', desc: 'Williams %R 교차' },
|
||||
{ value: 'TRIX', label: 'TRIX', desc: 'TRIX 기간 교차' },
|
||||
{ value: 'VR', label: 'VR', desc: 'VR 기간 교차' },
|
||||
{ value: 'PSYCHOLOGICAL', label: '심리도', desc: '심리도 기간 교차' },
|
||||
{ value: 'INVEST_PSYCHOLOGICAL', label: '투자심리도', desc: '투자심리도 교차' },
|
||||
{ value: 'DISPARITY', label: '이격도', desc: '이격도 기간 교차' },
|
||||
];
|
||||
|
||||
export function getCompositeDefaultPeriods(ind: string, def: CompositePeriodDef): { short: number; long: number } {
|
||||
switch (ind) {
|
||||
case 'RSI': return { short: def.rsiPeriod, long: Math.max(def.rsiPeriod * 2, 20) };
|
||||
case 'CCI': return { short: def.cciPeriod, long: Math.max(def.cciPeriod * 2, 26) };
|
||||
case 'ADX': return { short: def.adxPeriod, long: Math.max(def.adxPeriod * 2, 28) };
|
||||
case 'WILLIAMS_R': return { short: def.williamsR, long: Math.max(def.williamsR * 2, 28) };
|
||||
case 'TRIX': return { short: def.trixPeriod, long: Math.max(def.trixPeriod * 2, 24) };
|
||||
case 'VR': return { short: def.vrPeriod, long: Math.max(def.vrPeriod * 2, 20) };
|
||||
case 'PSYCHOLOGICAL':
|
||||
case 'NEW_PSYCHOLOGICAL': return { short: def.newPsy, long: Math.max(def.newPsy * 2, 24) };
|
||||
case 'INVEST_PSYCHOLOGICAL': return { short: def.investPsy, long: Math.max(def.investPsy * 2, 20) };
|
||||
case 'DISPARITY': return { short: def.dispShort, long: def.dispMid };
|
||||
default: return { short: 9, long: 20 };
|
||||
}
|
||||
}
|
||||
|
||||
export function compositeValueField(ind: string, period: number): string {
|
||||
switch (ind) {
|
||||
case 'RSI': return `RSI_VALUE_${period}`;
|
||||
case 'CCI': return `CCI_VALUE_${period}`;
|
||||
case 'ADX': return `ADX_VALUE_${period}`;
|
||||
case 'WILLIAMS_R': return `WILLIAMS_R_VALUE_${period}`;
|
||||
case 'TRIX': return `TRIX_VALUE_${period}`;
|
||||
case 'VR': return `VR_VALUE_${period}`;
|
||||
case 'PSYCHOLOGICAL':
|
||||
case 'NEW_PSYCHOLOGICAL': return `PSY_VALUE_${period}`;
|
||||
case 'INVEST_PSYCHOLOGICAL': return `INVEST_PSY_VALUE_${period}`;
|
||||
case 'DISPARITY': return `DISPARITY${period}`;
|
||||
default: return `${ind}_VALUE_${period}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function parsePeriodFromCompositeField(ind: string, field?: string): number | null {
|
||||
if (!field) return null;
|
||||
if (ind === 'DISPARITY' && field.startsWith('DISPARITY')) {
|
||||
const n = parseInt(field.slice('DISPARITY'.length), 10);
|
||||
return Number.isFinite(n) && n > 0 ? n : null;
|
||||
}
|
||||
const suffix = field.match(/_(\d+)$/);
|
||||
if (!suffix) return null;
|
||||
const n = parseInt(suffix[1], 10);
|
||||
return Number.isFinite(n) && n > 0 ? n : null;
|
||||
}
|
||||
|
||||
export function syncCompositeFields(cond: ConditionDSL): ConditionDSL {
|
||||
if (!cond.composite) return cond;
|
||||
const leftP = cond.leftPeriod
|
||||
?? parsePeriodFromCompositeField(cond.indicatorType, cond.leftField)
|
||||
?? undefined;
|
||||
const rightP = cond.rightPeriod
|
||||
?? parsePeriodFromCompositeField(cond.indicatorType, cond.rightField)
|
||||
?? undefined;
|
||||
if (leftP == null || rightP == null) {
|
||||
return {
|
||||
...cond,
|
||||
composite: true,
|
||||
...(leftP != null ? { leftPeriod: leftP } : null),
|
||||
...(rightP != null ? { rightPeriod: rightP } : null),
|
||||
};
|
||||
}
|
||||
return {
|
||||
...cond,
|
||||
composite: true,
|
||||
leftPeriod: leftP,
|
||||
rightPeriod: rightP,
|
||||
leftField: compositeValueField(cond.indicatorType, leftP),
|
||||
rightField: compositeValueField(cond.indicatorType, rightP),
|
||||
period: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeCompositeCondition(cond: ConditionDSL): ConditionDSL {
|
||||
if (cond.composite) return syncCompositeFields(cond);
|
||||
const leftP = parsePeriodFromCompositeField(cond.indicatorType, cond.leftField);
|
||||
const rightP = parsePeriodFromCompositeField(cond.indicatorType, cond.rightField);
|
||||
if (leftP && rightP && leftP !== rightP) {
|
||||
return syncCompositeFields({
|
||||
...cond,
|
||||
composite: true,
|
||||
leftPeriod: leftP,
|
||||
rightPeriod: rightP,
|
||||
});
|
||||
}
|
||||
return cond;
|
||||
}
|
||||
|
||||
export function makeCompositeCondition(
|
||||
ind: string,
|
||||
signalType: 'buy' | 'sell',
|
||||
def: CompositePeriodDef,
|
||||
): ConditionDSL {
|
||||
const { short, long } = getCompositeDefaultPeriods(ind, def);
|
||||
return syncCompositeFields({
|
||||
indicatorType: ind,
|
||||
conditionType: signalType === 'buy' ? 'CROSS_UP' : 'CROSS_DOWN',
|
||||
composite: true,
|
||||
leftPeriod: short,
|
||||
rightPeriod: long,
|
||||
candleRange: 1,
|
||||
});
|
||||
}
|
||||
|
||||
export function compositePeriodLabel(ind: string, def: CompositePeriodDef): string {
|
||||
const { short, long } = getCompositeDefaultPeriods(ind, def);
|
||||
return `${short} / ${long}`;
|
||||
}
|
||||
|
||||
export function compositeDisplayName(ind: string): string {
|
||||
const item = COMPOSITE_INDICATOR_ITEMS.find(i => i.value === ind);
|
||||
if (item) return `${item.label} + ${item.label}`;
|
||||
return `${ind} + ${ind}`;
|
||||
}
|
||||
|
||||
export function compositeElementLabel(ind: string, slot: 1 | 2): string {
|
||||
const item = COMPOSITE_INDICATOR_ITEMS.find(i => i.value === ind);
|
||||
const name = item?.label ?? ind;
|
||||
return `${name} ${slot}`;
|
||||
}
|
||||
Reference in New Issue
Block a user