import { createChart, CandlestickSeries, BarSeries, LineSeries, AreaSeries, BaselineSeries, HistogramSeries, createSeriesMarkers, type IChartApi, type ISeriesApi, type SeriesType, type MouseEventParams, type Time, type TickMarkFormatter, type LogicalRange, type IPriceLine, type ISeriesMarkersPluginApi, ColorType, LineStyle, CrosshairMode, } from 'lightweight-charts'; import { IndicatorFillPrimitive } from './IndicatorFillPrimitive'; import { BollingerBandFillPrimitive } from './BollingerBandFillPrimitive'; import { resolveBbBandBackground } from './bollingerConfig'; import type { OHLCVBar, ChartType, Theme, IndicatorConfig, Timeframe } from '../types'; import { formatChartAxisPrice, formatIndicatorAxisPrice } from './dataGenerator'; import { TRADE_BUY_COLOR, TRADE_SELL_COLOR } from './tradeSignalColors'; import { DEFAULT_CHART_TIME_FORMAT, formatUnixWithChartPattern, formatLwcTimeWithPattern, formatLwcTickMarkWithPattern, normalizeChartTimeFormat, } from './chartTimeFormat'; import { DEFAULT_DISPLAY_TIMEZONE } from './timezone'; import { sortIndicatorsForPaneLoad } from './indicatorPaneMerge'; import { calculateIndicator, enrichIndicatorConfig, getIndicatorDef, getHLineLabel, type PlotData, type MarkerData } from './indicatorRegistry'; import { IchimokuCloudPlugin, type IchimokuCloudPoint } from './IchimokuCloudPlugin'; const MAIN_PRICE_FORMAT = { type: 'custom' as const, minMove: 1, formatter: formatChartAxisPrice, }; const INDICATOR_PRICE_FORMAT = { type: 'custom' as const, minMove: 0.01, formatter: formatIndicatorAxisPrice, }; function priceFormatForIndicatorPane(paneIndex: number) { return paneIndex >= 2 ? INDICATOR_PRICE_FORMAT : undefined; } import { getIchimokuPlotTitle, isIchimokuCloudVisible, resolveIchimokuCloudColors, resolveIchimokuDisplacements, } from './ichimokuConfig'; import type { PlotDef, HLineStyle } from './indicatorRegistry'; import { mapHistogramSeriesData, histogramBarColor } from './plotColorUtils'; import { applyPaneSeparatorToChartOptions, resolveChartPaneSeparatorOptions, type ChartPaneSeparatorOptions, } from '../types/chartPaneSeparator'; function plotSeriesLineStyle(style?: HLineStyle): number { if (style === 'dashed') return LineStyle.Dashed; if (style === 'dotted') return LineStyle.Dotted; return LineStyle.Solid; } import { calcAutoFib } from './calculations'; interface ThemeTokens { bgColor: string; textColor: string; gridColor: string; borderColor: string; crosshairColor: string; upColor: string; downColor: string; } // 다크: 도쿄 나이트 스타일 (THEME_SETTINGS.md §2.2 / §3 darkChartColors) const DARK: ThemeTokens = { bgColor: '#1a1b26', textColor: '#c0caf5', gridColor: 'rgba(122, 162, 247, 0.10)', borderColor: 'rgba(122, 162, 247, 0.15)', crosshairColor: 'rgba(122, 162, 247, 0.60)', upColor: '#ff6b6b', // 한국 관례: 상승=빨간색 downColor: '#4dabf7', // 한국 관례: 하락=파란색 }; // 라이트 (THEME_SETTINGS.md §2.1 / §3 lightChartColors) const LIGHT: ThemeTokens = { bgColor: '#ffffff', textColor: '#212121', gridColor: '#f0f0f0', borderColor: 'rgba(0, 0, 0, 0.08)', crosshairColor: 'rgba(33, 150, 243, 0.50)', upColor: '#c62828', // 한국 관례: 상승=빨간색 downColor: '#1565c0', // 한국 관례: 하락=파란색 }; // 블루: 딥 네이비 (THEME_SETTINGS.md §2.3 / §3 blueChartColors) const BLUE: ThemeTokens = { bgColor: '#0c1929', textColor: '#e8f4ff', gridColor: 'rgba(94, 181, 255, 0.10)', borderColor: 'rgba(94, 181, 255, 0.15)', crosshairColor: 'rgba(94, 181, 255, 0.60)', upColor: '#ff5555', // 한국 관례: 상승=빨간색 downColor: '#55aaff', // 한국 관례: 하락=파란색 }; interface IndicatorEntry { id: string; type: string; seriesList: ISeriesApi[]; /** * seriesList 순서와 1:1 대응 * - plotId: 해당 시리즈가 매핑된 plots 객체의 키 (예: 'plot0', 'plot1') * → _applyLastPointsToSeries 에서 인덱스 대신 plotId 로 정확히 조회 * - isHistogram: histogram 시리즈 여부 (색상 재계산용) * - color: 기본 색상 */ seriesMeta: Array<{ plotId: string; isHistogram: boolean; color: string }>; paneIndex?: number; alertLines: IPriceLine[]; /** 수평 기준선(과매수/과매도/0선)의 IPriceLine 참조 — 첫 번째 시리즈에만 생성 */ hlineRefs: IPriceLine[]; /** 과매수/과매도 음영 채움 프리미티브 */ fillPrimitive?: import('./IndicatorFillPrimitive').IndicatorFillPrimitive; /** 볼린저밴드 어퍼~로우어 영역 채움 */ bbFillPrimitive?: BollingerBandFillPrimitive; config: IndicatorConfig; cloudPlugin?: IchimokuCloudPlugin; // 일목균형표 구름 플러그인 } interface AlertEntry { price: number; label: string; line: IPriceLine; } interface FibLevel { price: number; label: string; line: IPriceLine; } function getTheme(theme: Theme): ThemeTokens { if (theme === 'blue') return BLUE; if (theme === 'light') return LIGHT; return DARK; } function seriesByPlotId( entry: IndicatorEntry, plotId: string, ): ISeriesApi | undefined { const i = entry.seriesMeta.findIndex(m => m.plotId === plotId); return i >= 0 ? entry.seriesList[i] : undefined; } function shouldShowBbBandFill(config: IndicatorConfig): boolean { const bg = resolveBbBandBackground(config.bandBackground); if (!bg.visible) return false; if (config.hidden === true) return false; if (config.plotVisibility?.plot1 === false || config.plotVisibility?.plot2 === false) return false; return true; } function attachBbBandFill(entry: IndicatorEntry, config: IndicatorConfig): void { if (!shouldShowBbBandFill(config)) { detachBbBandFill(entry); return; } const basis = seriesByPlotId(entry, 'plot0'); const upper = seriesByPlotId(entry, 'plot1'); const lower = seriesByPlotId(entry, 'plot2'); if (!basis || !upper || !lower) return; const bg = resolveBbBandBackground(config.bandBackground); if (entry.bbFillPrimitive) { entry.bbFillPrimitive.updateBackground(bg); entry.bbFillPrimitive.requestRefresh(); return; } entry.bbFillPrimitive = new BollingerBandFillPrimitive(upper, lower, bg); basis.attachPrimitive(entry.bbFillPrimitive); } function detachBbBandFill(entry: IndicatorEntry): void { if (!entry.bbFillPrimitive) return; const basis = seriesByPlotId(entry, 'plot0'); if (basis) { try { basis.detachPrimitive(entry.bbFillPrimitive); } catch { /* ok */ } } entry.bbFillPrimitive = undefined; } export class ChartManager { private chart: IChartApi; private container: HTMLElement; private mainSeries: ISeriesApi | null = null; private volumeSeries: ISeriesApi<'Histogram'> | null = null; private indicators = new Map(); private alertEntries: AlertEntry[] = []; private fibLines: FibLevel[] = []; private drawingTool = 'cursor'; private drawingPoints: { time: Time; price: number }[] = []; private drawingLines: IPriceLine[] = []; private _clickHandler: ((p: MouseEventParams