diff --git a/apps/visualizer/src/features/chart/ChartPanel.tsx b/apps/visualizer/src/features/chart/ChartPanel.tsx index 8151989..2477b74 100644 --- a/apps/visualizer/src/features/chart/ChartPanel.tsx +++ b/apps/visualizer/src/features/chart/ChartPanel.tsx @@ -31,9 +31,12 @@ export default function ChartPanel({ const [fibStart, setFibStart] = useState(null); const [fib, setFib] = useState(null); const [fibDraft, setFibDraft] = useState(null); + const [fibMove, setFibMove] = useState<{ start: FibAnchor; origin: FibRetracement } | null>(null); + const [priceAutoScale, setPriceAutoScale] = useState(true); const chartApiRef = useRef(null); const activeToolRef = useRef(activeTool); const fibStartRef = useRef(fibStart); + const fibMoveRef = useRef<{ start: FibAnchor; origin: FibRetracement } | null>(fibMove); const pendingMoveRef = useRef(null); const rafRef = useRef(null); @@ -57,6 +60,7 @@ export default function ChartPanel({ useEffect(() => { activeToolRef.current = activeTool; + if (activeTool === 'fib-retracement') setFibMove(null); if (activeTool !== 'fib-retracement') { setFibStart(null); setFibDraft(null); @@ -67,9 +71,20 @@ export default function ChartPanel({ fibStartRef.current = fibStart; }, [fibStart]); + useEffect(() => { + fibMoveRef.current = fibMove; + }, [fibMove]); + useEffect(() => { const onKeyDown = (e: KeyboardEvent) => { if (e.key !== 'Escape') return; + + if (fibMoveRef.current) { + setFibMove(null); + setFibDraft(null); + return; + } + if (activeToolRef.current !== 'fib-retracement') return; setFibStart(null); setFibDraft(null); @@ -108,6 +123,8 @@ export default function ChartPanel({ onTimeframeChange={onTimeframeChange} showIndicators={showIndicators} onToggleIndicators={onToggleIndicators} + priceAutoScale={priceAutoScale} + onTogglePriceAutoScale={() => setPriceAutoScale((v) => !v)} seriesLabel={seriesLabel} isFullscreen={isFullscreen} onToggleFullscreen={() => setIsFullscreen((v) => !v)} @@ -126,6 +143,7 @@ export default function ChartPanel({ setFib(null); setFibStart(null); setFibDraft(null); + setFibMove(null); }} />
@@ -137,35 +155,68 @@ export default function ChartPanel({ bb20={indicators.bb20} showIndicators={showIndicators} fib={fibDraft ?? fib} + fibSelected={fibMove != null} + priceAutoScale={priceAutoScale} 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 }); + if (activeTool === 'fib-retracement') { + 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'); return; } - setFib({ a: fibStartRef.current, b: p }); - setFibStart(null); - fibStartRef.current = null; - setFibDraft(null); - setActiveTool('cursor'); + + const move = fibMoveRef.current; + if (move) { + const deltaLogical = p.logical - move.start.logical; + const deltaPrice = p.price - move.start.price; + setFib({ + a: { logical: move.origin.a.logical + deltaLogical, price: move.origin.a.price + deltaPrice }, + b: { logical: move.origin.b.logical + deltaLogical, price: move.origin.b.price + deltaPrice }, + }); + setFibMove(null); + setFibDraft(null); + return; + } + + if (p.target === 'fib' && fib) { + setFibMove({ start: p, origin: fib }); + setFibDraft(fib); + } }} 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 pointer = pendingMoveRef.current; + if (!pointer) return; + + const move = fibMoveRef.current; + if (move) { + const deltaLogical = pointer.logical - move.start.logical; + const deltaPrice = pointer.price - move.start.price; + setFibDraft({ + a: { logical: move.origin.a.logical + deltaLogical, price: move.origin.a.price + deltaPrice }, + b: { logical: move.origin.b.logical + deltaLogical, price: move.origin.b.price + deltaPrice }, + }); + return; + } + + if (activeToolRef.current !== 'fib-retracement') return; const start2 = fibStartRef.current; - if (!move || !start2) return; - setFibDraft({ a: start2, b: move }); + if (!start2) return; + setFibDraft({ a: start2, b: pointer }); }); }} /> diff --git a/apps/visualizer/src/features/chart/ChartToolbar.tsx b/apps/visualizer/src/features/chart/ChartToolbar.tsx index 71ed170..e4dcb39 100644 --- a/apps/visualizer/src/features/chart/ChartToolbar.tsx +++ b/apps/visualizer/src/features/chart/ChartToolbar.tsx @@ -5,6 +5,8 @@ type Props = { onTimeframeChange: (tf: string) => void; showIndicators: boolean; onToggleIndicators: () => void; + priceAutoScale: boolean; + onTogglePriceAutoScale: () => void; seriesLabel: string; isFullscreen: boolean; onToggleFullscreen: () => void; @@ -17,6 +19,8 @@ export default function ChartToolbar({ onTimeframeChange, showIndicators, onToggleIndicators, + priceAutoScale, + onTogglePriceAutoScale, seriesLabel, isFullscreen, onToggleFullscreen, @@ -41,6 +45,9 @@ export default function ChartToolbar({ + diff --git a/apps/visualizer/src/features/chart/FibRetracementPrimitive.ts b/apps/visualizer/src/features/chart/FibRetracementPrimitive.ts index c058add..d51bc19 100644 --- a/apps/visualizer/src/features/chart/FibRetracementPrimitive.ts +++ b/apps/visualizer/src/features/chart/FibRetracementPrimitive.ts @@ -54,6 +54,7 @@ type State = { fib: FibRetracement | null; series: ISeriesApi<'Candlestick', Time> | null; chart: SeriesAttachedParameter