import { useEffect, useMemo, useRef, useState } from 'react'; import { CandlestickSeries, ColorType, CrosshairMode, HistogramSeries, type IPrimitivePaneRenderer, type IPrimitivePaneView, type IChartApi, type ISeriesApi, type ISeriesPrimitive, LineStyle, LineSeries, type SeriesAttachedParameter, type Time, createChart, type UTCTimestamp, type CandlestickData, type HistogramData, type LineData, type WhitespaceData, } from 'lightweight-charts'; import type { Candle, SeriesPoint } from '../../lib/api'; import { FibRetracementPrimitive, type FibAnchor, type FibRetracement } from './FibRetracementPrimitive'; type Props = { candles: Candle[]; oracle?: SeriesPoint[]; sma20?: SeriesPoint[]; ema20?: SeriesPoint[]; bb20?: { upper: SeriesPoint[]; lower: SeriesPoint[]; mid: SeriesPoint[] }; showIndicators: boolean; showBuild: boolean; bucketSeconds: number; seriesKey: string; priceLines?: Array<{ id: string; title: string; price: number | null; color: string; lineWidth?: number; lineStyle?: LineStyle; axisLabelVisible?: boolean; }>; fib?: FibRetracement | null; fibOpacity?: number; fibSelected?: boolean; priceAutoScale?: boolean; onReady?: (api: { chart: IChartApi; candles: ISeriesApi<'Candlestick', UTCTimestamp> }) => void; onChartClick?: (p: FibAnchor & { target: 'chart' | 'fib' }) => void; onChartCrosshairMove?: (p: FibAnchor) => void; onPointerEvent?: (p: { type: 'pointerdown' | 'pointermove' | 'pointerup' | 'pointercancel'; logical: number; price: number; x: number; y: number; target: 'chart' | 'fib'; event: PointerEvent; }) => { consume?: boolean; capturePointer?: boolean } | void; }; type LinePoint = LineData | WhitespaceData; type BuildSample = { t: number; v: number }; const BUILD_UP_COLOR = '#22c55e'; const BUILD_DOWN_COLOR = '#ef4444'; const BUILD_FLAT_COLOR = '#60a5fa'; const BUILD_UP_SLICE = 'rgba(34,197,94,0.70)'; const BUILD_DOWN_SLICE = 'rgba(239,68,68,0.70)'; const BUILD_FLAT_SLICE = 'rgba(96,165,250,0.70)'; function toTime(t: number): UTCTimestamp { return t as UTCTimestamp; } function resolveBucketSeconds(bucketSeconds: number, candles: Candle[]): number { if (Number.isFinite(bucketSeconds) && bucketSeconds > 0) return bucketSeconds; if (candles.length >= 2) { const last = candles[candles.length - 1]?.time; const prev = candles[candles.length - 2]?.time; const delta = typeof last === 'number' && typeof prev === 'number' ? last - prev : 0; if (Number.isFinite(delta) && delta > 0) return delta; } return 60; } function samplePriceFromCandles(candles: Candle[]): number | null { for (let i = candles.length - 1; i >= 0; i -= 1) { const close = candles[i]?.close; if (typeof close !== 'number') continue; if (!Number.isFinite(close)) continue; if (close === 0) continue; return close; } return null; } function priceFormatForSample(price: number) { const abs = Math.abs(price); if (!Number.isFinite(abs) || abs === 0) return { type: 'price' as const, precision: 2, minMove: 0.01 }; if (abs >= 1000) return { type: 'price' as const, precision: 0, minMove: 1 }; if (abs >= 1) return { type: 'price' as const, precision: 2, minMove: 0.01 }; const exponent = Math.floor(Math.log10(abs)); // negative for abs < 1 const precision = Math.min(8, Math.max(4, -exponent + 3)); return { type: 'price' as const, precision, minMove: Math.pow(10, -precision) }; } function toCandleData(candles: Candle[]): CandlestickData[] { return candles.map((c) => ({ time: toTime(c.time), open: c.open, high: c.high, low: c.low, close: c.close, })); } function toVolumeData(candles: Candle[]): HistogramData[] { return candles.map((c) => { return { time: toTime(c.time), value: c.volume ?? 0, color: 'rgba(148,163,184,0.22)', }; }); } function toLineSeries(points: SeriesPoint[] | undefined): LinePoint[] { if (!points?.length) return []; return points.map((p) => p.value == null ? ({ time: toTime(p.time) } as WhitespaceData) : { time: toTime(p.time), value: p.value } ); } function colorForDelta(delta: number): string { if (delta > 0) return BUILD_UP_COLOR; if (delta < 0) return BUILD_DOWN_COLOR; return BUILD_FLAT_COLOR; } function sliceColorForDelta(delta: number): string { if (delta > 0) return BUILD_UP_SLICE; if (delta < 0) return BUILD_DOWN_SLICE; return BUILD_FLAT_SLICE; } type SliceDir = -1 | 0 | 1; function dirForDelta(delta: number): SliceDir { if (delta > 0) return 1; if (delta < 0) return -1; return 0; } function buildDeltaSeriesForCandle(candle: Candle, bs: number, samples: BuildSample[] | undefined): LinePoint[] { const eps = 1e-3; const startT = candle.time + eps; const endT = candle.time + bs - eps; if (!(endT > startT)) return []; const points: BuildSample[] = [{ t: startT, v: 0 }]; let lastT = startT; for (const p of samples || []) { let t = p.t; if (t <= lastT + eps) t = lastT + eps; if (t >= endT) break; points.push({ t, v: p.v }); lastT = t; } const finalDelta = candle.close - candle.open; if (endT > lastT + eps) { points.push({ t: endT, v: finalDelta }); } else if (points.length) { points[points.length - 1] = { ...points[points.length - 1]!, v: finalDelta }; } const out: LinePoint[] = [{ time: toTime(candle.time) } as WhitespaceData]; out.push({ time: toTime(points[0]!.t), value: points[0]!.v } as LineData); let lastLineIdx = out.length - 1; let lastVal = points[0]!.v; for (let i = 1; i < points.length; i += 1) { const v = points[i]!.v; const prev = out[lastLineIdx] as LineData; out[lastLineIdx] = { ...prev, color: colorForDelta(v - lastVal) } as LineData; out.push({ time: toTime(points[i]!.t), value: v } as LineData); lastLineIdx = out.length - 1; lastVal = v; } return out; } type BuildSlicesState = { enabled: boolean; candles: Candle[]; bucketSeconds: number; samples: Map; series: ISeriesApi<'Histogram', Time> | null; chart: SeriesAttachedParameter