가상투자 종목카드박스 레이아웃 수정
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
/** 복합지표 — 동일 지표 2개(서로 다른 기간) 간 교차·비교 조건 */
|
||||
import type { ConditionDSL } from './strategyTypes';
|
||||
import { DEFAULT_START_CANDLE, normalizeStartCandleType } from './strategyStartNodes';
|
||||
import { migratePriceExtremeCondition } from './priceExtremeIndicators';
|
||||
|
||||
export interface CompositePeriodDef {
|
||||
rsiPeriod: number;
|
||||
@@ -21,8 +22,40 @@ export interface CompositeIndicatorItem {
|
||||
desc: string;
|
||||
}
|
||||
|
||||
/** 복합지표 팔레트 — 동일 지표 타입 2인스턴스 교차 */
|
||||
/** 돈치안 채널 — 단기·장기 고가/저가선 비교 */
|
||||
export function isDonchianComposite(ind: string): boolean {
|
||||
return ind === 'DONCHIAN';
|
||||
}
|
||||
|
||||
/** @deprecated DONCHIAN 전용 — isDonchianComposite 사용 */
|
||||
export function isPriceExtremeComposite(ind: string): boolean {
|
||||
return isDonchianComposite(ind);
|
||||
}
|
||||
|
||||
export function donchianUpperField(period: number): string {
|
||||
return `DC_UPPER_${period}`;
|
||||
}
|
||||
|
||||
export function donchianLowerField(period: number): string {
|
||||
return `DC_LOWER_${period}`;
|
||||
}
|
||||
|
||||
export function donchianMiddleField(period: number): string {
|
||||
return `DC_MIDDLE_${period}`;
|
||||
}
|
||||
|
||||
/** DC_UPPER_9 / DC_LOWER_20 등에서 기간 추출 */
|
||||
export function parseDonchianPeriod(field?: string): number | null {
|
||||
if (!field) return null;
|
||||
const m = field.match(/^DC_(?:UPPER|LOWER|MIDDLE)_(\d+)$/);
|
||||
if (!m) return null;
|
||||
const n = parseInt(m[1], 10);
|
||||
return Number.isFinite(n) && n > 0 ? n : null;
|
||||
}
|
||||
|
||||
/** 복합지표 팔레트 */
|
||||
export const COMPOSITE_INDICATOR_ITEMS: CompositeIndicatorItem[] = [
|
||||
{ value: 'DONCHIAN', label: '돈치안 채널', desc: '단기·장기 고가/저가 채널 비교 (9·20일)' },
|
||||
{ value: 'RSI', label: 'RSI', desc: 'RSI 기간 교차' },
|
||||
{ value: 'CCI', label: 'CCI', desc: 'CCI 기간 교차' },
|
||||
{ value: 'ADX', label: 'ADX', desc: 'ADX 기간 교차' },
|
||||
@@ -36,6 +69,8 @@ export const COMPOSITE_INDICATOR_ITEMS: CompositeIndicatorItem[] = [
|
||||
|
||||
export function getCompositeDefaultPeriods(ind: string, def: CompositePeriodDef): { short: number; long: number } {
|
||||
switch (ind) {
|
||||
case 'DONCHIAN':
|
||||
return { short: 9, long: 20 };
|
||||
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) };
|
||||
@@ -52,6 +87,8 @@ export function getCompositeDefaultPeriods(ind: string, def: CompositePeriodDef)
|
||||
|
||||
export function compositeValueField(ind: string, period: number): string {
|
||||
switch (ind) {
|
||||
case 'DONCHIAN':
|
||||
return donchianUpperField(period);
|
||||
case 'RSI': return `RSI_VALUE_${period}`;
|
||||
case 'CCI': return `CCI_VALUE_${period}`;
|
||||
case 'ADX': return `ADX_VALUE_${period}`;
|
||||
@@ -86,6 +123,9 @@ const COMPOSITE_VALUE_PREFIX: Record<string, string> = {
|
||||
/** 복합지표용 값 라인 필드(CCI_VALUE_9 등)인지 — K_100 임계값은 false */
|
||||
export function isCompositeValueField(ind: string, field?: string): boolean {
|
||||
if (!field || isThresholdField(field)) return false;
|
||||
if (isDonchianComposite(ind)) {
|
||||
return parseDonchianPeriod(field) != null;
|
||||
}
|
||||
if (ind === 'DISPARITY') {
|
||||
return field.startsWith('DISPARITY') && field.length > 'DISPARITY'.length
|
||||
&& /^\d+$/.test(field.slice('DISPARITY'.length));
|
||||
@@ -97,6 +137,9 @@ export function isCompositeValueField(ind: string, field?: string): boolean {
|
||||
|
||||
export function parsePeriodFromCompositeField(ind: string, field?: string): number | null {
|
||||
if (!field || isThresholdField(field)) return null;
|
||||
if (isDonchianComposite(ind)) {
|
||||
return parseDonchianPeriod(field);
|
||||
}
|
||||
if (ind === 'DISPARITY' && field.startsWith('DISPARITY')) {
|
||||
const n = parseInt(field.slice('DISPARITY'.length), 10);
|
||||
return Number.isFinite(n) && n > 0 ? n : null;
|
||||
@@ -107,8 +150,32 @@ export function parsePeriodFromCompositeField(ind: string, field?: string): numb
|
||||
return Number.isFinite(n) && n > 0 ? n : null;
|
||||
}
|
||||
|
||||
/** 돈치안 — 단기·장기 고가/저가선 비교 */
|
||||
export function syncDonchianCompositeFields(cond: ConditionDSL): ConditionDSL {
|
||||
const leftP = cond.leftPeriod ?? 9;
|
||||
const rightP = cond.rightPeriod ?? 20;
|
||||
const leftCt = normalizeStartCandleType(cond.leftCandleType ?? DEFAULT_START_CANDLE);
|
||||
const rightCt = normalizeStartCandleType(cond.rightCandleType ?? DEFAULT_START_CANDLE);
|
||||
const defaultBand = (period: number) => donchianUpperField(period);
|
||||
|
||||
return {
|
||||
...cond,
|
||||
composite: true,
|
||||
leftPeriod: leftP,
|
||||
rightPeriod: rightP,
|
||||
leftField: cond.leftField ?? defaultBand(leftP),
|
||||
rightField: cond.rightField ?? defaultBand(rightP),
|
||||
leftCandleType: leftCt,
|
||||
rightCandleType: rightCt,
|
||||
period: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export function syncCompositeFields(cond: ConditionDSL): ConditionDSL {
|
||||
if (!cond.composite) return cond;
|
||||
if (isDonchianComposite(cond.indicatorType)) {
|
||||
return syncDonchianCompositeFields(cond);
|
||||
}
|
||||
const leftP = cond.leftPeriod
|
||||
?? parsePeriodFromCompositeField(cond.indicatorType, cond.leftField)
|
||||
?? undefined;
|
||||
@@ -139,6 +206,9 @@ export function syncCompositeFields(cond: ConditionDSL): ConditionDSL {
|
||||
}
|
||||
|
||||
export function normalizeCompositeCondition(cond: ConditionDSL): ConditionDSL {
|
||||
const migrated = migratePriceExtremeCondition(cond);
|
||||
if (migrated !== cond) return migrated;
|
||||
|
||||
if (cond.composite) {
|
||||
if (isThresholdField(cond.leftField) || isThresholdField(cond.rightField)) {
|
||||
return {
|
||||
@@ -151,8 +221,21 @@ export function normalizeCompositeCondition(cond: ConditionDSL): ConditionDSL {
|
||||
return syncCompositeFields(cond);
|
||||
}
|
||||
const ind = cond.indicatorType;
|
||||
if (parseDonchianPeriod(cond.leftField) != null && parseDonchianPeriod(cond.rightField) != null) {
|
||||
const leftP = parseDonchianPeriod(cond.leftField)!;
|
||||
const rightP = parseDonchianPeriod(cond.rightField)!;
|
||||
if (leftP !== rightP) {
|
||||
return syncDonchianCompositeFields({
|
||||
...cond,
|
||||
composite: true,
|
||||
indicatorType: 'DONCHIAN',
|
||||
leftPeriod: leftP,
|
||||
rightPeriod: rightP,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!isCompositeValueField(ind, cond.leftField) || !isCompositeValueField(ind, cond.rightField)) {
|
||||
return cond;
|
||||
return migratePriceExtremeCondition(cond);
|
||||
}
|
||||
const leftP = parsePeriodFromCompositeField(ind, cond.leftField);
|
||||
const rightP = parsePeriodFromCompositeField(ind, cond.rightField);
|
||||
@@ -164,7 +247,7 @@ export function normalizeCompositeCondition(cond: ConditionDSL): ConditionDSL {
|
||||
rightPeriod: rightP,
|
||||
});
|
||||
}
|
||||
return cond;
|
||||
return migratePriceExtremeCondition(cond);
|
||||
}
|
||||
|
||||
export function makeCompositeCondition(
|
||||
@@ -173,6 +256,18 @@ export function makeCompositeCondition(
|
||||
def: CompositePeriodDef,
|
||||
): ConditionDSL {
|
||||
const { short, long } = getCompositeDefaultPeriods(ind, def);
|
||||
if (isDonchianComposite(ind)) {
|
||||
return syncDonchianCompositeFields({
|
||||
indicatorType: ind,
|
||||
conditionType: signalType === 'buy' ? 'CROSS_UP' : 'CROSS_DOWN',
|
||||
composite: true,
|
||||
leftPeriod: short,
|
||||
rightPeriod: long,
|
||||
leftCandleType: DEFAULT_START_CANDLE,
|
||||
rightCandleType: DEFAULT_START_CANDLE,
|
||||
candleRange: 1,
|
||||
});
|
||||
}
|
||||
return syncCompositeFields({
|
||||
indicatorType: ind,
|
||||
conditionType: signalType === 'buy' ? 'CROSS_UP' : 'CROSS_DOWN',
|
||||
@@ -185,6 +280,16 @@ export function makeCompositeCondition(
|
||||
});
|
||||
}
|
||||
|
||||
/** 돈치안 복합 — 조건대상 드롭다운 옵션 */
|
||||
export function getDonchianFieldOpts(
|
||||
allPeriods: number[],
|
||||
): { value: string; label: string }[] {
|
||||
return [
|
||||
...allPeriods.map(p => ({ value: donchianUpperField(p), label: `${p}일 최고가선` })),
|
||||
...allPeriods.map(p => ({ value: donchianLowerField(p), label: `${p}일 최저가선` })),
|
||||
];
|
||||
}
|
||||
|
||||
export function compositePeriodLabel(ind: string, def: CompositePeriodDef): string {
|
||||
const { short, long } = getCompositeDefaultPeriods(ind, def);
|
||||
return `${short} / ${long}`;
|
||||
@@ -192,17 +297,24 @@ export function compositePeriodLabel(ind: string, def: CompositePeriodDef): stri
|
||||
|
||||
export function compositeDisplayName(ind: string): string {
|
||||
const item = COMPOSITE_INDICATOR_ITEMS.find(i => i.value === ind);
|
||||
if (ind === 'DONCHIAN') return '돈치안 채널 (9·20일)';
|
||||
if (item) return `${item.label} + ${item.label}`;
|
||||
return `${ind} + ${ind}`;
|
||||
}
|
||||
|
||||
export function compositeElementLabel(ind: string, slot: 1 | 2): string {
|
||||
if (ind === 'DONCHIAN') {
|
||||
return slot === 1 ? '채널 단기' : '채널 장기';
|
||||
}
|
||||
const item = COMPOSITE_INDICATOR_ITEMS.find(i => i.value === ind);
|
||||
const name = item?.label ?? ind;
|
||||
return `${name} ${slot}`;
|
||||
}
|
||||
|
||||
/** 복합지표 조건대상 라벨 — 예: CCI 1 라인(9일) */
|
||||
/** 복합지표 조건대상 라벨 */
|
||||
export function compositeFieldLabel(ind: string, slot: 1 | 2, period: number): string {
|
||||
if (ind === 'DONCHIAN') {
|
||||
return `${period}일 ${slot === 1 ? '단기' : '장기'} 고가선`;
|
||||
}
|
||||
return `${compositeElementLabel(ind, slot)} 라인(${period}일)`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user