전략편집기 기준값 보조지표 설정값 동기화

This commit is contained in:
Macbook
2026-05-28 09:20:06 +09:00
parent 9137864f48
commit e2816b037f
18 changed files with 710 additions and 153 deletions
+83 -63
View File
@@ -27,12 +27,14 @@ import {
getCompositePeriodPresetOptions,
getConditionRightPeriod,
getConditionThreshold,
getChartReferenceThreshold,
getConditionValuePeriod,
getDefaultIndicatorPeriod,
getPeriodPresetOptions,
getThresholdBounds,
getThresholdPresetOptions,
initConditionPeriodsInherit,
isThresholdOverridden,
isValuePeriodFieldStored,
parseThresholdField,
resolveFieldCustomKind,
@@ -44,6 +46,14 @@ import {
setConditionRightPeriod,
setConditionValuePeriod,
} from '../utils/conditionPeriods';
import {
THRESHOLD_HL_MID,
THRESHOLD_HL_OVER,
THRESHOLD_HL_UNDER,
isThresholdFieldOrSymbol,
isThresholdSymbol,
resolveInheritedThresholdField,
} from '../utils/thresholdSymbols';
import { STRATEGY_CANDLE_TYPE_OPTIONS } from '../utils/strategyStartNodes';
export interface StrategyDto {
@@ -292,13 +302,6 @@ export function buildStrategyEditorDef(): DefType {
return buildDef([]);
}
/**
* hline 임계값을 K_ 접두어 DSL 필드값으로 변환.
* 예: 70 → "K_70", -100 → "K_-100"
* 백엔드 StrategyDslToTa4jAdapter에서 "K_" 접두어를 파싱하여 FixedDecimalIndicator 생성.
*/
const kv = (price: number) => `K_${price}`;
// ─── 공통 조건 옵션 ───────────────────────────────────────────────────────────
const COND_OPTIONS = [
{ v: 'GT', l: '초과 (>)' },
@@ -361,9 +364,9 @@ export const getFieldOpts = (
const rsiP = cond ? getConditionValuePeriod({ ...cond, indicatorType: 'RSI' }, DEF) : DEF.rsiPeriod;
return condOpts([
{ value:'RSI_VALUE', label:`RSI 라인(${rsiP}일)` },
{ value:kv(over), label:`${overTerm}(${over})` },
{ value:kv(mid), label:`중앙선(${mid})` },
{ value:kv(under), label:`${underTerm}(${under})` },
{ value:THRESHOLD_HL_OVER, label:`${overTerm}(${over})` },
{ value:THRESHOLD_HL_MID, label:`중앙선(${mid})` },
{ value:THRESHOLD_HL_UNDER, label:`${underTerm}(${under})` },
]);
}
case 'STOCHASTIC': {
@@ -371,9 +374,9 @@ export const getFieldOpts = (
return condOpts([
{ value:'STOCH_K', label:`Stochastic %K(${DEF.stochK}일)` },
{ value:'STOCH_D', label:`Stochastic %D(${DEF.stochD}일)` },
{ value:kv(over), label:`${overTerm}(${over})` },
{ value:kv(mid), label:`중앙선(${mid})` },
{ value:kv(under), label:`${underTerm}(${under})` },
{ value:THRESHOLD_HL_OVER, label:`${overTerm}(${over})` },
{ value:THRESHOLD_HL_MID, label:`중앙선(${mid})` },
{ value:THRESHOLD_HL_UNDER, label:`${underTerm}(${under})` },
]);
}
case 'CCI': {
@@ -381,18 +384,18 @@ export const getFieldOpts = (
const cciP = cond ? getConditionValuePeriod({ ...cond, indicatorType: 'CCI' }, DEF) : DEF.cciPeriod;
return condOpts([
{ value:'CCI_VALUE', label:`CCI 라인(${cciP}일)` },
{ value:kv(over), label:`${overTerm}(${over})` },
{ value:kv(mid), label:`중앙선(${mid})` },
{ value:kv(under), label:`${underTerm}(${under})` },
{ value:THRESHOLD_HL_OVER, label:`${overTerm}(${over})` },
{ value:THRESHOLD_HL_MID, label:`중앙선(${mid})` },
{ value:THRESHOLD_HL_UNDER, label:`${underTerm}(${under})` },
]);
}
case 'ADX': {
const { over, mid, under } = th({ over: 40, mid: 25, under: 20 });
return condOpts([
{ value:'ADX_VALUE', label:`ADX 라인(${DEF.adxPeriod}일)` },
{ value:kv(over), label:`${overTerm}(${over})` },
{ value:kv(mid), label:`중앙선(${mid})` },
{ value:kv(under), label:`${underTerm}(${under})` },
{ value:THRESHOLD_HL_OVER, label:`${overTerm}(${over})` },
{ value:THRESHOLD_HL_MID, label:`중앙선(${mid})` },
{ value:THRESHOLD_HL_UNDER, label:`${underTerm}(${under})` },
]);
}
case 'TRIX': {
@@ -400,7 +403,7 @@ export const getFieldOpts = (
return condOpts([
{ value:'TRIX_VALUE', label:`TRIX 라인(${DEF.trixPeriod}일)` },
{ value:'TRIX_SIGNAL', label:`TRIX 시그널(${DEF.trixSignal}일)` },
{ value:kv(mid), label:`중앙선(${mid})` },
{ value:THRESHOLD_HL_MID, label:`중앙선(${mid})` },
]);
}
case 'DMI': {
@@ -408,9 +411,9 @@ export const getFieldOpts = (
return condOpts([
{ value:'PDI', label:`DI+(${DEF.dmiPeriod}일)` },
{ value:'MDI', label:`DI-(${DEF.dmiPeriod}일)` },
{ value:kv(over), label:`과열(${over})` },
{ value:kv(mid), label:`중간값(${mid})` },
{ value:kv(under), label:`침체(${under})` },
{ value:THRESHOLD_HL_OVER, label:`과열(${over})` },
{ value:THRESHOLD_HL_MID, label:`중간값(${mid})` },
{ value:THRESHOLD_HL_UNDER, label:`침체(${under})` },
]);
}
case 'OBV': return condOpts([
@@ -431,64 +434,67 @@ export const getFieldOpts = (
...DEF.maLines.slice(0,4).map((p, i) => ({ value:`EMA${p}`, label:`EMA${i+1}(${p}일)` })),
{ value:'CLOSE_PRICE', label:'종가' },
];
case 'DISPARITY': return condOpts([
case 'DISPARITY': {
const { mid } = th({ mid: 100 });
return condOpts([
{ value:`DISPARITY${DEF.dispUltra}`, label:`초단기 이격도(${DEF.dispUltra}일)` },
{ value:`DISPARITY${DEF.dispShort}`, label:`단기 이격도(${DEF.dispShort}일)` },
{ value:`DISPARITY${DEF.dispMid}`, label:`중기 이격도(${DEF.dispMid}일)` },
{ value:`DISPARITY${DEF.dispLong}`, label:`장기 이격도(${DEF.dispLong}일)` },
{ value:kv(100), label:'중앙선(100)' },
{ value:THRESHOLD_HL_MID, label:`중앙선(${mid})` },
]);
}
case 'PSYCHOLOGICAL':
case 'NEW_PSYCHOLOGICAL': {
const { over, mid, under } = th({ over:75, mid:50, under:25 });
return condOpts([
{ value:'PSY_VALUE', label:`심리도 라인(${DEF.newPsy}일)` },
{ value:kv(over), label:`${overTerm}(${over})` },
{ value:kv(mid), label:`중앙선(${mid})` },
{ value:kv(under), label:`${underTerm}(${under})` },
{ value:THRESHOLD_HL_OVER, label:`${overTerm}(${over})` },
{ value:THRESHOLD_HL_MID, label:`중앙선(${mid})` },
{ value:THRESHOLD_HL_UNDER, label:`${underTerm}(${under})` },
]);
}
case 'INVEST_PSYCHOLOGICAL': {
const { over, mid, under } = th({ over:75, mid:50, under:25 });
return condOpts([
{ value:'INVEST_PSY_VALUE', label:`투자심리도 라인(${DEF.investPsy}일)` },
{ value:kv(over), label:`${overTerm}(${over})` },
{ value:kv(mid), label:`중앙선(${mid})` },
{ value:kv(under), label:`${underTerm}(${under})` },
{ value:THRESHOLD_HL_OVER, label:`${overTerm}(${over})` },
{ value:THRESHOLD_HL_MID, label:`중앙선(${mid})` },
{ value:THRESHOLD_HL_UNDER, label:`${underTerm}(${under})` },
]);
}
case 'WILLIAMS_R': {
const { over, mid, under } = th({ over:-20, mid:-50, under:-80 });
return condOpts([
{ value:'WILLIAMS_R_VALUE', label:`Williams %R 라인(${DEF.williamsR}일)` },
{ value:kv(over), label:`${overTerm}(${over})` },
{ value:kv(mid), label:`중앙선(${mid})` },
{ value:kv(under), label:`${underTerm}(${under})` },
{ value:THRESHOLD_HL_OVER, label:`${overTerm}(${over})` },
{ value:THRESHOLD_HL_MID, label:`중앙선(${mid})` },
{ value:THRESHOLD_HL_UNDER, label:`${underTerm}(${under})` },
]);
}
case 'BWI': {
const { over, mid, under } = th({ over:80, mid:50, under:20 });
return condOpts([
{ value:'BWI_VALUE', label:`BWI 라인(${DEF.bwiPeriod}일)` },
{ value:kv(over), label:`${overTerm}(${over})` },
{ value:kv(mid), label:`중앙선(${mid})` },
{ value:kv(under), label:`${underTerm}(${under})` },
{ value:THRESHOLD_HL_OVER, label:`${overTerm}(${over})` },
{ value:THRESHOLD_HL_MID, label:`중앙선(${mid})` },
{ value:THRESHOLD_HL_UNDER, label:`${underTerm}(${under})` },
]);
}
case 'VR': {
const { over, mid, under } = th({ over:200, mid:100, under:50 });
return condOpts([
{ value:'VR_VALUE', label:`VR 라인(${DEF.vrPeriod}일)` },
{ value:kv(over), label:`${overTerm}(${over})` },
{ value:kv(mid), label:`중앙선(${mid})` },
{ value:kv(under), label:`${underTerm}(${under})` },
{ value:THRESHOLD_HL_OVER, label:`${overTerm}(${over})` },
{ value:THRESHOLD_HL_MID, label:`중앙선(${mid})` },
{ value:THRESHOLD_HL_UNDER, label:`${underTerm}(${under})` },
]);
}
case 'VOLUME_OSC': {
const { mid } = th({ mid:0 });
return condOpts([
{ value:'VOLUME_OSC_VALUE', label:`거래량 오실레이터 값(${DEF.volOscShort}일/${DEF.volOscLong}일)` },
{ value:kv(mid), label:`중앙선(${mid})` },
{ value:THRESHOLD_HL_MID, label:`중앙선(${mid})` },
]);
}
case 'MACD': {
@@ -497,7 +503,7 @@ export const getFieldOpts = (
{ value:'MACD_LINE', label:`MACD 라인(${DEF.macdFast}일/${DEF.macdSlow}일)` },
{ value:'SIGNAL_LINE', label:`시그널 라인(${DEF.macdSignal}일)` },
{ value:'HISTOGRAM', label:'히스토그램' },
{ value:kv(mid), label:`중앙선(${mid})` },
{ value:THRESHOLD_HL_MID, label:`중앙선(${mid})` },
]);
}
case 'BOLLINGER': return condOpts([
@@ -559,30 +565,25 @@ export const getFieldOpts = (
};
const getDefaultConditionFields = (ind: string, signalType: 'buy'|'sell', DEF: DefType): { l: string; r: string } => {
// 저장된 hline 과열선 값 우선 사용
const over = (def: number) => kv(DEF.hlThresh[ind]?.over ?? def);
const mid = (def: number) => kv(DEF.hlThresh[ind]?.mid ?? def);
const adxMid = DEF.hlThresh['ADX']?.mid ?? 25;
const map: Record<string, {l:string;r:string}> = {
RSI: { l:'RSI_VALUE', r: over(70) },
RSI: { l:'RSI_VALUE', r: THRESHOLD_HL_OVER },
STOCHASTIC: { l:'STOCH_K', r:'STOCH_D' },
CCI: { l:'CCI_VALUE', r: over(100) },
ADX: { l:'ADX_VALUE', r: kv(adxMid) },
CCI: { l:'CCI_VALUE', r: THRESHOLD_HL_OVER },
ADX: { l:'ADX_VALUE', r: THRESHOLD_HL_MID },
TRIX: { l:'TRIX_VALUE', r:'TRIX_SIGNAL' },
DMI: { l:'PDI', r:'MDI' },
OBV: { l:'OBV_LINE', r:'OBV_SIGNAL' },
VOLUME: { l:'VOLUME_VALUE', r:'VOLUME_MA' },
MA: { l:`MA${DEF.maLines[0]}`, r:`MA${DEF.maLines[1]}` },
EMA: { l:`EMA${DEF.maLines[0]}`, r:`EMA${DEF.maLines[1]}` },
DISPARITY: { l:`DISPARITY${DEF.dispUltra}`, r: mid(100) },
PSYCHOLOGICAL: { l:'PSY_VALUE', r: over(75) },
NEW_PSYCHOLOGICAL: { l:'PSY_VALUE', r: over(75) },
INVEST_PSYCHOLOGICAL: { l:'INVEST_PSY_VALUE', r: over(75) },
WILLIAMS_R: { l:'WILLIAMS_R_VALUE', r: over(-20) },
BWI: { l:'BWI_VALUE', r: over(80) },
VR: { l:'VR_VALUE', r: over(200) },
VOLUME_OSC: { l:'VOLUME_OSC_VALUE', r: mid(0) },
DISPARITY: { l:`DISPARITY${DEF.dispUltra}`, r: THRESHOLD_HL_MID },
PSYCHOLOGICAL: { l:'PSY_VALUE', r: THRESHOLD_HL_OVER },
NEW_PSYCHOLOGICAL: { l:'PSY_VALUE', r: THRESHOLD_HL_OVER },
INVEST_PSYCHOLOGICAL: { l:'INVEST_PSY_VALUE', r: THRESHOLD_HL_OVER },
WILLIAMS_R: { l:'WILLIAMS_R_VALUE', r: THRESHOLD_HL_OVER },
BWI: { l:'BWI_VALUE', r: THRESHOLD_HL_OVER },
VR: { l:'VR_VALUE', r: THRESHOLD_HL_OVER },
VOLUME_OSC: { l:'VOLUME_OSC_VALUE', r: THRESHOLD_HL_MID },
MACD: { l:'MACD_LINE', r:'SIGNAL_LINE' },
BOLLINGER: { l:'CLOSE_PRICE', r:'UPPER_BAND' },
DONCHIAN: signalType === 'buy'
@@ -682,10 +683,17 @@ export const nodeToText = (node: LogicNode, DEF: DefType = DEF_DEFAULTS): string
const tf = lCt !== rCt ? ` [${lCt}/${rCt}]` : ` [${lCt}]`;
return `${indName} - ${L} ${C} ${R}${tf}`;
}
const defaults = getDefaultConditionFields(c.indicatorType, 'buy', DEF);
const lv = resolveFieldOptionValue(c.indicatorType, c.leftField);
const rv = resolveFieldOptionValue(c.indicatorType, c.rightField);
const L = opts.find(o => o.value === lv)?.label ?? c.leftField ?? indName;
const rvRaw = c.rightField;
const rv = isThresholdOverridden(c)
? resolveFieldOptionValue(c.indicatorType, rvRaw)
: (resolveInheritedThresholdField(rvRaw, c.indicatorType, DEF.hlThresh)
?? resolveFieldOptionValue(c.indicatorType, rvRaw));
const L = opts.find(o => o.value === lv)?.label ?? c.leftField ?? indName;
const chartRef = getChartReferenceThreshold(c, 'right', defaults.r, DEF);
const R = opts.find(o => o.value === rv)?.label
?? (chartRef != null && !isThresholdOverridden(c) ? `기준(${chartRef})` : null)
?? (rv && parseThresholdField(rv) != null ? `임계(${parseThresholdField(rv)})` : rv ?? '');
if (R && R !== '선택안함') return `${indName} - ${L} ${C} ${R}`;
return `${indName} - ${L} ${C}`;
@@ -800,7 +808,13 @@ export const CondEditor: React.FC<CondEditorProps> = ({ cond, signalType, onChan
return isValid(v) ? v! : defaults.l;
};
const getRightValue = () => {
const v = resolveFieldOptionValue(normalized.indicatorType, normalized.rightField);
const raw = normalized.rightField;
const v = isThresholdOverridden(normalized)
? resolveFieldOptionValue(normalized.indicatorType, raw)
: (isThresholdFieldOrSymbol(raw)
? (resolveInheritedThresholdField(raw, normalized.indicatorType, def.hlThresh)
?? resolveFieldOptionValue(normalized.indicatorType, raw))
: resolveFieldOptionValue(normalized.indicatorType, raw));
return isValid(v) ? v! : defaults.r;
};
@@ -812,6 +826,11 @@ export const CondEditor: React.FC<CondEditorProps> = ({ cond, signalType, onChan
const handleCondType = (newType: string) => onChange(applyCondTypeDefaults(normalized, newType, def));
const handleRight = (v: string) => {
if (isThresholdSymbol(v)) {
const { targetValue: _t, ...rest } = normalized;
onChange({ ...rest, rightField: v, thresholdOverride: false });
return;
}
const thresholds: Record<string,number> = {
OVERBOUGHT_70: 70, NEUTRAL_50: 50, OVERSOLD_30: 30,
OVERBOUGHT_80: 80, OVERSOLD_20: 20,
@@ -1005,7 +1024,8 @@ export const CondEditor: React.FC<CondEditorProps> = ({ cond, signalType, onChan
fieldValue={rightValue}
isPresetField={fieldOpts.some(o => o.value === normalized.rightField)}
customKind={resolveFieldCustomKind(normalized.indicatorType, normalized.rightField)}
customNumber={getConditionThreshold(normalized, 'right', inheritedThresholdField)
customNumber={getConditionThreshold(normalized, 'right', inheritedThresholdField, def)
?? getChartReferenceThreshold(normalized, 'right', inheritedThresholdField, def)
?? parseThresholdField(normalized.rightField)
?? 0}
numberPresets={getThresholdPresetOptions(normalized.indicatorType)}
@@ -1014,7 +1034,7 @@ export const CondEditor: React.FC<CondEditorProps> = ({ cond, signalType, onChan
allowDecimal={normalized.indicatorType === 'CCI'}
disabled={isSlopeType || isHoldType}
onFieldChange={handleRight}
onCustomNumberChange={v => onChange(setConditionThreshold(normalized, 'right', v))}
onCustomNumberChange={v => onChange(setConditionThreshold(normalized, 'right', v, def))}
/>
</div>
{/* 조건 */}