goldenChat base source add
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* 볼린저밴드 상·하단 사이 영역 채움 (업비트 "백그라운드 그리기")
|
||||
*/
|
||||
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<Time> {
|
||||
private _ap: SeriesAttachedParameter<Time> | null = null;
|
||||
private _upper: ISeriesApi<SeriesType, Time>;
|
||||
private _lower: ISeriesApi<SeriesType, Time>;
|
||||
private _bg: BandBackgroundOptions;
|
||||
private _upperPts: ScreenPt[] = [];
|
||||
private _lowerPts: ScreenPt[] = [];
|
||||
|
||||
constructor(
|
||||
upperSeries: ISeriesApi<SeriesType, Time>,
|
||||
lowerSeries: ISeriesApi<SeriesType, Time>,
|
||||
background: BandBackgroundOptions,
|
||||
) {
|
||||
this._upper = upperSeries;
|
||||
this._lower = lowerSeries;
|
||||
this._bg = background;
|
||||
}
|
||||
|
||||
attached(ap: SeriesAttachedParameter<Time>): void {
|
||||
this._ap = ap;
|
||||
this._recompute();
|
||||
}
|
||||
detached(): void { this._ap = null; }
|
||||
|
||||
updateAllViews(): void { this._recompute(); }
|
||||
paneViews(): readonly IPrimitivePaneView[] {
|
||||
if (this._bg.visible === false || this._upperPts.length < 2) return [];
|
||||
return [new BandFillPaneView(this._upperPts, this._lowerPts, resolveFillColor(this._bg.color))];
|
||||
}
|
||||
|
||||
updateBackground(bg: BandBackgroundOptions): void {
|
||||
this._bg = bg;
|
||||
this._ap?.requestUpdate();
|
||||
}
|
||||
|
||||
requestRefresh(): void {
|
||||
this._recompute();
|
||||
this._ap?.requestUpdate();
|
||||
}
|
||||
|
||||
private _recompute(): void {
|
||||
if (!this._ap || this._bg.visible === false) {
|
||||
this._upperPts = [];
|
||||
this._lowerPts = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const ts = this._ap.chart.timeScale();
|
||||
const upperRaw = this._upper.data() as readonly { time: Time; value?: number }[];
|
||||
const lowerRaw = this._lower.data() as readonly { time: Time; value?: number }[];
|
||||
const lowerByTime = new Map<Time, number>();
|
||||
for (const d of lowerRaw) {
|
||||
if (d.value !== undefined && d.value !== null && !Number.isNaN(d.value)) {
|
||||
lowerByTime.set(d.time, d.value);
|
||||
}
|
||||
}
|
||||
|
||||
const upperPts: ScreenPt[] = [];
|
||||
const lowerPts: ScreenPt[] = [];
|
||||
|
||||
for (const u of upperRaw) {
|
||||
const lv = lowerByTime.get(u.time);
|
||||
if (u.value === undefined || u.value === null || Number.isNaN(u.value)) continue;
|
||||
if (lv === undefined || Number.isNaN(lv)) continue;
|
||||
const x = ts.timeToCoordinate(u.time);
|
||||
const uy = this._upper.priceToCoordinate(u.value);
|
||||
const ly = this._lower.priceToCoordinate(lv);
|
||||
if (x === null || uy === null || ly === null) continue;
|
||||
upperPts.push({ x: x as number, y: uy as number });
|
||||
lowerPts.push({ x: x as number, y: ly as number });
|
||||
}
|
||||
|
||||
this._upperPts = upperPts;
|
||||
this._lowerPts = lowerPts;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user