import React, { useMemo } from 'react'; import type { OHLCVBar } from '../../types'; import type { StrategyDto } from '../../utils/backendApi'; import { buildOscillatorPaneData, collectStrategyOscillatorPanes, defaultOscillatorPaneSpecs, } from '../../utils/strategyOscillatorSeries'; function spark(values: number[], w: number, h: number, min?: number, max?: number): string { if (!values.length) return ''; const lo = min ?? Math.min(...values); const hi = max ?? Math.max(...values); const range = hi - lo || 1; return values.map((v, i) => { const x = (i / Math.max(values.length - 1, 1)) * w; const y = h - ((v - lo) / range) * (h - 6) - 3; return `${i === 0 ? 'M' : 'L'}${x.toFixed(1)},${y.toFixed(1)}`; }).join(' '); } interface Props { bars: OHLCVBar[]; strategy?: StrategyDto; /** 부모 flex 영역 높이에 맞춰 pane 균등 분배 */ fillHeight?: boolean; } /** 전략 조건에 사용된 보조지표 하단 pane (없으면 RSI·MACD 기본) */ const StrategyOscillatorPanes: React.FC = ({ bars, strategy, fillHeight = false }) => { const panes = useMemo(() => { const specs = collectStrategyOscillatorPanes(strategy); const useSpecs = specs.length > 0 ? specs : defaultOscillatorPaneSpecs(); return buildOscillatorPaneData(bars, useSpecs); }, [bars, strategy]); if (panes.length === 0) return null; return (
{panes.map(pane => { const last = pane.values[pane.values.length - 1]; const lo = pane.refLines?.[0]; const hi = pane.refLines?.[pane.refLines.length - 1]; const path = spark( pane.values, 280, 52, lo != null && hi != null && pane.spec.registryType === 'RSI' ? 0 : undefined, lo != null && hi != null && pane.spec.registryType === 'RSI' ? 100 : undefined, ); return (
{pane.spec.label} {last != null ? (pane.spec.registryType === 'RSI' ? last.toFixed(1) : last.toFixed(2)) : '–'}
{pane.refLines?.map((r, i) => { const min = pane.refLines![0]; const max = pane.refLines![pane.refLines!.length - 1]; const y = 52 - ((r - min) / (max - min || 1)) * 44 - 4; return ( ); })} {!pane.refLines?.length && ( )}
); })}
); }; export default StrategyOscillatorPanes;