모의투자, 백테스팅 레이아웃 수정

This commit is contained in:
Macbook
2026-05-24 20:13:21 +09:00
parent 958b813f3b
commit af230a4233
23 changed files with 2394 additions and 782 deletions
@@ -0,0 +1,214 @@
import React, { useMemo } from 'react';
interface Candle {
x: number;
o: number;
h: number;
l: number;
c: number;
}
interface Point {
x: number;
y: number;
}
const W = 1400;
const H = 820;
const CANDLE_COUNT = 110;
const DISPLACE = 26;
/** Y축: 위로 갈수록(y↓) 가격 상승 — 좌하→우상 우상향 */
function buildUpwardCandles(): Candle[] {
const startX = 48;
const step = (W - 96) / (CANDLE_COUNT - 1);
const yStart = H - 120;
const yEnd = 100;
const out: Candle[] = [];
for (let i = 0; i < CANDLE_COUNT; i++) {
const t = i / (CANDLE_COUNT - 1);
const trend = yStart + (yEnd - yStart) * Math.pow(t, 0.92);
const ripple = Math.sin(i * 0.38) * 14 + Math.cos(i * 0.16) * 9 + Math.sin(i * 0.07) * 5;
const o = i === 0 ? trend + 8 : out[i - 1].c;
const c = trend + ripple * 0.55;
const bodyTop = Math.min(o, c);
const bodyBot = Math.max(o, c);
const wick = 4 + (i % 3);
out.push({
x: startX + i * step,
o,
c,
h: bodyTop - wick,
l: bodyBot + wick,
});
}
return out;
}
function ichimokuMid(candles: Candle[], i: number, period: number): number {
const from = Math.max(0, i - period + 1);
const slice = candles.slice(from, i + 1);
const highest = Math.min(...slice.map(c => c.h));
const lowest = Math.max(...slice.map(c => c.l));
return (highest + lowest) / 2;
}
function linePath(points: Point[]): string {
if (points.length < 2) return '';
return points.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(' ');
}
function buildKumoRegions(senkouA: Point[], senkouB: Point[]): { bullPaths: string[]; bearPaths: string[] } {
const n = Math.min(senkouA.length, senkouB.length);
const bullPaths: string[] = [];
const bearPaths: string[] = [];
let runStart = 0;
let runBull = senkouA[0].y <= senkouB[0].y;
const closeRun = (end: number) => {
if (end - runStart < 2) return;
const sliceA = senkouA.slice(runStart, end);
const sliceB = senkouB.slice(runStart, end);
const fwdA = sliceA.map(p => `${p.x},${p.y}`).join(' L');
const backB = [...sliceB].reverse().map(p => `${p.x},${p.y}`).join(' L');
const d = `M${fwdA} L${backB} Z`;
if (runBull) bullPaths.push(d);
else bearPaths.push(d);
};
for (let i = 1; i < n; i++) {
const bull = senkouA[i].y <= senkouB[i].y;
if (bull !== runBull) {
closeRun(i);
runStart = i;
runBull = bull;
}
}
closeRun(n);
return { bullPaths, bearPaths };
}
export default function SplashChartBackground() {
const model = useMemo(() => {
const candles = buildUpwardCandles();
const tenkan: Point[] = candles.map((c, i) => ({
x: c.x,
y: ichimokuMid(candles, i, 9),
}));
const kijun: Point[] = candles.map((c, i) => ({
x: c.x,
y: ichimokuMid(candles, i, 26),
}));
const senkouA: Point[] = [];
const senkouB: Point[] = [];
for (let i = 0; i < candles.length; i++) {
const future = candles[Math.min(i + DISPLACE, candles.length - 1)];
senkouA.push({
x: future.x,
y: (tenkan[i].y + kijun[i].y) / 2,
});
senkouB.push({
x: future.x,
y: ichimokuMid(candles, i, 52),
});
}
const { bullPaths, bearPaths } = buildKumoRegions(senkouA, senkouB);
const volumes = candles.map((c, i) => {
const body = Math.abs(c.c - c.o);
const vh = 10 + body * 1.4 + (Math.sin(i * 0.4) + 1) * 8;
return { x: c.x, h: vh };
});
return { candles, tenkan, kijun, senkouA, senkouB, bullPaths, bearPaths, volumes };
}, []);
const gridY = useMemo(() => {
const lines: number[] = [];
for (let y = 80; y <= H - 60; y += 70) lines.push(y);
return lines;
}, []);
return (
<svg
className="splash-chart-bg"
viewBox={`0 0 ${W} ${H}`}
preserveAspectRatio="xMidYMid slice"
aria-hidden
>
<defs>
<linearGradient id="splashKumoBull" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#22c55e" stopOpacity="0.28" />
<stop offset="100%" stopColor="#22c55e" stopOpacity="0.06" />
</linearGradient>
<linearGradient id="splashKumoBear" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#ef4444" stopOpacity="0.22" />
<stop offset="100%" stopColor="#ef4444" stopOpacity="0.05" />
</linearGradient>
</defs>
<rect width={W} height={H} fill="#0a0c10" />
{gridY.map(y => (
<line key={y} x1="0" y1={y} x2={W} y2={y} className="splash-chart-grid" />
))}
{/* 거래량 */}
<g className="splash-chart-volume">
{model.volumes.map((v, i) => (
<rect
key={i}
x={v.x - 4}
y={H - 52 - v.h}
width="8"
height={v.h}
className="splash-vol-bar"
rx="0.5"
/>
))}
</g>
{/* 일목 구름 */}
<g className="splash-chart-kumo">
{model.bearPaths.map((d, i) => (
<path key={`bear-${i}`} d={d} fill="url(#splashKumoBear)" />
))}
{model.bullPaths.map((d, i) => (
<path key={`bull-${i}`} d={d} fill="url(#splashKumoBull)" />
))}
</g>
{/* 일목 선행·전환·기준 */}
<path className="splash-ichi splash-ichi--senkou-b" d={linePath(model.senkouB)} />
<path className="splash-ichi splash-ichi--senkou-a" d={linePath(model.senkouA)} />
<path className="splash-ichi splash-ichi--kijun" d={linePath(model.kijun)} />
<path className="splash-ichi splash-ichi--tenkan" d={linePath(model.tenkan)} />
{/* 캔들 */}
<g className="splash-chart-candles">
{model.candles.map((c, i) => {
const up = c.c < c.o;
const top = Math.min(c.o, c.c);
const bodyH = Math.max(Math.abs(c.c - c.o), 3.5);
return (
<g key={i}>
<line x1={c.x} y1={c.h} x2={c.x} y2={c.l} className="splash-candle-wick" />
<rect
x={c.x - 6}
y={top}
width="12"
height={bodyH}
className={up ? 'splash-candle-up' : 'splash-candle-down'}
rx="1"
/>
</g>
);
})}
</g>
</svg>
);
}