n봉 존재 조건 수정
This commit is contained in:
@@ -18,6 +18,12 @@ import {
|
||||
simpleTemplateToNode,
|
||||
type StrategyTemplateDef,
|
||||
} from '../utils/strategyPresets';
|
||||
import type { CandleRangeMode } from '../utils/strategyTypes';
|
||||
import {
|
||||
resolveCandleRangeMode,
|
||||
candleRangeWindowBars,
|
||||
formatCandleRangeClause,
|
||||
} from '../utils/strategyTypes';
|
||||
|
||||
// ─── 타입 ─────────────────────────────────────────────────────────────────────
|
||||
type LogicNodeType = 'AND' | 'OR' | 'NOT' | 'CONDITION' | 'TIMEFRAME';
|
||||
@@ -33,6 +39,7 @@ interface ConditionDSL {
|
||||
compareValue?: number;
|
||||
slopePeriod?: number;
|
||||
holdDays?: number;
|
||||
candleRangeMode?: CandleRangeMode;
|
||||
candleRange?: number;
|
||||
}
|
||||
|
||||
@@ -572,8 +579,9 @@ const nodeToText = (node: LogicNode, DEF: DefType = DEF_DEFAULTS): string => {
|
||||
const L = opts.find(o => o.value === c.leftField)?.label ?? c.leftField ?? c.indicatorType;
|
||||
const R = opts.find(o => o.value === c.rightField)?.label ?? c.rightField ?? '';
|
||||
const C = condLabel(c.conditionType);
|
||||
if (R && R !== '선택안함') return `${c.indicatorType} - ${L} ${C} ${R}`;
|
||||
return `${c.indicatorType} - ${L} ${C}`;
|
||||
const rangeClause = formatCandleRangeClause(c);
|
||||
if (R && R !== '선택안함') return `${c.indicatorType} - ${rangeClause}${L} ${C} ${R}`;
|
||||
return `${c.indicatorType} - ${rangeClause}${L} ${C}`;
|
||||
}
|
||||
if (node.type === 'AND') {
|
||||
const parts = (node.children ?? []).map(c => nodeToText(c, DEF));
|
||||
@@ -712,14 +720,36 @@ const CondEditor: React.FC<CondEditorProps> = ({ cond, signalType, onChange, def
|
||||
{/* 캔들 범위 */}
|
||||
<div className="sp-cond-field">
|
||||
<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(cond)}
|
||||
onChange={e => {
|
||||
const mode = e.target.value as CandleRangeMode;
|
||||
if (mode === 'CURRENT') {
|
||||
onChange({ ...cond, candleRangeMode: mode, candleRange: 1 });
|
||||
} else {
|
||||
const n = candleRangeWindowBars(cond);
|
||||
onChange({ ...cond, 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(cond) !== 'CURRENT' && (
|
||||
<input
|
||||
type="number"
|
||||
className="sp-cond-num sp-cond-num--inline"
|
||||
min={2}
|
||||
max={500}
|
||||
value={candleRangeWindowBars(cond)}
|
||||
onChange={e => onChange({
|
||||
...cond,
|
||||
candleRange: Math.max(2, parseInt(e.target.value, 10) || 4),
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{/* 조건대상1 */}
|
||||
<div className="sp-cond-field">
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
import React from 'react';
|
||||
import type { LogicNode } from '../../utils/strategyTypes';
|
||||
import { CONDITION_LABEL } from '../../utils/strategyTypes';
|
||||
import { CONDITION_LABEL, resolveCandleRangeMode, candleRangeWindowBars } from '../../utils/strategyTypes';
|
||||
import { getStrategyIndicatorDisplayName } from '../../utils/strategyPaletteStorage';
|
||||
import { getIndicatorPaletteCategory } from '../strategyEditor/paletteCategories';
|
||||
import { nodeToText } from '../../utils/strategyEditorShared';
|
||||
@@ -56,9 +56,13 @@ const ConditionCard: React.FC<{ node: LogicNode }> = ({ node }) => {
|
||||
const descText = fullText.startsWith(prefix) ? fullText.slice(prefix.length) : fullText;
|
||||
const { left, mid: condStr, right } = splitDescByCondType(descText);
|
||||
|
||||
const candleRange = cond.candleRange != null && cond.candleRange > 0
|
||||
? `${cond.candleRange}캔들 전`
|
||||
: '현재 캔들';
|
||||
const rangeMode = resolveCandleRangeMode(cond);
|
||||
const rangeN = candleRangeWindowBars(cond);
|
||||
const candleRangeLabel = rangeMode === 'CURRENT'
|
||||
? '현재 캔들'
|
||||
: rangeMode === 'EXISTS_IN'
|
||||
? `최근 ${rangeN}봉 내 존재`
|
||||
: `최근 ${rangeN}봉 내 미존재`;
|
||||
|
||||
const condLabelDisplay = CONDITION_LABEL[cond.conditionType] ?? (condStr || cond.conditionType);
|
||||
|
||||
@@ -71,7 +75,7 @@ const ConditionCard: React.FC<{ node: LogicNode }> = ({ node }) => {
|
||||
<div className="scv-cond-fields">
|
||||
<div className="scv-cond-field">
|
||||
<span className="scv-cond-field-lbl">캔들범위</span>
|
||||
<span className="scv-cond-field-val">{candleRange}</span>
|
||||
<span className="scv-cond-field-val">{candleRangeLabel}</span>
|
||||
</div>
|
||||
{left && (
|
||||
<div className="scv-cond-field">
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 '';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user