Files
goldenChart/frontend/src/components/backtest/StrategyOscillatorPanes.tsx
T
2026-05-31 16:31:25 +09:00

97 lines
3.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;