Files
goldenChart/frontend/src/utils/IchimokuCloudPlugin.ts
T
Macbook c6976a2e03 fix: 일목 구름 zOrder·배치 후 갱신 및 잘못된 기간 파라미터 복원
프로덕션에서 구름이 다른 primitive/시리즈에 가려지지 않도록 bottom zOrder를 적용하고,
지표 배치·재로드 후 구름 데이터를 다시 적용한다. DB에 1로 저장된 기간은 기본값으로 복원한다.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 23:36:12 +09:00

188 lines
6.4 KiB
TypeScript

/**
* Ichimoku Cloud Fill Plugin
* LWC v5 ISeriesPrimitive를 이용해 선행스팬A/B 사이 구름 영역을 캔버스에 직접 렌더링합니다.
*
* 한국 관례:
* SpanA > SpanB → 양운 (빨간 구름, 상승 신호)
* SpanA <= SpanB → 음운 (청록 구름, 하락 신호)
*/
import type {
ISeriesPrimitive,
IPrimitivePaneView,
IPrimitivePaneRenderer,
SeriesAttachedParameter,
ISeriesApi,
SeriesType,
Time,
IChartApiBase,
} from 'lightweight-charts';
export interface IchimokuCloudPoint {
time: number; // Unix timestamp (seconds)
spanA: number;
spanB: number;
}
function toCanvasFillColor(color: string): string {
if (color.startsWith('rgba(') || color.startsWith('rgb(')) return color;
const hex = color.startsWith('#') ? color.slice(1) : color;
if (hex.length < 6) return color;
const r = parseInt(hex.slice(0, 2), 16);
const g = parseInt(hex.slice(2, 4), 16);
const b = parseInt(hex.slice(4, 6), 16);
const a = hex.length >= 8
? parseInt(hex.slice(6, 8), 16) / 255
: 1;
return `rgba(${r}, ${g}, ${b}, ${a})`;
}
// ─── Renderer ──────────────────────────────────────────────────────────────
class CloudRenderer implements IPrimitivePaneRenderer {
private _chart: IChartApiBase<Time>;
private _series: ISeriesApi<SeriesType>;
private _data: IchimokuCloudPoint[];
private _bullishColor: string;
private _bearishColor: string;
private _bullishVisible: boolean;
private _bearishVisible: boolean;
constructor(
chart: IChartApiBase<Time>,
series: ISeriesApi<SeriesType>,
data: IchimokuCloudPoint[],
bullishColor: string,
bearishColor: string,
bullishVisible: boolean,
bearishVisible: boolean,
) {
this._chart = chart;
this._series = series;
this._data = data;
this._bullishColor = bullishColor;
this._bearishColor = bearishColor;
this._bullishVisible = bullishVisible;
this._bearishVisible = bearishVisible;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
draw(target: any): void {
if (this._data.length < 2) return;
const timeScale = this._chart.timeScale();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
target.useBitmapCoordinateSpace(({ context: ctx, horizontalPixelRatio: hpr, verticalPixelRatio: vpr }: any) => {
for (let i = 0; i < this._data.length - 1; i++) {
const a = this._data[i];
const b = this._data[i + 1];
const x1 = timeScale.timeToCoordinate(a.time as Time);
const x2 = timeScale.timeToCoordinate(b.time as Time);
if (x1 == null || x2 == null) continue;
const aAy = this._series.priceToCoordinate(a.spanA);
const aBy = this._series.priceToCoordinate(a.spanB);
const bAy = this._series.priceToCoordinate(b.spanA);
const bBy = this._series.priceToCoordinate(b.spanB);
if (aAy == null || aBy == null || bAy == null || bBy == null) continue;
const bullish = (a.spanA + b.spanA) > (a.spanB + b.spanB);
if (bullish && !this._bullishVisible) continue;
if (!bullish && !this._bearishVisible) continue;
ctx.save();
ctx.beginPath();
ctx.moveTo(x1 * hpr, aAy * vpr);
ctx.lineTo(x2 * hpr, bAy * vpr);
ctx.lineTo(x2 * hpr, bBy * vpr);
ctx.lineTo(x1 * hpr, aBy * vpr);
ctx.closePath();
ctx.fillStyle = bullish ? this._bullishColor : this._bearishColor;
ctx.fill();
ctx.restore();
}
});
}
}
// ─── Pane View ─────────────────────────────────────────────────────────────
class CloudPaneView implements IPrimitivePaneView {
private _plugin: IchimokuCloudPlugin;
constructor(plugin: IchimokuCloudPlugin) { this._plugin = plugin; }
renderer(): IPrimitivePaneRenderer | null {
const chart = this._plugin.getChart();
const series = this._plugin.getSeries();
if (!chart || !series) return null;
return new CloudRenderer(
chart,
series,
this._plugin.getCloudData(),
this._plugin.getBullishColor(),
this._plugin.getBearishColor(),
this._plugin.getBullishVisible(),
this._plugin.getBearishVisible(),
);
}
/** BB·RSI 영역 등 다른 primitive/시리즈보다 아래에 그려 구름이 가려지지 않게 함 */
zOrder(): 'bottom' { return 'bottom'; }
}
// ─── Plugin ────────────────────────────────────────────────────────────────
export class IchimokuCloudPlugin implements ISeriesPrimitive<Time> {
private _chart: IChartApiBase<Time> | null = null;
private _series: ISeriesApi<SeriesType> | null = null;
private _data: IchimokuCloudPoint[] = [];
private _bullishColor = 'rgba(239, 83, 80, 0.2)';
private _bearishColor = 'rgba(38, 166, 154, 0.2)';
private _bullishVisible = true;
private _bearishVisible = true;
private _updateFn: (() => void) | null = null;
attached({ chart, series, requestUpdate }: SeriesAttachedParameter<Time>): void {
this._chart = chart;
this._series = series;
this._updateFn = requestUpdate;
}
detached(): void {
this._chart = null;
this._series = null;
}
setCloudData(data: IchimokuCloudPoint[]): void {
this._data = data;
this._updateFn?.();
}
setColors(bullishColor: string, bearishColor: string): void {
this._bullishColor = toCanvasFillColor(bullishColor);
this._bearishColor = toCanvasFillColor(bearishColor);
this._updateFn?.();
}
setVisibility(bullishVisible: boolean, bearishVisible: boolean): void {
this._bullishVisible = bullishVisible;
this._bearishVisible = bearishVisible;
this._updateFn?.();
}
getBullishColor() { return this._bullishColor; }
getBearishColor() { return this._bearishColor; }
getBullishVisible() { return this._bullishVisible; }
getBearishVisible() { return this._bearishVisible; }
updateAllViews(): void {
this._updateFn?.();
}
paneViews(): readonly IPrimitivePaneView[] {
if (!this._chart || !this._series) return [];
return [new CloudPaneView(this)];
}
getChart() { return this._chart; }
getSeries() { return this._series; }
getCloudData() { return this._data; }
}