모의투자, 백테스팅 레이아웃 수정
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user