지표탭 추가

This commit is contained in:
Macbook
2026-05-27 09:32:26 +09:00
parent d0985f8f0f
commit 8876dbd752
12 changed files with 595 additions and 73 deletions
+57 -15
View File
@@ -1,7 +1,7 @@
/** Shared strategy editor utilities — extracted from StrategyPage */
import React from 'react';
import type { IndicatorConfig } from '../types/index';
import { getIndicatorDef, getHLineLabel } from '../utils/indicatorRegistry';
import { getIndicatorDef, getHLineLabel, type PlotDef, type HLineDef } from '../utils/indicatorRegistry';
import type { LogicNode, LogicNodeType, ConditionDSL } from '../utils/strategyTypes';
import {
compositeDisplayName,
@@ -32,7 +32,7 @@ import {
getPeriodPresetOptions,
getThresholdBounds,
getThresholdPresetOptions,
initConditionPeriods,
initConditionPeriodsInherit,
isValuePeriodFieldStored,
parseThresholdField,
resolveFieldCustomKind,
@@ -256,9 +256,45 @@ export function buildDef(activeIndicators: IndicatorConfig[]): DefType {
}
/**
* 전략편집기 전용 DEF — 차트 activeIndicators와 무관한 고정 기본값.
* 조건 노드간·임계값은 ConditionDSL(leftPeriod/rightPeriod/period)에만 저장된다.
* 전략편집기 DEF — DB 보조지표 설정(파라미터·hline)에서 구성.
* 조건 노드 기본값은 valuePeriodOverride/thresholdOverride=false 일 때 여기서 상속.
*/
export function buildStrategyEditorDefFromSettings(
getParams: (
type: string,
defaults?: Record<string, number | string | boolean>,
) => Record<string, number | string | boolean>,
getVisualConfig: (
type: string,
defaultPlots?: PlotDef[],
defaultHlines?: HLineDef[],
) => { plots: PlotDef[]; hlines: HLineDef[]; cloudColors?: unknown },
): DefType {
const registryTypes = [
'RSI', 'MACD', 'CCI', 'Stochastic', 'ADX', 'TRIX', 'DMI', 'BollingerBands',
'IchimokuCloud', 'WilliamsPercentRange', 'VolumeOscillator', 'VR', 'Disparity',
'Psychological', 'InvestPsychological', 'OBV', 'DonchianChannels', 'PriceExtreme',
'SMA',
];
const configs: IndicatorConfig[] = [];
for (const type of registryTypes) {
const def = getIndicatorDef(type);
if (!def) continue;
const params = getParams(type, def.defaultParams as Record<string, number | string | boolean>);
const { plots, hlines, cloudColors } = getVisualConfig(type, def.plots, def.hlines);
configs.push({
id: `se-def-${type}`,
type,
params: { ...params },
plots,
hlines,
...(cloudColors ? { cloudColors } : {}),
} as IndicatorConfig);
}
return buildDef(configs);
}
/** @deprecated buildStrategyEditorDefFromSettings 사용 — 하드코딩 폴백 */
export function buildStrategyEditorDef(): DefType {
return buildDef([]);
}
@@ -529,7 +565,7 @@ export const getFieldOpts = (
}
};
const getDefaultFields = (ind: string, signalType: 'buy'|'sell', DEF: DefType): { l: string; r: string } => {
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);
@@ -566,6 +602,8 @@ const getDefaultFields = (ind: string, signalType: 'buy'|'sell', DEF: DefType):
return map[ind] ?? { l:'NONE', r:'NONE' };
};
export { getDefaultConditionFields };
// conditionType 변경 시 자동 필드 조정
const applyCondTypeDefaults = (cond: ConditionDSL, newCondType: string, DEF: DefType): ConditionDSL => {
const updated = { ...cond, conditionType: newCondType };
@@ -579,7 +617,7 @@ const applyCondTypeDefaults = (cond: ConditionDSL, newCondType: string, DEF: Def
}
const fieldOpts = getFieldOpts(cond.indicatorType, 'buy', DEF);
const isValid = (v: string|undefined) => !!v && fieldOpts.some(o => o.value === v);
const def = getDefaultFields(cond.indicatorType, 'buy', DEF);
const def = getDefaultConditionFields(cond.indicatorType, 'buy', DEF);
if (['GT','LT','GTE','LTE','EQ','NEQ','CROSS_UP','CROSS_DOWN','DIFF_GT','DIFF_LT'].includes(newCondType)) {
if (!isValid(updated.leftField)) updated.leftField = def.l;
@@ -761,7 +799,7 @@ export const CondEditor: React.FC<CondEditorProps> = ({ cond, signalType, onChan
const condOpts = normalized.indicatorType === 'ICHIMOKU' ? ICHIMOKU_CONDS : COND_OPTIONS;
const isValid = (v: string|undefined) => !!v && fieldOpts.some(o => o.value === v);
const defaults = getDefaultFields(normalized.indicatorType, signalType, def);
const defaults = getDefaultConditionFields(normalized.indicatorType, signalType, def);
const getLeftValue = () => {
const v = resolveFieldOptionValue(normalized.indicatorType, normalized.leftField);
@@ -791,7 +829,10 @@ export const CondEditor: React.FC<CondEditorProps> = ({ cond, signalType, onChan
ZERO_LINE: 0,
};
let upd: ConditionDSL = { ...normalized, rightField: v };
if (thresholds[v] !== undefined) upd.targetValue = thresholds[v];
if (thresholds[v] !== undefined) {
upd.targetValue = thresholds[v];
upd.thresholdOverride = true;
}
const priorP = parsePriorExtremePeriod(v);
if (priorP != null && isPriceExtremeIndicator(normalized.indicatorType)) {
upd = syncPriceExtremeSimpleFields({ ...upd, period: priorP });
@@ -816,9 +857,9 @@ export const CondEditor: React.FC<CondEditorProps> = ({ cond, signalType, onChan
?? parsePeriodFromCompositeField(normalized.indicatorType, field);
if (p == null) return;
if (side === 'left') {
onChange(syncCompositeFields({ ...normalized, leftPeriod: p, leftField: field }));
onChange(syncCompositeFields({ ...normalized, leftPeriod: p, leftField: field, valuePeriodOverride: true }));
} else {
onChange(syncCompositeFields({ ...normalized, rightPeriod: p, rightField: field }));
onChange(syncCompositeFields({ ...normalized, rightPeriod: p, rightField: field, rightPeriodOverride: true }));
}
};
@@ -954,7 +995,7 @@ export const CondEditor: React.FC<CondEditorProps> = ({ cond, signalType, onChan
const base = bases[normalized.indicatorType];
if (base && v === base) {
const p = getConditionValuePeriod(normalized, def);
onChange({ ...normalized, leftField: `${base}_${p}`, period: p });
onChange({ ...normalized, leftField: `${base}_${p}`, period: p, valuePeriodOverride: true });
} else {
onChange({ ...normalized, leftField: v });
}
@@ -1077,8 +1118,7 @@ export const makeNode = (
}
return { id: genId(), type: 'CONDITION', condition };
}
const def = getDefaultFields(value, signalType, DEF);
const period = options?.period ?? getDefaultIndicatorPeriod(value, DEF);
const def = getDefaultConditionFields(value, signalType, DEF);
let conditionType = signalType === 'buy' ? 'CROSS_UP' : 'CROSS_DOWN';
if (value === 'NEW_HIGH' && signalType === 'buy') conditionType = 'CROSS_UP';
if (value === 'NEW_LOW' && signalType === 'sell') conditionType = 'CROSS_DOWN';
@@ -1088,9 +1128,11 @@ export const makeNode = (
leftField: def.l,
rightField: def.r,
candleRange: 1,
period,
valuePeriodOverride: false,
thresholdOverride: false,
rightPeriodOverride: false,
};
const condition = initConditionPeriods(value, baseCondition, DEF);
const condition = initConditionPeriodsInherit(value, baseCondition, DEF);
return {
id: genId(), type: 'CONDITION',
condition: isPriceExtremeIndicator(value)