가상투자 종목카드박스 레이아웃 수정

This commit is contained in:
Macbook
2026-05-26 16:42:57 +09:00
parent 7cf43609dc
commit c0d21ac825
25 changed files with 1632 additions and 61 deletions
+48
View File
@@ -4,11 +4,20 @@ import {
compositeFieldLabel,
compositeValueField,
getCompositeDefaultPeriods,
getDonchianFieldOpts,
isDonchianComposite,
isThresholdField,
parseDonchianPeriod,
parsePeriodFromCompositeField,
syncCompositeFields,
type CompositePeriodDef,
} from './compositeIndicators';
import {
isPriceExtremeIndicator,
migratePriceExtremeCondition,
parsePriorExtremePeriod,
syncPriceExtremeSimpleFields,
} from './priceExtremeIndicators';
import { DEFAULT_START_CANDLE, normalizeStartCandleType } from './strategyStartNodes';
export interface IndicatorPeriodDef {
@@ -36,6 +45,9 @@ const VALUE_FIELD_PREFIX: Record<string, string> = {
export function getDefaultIndicatorPeriod(indicatorType: string, def: IndicatorPeriodDef): number {
switch (indicatorType) {
case 'NEW_HIGH':
case 'NEW_LOW':
return 9;
case 'RSI': return def.rsiPeriod;
case 'CCI': return def.cciPeriod;
case 'ADX': return def.adxPeriod;
@@ -59,6 +71,12 @@ function parseFieldPeriod(field?: string): number | null {
/** 조건 노드의 RSI 등 값(좌측) 기간 */
export function getConditionValuePeriod(cond: ConditionDSL, def: IndicatorPeriodDef): number {
if (isPriceExtremeIndicator(cond.indicatorType)) {
if (cond.period && cond.period > 0) return cond.period;
const fromRight = parsePriorExtremePeriod(cond.rightField);
if (fromRight) return fromRight;
return getDefaultIndicatorPeriod(cond.indicatorType, def);
}
if (cond.composite) {
if (cond.leftPeriod && cond.leftPeriod > 0) return cond.leftPeriod;
const fromField = parsePeriodFromCompositeField(cond.indicatorType, cond.leftField);
@@ -100,6 +118,7 @@ export function isValuePeriodFieldStored(indicatorType: string, field?: string):
}
export function usesValuePeriodField(cond: ConditionDSL): boolean {
if (isPriceExtremeIndicator(cond.indicatorType)) return true;
if (cond.composite) return true;
const prefix = VALUE_FIELD_PREFIX[cond.indicatorType];
if (!prefix) return false;
@@ -133,6 +152,9 @@ export function setConditionValuePeriod(
_def: IndicatorPeriodDef,
): ConditionDSL {
const p = Math.max(1, Math.min(500, Math.round(period)));
if (isPriceExtremeIndicator(cond.indicatorType)) {
return syncPriceExtremeSimpleFields({ ...cond, period: p });
}
if (cond.composite) {
return syncCompositeFields({ ...cond, composite: true, leftPeriod: p });
}
@@ -170,6 +192,10 @@ export function initConditionPeriods(
condition: ConditionDSL,
def: IndicatorPeriodDef,
): ConditionDSL {
if (isPriceExtremeIndicator(indicatorType)) {
const period = condition.period ?? getDefaultIndicatorPeriod(indicatorType, def);
return syncPriceExtremeSimpleFields({ ...condition, period });
}
if (condition.composite) return condition;
const prefix = VALUE_FIELD_PREFIX[indicatorType];
if (!prefix) return condition;
@@ -191,9 +217,19 @@ 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);
if (isPriceExtremeIndicator(indicatorType)) {
return [...new Set([base, 5, 9, 10, 20, 55, ...COMMON_PERIODS])].sort((a, b) => a - b);
}
return [...new Set([base, ...COMMON_PERIODS])].sort((a, b) => a - b);
}
export function getPeriodSettingsLabel(cond: ConditionDSL): string | null {
if (isPriceExtremeIndicator(cond.indicatorType)) {
return cond.indicatorType === 'NEW_LOW' ? '신저가 기간 (봉)' : '신고가 기간 (봉)';
}
return null;
}
/** 복합지표 요소1(단기)·요소2(장기) 기간 프리셋 */
export function getCompositePeriodPresetOptions(
indicatorType: string,
@@ -242,6 +278,18 @@ export function getCompositeFieldOpts(
: getConditionRightPeriod(cond, def);
const presets = getCompositePeriodPresetOptions(indicatorType, def, side);
const periods = [...new Set([...presets, current])].sort((a, b) => a - b);
if (isDonchianComposite(indicatorType)) {
const opts = getDonchianFieldOpts(periods);
const cur = slot === 1 ? cond.leftField : cond.rightField;
if (cur && !opts.some(o => o.value === cur)) {
const p = parseDonchianPeriod(cur);
const label = p != null ? compositeFieldLabel(indicatorType, slot, p) : cur;
return [{ value: cur, label }, ...opts];
}
return opts;
}
return periods.map(p => ({
value: compositeValueField(indicatorType, p),
label: compositeFieldLabel(indicatorType, slot, p),