97 lines
3.5 KiB
TypeScript
97 lines
3.5 KiB
TypeScript
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<Props> = ({ 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 (
|
||
<div className={`btd-osc-stack${fillHeight ? ' btd-osc-stack--fill' : ''}`}>
|
||
{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 (
|
||
<div key={pane.spec.key} className={`btd-osc-pane${fillHeight ? ' btd-osc-pane--fill' : ''}`}>
|
||
<div className="btd-osc-head">
|
||
<span className="btd-osc-label">{pane.spec.label}</span>
|
||
<span className="btd-osc-val">
|
||
{last != null
|
||
? (pane.spec.registryType === 'RSI' ? last.toFixed(1) : last.toFixed(2))
|
||
: '–'}
|
||
</span>
|
||
</div>
|
||
<svg className="btd-osc-svg" viewBox="0 0 280 52" preserveAspectRatio="none">
|
||
{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 (
|
||
<line
|
||
key={i}
|
||
x1="0"
|
||
y1={y}
|
||
x2="280"
|
||
y2={y}
|
||
className={`btd-osc-ref${i === 0 || i === pane.refLines!.length - 1 ? ' btd-osc-ref--hi' : ''}`}
|
||
/>
|
||
);
|
||
})}
|
||
{!pane.refLines?.length && (
|
||
<line x1="0" y1="26" x2="280" y2="26" className="btd-osc-ref" />
|
||
)}
|
||
<path
|
||
d={path}
|
||
className="btd-osc-line"
|
||
fill="none"
|
||
stroke={pane.color}
|
||
strokeWidth="1.5"
|
||
/>
|
||
</svg>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default StrategyOscillatorPanes;
|