fix: 일목 구름을 rawBars 기준으로 계산해 dev/프로덕션 동일화

서버 프로덕션 번들에서 lightweight-charts-indicators plot3·plot4가
spanA와 spanB에 동일 값을 줄 수 있어 구름이 사라짐. 로컬 vite dev는 정상.

plot0~4 의존을 제거하고 OHLCV rawBars만으로 선행스팬·구름을 계산한다.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Macbook
2026-06-02 23:06:43 +09:00
parent 6527793eb6
commit 2950442f69
+23 -29
View File
@@ -1368,7 +1368,7 @@ export class ChartManager {
return out;
}
/** 특정 바 인덱스에서 Donchian 중선을 계산 */
/** 특정 바 인덱스에서 Donchian 중선 (전환선·기준선·선행스팬B 공통) */
private _donchianAt(idx: number, period: number): number | null {
const start = idx - period + 1;
if (start < 0) return null;
@@ -1380,33 +1380,31 @@ export class ChartManager {
return (hi + lo) / 2;
}
/** 구름 데이터 빌드: 과거(역사 데이터) + 미래 26봉 포함 */
/**
* 일목 구름: rawBars만으로 계산 (vite dev / 프로덕션 번들 동일 결과).
* lightweight-charts-indicators 의 plot3·plot4는 서버 프로덕션 빌드에서
* spanA===spanB 로 붕괴할 수 있어 사용하지 않는다.
*/
private _buildIchimokuCloudData(
_spanAPlot: PlotData,
_spanBPlot: PlotData,
convPlot: PlotData,
basePlot: PlotData,
conversionPeriods: number,
basePeriods: number,
displacement: number,
laggingPeriod: number,
): IchimokuCloudPoint[] {
// 라이브러리의 plot3/plot4(SpanA/SpanB)는 프로덕션 빌드에서
// 동일한 값을 반환하는 버그가 있으므로, convPlot(전환선)/basePlot(기준선)과
// rawBars 기반 _donchianAt으로 직접 재계산한다.
const N = this.rawBars.length;
const historical: IchimokuCloudPoint[] = [];
for (let i = displacement - 1; i < N; i++) {
const srcIdx = i - (displacement - 1);
const conv = convPlot[srcIdx]?.value;
const base = basePlot[srcIdx]?.value;
if (conv == null || isNaN(conv) || base == null || isNaN(base)) continue;
const spanA = (conv + base) / 2;
for (let i = displacement; i < N; i++) {
const srcIdx = i - displacement;
const tenkan = this._donchianAt(srcIdx, conversionPeriods);
const kijun = this._donchianAt(srcIdx, basePeriods);
if (tenkan == null || kijun == null) continue;
const spanA = (tenkan + kijun) / 2;
const spanB = this._donchianAt(srcIdx, laggingPeriod);
if (spanB === null) continue;
if (spanB == null) continue;
historical.push({ time: this.rawBars[i].time, spanA, spanB });
}
// 미래 구름
if (N < 2) return historical;
const interval = this.rawBars[N - 1].time - this.rawBars[N - 2].time;
const lastTime = this.rawBars[N - 1].time;
@@ -1415,12 +1413,12 @@ export class ChartManager {
for (let k = 1; k <= displacement; k++) {
const srcIdx = N - displacement - 1 + k;
if (srcIdx < 0 || srcIdx >= N) continue;
const conv = convPlot[srcIdx]?.value;
const base = basePlot[srcIdx]?.value;
if (conv == null || isNaN(conv) || base == null || isNaN(base)) continue;
const spanA = (conv + base) / 2;
const tenkan = this._donchianAt(srcIdx, conversionPeriods);
const kijun = this._donchianAt(srcIdx, basePeriods);
if (tenkan == null || kijun == null) continue;
const spanA = (tenkan + kijun) / 2;
const spanB = this._donchianAt(srcIdx, laggingPeriod);
if (spanB === null) continue;
if (spanB == null) continue;
future.push({ time: lastTime + k * interval, spanA, spanB });
}
return [...historical, ...future];
@@ -1782,17 +1780,13 @@ export class ChartManager {
return;
}
const spanAPlot = plots['plot3'] as PlotData | undefined;
const spanBPlot = plots['plot4'] as PlotData | undefined;
const convPlot = plots['plot0'] as PlotData ?? [];
const basePlot = plots['plot1'] as PlotData ?? [];
if (!spanAPlot || !spanBPlot) return;
const { senkou: displacement } = resolveIchimokuDisplacements(config.params);
const conversionPeriods = (config.params?.conversionPeriods as number) ?? 9;
const basePeriods = (config.params?.basePeriods as number) ?? 26;
const laggingPeriod = (config.params?.laggingSpan2Periods as number) ?? 52;
const cloud = this._buildIchimokuCloudData(
spanAPlot, spanBPlot, convPlot, basePlot, displacement, laggingPeriod,
conversionPeriods, basePeriods, displacement, laggingPeriod,
);
plugin.setCloudData(cloud.length >= 2 ? cloud : []);
}