매매 시그널 알림 화면 수정

This commit is contained in:
Macbook
2026-05-29 00:46:20 +09:00
parent cbad62a5b0
commit e43b5cbd5a
45 changed files with 2186 additions and 415 deletions
+111 -10
View File
@@ -25,7 +25,14 @@ import { BollingerBandFillPrimitive } from './BollingerBandFillPrimitive';
import { resolveBbBandBackground } from './bollingerConfig';
import type { OHLCVBar, ChartType, Theme, IndicatorConfig, Timeframe } from '../types';
import { formatChartAxisPrice } from './dataGenerator';
import { formatLwcTime, formatLwcTickMark, DEFAULT_DISPLAY_TIMEZONE } from './timezone';
import {
DEFAULT_CHART_TIME_FORMAT,
formatUnixWithChartPattern,
formatLwcTimeWithPattern,
formatLwcTickMarkWithPattern,
normalizeChartTimeFormat,
} from './chartTimeFormat';
import { DEFAULT_DISPLAY_TIMEZONE } from './timezone';
import { sortIndicatorsForPaneLoad } from './indicatorPaneMerge';
import { calculateIndicator, enrichIndicatorConfig, getIndicatorDef, getHLineLabel, type PlotData, type MarkerData } from './indicatorRegistry';
import { IchimokuCloudPlugin, type IchimokuCloudPoint } from './IchimokuCloudPlugin';
@@ -203,6 +210,7 @@ export class ChartManager {
private currentChartType: ChartType = 'candlestick';
private displayTimezone = DEFAULT_DISPLAY_TIMEZONE;
private displayTimeframe: Timeframe = '1D';
private chartTimeFormat = DEFAULT_CHART_TIME_FORMAT;
private mainMarkersPlugin: ISeriesMarkersPluginApi<Time> | null = null;
private patternMarkers: Array<{ id: string; markers: MarkerData[] }> = [];
/** 백테스팅 매수/매도 시그널 마커 */
@@ -461,29 +469,37 @@ export class ChartManager {
// ─── Display timezone ───────────────────────────────────────────────────
private _tickMarkFormatter(): TickMarkFormatter {
const tf = this.displayTimeframe;
const tz = this.displayTimezone;
const fmt = this.chartTimeFormat;
return (time, tickMarkType) =>
formatLwcTickMark(
time as Time,
tickMarkType,
this.displayTimeframe,
this.displayTimezone,
);
formatLwcTickMarkWithPattern(time as Time, tickMarkType, tf, tz, fmt);
}
private _applyDisplayTimezoneOptions(): void {
const tf = this.displayTimeframe;
const tz = this.displayTimezone;
const fmt = this.chartTimeFormat;
const timeFormatter = (time: Time) =>
formatLwcTime(time as number | { year: number; month: number; day: number }, tf, tz);
formatLwcTimeWithPattern(
time as number | { year: number; month: number; day: number },
tf,
tz,
fmt,
);
this.chart.applyOptions({
localization: { timeFormatter, priceFormatter: formatChartAxisPrice },
timeScale: { tickMarkFormatter: this._tickMarkFormatter() },
});
this._notifyPaneLayout();
}
setDisplayTimezone(tz: string, timeframe?: Timeframe): void {
setDisplayTimezone(tz: string, timeframe?: Timeframe, chartTimeFormat?: string): void {
this.displayTimezone = tz || DEFAULT_DISPLAY_TIMEZONE;
if (timeframe) this.displayTimeframe = timeframe;
if (chartTimeFormat != null) {
this.chartTimeFormat = normalizeChartTimeFormat(chartTimeFormat);
}
this._applyDisplayTimezoneOptions();
}
@@ -805,12 +821,27 @@ export class ChartManager {
/** 파라미터 변경된 지표만 제거 후 재추가 (전체 보조지표 재로드 회피) */
async refreshIndicators(configs: IndicatorConfig[]): Promise<void> {
if (configs.length === 0) return;
this.cancelPendingIndicatorUpdates();
const loadGen = ++this._dataGeneration;
let any = false;
for (const c of configs) {
if (this._detachIndicatorEntry(c.id)) any = true;
}
if (any) this._trimTrailingEmptySubPanes();
await this.addIndicatorsBatch(configs);
if (this._indicatorLoadStale(loadGen)) return;
for (const config of configs) {
if (this._indicatorLoadStale(loadGen)) break;
await this.addIndicator(config, { skipLayout: true });
}
if (this._indicatorLoadStale(loadGen)) return;
this._removeOrphanSubPanes();
if (this._activeIndicatorPaneIndices().size > 0) {
this.resetPaneHeights(this._lastLayoutAvailableHeight);
this._notifyPaneLayout();
}
}
/** 메인 시리즈 마커 플러그인 해제 (setData 등 시리즈 교체 전 호출) */
@@ -2437,6 +2468,76 @@ export class ChartManager {
}
}
/** 캔들 pane(0) 플롯 영역 너비 (우측 가격축 제외) */
private _candlePlotWidth(): number {
try {
const ps = this.mainSeries?.priceScale() ?? this.chart.priceScale('right', 0);
const scaleW = ps.width();
return Math.max(40, this.container.clientWidth - scaleW);
} catch {
return Math.max(40, this.container.clientWidth - 56);
}
}
/**
* 캔들 pane 하단 시간축 오버레이 — 거래량·보조지표가 있을 때 캔들 영역 바로 아래에도 시각 표시.
*/
getCandlePaneTimeAxisOverlay(): {
topY: number;
height: number;
plotWidth: number;
labels: Array<{ x: number; text: string }>;
} | null {
if (!this.mainSeries || this.rawBars.length < 2) return null;
const layouts = this.getPaneLayouts();
const hasLowerPane = layouts.some(l => l.paneIndex > 0 && l.height > 8);
if (!hasLowerPane) return null;
const main = layouts.find(l => l.paneIndex === 0);
if (!main || main.height < 48) return null;
const AXIS_H = 22;
const ts = this.chart.timeScale();
const lr = ts.getVisibleLogicalRange();
if (!lr) return null;
const plotWidth = this._candlePlotWidth();
const from = Math.max(0, Math.ceil(lr.from));
const to = Math.min(this.rawBars.length - 1, Math.floor(lr.to));
if (to <= from) return null;
const span = to - from;
const targetCount = Math.max(4, Math.min(12, Math.floor(plotWidth / 76)));
const step = Math.max(1, Math.ceil(span / targetCount));
const labels: Array<{ x: number; text: string }> = [];
const fmt = this.chartTimeFormat;
const tz = this.displayTimezone;
for (let i = from; i <= to; i += step) {
const bar = this.rawBars[i];
if (!bar?.time) continue;
const coord = ts.timeToCoordinate(bar.time as Time);
if (coord == null) continue;
const x = Number(coord);
if (!Number.isFinite(x) || x < 12 || x > plotWidth - 12) continue;
labels.push({
x,
text: formatUnixWithChartPattern(bar.time, fmt, tz),
});
}
if (labels.length === 0) return null;
return {
topY: main.topY + main.height - AXIS_H,
height: AXIS_H,
plotWidth,
labels,
};
}
/** 하단 시간축 영역 클릭 여부 */
isOnTimeAxis(chartY: number): boolean {
try {