chore: initial import

This commit is contained in:
u1
2026-01-06 12:33:47 +01:00
commit ed37565e25
38 changed files with 5707 additions and 0 deletions

View File

@@ -0,0 +1,177 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import type { Candle, ChartIndicators } from '../../lib/api';
import Card from '../../ui/Card';
import ChartSideToolbar from './ChartSideToolbar';
import ChartToolbar from './ChartToolbar';
import TradingChart from './TradingChart';
import type { FibAnchor, FibRetracement } from './FibRetracementPrimitive';
import type { IChartApi } from 'lightweight-charts';
type Props = {
candles: Candle[];
indicators: ChartIndicators;
timeframe: string;
onTimeframeChange: (tf: string) => void;
showIndicators: boolean;
onToggleIndicators: () => void;
seriesLabel: string;
};
export default function ChartPanel({
candles,
indicators,
timeframe,
onTimeframeChange,
showIndicators,
onToggleIndicators,
seriesLabel,
}: Props) {
const [isFullscreen, setIsFullscreen] = useState(false);
const [activeTool, setActiveTool] = useState<'cursor' | 'fib-retracement'>('cursor');
const [fibStart, setFibStart] = useState<FibAnchor | null>(null);
const [fib, setFib] = useState<FibRetracement | null>(null);
const [fibDraft, setFibDraft] = useState<FibRetracement | null>(null);
const chartApiRef = useRef<IChartApi | null>(null);
const activeToolRef = useRef(activeTool);
const fibStartRef = useRef<FibAnchor | null>(fibStart);
const pendingMoveRef = useRef<FibAnchor | null>(null);
const rafRef = useRef<number | null>(null);
useEffect(() => {
if (!isFullscreen) return;
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') setIsFullscreen(false);
};
window.addEventListener('keydown', onKeyDown);
return () => window.removeEventListener('keydown', onKeyDown);
}, [isFullscreen]);
useEffect(() => {
document.body.classList.toggle('chartFullscreen', isFullscreen);
return () => document.body.classList.remove('chartFullscreen');
}, [isFullscreen]);
const cardClassName = useMemo(() => {
return ['chartCard', isFullscreen ? 'chartCard--fullscreen' : null].filter(Boolean).join(' ');
}, [isFullscreen]);
useEffect(() => {
activeToolRef.current = activeTool;
if (activeTool !== 'fib-retracement') {
setFibStart(null);
setFibDraft(null);
}
}, [activeTool]);
useEffect(() => {
fibStartRef.current = fibStart;
}, [fibStart]);
useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => {
if (e.key !== 'Escape') return;
if (activeToolRef.current !== 'fib-retracement') return;
setFibStart(null);
setFibDraft(null);
setActiveTool('cursor');
};
window.addEventListener('keydown', onKeyDown);
return () => window.removeEventListener('keydown', onKeyDown);
}, []);
useEffect(() => {
return () => {
if (rafRef.current != null) cancelAnimationFrame(rafRef.current);
};
}, []);
function zoomTime(factor: number) {
const chart = chartApiRef.current;
if (!chart) return;
const ts = chart.timeScale();
const range = ts.getVisibleLogicalRange();
if (!range) return;
const from = range.from as number;
const to = range.to as number;
const span = Math.max(5, (to - from) * factor);
const center = (from + to) / 2;
ts.setVisibleLogicalRange({ from: center - span / 2, to: center + span / 2 });
}
return (
<>
{isFullscreen ? <div className="chartBackdrop" onClick={() => setIsFullscreen(false)} /> : null}
<Card className={cardClassName}>
<div className="chartCard__toolbar">
<ChartToolbar
timeframe={timeframe}
onTimeframeChange={onTimeframeChange}
showIndicators={showIndicators}
onToggleIndicators={onToggleIndicators}
seriesLabel={seriesLabel}
isFullscreen={isFullscreen}
onToggleFullscreen={() => setIsFullscreen((v) => !v)}
/>
</div>
<div className="chartCard__content">
<ChartSideToolbar
timeframe={timeframe}
activeTool={activeTool}
hasFib={fib != null || fibDraft != null}
onToolChange={setActiveTool}
onZoomIn={() => zoomTime(0.8)}
onZoomOut={() => zoomTime(1.25)}
onResetView={() => chartApiRef.current?.timeScale().resetTimeScale()}
onClearFib={() => {
setFib(null);
setFibStart(null);
setFibDraft(null);
}}
/>
<div className="chartCard__chart">
<TradingChart
candles={candles}
oracle={indicators.oracle}
sma20={indicators.sma20}
ema20={indicators.ema20}
bb20={indicators.bb20}
showIndicators={showIndicators}
fib={fibDraft ?? fib}
onReady={({ chart }) => {
chartApiRef.current = chart;
}}
onChartClick={(p) => {
if (activeTool !== 'fib-retracement') return;
if (!fibStartRef.current) {
fibStartRef.current = p;
setFibStart(p);
setFibDraft({ a: p, b: p });
return;
}
setFib({ a: fibStartRef.current, b: p });
setFibStart(null);
fibStartRef.current = null;
setFibDraft(null);
setActiveTool('cursor');
}}
onChartCrosshairMove={(p) => {
if (activeToolRef.current !== 'fib-retracement') return;
const start = fibStartRef.current;
if (!start) return;
pendingMoveRef.current = p;
if (rafRef.current != null) return;
rafRef.current = window.requestAnimationFrame(() => {
rafRef.current = null;
const move = pendingMoveRef.current;
const start2 = fibStartRef.current;
if (!move || !start2) return;
setFibDraft({ a: start2, b: move });
});
}}
/>
</div>
</div>
</Card>
</>
);
}