fix: 일목균형표 구름 - 프로덕션 빌드 SpanA==SpanB 버그 수정

프로덕션 빌드에서 lightweight-charts-indicators 라이브러리가
plot3(SpanA)와 plot4(SpanB)에 동일한 값을 반환하는 버그 존재.
SpanA==SpanB 시 폴리곤 면적=0으로 구름이 보이지 않음.

해결: 라이브러리의 plot3/plot4 대신 convPlot(전환선)/basePlot(기준선)과
rawBars 기반 _donchianAt()으로 SpanA/SpanB를 직접 계산

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Macbook
2026-06-02 22:23:51 +09:00
parent 27e6f644e8
commit cc067ce2d1
2 changed files with 22 additions and 45 deletions
+17 -20
View File
@@ -1382,34 +1382,31 @@ export class ChartManager {
/** 구름 데이터 빌드: 과거(역사 데이터) + 미래 26봉 포함 */ /** 구름 데이터 빌드: 과거(역사 데이터) + 미래 26봉 포함 */
private _buildIchimokuCloudData( private _buildIchimokuCloudData(
spanAPlot: PlotData, _spanAPlot: PlotData,
spanBPlot: PlotData, _spanBPlot: PlotData,
convPlot: PlotData, convPlot: PlotData,
basePlot: PlotData, basePlot: PlotData,
displacement: number, displacement: number,
laggingPeriod: number, laggingPeriod: number,
): IchimokuCloudPoint[] { ): IchimokuCloudPoint[] {
// 과거 구름 // 라이브러리의 plot3/plot4(SpanA/SpanB)는 프로덕션 빌드에서
const bMap = new Map( // 동일한 값을 반환하는 버그가 있으므로, convPlot(전환선)/basePlot(기준선)과
(spanBPlot ?? []).filter(p => p.value != null && !isNaN(p.value)).map(p => [p.time, p.value]) // rawBars 기반 _donchianAt으로 직접 재계산한다.
); const N = this.rawBars.length;
const historical: IchimokuCloudPoint[] = (spanAPlot ?? []) const historical: IchimokuCloudPoint[] = [];
.filter(p => p.value != null && !isNaN(p.value) && bMap.has(p.time))
.map(p => ({ time: p.time as number, spanA: p.value, spanB: bMap.get(p.time)! }));
// 진단 로그: spanA/spanB 원본 값 비교 for (let i = displacement - 1; i < N; i++) {
const aValid = (spanAPlot ?? []).filter(p => p.value != null && !isNaN(p.value)); const srcIdx = i - (displacement - 1);
const bValid = (spanBPlot ?? []).filter(p => p.value != null && !isNaN(p.value)); const conv = convPlot[srcIdx]?.value;
const aLast3 = aValid.slice(-3).map(p => p.value); const base = basePlot[srcIdx]?.value;
const bLast3 = bValid.slice(-3).map(p => p.value); if (conv == null || isNaN(conv) || base == null || isNaN(base)) continue;
const histLast3 = historical.slice(-3).map(p => ({ a: p.spanA, b: p.spanB, eq: p.spanA === p.spanB })); const spanA = (conv + base) / 2;
const maxHistDiff = historical.length > 0 ? Math.max(...historical.map(p => Math.abs(p.spanA - p.spanB))) : 0; const spanB = this._donchianAt(srcIdx, laggingPeriod);
console.log('[IchimokuCloud] BUILD RAW: spanA last3=', aLast3, 'spanB last3=', bLast3, if (spanB === null) continue;
'| hist last3=', JSON.stringify(histLast3), '| maxDiff=', maxHistDiff, historical.push({ time: this.rawBars[i].time, spanA, spanB });
'| histLen=', historical.length, 'sameRef=', spanAPlot === spanBPlot); }
// 미래 구름 // 미래 구름
const N = this.rawBars.length;
if (N < 2) return historical; if (N < 2) return historical;
const interval = this.rawBars[N - 1].time - this.rawBars[N - 2].time; const interval = this.rawBars[N - 1].time - this.rawBars[N - 2].time;
const lastTime = this.rawBars[N - 1].time; const lastTime = this.rawBars[N - 1].time;
+3 -23
View File
@@ -71,35 +71,24 @@ class CloudRenderer implements IPrimitivePaneRenderer {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
target.useBitmapCoordinateSpace(({ context: ctx, horizontalPixelRatio: hpr, verticalPixelRatio: vpr }: any) => { target.useBitmapCoordinateSpace(({ context: ctx, horizontalPixelRatio: hpr, verticalPixelRatio: vpr }: any) => {
// spanA/spanB 차이 진단
const maxPriceDiff = Math.max(...this._data.map(d => Math.abs(d.spanA - d.spanB)));
const mid = this._data[Math.floor(this._data.length / 2)];
const last = this._data[this._data.length - 1];
console.log('[IchimokuCloud] DATA CHECK: maxPriceDiff=', maxPriceDiff,
'| mid[', Math.floor(this._data.length / 2), '] spanA=', mid.spanA, 'spanB=', mid.spanB,
'| last spanA=', last.spanA, 'spanB=', last.spanB,
'canvas_w=', ctx.canvas?.width, 'canvas_h=', ctx.canvas?.height, 'hpr=', hpr, 'vpr=', vpr);
let drawn = 0, skippedTime = 0, skippedPrice = 0;
for (let i = 0; i < this._data.length - 1; i++) { for (let i = 0; i < this._data.length - 1; i++) {
const a = this._data[i]; const a = this._data[i];
const b = this._data[i + 1]; const b = this._data[i + 1];
const x1 = timeScale.timeToCoordinate(a.time as Time); const x1 = timeScale.timeToCoordinate(a.time as Time);
const x2 = timeScale.timeToCoordinate(b.time as Time); const x2 = timeScale.timeToCoordinate(b.time as Time);
if (x1 == null || x2 == null) { skippedTime++; continue; } if (x1 == null || x2 == null) continue;
const aAy = this._series.priceToCoordinate(a.spanA); const aAy = this._series.priceToCoordinate(a.spanA);
const aBy = this._series.priceToCoordinate(a.spanB); const aBy = this._series.priceToCoordinate(a.spanB);
const bAy = this._series.priceToCoordinate(b.spanA); const bAy = this._series.priceToCoordinate(b.spanA);
const bBy = this._series.priceToCoordinate(b.spanB); const bBy = this._series.priceToCoordinate(b.spanB);
if (aAy == null || aBy == null || bAy == null || bBy == null) { skippedPrice++; continue; } if (aAy == null || aBy == null || bAy == null || bBy == null) continue;
const bullish = (a.spanA + b.spanA) > (a.spanB + b.spanB); const bullish = (a.spanA + b.spanA) > (a.spanB + b.spanB);
if (bullish && !this._bullishVisible) continue; if (bullish && !this._bullishVisible) continue;
if (!bullish && !this._bearishVisible) continue; if (!bullish && !this._bearishVisible) continue;
drawn++;
ctx.save(); ctx.save();
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(x1 * hpr, aAy * vpr); ctx.moveTo(x1 * hpr, aAy * vpr);
@@ -111,8 +100,6 @@ class CloudRenderer implements IPrimitivePaneRenderer {
ctx.fill(); ctx.fill();
ctx.restore(); ctx.restore();
} }
console.log('[IchimokuCloud] drawn=', drawn, 'skippedTime=', skippedTime, 'skippedPrice=', skippedPrice,
'bullishColor=', this._bullishColor, 'bearishColor=', this._bearishColor);
}); });
} }
} }
@@ -153,17 +140,14 @@ export class IchimokuCloudPlugin implements ISeriesPrimitive<Time> {
this._chart = chart; this._chart = chart;
this._series = series; this._series = series;
this._updateFn = requestUpdate; this._updateFn = requestUpdate;
console.log('[IchimokuCloud] attached. series:', !!series, 'chart:', !!chart);
} }
detached(): void { detached(): void {
console.log('[IchimokuCloud] detached. data.length:', this._data.length);
this._chart = null; this._chart = null;
this._series = null; this._series = null;
} }
setCloudData(data: IchimokuCloudPoint[]): void { setCloudData(data: IchimokuCloudPoint[]): void {
console.log('[IchimokuCloud] setCloudData:', data.length, 'points, chart:', !!this._chart, 'series:', !!this._series, 'updateFn:', !!this._updateFn);
this._data = data; this._data = data;
this._updateFn?.(); this._updateFn?.();
} }
@@ -186,11 +170,7 @@ export class IchimokuCloudPlugin implements ISeriesPrimitive<Time> {
getBearishVisible() { return this._bearishVisible; } getBearishVisible() { return this._bearishVisible; }
paneViews(): readonly IPrimitivePaneView[] { paneViews(): readonly IPrimitivePaneView[] {
console.log('[IchimokuCloud] paneViews called. chart:', !!this._chart, 'series:', !!this._series, 'data:', this._data.length); if (!this._chart || !this._series) return [];
if (!this._chart || !this._series) {
console.warn('[IchimokuCloud] paneViews: chart or series is null → returning []');
return [];
}
return [new CloudPaneView(this)]; return [new CloudPaneView(this)];
} }