182 lines
6.3 KiB
TypeScript
182 lines
6.3 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;
|
|
|
|
// 한국 관례: SpanA > SpanB → 양운(빨강), SpanA <= SpanB → 음운(청록)
|
|
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(),
|
|
);
|
|
}
|
|
}
|
|
|
|
// ─── 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; }
|
|
|
|
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; }
|
|
}
|