/** * 볼린저밴드 상·하단 사이 영역 채움 (업비트 "백그라운드 그리기") */ import type { ISeriesPrimitive, IPrimitivePaneView, SeriesAttachedParameter, Time, SeriesType, ISeriesApi, } from 'lightweight-charts'; import type { CanvasRenderingTarget2D, MediaCoordinatesRenderingScope } from 'fancy-canvas'; interface ScreenPt { x: number; y: number } function hexToRgba(hex: string, alpha: number): string { const h = hex.startsWith('#') ? hex.slice(1) : hex; const r = parseInt(h.slice(0, 2), 16); const g = parseInt(h.slice(2, 4), 16); const b = parseInt(h.slice(4, 6), 16); const a = h.length >= 8 ? parseInt(h.slice(6, 8), 16) / 255 : alpha; return `rgba(${r},${g},${b},${a})`; } function resolveFillColor(color: string): string { if (color.length >= 9) { return hexToRgba(color.slice(0, 7), parseInt(color.slice(7, 9), 16) / 255); } return hexToRgba(color.slice(0, 7), 0.15); } class BandFillRenderer { constructor( private readonly upper: ScreenPt[], private readonly lower: ScreenPt[], private readonly fillColor: string, ) {} draw(target: CanvasRenderingTarget2D): void { if (this.upper.length < 2 || this.lower.length < 2) return; target.useMediaCoordinateSpace((scope: MediaCoordinatesRenderingScope) => { const ctx = scope.context; const n = this.upper.length; ctx.save(); ctx.fillStyle = this.fillColor; ctx.beginPath(); ctx.moveTo(this.upper[0].x, this.upper[0].y); for (let i = 1; i < n; i++) ctx.lineTo(this.upper[i].x, this.upper[i].y); for (let i = n - 1; i >= 0; i--) ctx.lineTo(this.lower[i].x, this.lower[i].y); ctx.closePath(); ctx.fill(); ctx.restore(); }); } } class BandFillPaneView implements IPrimitivePaneView { constructor( private readonly upper: ScreenPt[], private readonly lower: ScreenPt[], private readonly fillColor: string, ) {} renderer() { return new BandFillRenderer(this.upper, this.lower, this.fillColor); } zOrder(): 'bottom' { return 'bottom'; } } export interface BandBackgroundOptions { visible: boolean; color: string; } /** basis 시리즈에 부착, upper/lower 데이터로 채움 */ export class BollingerBandFillPrimitive implements ISeriesPrimitive