diff --git a/frontend/src/components/BacktestHistoryPage.tsx b/frontend/src/components/BacktestHistoryPage.tsx index 2cbeb6d..1737a4d 100644 --- a/frontend/src/components/BacktestHistoryPage.tsx +++ b/frontend/src/components/BacktestHistoryPage.tsx @@ -14,7 +14,13 @@ import { } from '../utils/backendApi'; import type { Theme } from '../types'; import { buildEquityFromSignals } from '../utils/backtestEquity'; -import { normalizeEpochSec, normalizeEpochSecOptional } from '../utils/backtestUiUtils'; +import { + formatChartTypeKo, + formatTimeframeKo, + normalizeEpochSec, + normalizeEpochSecOptional, + toUpbitMarket, +} from '../utils/backtestUiUtils'; import BacktestExecutionList, { type ExecutionListTab } from './backtest/BacktestExecutionList'; import BacktestAnalysisChart from './backtest/BacktestAnalysisChart'; import BacktestKpiPanel from './backtest/BacktestKpiPanel'; @@ -29,10 +35,6 @@ import { import '../styles/strategyEditorTheme.css'; import { getKoreanName } from '../utils/marketNameCache'; -function toUpbitMarket(symbol: string): string { - return symbol.startsWith('KRW-') ? symbol : `KRW-${symbol}`; -} - const LEFT_KEY = 'btd-left-width'; const RIGHT_KEY = 'btd-right-width'; const LEFT_DEFAULT = 380; @@ -199,7 +201,10 @@ export function BacktestHistoryPage({ theme = 'dark' }: Props) { return { ko: getKoreanName(market), strategy: selectedBacktest.strategyName || '전략 없음', - meta: selectedBacktest.timeframe, + metaParts: [ + formatTimeframeKo(selectedBacktest.timeframe), + formatChartTypeKo('candlestick'), + ], }; } if (tab === 'live' && selectedLive) { @@ -207,11 +212,15 @@ export function BacktestHistoryPage({ theme = 'dark' }: Props) { return { ko: getKoreanName(market), strategy: selectedLive.strategyLabel, - meta: selectedLive.sourceLabel, + metaParts: [ + formatTimeframeKo(chartProps?.timeframe ?? '1h'), + formatChartTypeKo('candlestick'), + selectedLive.sourceLabel, + ], }; } return null; - }, [tab, selectedBacktest, selectedLive]); + }, [tab, selectedBacktest, selectedLive, chartProps]); const footerTrades = equityData.trades; @@ -276,7 +285,12 @@ export function BacktestHistoryPage({ theme = 'dark' }: Props) {

{centerHead.ko}

{centerHead.strategy} - {centerHead.meta ? · {centerHead.meta} : null} + {centerHead.metaParts.length > 0 && ( + + {' · '} + {centerHead.metaParts.filter(Boolean).join(' · ')} + + )}

)} diff --git a/frontend/src/components/backtest/BacktestAnalysisChart.tsx b/frontend/src/components/backtest/BacktestAnalysisChart.tsx index 3a1b910..519bbf4 100644 --- a/frontend/src/components/backtest/BacktestAnalysisChart.tsx +++ b/frontend/src/components/backtest/BacktestAnalysisChart.tsx @@ -8,7 +8,13 @@ import type { ChartManager } from '../../utils/ChartManager'; import { useHistoryLoader, LOAD_MORE_TRIGGER } from '../../hooks/useHistoryLoader'; import { isUpbitMarket } from '../../utils/upbitApi'; import { DEFAULT_DISPLAY_TIMEZONE } from '../../utils/timezone'; -import { normalizeChartTimeframe, timeframeBarSeconds } from '../../utils/backtestUiUtils'; +import { + formatChartTypeKo, + formatTimeframeKo, + normalizeChartTimeframe, + timeframeBarSeconds, +} from '../../utils/backtestUiUtils'; +import { getKoreanName } from '../../utils/marketNameCache'; import { useIndicatorSettings } from '../../hooks/useIndicatorSettings'; import { useAppSettings } from '../../hooks/useAppSettings'; import { enrichIndicatorConfig, getIndicatorDef } from '../../utils/indicatorRegistry'; @@ -294,7 +300,9 @@ const BacktestAnalysisChart: React.FC = ({ if (mgr) applyPaneRatio(mgr); }, [paneAreaRatio, applyPaneRatio]); - const symLabel = market.replace(/^KRW-/, ''); + const koName = getKoreanName(market); + const tfKo = formatTimeframeKo(chartTimeframe); + const chartTypeKo = formatChartTypeKo(chartType); const stratTf = chartTimeframe; const priceLabels = appDefaults.chartSeriesPriceLabels !== false; @@ -303,9 +311,9 @@ const BacktestAnalysisChart: React.FC = ({
- {symLabel} - {chartTimeframe} - Candle + {koName} + {tfKo} + {chartTypeKo}
diff --git a/frontend/src/components/backtest/BacktestExecutionList.tsx b/frontend/src/components/backtest/BacktestExecutionList.tsx index f837734..8246fa9 100644 --- a/frontend/src/components/backtest/BacktestExecutionList.tsx +++ b/frontend/src/components/backtest/BacktestExecutionList.tsx @@ -4,13 +4,16 @@ import type { LiveExecutionItem } from '../../utils/liveExecutionGroups'; import BacktestSparkline from './BacktestSparkline'; import { buildEquityFromSignals } from '../../utils/backtestEquity'; import { paperTradesToSignals } from '../../utils/liveExecutionGroups'; -import { fmtListTimestamp, fmtShortTimestamp, pct, pctAbs } from '../../utils/backtestUiUtils'; +import { + fmtListTimestamp, + fmtShortTimestamp, + formatTimeframeKo, + pct, + pctAbs, + toUpbitMarket, +} from '../../utils/backtestUiUtils'; import { getKoreanName } from '../../utils/marketNameCache'; -function toUpbitMarket(symbol: string): string { - return symbol.startsWith('KRW-') ? symbol : `KRW-${symbol}`; -} - export type ExecutionListTab = 'backtest' | 'live'; export type BacktestListSort = 'strategy' | 'symbol' | 'return'; @@ -95,7 +98,7 @@ export default function BacktestExecutionList({
{ko} - {strategy} · {r.timeframe} + {strategy} · {formatTimeframeKo(r.timeframe)} {fmtListTimestamp(r.createdAt)}
diff --git a/frontend/src/utils/ChartManager.ts b/frontend/src/utils/ChartManager.ts index 670dcf2..7a99f3c 100644 --- a/frontend/src/utils/ChartManager.ts +++ b/frontend/src/utils/ChartManager.ts @@ -71,6 +71,7 @@ import { type ChartCustomOverlaySlotId, createDefaultChartCustomOverlaySelection, isCustomIchimokuCloudVisible, + isCustomOverlayPlotControlledType, isCustomPlotSelected, } from './chartCustomOverlay'; import type { PlotDef, HLineStyle } from './indicatorRegistry'; @@ -1372,12 +1373,14 @@ export class ChartManager { } const showPriceLabel = this.shouldShowSeriesPriceLabel(_config, info.lastVal, pane); + const lineVisible = this._resolveSeriesVisible(_config, info.id); const series = this.chart.addSeries(LineSeries, { color: info.color, lineWidth: 1, priceLineVisible: false, - lastValueVisible: showPriceLabel, - title: showPriceLabel ? info.title : '', + lastValueVisible: showPriceLabel && lineVisible, + title: showPriceLabel && lineVisible ? info.title : '', + visible: lineVisible, }, pane); series.setData(seriesData); seriesList.push(series); @@ -2435,9 +2438,13 @@ export class ChartManager { /** 사용자 hidden + 세션 오버레이 토글 — entry.config.hidden 은 사용자 의도만 반영 */ private _isIndicatorEffectivelyVisible(config: IndicatorConfig): boolean { + if (config.hidden === true) return false; + if (this._isCustomOverlayApplied() && isCustomOverlayPlotControlledType(config.type)) { + return true; + } const overlayKey = chartOverlayKeyForType(config.type); if (overlayKey && !this._chartOverlayVisibility[overlayKey]) return false; - return config.hidden !== true; + return true; } getChartCustomOverlayActive(slot: ChartCustomOverlaySlotId = 'custom'): boolean { @@ -2458,14 +2465,15 @@ export class ChartManager { selection: ChartCustomOverlaySelection, slot: ChartCustomOverlaySlotId = 'custom', ): void { + const defaults = createDefaultChartCustomOverlaySelection(); this._customOverlaySlots[slot].selection = { - smaPlots: { ...selection.smaPlots }, - bollinger: { ...selection.bollinger }, - ichimoku: { ...selection.ichimoku }, + smaPlots: { ...defaults.smaPlots, ...selection.smaPlots }, + bollinger: { ...defaults.bollinger, ...selection.bollinger }, + ichimoku: { ...defaults.ichimoku, ...selection.ichimoku }, candle: selection.candle, }; if (this._customOverlaySlots[slot].active) { - this.syncChartOverlayVisibility(); + this._syncCustomOverlayVisibilityToChart(); } } @@ -2477,7 +2485,18 @@ export class ChartManager { const other: ChartCustomOverlaySlotId = slot === 'custom' ? 'custom1' : 'custom'; this._customOverlaySlots[other].active = false; } + this._syncCustomOverlayVisibilityToChart(); + } + + /** Custom 적용 중 — plot·캔들·구름 가시성을 선택값과 동기화 */ + private _syncCustomOverlayVisibilityToChart(): void { this.syncChartOverlayVisibility(); + if (!this._isCustomOverlayApplied()) return; + for (const entry of this.indicators.values()) { + const config = entry.config; + if (!config || !isCustomOverlayPlotControlledType(config.type)) continue; + this.applyIndicatorStyle(config); + } } /** custom 미적용 시 지표 설정 plotVisibility, 적용 시 custom 선택만 반영 */ @@ -2496,9 +2515,11 @@ export class ChartManager { } private _resolveCandleVisible(): boolean { + if (this._isCustomOverlayApplied()) { + return this._getAppliedCustomOverlaySelection().candle; + } if (!this._chartOverlayVisibility.candle) return false; - if (!this._isCustomOverlayApplied()) return true; - return this._getAppliedCustomOverlaySelection().candle; + return true; } private _shouldShowBbBandFillForConfig(config: IndicatorConfig): boolean { diff --git a/frontend/src/utils/backtestUiUtils.ts b/frontend/src/utils/backtestUiUtils.ts index 91869b0..5498c09 100644 --- a/frontend/src/utils/backtestUiUtils.ts +++ b/frontend/src/utils/backtestUiUtils.ts @@ -1,4 +1,41 @@ -import type { Timeframe } from '../types'; +import type { ChartType, Timeframe } from '../types'; + +export function toUpbitMarket(symbol: string): string { + return symbol.startsWith('KRW-') ? symbol : `KRW-${symbol}`; +} + +const TIMEFRAME_KO: Record = { + '1m': '1분', + '3m': '3분', + '5m': '5분', + '10m': '10분', + '15m': '15분', + '30m': '30분', + '1h': '1시간', + '4h': '4시간', + '1D': '1일', + '1W': '1주', + '1M': '1월', +}; + +/** 차트·목록용 시간봉 한글 라벨 */ +export function formatTimeframeKo(raw: string | undefined): string { + const tf = normalizeChartTimeframe(raw); + return TIMEFRAME_KO[tf] ?? tf; +} + +const CHART_TYPE_KO: Record = { + candlestick: '캔들스틱', + bar: '바', + line: '라인', + area: '영역', + baseline: '기준선', +}; + +export function formatChartTypeKo(type: ChartType | string | undefined): string { + if (!type) return '캔들스틱'; + return CHART_TYPE_KO[type as ChartType] ?? String(type); +} export function normalizeChartTimeframe(raw: string | undefined): Timeframe { const t = (raw ?? '1h').trim(); diff --git a/frontend/src/utils/chartCustomOverlay.ts b/frontend/src/utils/chartCustomOverlay.ts index f6b24f2..0a5718c 100644 --- a/frontend/src/utils/chartCustomOverlay.ts +++ b/frontend/src/utils/chartCustomOverlay.ts @@ -88,6 +88,17 @@ export const ICHIMOKU_CUSTOM_ROWS: Array<{ { key: 'bearishCloud', label: '하락 구름' }, ]; +/** Custom 적용 시 plot 단위로 표시를 제어하는 지표 type */ +export const CUSTOM_OVERLAY_PLOT_CONTROLLED_TYPES = [ + 'SMA', + 'BollingerBands', + 'IchimokuCloud', +] as const; + +export function isCustomOverlayPlotControlledType(type: string): boolean { + return (CUSTOM_OVERLAY_PLOT_CONTROLLED_TYPES as readonly string[]).includes(type); +} + /** 지표 설정 plotVisibility 와 무관 — custom 적용 시 이 맵만 사용 */ export function isCustomPlotSelected( sel: ChartCustomOverlaySelection,