보조지표 설정 저장 오류

This commit is contained in:
Macbook
2026-06-16 01:08:58 +09:00
parent e93164deda
commit d34a824174
9 changed files with 80 additions and 57 deletions
+4 -2
View File
@@ -1589,13 +1589,14 @@ export function useChartWorkspace({
cfg = { ...fromConfig, id: newIndId(), type: def.type };
} else {
const params = getParams(type, def.defaultParams);
const { plots, hlines, cloudColors, bandBackground } = getVisualConfig(type, def.plots, def.hlines);
const { plots, hlines, cloudColors, bandBackground, plotVisibility } = getVisualConfig(type, def.plots, def.hlines);
cfg = {
id: newIndId(),
type: def.type,
params,
plots,
hlines,
...(plotVisibility ? { plotVisibility } : {}),
...(cloudColors ? { cloudColors } : {}),
...(bandBackground ? { bandBackground } : {}),
};
@@ -1695,6 +1696,7 @@ export function useChartWorkspace({
updated.hlines,
updated.cloudColors,
updated.bandBackground,
updated.plotVisibility,
);
const applyUpdate = (prev: IndicatorConfig[]): IndicatorConfig[] => {
@@ -1785,7 +1787,7 @@ export function useChartWorkspace({
applyChartIndicators(sortIndicatorsByTypeOrder(chartIndicators));
allConfigs.forEach(ind => {
saveParams(ind.type, ind.params);
saveVisual(ind.type, ind.plots, ind.hlines, ind.cloudColors, ind.bandBackground);
saveVisual(ind.type, ind.plots, ind.hlines, ind.cloudColors, ind.bandBackground, ind.plotVisibility);
});
setShowBulkIndSettings(false);
}, [applyChartIndicators, saveParams, saveVisual]);
+1
View File
@@ -650,6 +650,7 @@ const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot
updated.hlines,
updated.cloudColors,
updated.bandBackground,
updated.plotVisibility,
);
}, [saveParams, saveVisual]);
@@ -23,6 +23,7 @@ import type { EvalIndicatorParams } from '../../utils/strategyEvaluationParams';
import { DISPLAY_COUNT } from '../../hooks/useUpbitData';
import { buildVirtualTradingChartIndicators } from '../../utils/strategyToChartIndicators';
import { buildEvalParamsFromStrategy } from '../../utils/strategyEvaluationParams';
import { indicatorDefaultsFingerprint } from '../../utils/indicatorDefaultsFingerprint';
import { repairUtf8Mojibake } from '../../utils/textEncoding';
import StrategyEvaluationBarSelector, {
resolveBarIndexAtChartClick,
@@ -102,7 +103,7 @@ const StrategyEvaluationChart: React.FC<Props> = ({
evalLoading = false,
onSignalsChange,
}) => {
const { getParams: baseGetParams, getVisualConfig: baseGetVisual } = useIndicatorSettings();
const { getParams: baseGetParams, getVisualConfig: baseGetVisual, settingsRevision } = useIndicatorSettings();
const getParams = getParamsOverride ?? baseGetParams;
const getVisualConfig = getVisualConfigOverride ?? baseGetVisual;
const { defaults: appDefaults } = useAppSettings();
@@ -126,7 +127,7 @@ const StrategyEvaluationChart: React.FC<Props> = ({
const indicators = useMemo(() => {
if (!strategy) return [] as IndicatorConfig[];
return buildVirtualTradingChartIndicators(strategy, getParams, getVisualConfig);
}, [strategy, getParams, getVisualConfig]);
}, [strategy, getParams, getVisualConfig, settingsRevision]);
const auxPaneCount = useMemo(
() => countNonOverlayIndicatorPanes(indicators, isOverlayType),
@@ -145,6 +146,11 @@ const StrategyEvaluationChart: React.FC<Props> = ({
[strategy, getParams],
);
const indicatorRenderKey = useMemo(
() => indicators.map(i => indicatorDefaultsFingerprint(i)).join('|'),
[indicators],
);
const resolveInitialDisplayCount = useCallback(
(plotWidth: number) => {
const approx = Math.floor(plotWidth / 6);
@@ -473,7 +479,7 @@ const StrategyEvaluationChart: React.FC<Props> = ({
{!loading && !error && bars.length > 0 && (
<>
<TradingChart
key={`${market}-${timeframe}-${strategy?.id ?? 0}-${paramsRevision}-${indicators.map(i => i.id).join(',')}`}
key={`${market}-${timeframe}-${strategy?.id ?? 0}-${paramsRevision}-${settingsRevision}-${indicatorRenderKey}`}
bars={bars}
barsMarket={market}
market={market}
+18 -1
View File
@@ -47,6 +47,8 @@ type AllSettings = Record<string, ParamMap>;
export interface IndicatorVisual {
plots?: PlotDef[];
hlines?: HLineDef[];
/** 지표선 on/off — plot id → visible */
plotVisibility?: Record<string, boolean>;
cloudColors?: IchimokuCloudColors;
/** 볼린저밴드 어퍼~로우어 배경 (업비트 백그라운드 그리기) */
bandBackground?: HlinesBackground;
@@ -56,6 +58,7 @@ export interface IndicatorVisual {
export interface IndicatorVisualConfig {
plots: PlotDef[];
hlines: HLineDef[];
plotVisibility?: Record<string, boolean>;
cloudColors?: IchimokuCloudColors;
bandBackground?: HlinesBackground;
}
@@ -243,6 +246,7 @@ export function useIndicatorSettings(sessionKey = 0, enabled = true) {
plots: cfg.plots,
hlines: cfg.hlines,
};
if (cfg.plotVisibility) visual.plotVisibility = cfg.plotVisibility;
if (cfg.cloudColors) visual.cloudColors = cfg.cloudColors;
if (cfg.type === 'BollingerBands' && cfg.bandBackground) {
visual.bandBackground = cfg.bandBackground;
@@ -321,10 +325,21 @@ export function useIndicatorSettings(sessionKey = 0, enabled = true) {
? resolveBbBandBackground(saved?.bandBackground)
: undefined;
const enriched = enrichIndicatorConfig({
id: '',
type,
params: (def?.defaultParams ?? {}) as Record<string, number | string | boolean>,
plots,
plotVisibility: saved?.plotVisibility,
});
// 깊은 복사: 여러 슬롯이 같은 참조를 공유하지 않도록
return {
plots: plots.map(p => ({ ...p })),
plots: (enriched.plots ?? plots).map(p => ({ ...p })),
hlines: hlines.map(h => ({ ...h })),
plotVisibility: enriched.plotVisibility
? { ...enriched.plotVisibility }
: undefined,
cloudColors: cloudColors ? { ...cloudColors } : undefined,
bandBackground: bandBackground ? { ...bandBackground } : undefined,
};
@@ -343,8 +358,10 @@ export function useIndicatorSettings(sessionKey = 0, enabled = true) {
hlines?: HLineDef[],
cloudColors?: IchimokuCloudColors,
bandBackground?: HlinesBackground,
plotVisibility?: Record<string, boolean>,
) => {
const visual: IndicatorVisual = { plots, hlines };
if (plotVisibility != null) visual.plotVisibility = plotVisibility;
if (cloudColors) visual.cloudColors = cloudColors;
if (type === 'BollingerBands') {
visual.bandBackground = bandBackground ?? resolveBbBandBackground(_visualCache?.[type]?.bandBackground);
+23 -5
View File
@@ -391,25 +391,43 @@ export async function saveIndicatorSettings(
* 앱 시작 시 호출 — 전역 시각 기본값을 DB 에서 가져온다.
*/
export async function loadIndicatorVisualSettings(): Promise<
Record<string, { plots?: unknown[]; hlines?: unknown[]; cloudColors?: unknown; bandBackground?: unknown }>
Record<string, {
plots?: unknown[];
hlines?: unknown[];
plotVisibility?: Record<string, boolean>;
cloudColors?: unknown;
bandBackground?: unknown;
}>
> {
return (
(await request<Record<string, { plots?: unknown[]; hlines?: unknown[]; cloudColors?: unknown; bandBackground?: unknown }>>(
(await request<Record<string, {
plots?: unknown[];
hlines?: unknown[];
plotVisibility?: Record<string, boolean>;
cloudColors?: unknown;
bandBackground?: unknown;
}>>(
'/indicator-settings/visual'
)) ?? {}
);
}
/**
* 특정 지표의 시각 설정(색상·선굵기·수평선)을 저장.
* 특정 지표의 시각 설정(색상·선굵기·수평선·지표선 on/off)을 저장.
* 사용자가 IndicatorSettingsModal 에서 색상·선굵기를 변경할 때 호출.
*
* @param indicatorType 지표 타입 (e.g. "RSI")
* @param visual { plots: PlotDef[], hlines: HLineDef[] }
* @param visual { plots, hlines, plotVisibility, ... }
*/
export async function saveIndicatorVisualSettings(
indicatorType: string,
visual: { plots?: unknown[]; hlines?: unknown[]; cloudColors?: unknown; bandBackground?: unknown },
visual: {
plots?: unknown[];
hlines?: unknown[];
plotVisibility?: Record<string, boolean>;
cloudColors?: unknown;
bandBackground?: unknown;
},
): Promise<void> {
await request(`/indicator-settings/${encodeURIComponent(indicatorType)}/visual`, {
method: 'PATCH',
@@ -45,6 +45,7 @@ function overlayWorkspaceVisualOntoStrategyIndicator(
hlines: ws.hlines?.length ? ws.hlines : ind.hlines,
cloudColors: ws.cloudColors ?? ind.cloudColors,
bandBackground: ws.bandBackground ?? ind.bandBackground,
plotVisibility: ws.plotVisibility ?? ind.plotVisibility,
};
}
@@ -153,6 +154,8 @@ export function mergeGlobalDefaultsOntoChartIndicators(
hlines: [],
cloudColors: undefined,
bandBackground: undefined,
/** strat_* — DB visual 기본값(plotVisibility)을 항상 따름 */
plotVisibility: undefined,
}
: ind;
const cfg = buildMainIndicatorConfig(ind.type, [baseInd], getParams, getVisualConfig);
+3 -1
View File
@@ -152,6 +152,7 @@ type GetVisual = (
) => {
plots: PlotDef[];
hlines: HLineDef[];
plotVisibility?: Record<string, boolean>;
cloudColors?: IchimokuCloudColors;
bandBackground?: import('../types').HlinesBackground;
};
@@ -178,7 +179,7 @@ export function buildChartIndicatorConfig(
if (!def) return null;
const params = getParams(type, def.defaultParams as ParamRecord);
const { plots, hlines, cloudColors, bandBackground } = getVisualConfig(
const { plots, hlines, cloudColors, bandBackground, plotVisibility } = getVisualConfig(
type,
def.plots,
def.hlines ?? [],
@@ -190,6 +191,7 @@ export function buildChartIndicatorConfig(
params,
plots,
hlines,
...(plotVisibility ? { plotVisibility } : {}),
...(cloudColors ? { cloudColors } : {}),
...(bandBackground ? { bandBackground } : {}),
...extras,
@@ -55,7 +55,13 @@ export function initializeIndicatorConfigForEditor(
type: string,
defaultPlots: PlotDef[],
defaultHlines?: HLineDef[],
) => { plots: PlotDef[]; hlines: HLineDef[]; cloudColors?: IchimokuCloudColors; bandBackground?: HlinesBackground },
) => {
plots: PlotDef[];
hlines: HLineDef[];
plotVisibility?: Record<string, boolean>;
cloudColors?: IchimokuCloudColors;
bandBackground?: HlinesBackground;
},
): IndicatorConfig {
const def = getIndicatorDef(raw.type);
if (!def) return enrichIndicatorConfig(raw);
@@ -79,6 +85,7 @@ export function initializeIndicatorConfigForEditor(
hlines: raw.hlines?.length ? raw.hlines : visual?.hlines,
cloudColors: raw.cloudColors ?? visual?.cloudColors,
bandBackground: raw.bandBackground ?? visual?.bandBackground,
plotVisibility: raw.plotVisibility ?? visual?.plotVisibility,
});
}
+11 -44
View File
@@ -9,14 +9,11 @@ import {
getIndicatorDef,
getHLineLabel,
mergePlotDefs,
normalizeObvParams,
type HLineDef,
} from './indicatorRegistry';
import { initializeIndicatorConfigForEditor } from './indicatorSettingsEditor';
import { mergeGlobalDefaultsOntoChartIndicators } from './indicatorMainConfig';
import { buildMainIndicatorConfig } from './indicatorMainConfig';
import { normalizeSmaConfig } from './smaConfig';
import { normalizeIchimokuConfig } from './ichimokuConfig';
import { buildChartIndicatorConfig } from './indicatorPaneMerge';
import type { EditorConditionState } from './strategyConditionSerde';
import { collectUiEvaluationTimeframes } from './strategyTimeframeSync';
@@ -66,7 +63,9 @@ type GetVisual = (
) => {
plots: import('./indicatorRegistry').PlotDef[];
hlines: HLineDef[];
plotVisibility?: Record<string, boolean>;
cloudColors?: import('./ichimokuConfig').IchimokuCloudColors;
bandBackground?: import('../types').HlinesBackground;
};
/** 알림·가상투자 등 전략에서 생성된 차트 지표 id (워크스페이스 ind_* 와 구분) */
@@ -205,46 +204,14 @@ function buildOneIndicator(
getVisual: GetVisual,
stableId?: string,
): IndicatorConfig | null {
const def = getIndicatorDef(ref.registryType);
if (!def) return null;
const params = getParams(ref.registryType, def.defaultParams as ParamRecord);
const { plots, hlines, cloudColors } = getVisual(ref.registryType, def.plots, def.hlines ?? []);
let cfg: IndicatorConfig = {
id: stableId ?? stableStrategyIndicatorId(ref),
type: def.type,
params: { ...params },
plots,
hlines,
...(cloudColors ? { cloudColors } : {}),
};
/**
* 모든 보조지표의 기간(period)은 반드시 지표 설정(getParams, DB 저장값)을 사용한다.
* 전략 DSL 조건의 period/leftPeriod/rightPeriod 로 덮어쓰지 않는다.
* 조건의 기간 필드는 부정확(미설정 시 1 등)할 수 있어 SMA(1)=본선 겹침 등
* 잘못된 그래프를 만든다. 설정에 저장된 값(예: OBV 신호선 9일)이 유일한 기준.
* cfg.params 는 이미 getParams(저장 설정값) 결과이므로 그대로 사용.
*/
if (ref.registryType === 'SMA') {
const mainSma = buildMainIndicatorConfig('SMA', [], getParams, getVisual as Parameters<typeof buildMainIndicatorConfig>[3]);
cfg = normalizeSmaConfig({
...cfg,
params: mainSma?.params ?? cfg.params,
plots: mainSma?.plots ?? cfg.plots,
plotVisibility: mainSma?.plotVisibility ?? cfg.plotVisibility,
});
} else if (ref.registryType === 'OBV') {
cfg = {
...cfg,
params: normalizeObvParams({ ...cfg.params }),
};
}
if (ref.registryType === 'IchimokuCloud') {
cfg = normalizeIchimokuConfig(cfg);
}
const id = stableId ?? stableStrategyIndicatorId(ref);
let cfg = buildChartIndicatorConfig(
ref.registryType,
id,
getParams,
getVisual as Parameters<typeof buildChartIndicatorConfig>[3],
);
if (!cfg) return null;
if (ref.targetValue != null) {
cfg = { ...cfg, hlines: applyTargetHlines(cfg.hlines ?? [], ref.targetValue) };