전략편집기 기준값 보조지표 설정값 동기화
This commit is contained in:
@@ -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>
|
||||
{/* 조건 */}
|
||||
|
||||
Reference in New Issue
Block a user