일목균형표 수정

This commit is contained in:
Macbook
2026-05-27 23:36:48 +09:00
parent 9cee6387c3
commit 8cc0d1c88c
73 changed files with 2256 additions and 334 deletions
+79 -30
View File
@@ -39,8 +39,15 @@ import {
getIchimokuPlotTitle,
isIchimokuCloudVisible,
resolveIchimokuCloudColors,
resolveIchimokuDisplacements,
} from './ichimokuConfig';
import type { PlotDef, HLineStyle } from './indicatorRegistry';
import { mapHistogramSeriesData, histogramBarColor } from './plotColorUtils';
import {
applyPaneSeparatorToChartOptions,
resolveChartPaneSeparatorOptions,
type ChartPaneSeparatorOptions,
} from '../types/chartPaneSeparator';
function plotSeriesLineStyle(style?: HLineStyle): number {
if (style === 'dashed') return LineStyle.Dashed;
@@ -217,6 +224,10 @@ export class ChartManager {
/** 차트 하단 거래량 pane 표시 */
private _volumeVisible = true;
/** pane 간 구분선 (설정 화면) */
private _paneSeparatorOptions: ChartPaneSeparatorOptions =
resolveChartPaneSeparatorOptions(null, 'dark');
/** applyPaneLayout / resetPaneHeights 에서 측정한 마지막 가용 높이(px) */
private _lastLayoutAvailableHeight?: number;
@@ -231,7 +242,7 @@ export class ChartManager {
background: { type: ColorType.Solid, color: t.bgColor },
textColor: t.textColor,
fontSize: 12,
panes: { enableResize: false },
panes: { enableResize: false, separatorColor: 'transparent', separatorHoverColor: 'transparent' },
},
grid: {
vertLines: { color: t.gridColor },
@@ -258,6 +269,8 @@ export class ChartManager {
});
this._applyDisplayTimezoneOptions();
this._paneSeparatorOptions = resolveChartPaneSeparatorOptions(null, theme);
this.chart.applyOptions(applyPaneSeparatorToChartOptions(this._paneSeparatorOptions));
// 크로스헤어 이동 시 hover 중인 시리즈 추적 (오버레이 클릭 감지에 사용)
this.chart.subscribeCrosshairMove((params: MouseEventParams<Time>) => {
@@ -470,7 +483,11 @@ export class ChartManager {
this.currentTheme = theme;
const t = getTheme(theme);
this.chart.applyOptions({
layout: { background: { type: ColorType.Solid, color: t.bgColor }, textColor: t.textColor },
layout: {
background: { type: ColorType.Solid, color: t.bgColor },
textColor: t.textColor,
panes: applyPaneSeparatorToChartOptions(this._paneSeparatorOptions).layout.panes,
},
grid: { vertLines: { color: t.gridColor }, horzLines: { color: t.gridColor } },
crosshair: { vertLine: { color: t.crosshairColor, labelBackgroundColor: '#9598A1' }, horzLine: { color: t.crosshairColor, labelBackgroundColor: '#9598A1' } },
timeScale: { borderColor: t.borderColor },
@@ -480,6 +497,20 @@ export class ChartManager {
this._applyVolumeSeriesTheme(t);
}
getContainer(): HTMLElement {
return this.container;
}
getPaneSeparatorOptions(): ChartPaneSeparatorOptions {
return this._paneSeparatorOptions;
}
setPaneSeparatorOptions(opts: ChartPaneSeparatorOptions): void {
this._paneSeparatorOptions = opts;
this.chart.applyOptions(applyPaneSeparatorToChartOptions(opts));
this._notifyPaneLayout();
}
private _applyMainSeriesTheme(t: ThemeTokens): void {
if (!this.mainSeries) return;
switch (this.currentChartType) {
@@ -582,14 +613,7 @@ export class ChartManager {
let series: ISeriesApi<SeriesType>;
if (plotDef.type === 'histogram') {
series = this.chart.addSeries(HistogramSeries, { color: plotDef.color }, pane);
const colorData = filtered.map((p, i, arr) => ({
time: p.time as Time,
value: p.value,
color: p.color ?? (i > 0 && p.value < arr[i - 1].value
? '#ef535088'
: p.value < 0 ? '#ef535088' : `${plotDef.color}88`),
}));
series.setData(colorData);
series.setData(mapHistogramSeriesData(filtered, plotDef));
seriesMeta.push({ plotId: plotDef.id, isHistogram: true, color: plotDef.color });
} else {
const isPlotVisible = !indicatorHidden && config.plotVisibility?.[plotDef.id] !== false;
@@ -743,6 +767,8 @@ export class ChartManager {
await this.addIndicator(ind);
}
if (this._indicatorLoadStale(loadGen)) return;
// 빈 pane stretch 정리 + 캔들 pane 비율 복구
this.resetPaneHeights();
this._notifyPaneLayout();
@@ -907,7 +933,7 @@ export class ChartManager {
_config: IndicatorConfig,
seriesList: ISeriesApi<SeriesType>[],
): Promise<{ cloudPlugin: IchimokuCloudPlugin; seriesMeta: IndicatorEntry['seriesMeta'] }> {
const displacement = (_config.params?.displacement as number) ?? 26;
const { senkou: displacement } = resolveIchimokuDisplacements(_config.params);
const laggingPeriod = (_config.params?.laggingSpan2Periods as number) ?? 52;
const seriesMeta: IndicatorEntry['seriesMeta'] = [];
@@ -1233,7 +1259,7 @@ export class ChartManager {
// ── IchimokuCloud: SpanA/SpanB 미래 프로젝션 갱신 ──────────────────
if (updateFutureSpans && entry.type === 'IchimokuCloud') {
const config = entry.config;
const disp = (config.params?.displacement as number) ?? 26;
const { senkou: disp } = resolveIchimokuDisplacements(config.params);
const lagPeriod = (config.params?.laggingSpan2Periods as number) ?? 52;
const convPlot = (plots['plot0'] as PlotData) ?? [];
const basePlot = (plots['plot1'] as PlotData) ?? [];
@@ -1268,11 +1294,12 @@ export class ChartManager {
// ── Histogram: 색상 재계산 ──────────────────────────────────────────
if (meta.isHistogram) {
const prevVal = plotData.length >= 2 ? plotData[plotData.length - 2]?.value : null;
const isDown = prevVal !== null && (curVal as number) < (prevVal as number);
const isNeg = (curVal as number) < 0;
const barColor = lastPoint.color
?? (isDown || isNeg ? '#ef535088' : `${meta.color}88`);
const plotDef = (entry.config?.plots ?? getIndicatorDef(entry.type)?.plots ?? [])
.find(p => p.id === meta.plotId);
const prevVal = plotData.length >= 2 ? plotData[plotData.length - 2]?.value : null;
const barColor = plotDef
? histogramBarColor(curVal as number, prevVal, plotDef, lastPoint.color)
: (lastPoint.color ?? '#26A69A88');
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(entry.seriesList[i] as ISeriesApi<any>).update({
time: lastPoint.time as Time,
@@ -1294,6 +1321,29 @@ export class ChartManager {
}
}
/** histogram 막대 색상(상승/하락) 변경 후 전체 재색칠 */
private async _repaintHistogramSeries(indicatorId: string, config: IndicatorConfig): Promise<void> {
const entry = this.indicators.get(indicatorId);
if (!entry || this.rawBars.length === 0) return;
const plotDefs = config.plots ?? getIndicatorDef(config.type)?.plots ?? [];
const plotById = Object.fromEntries(plotDefs.map((p: PlotDef) => [p.id, p]));
try {
const { plots } = await calculateIndicator(config.type, this.rawBars.slice(), config.params ?? {});
for (let i = 0; i < entry.seriesList.length; i++) {
const meta = entry.seriesMeta[i];
if (!meta?.isHistogram) continue;
const plot = plotById[meta.plotId];
if (!plot) continue;
const plotData = plots[meta.plotId] as PlotData | undefined;
if (!plotData?.length) continue;
const filtered = plotData.filter(p => p.value !== null && !isNaN(p.value));
if (filtered.length === 0) continue;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(entry.seriesList[i] as ISeriesApi<any>).setData(mapHistogramSeriesData(filtered, plot));
}
} catch { /* ignore */ }
}
/** 일목균형표 구름 플러그인 — 색상·가시성·데이터 동기화 */
private _applyIchimokuCloudPlugin(
plugin: IchimokuCloudPlugin,
@@ -1302,6 +1352,7 @@ export class ChartManager {
): void {
const colors = resolveIchimokuCloudColors(config.cloudColors);
plugin.setColors(colors.bullishColor, colors.bearishColor);
plugin.setVisibility(colors.bullishVisible !== false, colors.bearishVisible !== false);
if (!isIchimokuCloudVisible(config)) {
plugin.setCloudData([]);
@@ -1314,7 +1365,7 @@ export class ChartManager {
const basePlot = plots['plot1'] as PlotData ?? [];
if (!spanAPlot || !spanBPlot) return;
const displacement = (config.params?.displacement as number) ?? 26;
const { senkou: displacement } = resolveIchimokuDisplacements(config.params);
const laggingPeriod = (config.params?.laggingSpan2Periods as number) ?? 52;
const cloud = this._buildIchimokuCloudData(
@@ -1990,6 +2041,12 @@ export class ChartManager {
}
entry.config = config;
if (entry.seriesMeta.some(m => m.isHistogram)) {
void this._repaintHistogramSeries(config.id, config);
}
if (entry.seriesMeta.some(m => m.isHistogram)) {
void this._repaintHistogramSeries(config.id, config);
}
if (this._candleOnlyLayout && this._isSubPaneIndicator(entry)) {
this._hideSubPaneIndicator(entry);
@@ -2773,18 +2830,10 @@ export class ChartManager {
if (filtered.length === 0) continue;
if (meta.isHistogram) {
const plotDef = (entry.config?.plots ?? getIndicatorDef(entry.type)?.plots ?? [])
.find(p => p.id === meta.plotId) ?? { id: meta.plotId, title: '', color: meta.color, type: 'histogram' as const };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(series as ISeriesApi<any>).setData(
filtered.map((p, idx, arr) => ({
time: p.time as Time,
value: p.value,
color: p.color ?? (
idx > 0 && p.value < arr[idx - 1].value
? '#ef535088'
: p.value < 0 ? '#ef535088' : `${meta.color}88`
),
}))
);
(series as ISeriesApi<any>).setData(mapHistogramSeriesData(filtered, plotDef));
} else {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(series as ISeriesApi<any>).setData(
@@ -2809,7 +2858,7 @@ export class ChartManager {
entry: IndicatorEntry,
plots: Record<string, PlotData>,
): void {
const displacement = (entry.config?.params?.displacement as number) ?? 26;
const { senkou: displacement } = resolveIchimokuDisplacements(entry.config?.params);
const laggingPeriod = (entry.config?.params?.laggingSpan2Periods as number) ?? 52;
const convPlot = (plots['plot0'] as PlotData) ?? [];
const basePlot = (plots['plot1'] as PlotData) ?? [];