From d34a824174fa96aa33357f7fde1cd6ca4b25a110 Mon Sep 17 00:00:00 2001 From: Macbook Date: Tue, 16 Jun 2026 01:08:58 +0900 Subject: [PATCH] =?UTF-8?q?=EB=B3=B4=EC=A1=B0=EC=A7=80=ED=91=9C=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=A0=80=EC=9E=A5=20=EC=98=A4=EB=A5=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/chart/useChartWorkspace.ts | 6 +- frontend/src/components/ChartSlot.tsx | 1 + .../StrategyEvaluationChart.tsx | 12 +++- frontend/src/hooks/useIndicatorSettings.ts | 19 ++++++- frontend/src/utils/backendApi.ts | 28 ++++++++-- frontend/src/utils/indicatorMainConfig.ts | 3 + frontend/src/utils/indicatorPaneMerge.ts | 4 +- frontend/src/utils/indicatorSettingsEditor.ts | 9 ++- .../src/utils/strategyToChartIndicators.ts | 55 ++++--------------- 9 files changed, 80 insertions(+), 57 deletions(-) diff --git a/frontend/src/chart/useChartWorkspace.ts b/frontend/src/chart/useChartWorkspace.ts index dba8a8a..61dd182 100644 --- a/frontend/src/chart/useChartWorkspace.ts +++ b/frontend/src/chart/useChartWorkspace.ts @@ -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]); diff --git a/frontend/src/components/ChartSlot.tsx b/frontend/src/components/ChartSlot.tsx index 750de6d..befb24b 100644 --- a/frontend/src/components/ChartSlot.tsx +++ b/frontend/src/components/ChartSlot.tsx @@ -650,6 +650,7 @@ const ChartSlot = forwardRef(function ChartSlot updated.hlines, updated.cloudColors, updated.bandBackground, + updated.plotVisibility, ); }, [saveParams, saveVisual]); diff --git a/frontend/src/components/strategyEvaluation/StrategyEvaluationChart.tsx b/frontend/src/components/strategyEvaluation/StrategyEvaluationChart.tsx index 76f7503..df64f38 100644 --- a/frontend/src/components/strategyEvaluation/StrategyEvaluationChart.tsx +++ b/frontend/src/components/strategyEvaluation/StrategyEvaluationChart.tsx @@ -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 = ({ 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 = ({ 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 = ({ [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 = ({ {!loading && !error && bars.length > 0 && ( <> i.id).join(',')}`} + key={`${market}-${timeframe}-${strategy?.id ?? 0}-${paramsRevision}-${settingsRevision}-${indicatorRenderKey}`} bars={bars} barsMarket={market} market={market} diff --git a/frontend/src/hooks/useIndicatorSettings.ts b/frontend/src/hooks/useIndicatorSettings.ts index 0485060..666f52a 100644 --- a/frontend/src/hooks/useIndicatorSettings.ts +++ b/frontend/src/hooks/useIndicatorSettings.ts @@ -47,6 +47,8 @@ type AllSettings = Record; export interface IndicatorVisual { plots?: PlotDef[]; hlines?: HLineDef[]; + /** 지표선 on/off — plot id → visible */ + plotVisibility?: Record; cloudColors?: IchimokuCloudColors; /** 볼린저밴드 어퍼~로우어 배경 (업비트 백그라운드 그리기) */ bandBackground?: HlinesBackground; @@ -56,6 +58,7 @@ export interface IndicatorVisual { export interface IndicatorVisualConfig { plots: PlotDef[]; hlines: HLineDef[]; + plotVisibility?: Record; 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, + 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, ) => { 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); diff --git a/frontend/src/utils/backendApi.ts b/frontend/src/utils/backendApi.ts index fa582bd..f90ccf1 100644 --- a/frontend/src/utils/backendApi.ts +++ b/frontend/src/utils/backendApi.ts @@ -391,25 +391,43 @@ export async function saveIndicatorSettings( * 앱 시작 시 호출 — 전역 시각 기본값을 DB 에서 가져온다. */ export async function loadIndicatorVisualSettings(): Promise< - Record + Record; + cloudColors?: unknown; + bandBackground?: unknown; + }> > { return ( - (await request>( + (await request; + 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; + cloudColors?: unknown; + bandBackground?: unknown; + }, ): Promise { await request(`/indicator-settings/${encodeURIComponent(indicatorType)}/visual`, { method: 'PATCH', diff --git a/frontend/src/utils/indicatorMainConfig.ts b/frontend/src/utils/indicatorMainConfig.ts index f423710..819a10f 100644 --- a/frontend/src/utils/indicatorMainConfig.ts +++ b/frontend/src/utils/indicatorMainConfig.ts @@ -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); diff --git a/frontend/src/utils/indicatorPaneMerge.ts b/frontend/src/utils/indicatorPaneMerge.ts index 7b95c33..0d467ac 100644 --- a/frontend/src/utils/indicatorPaneMerge.ts +++ b/frontend/src/utils/indicatorPaneMerge.ts @@ -152,6 +152,7 @@ type GetVisual = ( ) => { plots: PlotDef[]; hlines: HLineDef[]; + plotVisibility?: Record; 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, diff --git a/frontend/src/utils/indicatorSettingsEditor.ts b/frontend/src/utils/indicatorSettingsEditor.ts index 2f87a84..b39a121 100644 --- a/frontend/src/utils/indicatorSettingsEditor.ts +++ b/frontend/src/utils/indicatorSettingsEditor.ts @@ -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; + 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, }); } diff --git a/frontend/src/utils/strategyToChartIndicators.ts b/frontend/src/utils/strategyToChartIndicators.ts index cb83891..e47378e 100644 --- a/frontend/src/utils/strategyToChartIndicators.ts +++ b/frontend/src/utils/strategyToChartIndicators.ts @@ -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; 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[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[3], + ); + if (!cfg) return null; if (ref.targetValue != null) { cfg = { ...cfg, hlines: applyTargetHlines(cfg.hlines ?? [], ref.targetValue) };