Files
goldenChart/frontend/src/components/backtest/BacktestOscillatorPanes.tsx
T
2026-05-25 20:23:54 +09:00

99 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';
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(' ');
}
function computeRsi(closes: number[], period = 14): number[] {
if (closes.length < period + 1) return [];
const out: number[] = [];
let avgGain = 0;
let avgLoss = 0;
for (let i = 1; i <= period; i++) {
const d = closes[i] - closes[i - 1];
if (d >= 0) avgGain += d; else avgLoss -= d;
}
avgGain /= period;
avgLoss /= period;
for (let i = period; i < closes.length; i++) {
const d = closes[i] - closes[i - 1];
const gain = d > 0 ? d : 0;
const loss = d < 0 ? -d : 0;
avgGain = (avgGain * (period - 1) + gain) / period;
avgLoss = (avgLoss * (period - 1) + loss) / period;
const rs = avgLoss === 0 ? 100 : avgGain / avgLoss;
out.push(100 - 100 / (1 + rs));
}
return out;
}
function computeMacd(closes: number[]): number[] {
const ema = (arr: number[], p: number) => {
const k = 2 / (p + 1);
let v = arr[0];
return arr.map((x, i) => (i === 0 ? x : (v = x * k + v * (1 - k))));
};
const e12 = ema(closes, 12);
const e26 = ema(closes, 26);
return e12.map((v, i) => v - e26[i]);
}
interface Props {
bars: OHLCVBar[];
}
/** 백테스팅 분석 — RSI / MACD 하단 pane (참조 UI) */
const BacktestOscillatorPanes: React.FC<Props> = ({ bars }) => {
const { rsiPath, macdPath, rsiLast, macdLast } = useMemo(() => {
const closes = bars.map(b => b.close);
const rsi = computeRsi(closes).slice(-80);
const macd = computeMacd(closes).slice(-80);
return {
rsiPath: spark(rsi, 280, 52, 0, 100),
macdPath: spark(macd, 280, 52),
rsiLast: rsi[rsi.length - 1],
macdLast: macd[macd.length - 1],
};
}, [bars]);
if (bars.length < 20) return null;
return (
<div className="btd-osc-stack">
<div className="btd-osc-pane">
<div className="btd-osc-head">
<span className="btd-osc-label">RSI (14)</span>
<span className="btd-osc-val">{rsiLast != null ? rsiLast.toFixed(1) : ''}</span>
</div>
<svg className="btd-osc-svg" viewBox="0 0 280 52" preserveAspectRatio="none">
<line x1="0" y1="15" x2="280" y2="15" className="btd-osc-ref btd-osc-ref--hi" />
<line x1="0" y1="26" x2="280" y2="26" className="btd-osc-ref" />
<line x1="0" y1="37" x2="280" y2="37" className="btd-osc-ref btd-osc-ref--lo" />
<path d={rsiPath} className="btd-osc-line btd-osc-line--rsi" fill="none" />
</svg>
</div>
<div className="btd-osc-pane">
<div className="btd-osc-head">
<span className="btd-osc-label">MACD (12, 26, 9)</span>
<span className="btd-osc-val">{macdLast != null ? macdLast.toFixed(2) : ''}</span>
</div>
<svg className="btd-osc-svg" viewBox="0 0 280 52" preserveAspectRatio="none">
<line x1="0" y1="26" x2="280" y2="26" className="btd-osc-ref" />
<path d={macdPath} className="btd-osc-line btd-osc-line--macd" fill="none" />
</svg>
</div>
</div>
);
};
export default BacktestOscillatorPanes;