React SVG를 사용해서 candlestick 그리기

개발을 하다가, candlestick을 화면에 표시를 해야 했었고, 그때 사용한 코드를 정리

import { useMemo } from "react";

/* 전체 폭 */
const WIDTH = 16;

/* 심지 폭 */
const WICK_WITH = 1.5;

/* 몸통 폭 */
const BODY_WIDTH = 6;

/* 몸통 시작점 값 */
const BODY_X = (WIDTH - BODY_WIDTH) / 2;

/* 차트 절대 높이 */
const CHART_HEIGHT = 35;

const trendColors = {
  foreground: "#ffffff",
  trendUp: "#00ff00",
  trendDown: "#ff0000",
};

export default function Candlestick({
  openPrice,
  closePrice,
  highPrice,
  lowPrice,
}) {
  const priceRange = highPrice - lowPrice;

  const { isUp, color, bodyY, bodyHeight, wickTopY, wickBottomY } =
    useMemo(() => {
      if (priceRange === 0) {
        // 값이 모두 같을 때 처리
        return {
          isUp: false,
          color: trendColors.foreground, // 상승/하락이 아닌 중립 색상 추가
          bodyY: CHART_HEIGHT / 2,
          bodyHeight: 0,
        };
      }

      const isUp = closePrice > openPrice; // 상승 여부
      const color = isUp ? trendColors.trendUp : trendColors.trendDown; // 상승/하락 색상 적용

      const scale = CHART_HEIGHT / priceRange; // 가격과 픽셀 간 비율
      const bodyHeight = Math.abs(closePrice - openPrice) * scale; // 시가-종가 차이
      const bodyY = isUp
        ? (highPrice - closePrice) * scale // 상승: 종가 기준
        : (highPrice - openPrice) * scale; // 하락: 시가 기준

      return { isUp, color, bodyY, bodyHeight };
    }, [openPrice, closePrice, highPrice, lowPrice, priceRange, trendColors]);

  return (
    <svg
      width={WIDTH}
      height={CHART_HEIGHT}
      viewBox={`0 0 ${WIDTH} ${CHART_HEIGHT}`}
    >
      {/* 심지 (wick) */}
      <line
        /* 중앙 값 x1, x2 */
        x1={WIDTH / 2}
        y1={0}
        x2={WIDTH / 2}
        y2={CHART_HEIGHT}
        stroke={color}
        strokeWidth={WICK_WITH}
      />
      {/* 몸통 (body) */}
      <rect
        x={BODY_X}
        y={bodyY}
        width={BODY_WIDTH}
        height={bodyHeight}
        fill={color}
      />
    </svg>
  );
}


import "./styles.css";

export default function App() {
  return (
    <div
      style={{
        display: "flex",
        gap: "20px",
        background: "#222",
        padding: "20px",
      }}
    >
      {/* 상승하는 캔들 */}
      <Candlestick openPrice={10} closePrice={15} highPrice={18} lowPrice={8} />

      {/* 하락하는 캔들 */}
      <Candlestick openPrice={15} closePrice={10} highPrice={18} lowPrice={8} />

      {/* 시가와 종가가 같은 중립 캔들 */}
      <Candlestick
        openPrice={12}
        closePrice={12}
        highPrice={16}
        lowPrice={10}
      />

      {/* 작은 몸통, 긴 심지 */}
      <Candlestick openPrice={13} closePrice={14} highPrice={20} lowPrice={8} />

      {/* 큰 몸통, 짧은 심지 */}
      <Candlestick openPrice={8} closePrice={18} highPrice={19} lowPrice={7} />
    </div>
  );
}

scale: 가격을 픽셀 단위로 변환하는 비율

const scale = CHART_HEIGHT / priceRange;
  • CHART_HEIGHT: 차트의 높이 (픽셀 단위, 35px)
  • priceRange: 가격 범위 (highPrice – lowPrice)
  • scale: 1원(또는 1달러)당 차트에서 몇 픽셀인지 계산

예제

  • highPrice = 18, lowPrice = 8
  • priceRange = 18 – 8 = 10
  • scale = 35 / 10 = 3.5
    • 즉, 가격 1단위 = 3.5px

bodyHeight: 몸통의 높이

const bodyHeight = Math.abs(closePrice - openPrice) * scale;
  • closePrice – openPrice: 몸통의 실제 가격 변동 폭
  • Math.abs(…): 시가 < 종가(상승)든, 시가 > 종가(하락)이든 항상 양수로 변환
  • scale: 픽셀 단위 변환

예제

  • openPrice = 10, closePrice = 15
  • Math.abs(15 – 10) = 5
  • bodyHeight = 5 * 3.5 = 17.5px

bodyY: 몸통의 시작 위치

const bodyY = isUp
  ? (highPrice - closePrice) * scale // 상승: 종가 기준
  : (highPrice - openPrice) * scale; // 하락: 시가 기준
  • 캔들스틱의 y 좌표는 highPrice에서부터 계산됨.
  • 상승(isUp = true): 종가(closePrice) 기준으로 계산
  • 하락(isUp = false): 시가(openPrice) 기준으로 계산

예제 (상승)

  • highPrice = 18, closePrice = 15
  • (18 – 15) * 3.5 = 3 * 3.5 = 10.5
  • bodyY = 10.5px → 위에서부터 10.5px 아래에 위치

예제 (하락)

  • highPrice = 18, openPrice = 15
  • (18 - 15) * 3.5 = 10.5
  • bodyY = 10.5px → 위에서부터 10.5px 아래에 위치

정리

  1. scale → 가격을 차트 높이에 맞게 픽셀 단위로 변환
  2. bodyHeight → 몸통의 높이를 계산
  3. bodyY → 몸통이 시작될 위치를 결정

캔들스틱 차트가 내포하는 의미: https://steemit.com/kr/@phuzion7/candlestick-patterns

Leave a Comment