n봉 존재 조건 수정

This commit is contained in:
Macbook
2026-06-12 23:40:20 +09:00
parent b55349062f
commit 4a6be82c15
8 changed files with 365 additions and 32 deletions
@@ -2,7 +2,11 @@
* 전략 조건 DSL → 알기 쉬운 한국어 서술형 설명
*/
import type { LogicNode } from './strategyTypes';
import { CONDITION_LABEL } from './strategyTypes';
import {
resolveCandleRangeMode,
candleRangeWindowBars,
formatCandleRangeClause,
} from './strategyTypes';
import {
collectEditorBranches,
normalizeStartCombineOp,
@@ -163,7 +167,8 @@ function describeCondition(
}
if (right && right !== '선택안함') {
return describeConditionType(ct, left, right, cond);
const prefix = formatCandleRangeClause(cond);
return prefix + describeConditionType(ct, left, right, cond);
}
const label = CONDITION_LABEL[ct] ?? ct;
+42 -11
View File
@@ -2,8 +2,14 @@
import React from 'react';
import type { IndicatorConfig } from '../types/index';
import { getIndicatorDef, getHLineLabel, INDICATOR_REGISTRY, type PlotDef, type HLineDef } from '../utils/indicatorRegistry';
import type { LogicNode, LogicNodeType, ConditionDSL } from '../utils/strategyTypes';
import { CONDITION_LABEL, INDICATOR_CONDITIONS } from '../utils/strategyTypes';
import type { LogicNode, LogicNodeType, ConditionDSL, CandleRangeMode } from '../utils/strategyTypes';
import {
CONDITION_LABEL,
INDICATOR_CONDITIONS,
resolveCandleRangeMode,
candleRangeWindowBars,
formatCandleRangeClause,
} from '../utils/strategyTypes';
import {
COMPOSITE_INDICATOR_ITEMS,
compositeDisplayName,
@@ -791,8 +797,9 @@ export const nodeToText = (node: LogicNode, DEF: DefType = DEF_DEFAULTS): string
const op = c.conditionType === 'LOWEST_LTE' ? '≤' : '≥';
return `${indName} - 최근 ${n}${L} 최저 ${op} ${R || '침체선'}`;
}
if (R && R !== '선택안함') return `${indName} - ${L} ${C} ${R}`;
return `${indName} - ${L} ${C}`;
const rangeClause = formatCandleRangeClause(c);
if (R && R !== '선택안함') return `${indName} - ${rangeClause}${L} ${C} ${R}`;
return `${indName} - ${rangeClause}${L} ${C}`;
}
if (node.type === 'AND') {
if (isStableStrategyPairRoot(node) && node.stableStrategyPair?.mode === 'buy') {
@@ -1142,14 +1149,38 @@ export const CondEditor: React.FC<CondEditorProps> = ({
) : (
<>
<label className="sp-cond-lbl"> </label>
<select className="sp-cond-sel" value={cond.candleRange ?? 1}
onChange={e => onChange({ ...cond, candleRange: Number(e.target.value) })}>
<option value={1}> </option>
<option value={2}>2 </option>
<option value={3}>3 </option>
<option value={4}>4 </option>
<option value={5}>5 </option>
<select
className="sp-cond-sel"
value={resolveCandleRangeMode(normalized)}
onChange={e => {
const mode = e.target.value as CandleRangeMode;
if (mode === 'CURRENT') {
onChange({ ...normalized, candleRangeMode: mode, candleRange: 1 });
} else {
const n = candleRangeWindowBars(normalized);
onChange({ ...normalized, candleRangeMode: mode, candleRange: n >= 2 ? n : 4 });
}
}}
>
<option value="CURRENT"> </option>
<option value="EXISTS_IN">N봉 </option>
<option value="NOT_EXISTS_IN">N봉 </option>
</select>
{resolveCandleRangeMode(normalized) !== 'CURRENT' && (
<input
type="number"
className="sp-cond-num sp-cond-num--inline"
min={2}
max={500}
value={candleRangeWindowBars(normalized)}
placeholder="N"
title="윈도우 봉 수"
onChange={e => onChange({
...normalized,
candleRange: Math.max(2, parseInt(e.target.value, 10) || 4),
})}
/>
)}
</>
)}
</div>
+33
View File
@@ -2,6 +2,9 @@
export type LogicNodeType = 'AND' | 'OR' | 'NOT' | 'CONDITION' | 'TIMEFRAME';
/** CURRENT=현재 봉만, EXISTS_IN=최근 N봉 내 1회 이상, NOT_EXISTS_IN=최근 N봉 내 없음 */
export type CandleRangeMode = 'CURRENT' | 'EXISTS_IN' | 'NOT_EXISTS_IN';
export interface ConditionDSL {
indicatorType: string;
conditionType: string;
@@ -21,6 +24,9 @@ export interface ConditionDSL {
holdDays?: number;
/** LOWEST_LTE/GTE — 최근 N봉 rolling min/max 비교 */
lookbackPeriod?: number;
/** 캔들 범위 모드 — 미설정 시 candleRange>1 이면 EXISTS_IN 으로 해석 */
candleRangeMode?: CandleRangeMode;
/** EXISTS_IN/NOT_EXISTS_IN 일 때 윈도우 크기(N). CURRENT 일 때는 1 */
candleRange?: number;
/** false=보조지표 설정 기본값 상속, true=전략 조건 전용 기간 */
valuePeriodOverride?: boolean;
@@ -365,3 +371,30 @@ export function loadStrategiesFromStorage(): StrategyDSLDto[] {
export function saveStrategiesToStorage(strategies: StrategyDSLDto[]): void {
localStorage.setItem(STORAGE_KEY, JSON.stringify(strategies));
}
/** candleRangeMode 미설정 시 candleRange>1 → EXISTS_IN (구버전 호환) */
export function resolveCandleRangeMode(
cond: Pick<ConditionDSL, 'candleRangeMode' | 'candleRange'>,
): CandleRangeMode {
if (cond.candleRangeMode) return cond.candleRangeMode;
const n = cond.candleRange ?? 1;
return n <= 1 ? 'CURRENT' : 'EXISTS_IN';
}
export function candleRangeWindowBars(
cond: Pick<ConditionDSL, 'candleRangeMode' | 'candleRange'>,
): number {
const mode = resolveCandleRangeMode(cond);
if (mode === 'CURRENT') return 1;
return Math.max(2, cond.candleRange ?? 4);
}
export function formatCandleRangeClause(
cond: Pick<ConditionDSL, 'candleRangeMode' | 'candleRange'>,
): string {
const mode = resolveCandleRangeMode(cond);
const n = candleRangeWindowBars(cond);
if (mode === 'EXISTS_IN') return `최근 ${n}봉 내 `;
if (mode === 'NOT_EXISTS_IN') return `최근 ${n}봉 내 미존재 · `;
return '';
}