실시간 차트 신고가,신저가 표시

This commit is contained in:
Macbook
2026-06-12 21:00:52 +09:00
parent d54dc6e2ff
commit 046bcc6379
15 changed files with 509 additions and 39 deletions
+92 -8
View File
@@ -33,6 +33,7 @@ import {
type TradeSignalLabelItem,
type TradeSignalMarkerWithPrice,
} from './tradeSignalLabels';
import { computePriceExtremeChartMarkers } from './priceExtremeChartEvents';
import {
DEFAULT_CHART_TIME_FORMAT,
formatLwcTimeWithPattern,
@@ -274,6 +275,9 @@ export class ChartManager {
private backtestMarkers: TradeSignalMarkerWithPrice[] = [];
/** 실시간 전략 체크 마커 */
private liveStrategyMarkers: TradeSignalMarkerWithPrice[] = [];
/** 9·20일 신고가·신저가 마커 */
private priceExtremeMarkers: TradeSignalMarkerWithPrice[] = [];
private _priceExtremeLabelsVisible = false;
private _tradeSignalLabelListeners = new Set<() => void>();
private compareSeriesMap = new Map<string, ISeriesApi<SeriesType>>();
/** 캔들 pane 오버레이 그룹 표시 (config.hidden 과 별도 — in-place 토글용) */
@@ -515,7 +519,7 @@ export class ChartManager {
panesInit[0].setStretchFactor(1);
}
this._reapplyAllPatternMarkers();
this._refreshPriceExtremeMarkers();
// 종목 전환·초기 로드 — bar 수가 적을 때 음수 logical from 은 캔들이 우측으로 몰림
if (this._auxIndicatorOnlyLayout && bars.length <= 21) {
@@ -550,7 +554,7 @@ export class ChartManager {
} else {
ts.fitContent();
}
this._notifyViewport();
this._scheduleNotifyViewport();
return;
}
@@ -563,7 +567,7 @@ export class ChartManager {
from: bars[startIdx].time as Time,
to: to as Time,
});
this._notifyViewport();
this._scheduleNotifyViewport();
}
private _createMainSeries(chartType: ChartType, t: ThemeTokens): ISeriesApi<SeriesType> {
@@ -1777,7 +1781,14 @@ export class ChartManager {
color: m.color,
text: '',
}));
const all = [...patternAll, ...backtestAll, ...liveAll];
const priceExtremeAll = this.priceExtremeMarkers.map(m => ({
time: m.time as Time,
position: m.position,
shape: m.shape,
color: m.color,
text: '',
}));
const all = [...patternAll, ...backtestAll, ...liveAll, ...priceExtremeAll];
if (this.mainMarkersPlugin) {
try {
if (this.mainMarkersPlugin.getSeries() !== this.mainSeries) {
@@ -1876,9 +1887,50 @@ export class ChartManager {
return [
...this.backtestMarkers.map(toTradeSignalLabelItem),
...this.liveStrategyMarkers.map(toTradeSignalLabelItem),
...this.priceExtremeMarkers.map(toTradeSignalLabelItem),
];
}
/** rawBars 기준 9·20일 신고가·신저가 마커·라벨 재계산 */
private _refreshPriceExtremeMarkers(): void {
if (!this._priceExtremeLabelsVisible) {
if (this.priceExtremeMarkers.length === 0) return;
this.priceExtremeMarkers = [];
this._reapplyAllPatternMarkers();
this._notifyTradeSignalLabels();
return;
}
const next = computePriceExtremeChartMarkers(this.rawBars);
if (this._samePriceExtremeMarkers(next, this.priceExtremeMarkers)) return;
this.priceExtremeMarkers = next;
this._reapplyAllPatternMarkers();
this._notifyTradeSignalLabels();
}
/** 실시간 차트 — 9·20일 신고가·신저가 라벨·마커 표시 */
setPriceExtremeLabelsVisible(visible: boolean): void {
if (this._priceExtremeLabelsVisible === visible) return;
this._priceExtremeLabelsVisible = visible;
this._refreshPriceExtremeMarkers();
}
isPriceExtremeLabelsVisible(): boolean {
return this._priceExtremeLabelsVisible;
}
private _samePriceExtremeMarkers(
a: TradeSignalMarkerWithPrice[],
b: TradeSignalMarkerWithPrice[],
): boolean {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (a[i].time !== b[i].time || a[i].text !== b[i].text || a[i].position !== b[i].position) {
return false;
}
}
return true;
}
subscribeTradeSignalLabels(cb: () => void): () => void {
this._tradeSignalLabelListeners.add(cb);
return () => { this._tradeSignalLabelListeners.delete(cb); };
@@ -2281,6 +2333,7 @@ export class ChartManager {
// 4) 보조지표 마지막 포인트 스로틀 갱신
this._scheduleIndicatorLastUpdate();
this._refreshPriceExtremeMarkers();
}
/**
@@ -2621,6 +2674,7 @@ export class ChartManager {
if (this._pendingIndUpdate) {
this._scheduleIndicatorLastUpdate();
}
this._refreshPriceExtremeMarkers();
}
/**
@@ -2652,6 +2706,7 @@ export class ChartManager {
if (!this._indicatorLoadStale(loadGen)) {
this._finalizeIndicatorPaneLayout();
}
this._refreshPriceExtremeMarkers();
}
// ─── Alerts ──────────────────────────────────────────────────────────────
@@ -2896,7 +2951,11 @@ export class ChartManager {
/** 가시 시간 범위 변경 알림 (커스텀 패닝·줌 등 LWC 이벤트 미발생 경로 포함) */
private readonly _viewportListeners = new Set<() => void>();
private _viewportLwcHooked = false;
private readonly _lwcViewportHandler = (): void => { this._notifyViewport(); };
private _viewportNotifyQueued = false;
private readonly _lwcViewportHandler = (): void => {
this._notifyViewport();
this._scheduleNotifyViewport();
};
private _notifyViewport(): void {
for (const cb of this._viewportListeners) {
@@ -2904,6 +2963,18 @@ export class ChartManager {
}
}
/** LWC timeScale·좌표 변환 반영 후 오버레이 재그리기 (연속 패닝 중 cancel 로 redraw 누락 방지) */
private _scheduleNotifyViewport(): void {
if (this._viewportNotifyQueued) return;
this._viewportNotifyQueued = true;
requestAnimationFrame(() => {
requestAnimationFrame(() => {
this._viewportNotifyQueued = false;
this._notifyViewport();
});
});
}
private _hookViewportLwc(): void {
if (this._viewportLwcHooked) return;
this._viewportLwcHooked = true;
@@ -2923,6 +2994,7 @@ export class ChartManager {
try {
this.chart.timeScale().setVisibleLogicalRange(range);
this._notifyViewport();
this._scheduleNotifyViewport();
} catch { /* 데이터 로드 전 */ }
}
@@ -2931,9 +3003,10 @@ export class ChartManager {
this._notifyPaneLayout();
}
/** 시간축 오버레이 등 — 뷰포트 동기화 강제 (패닝 종료 시 등) */
/** 시간축 오버레이 등 — 뷰포트 동기화 강제 (패닝·스크롤 중) */
notifyViewportChanged(): void {
this._notifyViewport();
this._scheduleNotifyViewport();
}
/** visible logical range → 플롯 X (LWC 좌표 API와 동일한 선형 매핑) */
@@ -3590,7 +3663,10 @@ export class ChartManager {
}
// ─── Layout ───────────────────────────────────────────────────────────────
fitContent(): void { this.chart.timeScale().fitContent(); }
fitContent(): void {
this.chart.timeScale().fitContent();
this._scheduleNotifyViewport();
}
/** 시간축 확대 (visible logical range 20% 축소) */
zoomIn(): void {
@@ -4167,6 +4243,8 @@ export class ChartManager {
this._mainPriceVisibleRange = next;
try {
ps.setVisibleRange(next);
this._notifyViewport();
this._scheduleNotifyViewport();
} catch { /* 범위 미설정 등 */ }
}
@@ -4223,6 +4301,7 @@ export class ChartManager {
try {
this.chart.timeScale().setVisibleRange({ from: from as Time, to: to as Time });
this._notifyViewport();
this._scheduleNotifyViewport();
} catch { /* 데이터 로딩 전 호출 무시 */ }
}
@@ -4262,7 +4341,10 @@ export class ChartManager {
* 사용자가 과거 데이터를 탐색하다가 최신 캔들로 즉시 이동할 때 호출
* (ref: https://tradingview.github.io/lightweight-charts/tutorials/demos/realtime-updates)
*/
scrollToRealTime(): void { this.chart.timeScale().scrollToRealTime(); }
scrollToRealTime(): void {
this.chart.timeScale().scrollToRealTime();
this._scheduleNotifyViewport();
}
/** 줌 툴: 두 X 픽셀 위치(canvas 내부)를 시간 범위로 변환하여 확대 */
zoomToXRange(x0: number, x1: number): void {
@@ -4272,6 +4354,7 @@ export class ChartManager {
try {
this.chart.timeScale().setVisibleRange({ from: t0 as import('lightweight-charts').Time, to: t1 as import('lightweight-charts').Time });
this._notifyViewport();
this._scheduleNotifyViewport();
} catch { /* ok */ }
}
@@ -4430,6 +4513,7 @@ export class ChartManager {
// 보조지표 전체 재계산 (새 데이터가 왼쪽에 추가됐으므로 전체 갱신 필요)
await this._refreshAllIndicatorsData();
this._refreshPriceExtremeMarkers();
return newBars.length;
}