백테스팅 적용

This commit is contained in:
Macbook
2026-05-25 20:23:54 +09:00
parent 02d14e4b2b
commit cae1c3a624
33 changed files with 3399 additions and 385 deletions
@@ -0,0 +1,98 @@
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;