[HighCharts] Stock Chart 구현해보기

windowook·2024년 11월 8일
post-thumbnail

🌱 소개

https://www.highcharts.com/

공식 홈페이지에서 소개하는 것과 같이 HighCharts는 다양한 유형의 차트를 지원하는 자바스크립트 라이브러리입니다. 크게 Core, Stock, Maps, DashBoard, Gantt 총 5가지의 카테고리가 있고

https://www.highcharts.com/demo#highcharts-demo-line-charts

세부적으로 엄청나게 다양한 차트 데모를 가지고 있습니다.
각 데모는 코드를 제공하고, 공식 문서를 읽으면서 개발자가 쉽게 커스텀할 수 있습니다.
이번에 구현해 볼 차트는 Stock chart with GUI입니다.

위 차트는 이름부터 보시면 아시겠지만 주식이나 코인과 같은 캔들 데이터를 시각화하여 차트로 나타내는데에 특화되어있는 차트입니다. 저는 이 차트를 이용하여 실시간 코인 차트를 시각화 하는데에 사용했습니다. 그 경험을 바탕으로 어떻게 구현하는지 차근차근 설명하도록 하겠습니다.

🌱 설치

https://www.highcharts.com/docs/getting-started/install-from-npm

// npm
npm install highcharts --save
npm install highcharts-react-official -- save

// yarn
yarn add highcharts
yarn add highcharts-react-official

먼저 터미널에서 위 명령어를 실행하여 프로젝트에 패키지를 설치해줍니다. highcharts-react-official은 리액트를 사용하는 프로젝트에서 차트를 컴포넌트 형태로 사용할 수 있게 해줍니다.

 <HighchartsReact
   highcharts={Highcharts}
   constructorType={'stockChart'}
   options={options}
 />

위와 같이 말이죠. HighCharts와 컨스트럭터 타입은 상수값으로 prop 형태로 전달해주면 되고 옵션은 초기 옵션을 설정해주고 상태로 관리하면서 컴포넌트 안에서 수정하고 싶은 부분만 바꿔주면 됩니다. 저는 이 라이브러리도 사용하여 구현했으니 여러분도 추천드립니다.

🌱 옵션 설정 설명

우선 전체 코드를 보여드리고, 세세하게 설명하도록 하겠습니다.

// Chart.jsx
import { useState, useEffect, useCallback, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { globalColors } from '@/globalColors';
import { Box } from '@mui/material';
import axios from 'axios';
import Highcharts from 'highcharts/highstock';
import HighchartsReact from 'highcharts-react-official';
import indicators from 'highcharts/indicators/indicators';

indicators(Highcharts);
Highcharts.setOptions({
  lang: {
    rangeSelectorZoom: '기간', // 범위 설렉터 설명
  },
  time: {
    useUTC: false, // UTC 시간 사용 여부
  },
});

const initialOptions = {
  chart: {
    maxWidth: 900,
    height: 400,
    zooming: {
      mouseWheel: {
        enabled: true, // 마우스 휠줌 가능
        sensitivity: 1.3, // 감도
      },
    },
  },

  accessibility: {
    enabled: false,
  },

  credits: {
    enabled: true, // 차트 우측 하단에 Highcharts.com 표시 여부
    text: 'Mr Cryp',
  },

  navigator: {
    enabled: true, // 구간을 선택할 수 있는 네비게이터 사용 여부
  },

  yAxis: [
    {
      labels: {
        align: 'right', // 정렬
        x: -4, // 차트 우측으로부터의 거리
        // y축 천 단위 구분 기호 설정
        formatter: function () {
          return Highcharts.numberFormat(Number(this.value), 0, '', ',');
        },
      },
      height: '80%', // y축 높이
      lineWidth: 2, // y축 선 굵기
      // 마우스 포인터 위치를 나타내는 크로스헤어
      crosshair: {
        snap: false,
      },
    },
    {
      labels: {
        align: 'right',
        x: -3,
      },
      top: '80%',
      height: '20%', // 레이블의 높이
      offset: 0,
      lineWidth: 2, // 볼륨 선 굵기
    },
  ],

  plotOptions: {
    candlestick: {
      color: globalColors.color_neg['400'], // 음봉
      upColor: globalColors.color_pos['400'], // 양봉
    },
    sma: {
      linkedTo: 'upbit', // 이동평균선 연결
      lineWidth: 0.8, // 이동평균선 굵기
      zIndex: 1, // 이동평균선 z-index
      marker: {
        enabled: false, // 마커 표시 여부
      },
      enableMouseTracking: false, // 마우스 트래커 표시 여부
    },
  },

  tooltip: {
    shared: true, // 여러 series를 한 번에 설정하는 옵션
    formatter: function () {
      let tooltipText = `<b>${Highcharts.dateFormat('%Y-%m-%d %H:%M:%S', this.x)}</b><br/><br/>`; // x축 기준 시간

      this.points.forEach(point => {
        if (point.series.type === 'candlestick') {
          const color =
            point?.point?.close > point?.point?.open
              ? globalColors.color_pos
              : globalColors.color_neg;

          tooltipText += `
            <span style="color:${color[400]}">●</span> <b>${point.series.name}</b><br/>
            시가: ${Highcharts.numberFormat(point.point.open, 0, '.', ',')}<br/>
            고가: ${Highcharts.numberFormat(point.point.high, 0, '.', ',')}<br/>
            저가: ${Highcharts.numberFormat(point.point.low, 0, '.', ',')}<br/>
            종가: ${Highcharts.numberFormat(point.point.close, 0, '.', ',')}<br/><br/>
          `;
        } else if (point.series.type === 'column') {
          tooltipText += `<span style="color:${point.color}">●</span> <b>${point.series.name}</b><br/>${point.y}<br/>`;
        }
      });

      return tooltipText;
    },

    style: {
      fontSize: '0.75rem', // 툴팁의 폰트 크기
    },

    backgroundColor: globalColors.tooltip_bgColor,
    borderRadius: 4,
    borderWidth: 1,
    shadow: false,
  },
};

export default function Chart() {
  const [options, setOptions] = useState(initialOptions);
  const [candles, setCandles] = useState([]);

  const code = useSelector(state => state.chart.code);

  const fetchCandles = useCallback(
    async type => {
      let fetchedCandles;
      try {
        const response = await axios.get('/api/candles', {
          params: {
            type,
            unit: type.replace('min', ''),
            ticker: code,
            count: 200,
          },
        });
        fetchedCandles = response.data;
      } catch (error) {
        console.error('캔들 다운로드 중 에러 발생 :', error);
        return;
      }
      setCandles(fetchedCandles);
    },
    [code],
  );

  useEffect(() => {
    fetchCandles('1min'); // 기본값 1분봉
  }, [fetchCandles]);

  const rangeSelector = useMemo(
    () => ({
      allButtonsEnabled: true,
      inputEnabled: false,
      buttons: [
        {
          text: '1분봉',
          events: {
            click: () => fetchCandles('1min'),
          },
        },
        {
          text: '5분봉',
          events: {
            click: () => fetchCandles('5min'),
          },
        },
        {
          text: '일봉',
          events: {
            click: () => fetchCandles('days'),
          },
        },
        {
          text: '주봉',
          events: {
            click: () => fetchCandles('weeks'),
          },
        },
        {
          text: '월봉',
          events: {
            click: () => fetchCandles('months'),
          },
        },
      ],
    }),
    [fetchCandles],
  );

  useEffect(() => {
    if (candles.length > 0) {
      candles.sort((a, b) => a.timestamp - b.timestamp);
      const ohlc = candles.map(candle => [
        candle.timestamp,
        candle.opening_price,
        candle.high_price,
        candle.low_price,
        candle.trade_price,
      ]);
      const minTimestamp = ohlc[0][0];
      const maxTimestamp = ohlc[ohlc.length - 1][0];
      const volume = candles.map(candle => ({
        x: candle.timestamp,
        y: candle.candle_acc_trade_volume,
        color:
          candle.opening_price <= candle.trade_price
            ? globalColors.hotpink['200']
            : globalColors.skyblue['200'],
      }));

      setOptions(prevOptions => ({
        ...prevOptions,
        // x 축
        xAxis: {
          min: minTimestamp,
          max: maxTimestamp,
        },
        // 범위 셀렉터
        rangeSelector,
        // 시리즈
        series: [
          // 기간별 캔들스틱 차트
          {
            type: 'candlestick',
            name: code,
            id: 'upbit',
            data: ohlc,
          },
          // 이동평균선 15
          {
            type: 'sma',
            params: {
              period: 15,
            },
            color: globalColors.sma_15,
          },
          // 이동평균선 50
          {
            type: 'sma',
            params: {
              period: 50,
            },
            color: globalColors.sma_50,
          },
          // 누적 거래량 막대 그래프
          {
            type: 'column',
            name: '누적 거래량',
            data: volume,
            yAxis: 1,
          },
        ],
      }));
    }
  }, [candles, code, fetchCandles, rangeSelector]);

  return (
    <Box>
      <HighchartsReact
        highcharts={Highcharts}
        constructorType={'stockChart'}
        options={options}
      />
    </Box>
  );
}

Highcharts.setOptions({lang: {}})

https://api.highcharts.com/highstock/lang.thousandsSep

lang은 언어 객체입니다. lang은 Highcharts의 언어 관련 설정을 담당하며, 각 차트 인스턴스별로 설정할 수 있는 것이 아니라, 전역 기본 설정으로만 사용할 수 있습니다.

차트가 초기화되기 전에 설정하려면 Highcharts.setOptions을 사용합니다.
setOptions는 차트의 전역 기본 설정을 변경하기 위해 사용하는 메서드입니다.

  • thousandsSep : 천 단위를 구분하는 문자를 설정합니다. 보통 ','로 설정합니다.
  • rangeSelectorZoom: 범위 셀렉터에 대한 이름(설명)을 설정합니다.

설정한 옵션으로 나타난 결과입니다. 셀렉터 가장 좌측에 rangeSelectorZoom으로 설정한 '기간'이, 툴팁의 가격에는 천 단위 구분자로 ','이 적용된 모습입니다.

indicators

indicators는 Highcharts 모듈의 일부로, 이를 사용하면 차트에 기술적 분석 지표를 표시할 수 있게 됩니다. 이동 평균선(SMA)과 같은 인디케이터를 처리할 수 있게 되어 plotOptions 및 series 속성에서 sma 타입의 시리즈를 추가할 수 있게 합니다.

{
  type: 'column',
  name: '누적 거래량',
  data: volume,
  yAxis: 1,
}

initialOptions

컴포넌트 안에서 useState로 관리되고 있는 Highcharts의 options의 초기값 역할인 객체입니다. 여기서 설정해준 옵션들은 후에 컴포넌트 내부에서 useEffect 스코프 안에서 실행되는 setOptions로 덮어써지지 않는다면 계속 차트의 옵션으로 유지됩니다.

chart
차트 자체의 속성을 설정합니다. 이 옵션은 제가 사용한 속성을 위주로만 설명하겠습니다.

  • maxWidth: 최대 폭을 설정합니다.
  • height: 고정된 높이를 설정합니다.
  • zooming: 줌인과 줌아웃에 관한 옵션을 설정합니다. 하위 옵션들이 있습니다.
    • mousewheel: 마우스 휠로 줌인과 줌아웃을 할 때의 옵션을 설정합니다.
      • enabled: true라면 마우스 휠로 줌인과 줌아웃을 가능하게 합니다.
      • sensitivity: 마우스 휠의 감도를 설정합니다. 이 값이 크면 휠 속도가 빠릅니다.

accessibility
Highcharts에서 제공하는 웹 접근성 개선에 관한 속성을 설정합니다.

보시는바와 같이 키보드로만 차트를 조작할 수 있게 도와주는 keyboardNavigation이나, 시각장애인을 위한 screenReaderSection, 색약인 사람들을 위해서 색상 극대비를 적용시켜주는 highContrastMode와 Theme 옵션을 갖고 있습니다.

저는 따로 이 설정들을 해주지 않았습니다. 혹시나 이러한 웹 접근성도 관심이 있으시거나 필요하다면 공식 문서를 읽으시면서 옵션을 설정해주시면 될 것 같습니다.

credits
차트가 표시되는 영역 우측 하단에 크레딧을 표시할 것인지 여부와 연결할 링크, 텍스트 등을 설정할 수 있습니다.

기본은 'Highcharts.com'이라는 텍스트가 표시됩니다. 여러분이 프로젝트에서 차트를 구현한다면 여기다 프로젝트 이름을 넣어도 좋을 것 같습니다.

navigator
차트 하단에 차트로 표현되는 시간대의 구간을 조정할 수 있는 네비게이터를 설정합니다.

Stack Overflow에서 가져온 이미지입니다. 빨간색 동그라미가 쳐진 부분이 네비게이터입니다.

여기서도 height이나 margin, outlineColor와 같은 레이아웃의 스타일 설정도 가능하고 x축과 y축에 대한 속성도 있으니 필요에 따라 추가하여 옵션을 설정하시면 될 것 같습니다.

yAxis
차트에서 핵심인 y축 데이터에 관한 설정입니다. yAxis는 배열 형태로 다수의 시리즈에 대해서 옵션을 설정할 수 있습니다. 시리즈란 한국어로 시계열을 말합니다. 일정 시간 간격으로 배치된 데이터들의 수열을 의미합니다.

현재 제가 구현한 코드에서는 캔들 데이터를 받아와서 만들어놓은

  • ohlc라는 데이터를 연결해 둔 캔들스틱 차트
  • y축 누적 거래량을 의미하는 컬럼 차트(막대 그래프)

총 2개의 시리즈가 존재합니다.

// 기간별 캔들스틱 차트
{
  type: 'candlestick',
  name: code,
  id: 'upbit',
  data: ohlc,
}

// 누적 거래량 막대 그래프
{
  type: 'column',
  name: '누적 거래량',
  data: volume,
  yAxis: 1,
}

다수의 시리즈를 yAxis에서 설정할 때는 시리즈 간 순서를 설정해야 합니다. 저는 누적 거래량 시리즈에 yAxis 값을 1로 설정해줬는데 yAxis 옵션 배열에서 인덱스 1번째의 객체로 설정하겠다는 것을 의미합니다. 그럼 객체 안에 정의된 옵션들을 설명하겠습니다.

  • labels: 금액을 나타내는 라벨에 대한 옵션을 설정합니다.
    • align: 정렬 위치를 설정합니다.
    • x: 차트 영역에서 정렬 위치를 기준으로 떨어진 거리를 설정합니다. 픽셀 단위로 설정됩니다.
    • formatter function (): 포매터 함수는 원하는 설정을 함수 스코프 안에서 구현할 수 있게
      Highcharts에서 제공하는 함수입니다. 저는 금액에 구분기호를 설정하기 위해서 Highcharts의
      numberFormat 메서드를 같이 사용하여 천 단위 구분기호에 ','를 설정했습니다.
    • Highcharts.numberFormat(): 파라미터를 설명해드리겠습니다.
      • 1번째: 포맷할 숫자입니다.
      • 2번째: 소수점 이하 자릿수입니다. 0은 정수로 표현하겠다는 의미입니다.
      • 3번째: 소수점 기호를 지정합니다. 2번째가 0이면 빈 문자열을 전달합니다.
      • 마지막: 천 단위 구분자를 지정합니다. ','로 지정하시면 됩니다.
    • height: 차트 영역 안에서 y축의 높이를 설정합니다.
    • lineWidth: y축의 선 굵기를 설정합니다.
    • crosshair: 차트에서 마우스포인터를 따라 표시되는 크로스헤어(십자선)에 대한 옵션을 설정합니다.
      • snap: 십자선이 데이터의 포인트를 따라 움직이게할지, 마우스 포인터를 기준으로 따라다니게 할지
        설정합니다. false로 설정해야 마우스 포인터를 기준으로 따라다닙니다.
    • offset: y축과 차트 영역 사이의 간격을 설정합니다.

plotOptions
차트의 특정 시리즈 타입에 대한 전역 옵션을 설정하는 데 사용됩니다.

  • candlestick: 캔들스틱 차트의 옵션을 설정합니다.
    • color: 캔들스틱의 기본 컬러입니다. 음봉을 의미한다고 생각하시면 됩니다.
    • upColor: 가격이 상승중일 때의 캔들스틱 컬러, 양봉을 의미한다고 생각하시면 됩니다.
  • sma: 이동평균선의 옵션을 설정합니다.
    • linkedTo: 어떤 시리즈와 연결할지 설정합니다. 시리즈의 id를 문자열로 전달됩니다.
    • lineWidth: 이동평균선의 굵기를 설정합니다.
    • zIndex: 이동평균선의 z-index를 설정합니다.
    • marker: 마커의 표시여부를 선택합니다. 마커는 이동평균선의 period만큼 표시되기 때문에
      저는 표시되지 않게 만들었습니다. 추이를 보는데에 마커까지는 필요하지 않다고 생각했습니다.
    • enableMouseTracking: 마우스 포인터와 x축과 y축이 같은 위치에 하이라이트가 됩니다.
      이 또한 저는 표시되지 않게 설정했습니다.

tooltip
차트 위에 마우스 포인터를 올렸을 때 x축과 y축을 기준으로 해당 시간의 시리즈 정보들을 표시해주는 툴팁 박스에 대한 옵션을 설정합니다.

  • shared: true로 설정하면 툴팁에 여러 시리즈를 함께 설정합니다. false라면 하나의 시리즈만 나타냅니다.
  • formatter function()
    • tooltipText: 툴팁 안에 표시될 텍스트를 템플릿 리터럴로 만들었습니다.
      먼저 시간을 표시하고 캔들스틱과 누적거래량의 정보를 템플릿에 더하는 방식으로 포맷을 만들었습니다.
    • this: 여기서 this는 툴팁이 표시되는 시점의 데이터 포인트와 시리즈 정보를 포함하는 객체입니다.
      현재 툴팁과 관련된 데이터 컨텍스트를 참조합니다.
  • style: 툴팁의 스타일 관련 속성입니다.
    • fontSize: 툴팁박스 내 폰트 크기를 설정합니다.
  • backgroundColor: 배경색을 설정합니다.
  • borderRadius: 모서리의 둥근 정도를 설정합니다.
  • borderWidth: 경계선의 굵기를 설정합니다.
  • shadow: 그림자를 활성화할지 여부를 설정합니다. 이걸 true로 하면 제가 설정해둔 배경색의 opacity 50%가 풀려버려서 저는 false로 설정해줬습니다. 혹시라도 반투명한 툴팁으로 배경색을 지정해두신다면
    이 옵션은 false로 해두셔야 합니다.

초기 옵션인 initialOptions를 설정하는 과정은 끝났습니다. 그럼 useEffect에서 시리즈에 사용할 데이터를 어떻게 저장하고 새로운 options를 설정하는 과정을 설명한 뒤 마치도록 하겠습니다.

시리즈 데이터 저장과 새로운 options 설정

useEffect(() => {
    if (candles.length > 0) {
      candles.sort((a, b) => a.timestamp - b.timestamp);
      const ohlc = candles.map(candle => [
        candle.timestamp,
        candle.opening_price,
        candle.high_price,
        candle.low_price,
        candle.trade_price,
      ]);
      const minTimestamp = ohlc[0][0];
      const maxTimestamp = ohlc[ohlc.length - 1][0];
      const volume = candles.map(candle => ({
        x: candle.timestamp,
        y: candle.candle_acc_trade_volume,
        color:
          candle.opening_price <= candle.trade_price
            ? globalColors.skyblue['200']
            : globalColors.hotpink['200'],
      }));

      setOptions(prevOptions => ({
        ...prevOptions,
        // x 축
        xAxis: {
          min: minTimestamp,
          max: maxTimestamp,
        },
        // 범위 셀렉터
        rangeSelector,
        // 시리즈
        series: [
          // 기간별 캔들스틱 차트
          {
            type: 'candlestick',
            name: code,
            id: 'upbit',
            data: ohlc,
          },
          // 이동평균선 15
          {
            type: 'sma',
            params: {
              period: 15,
            },
            color: globalColors.sma_15,
          },
          // 이동평균선 50
          {
            type: 'sma',
            params: {
              period: 50,
            },
            color: globalColors.sma_50,
          },
          // 누적 거래량 막대 그래프
          {
            type: 'column',
            name: '누적 거래량',
            data: volume,
            yAxis: 1,
          },
        ],
      }));
    }
  }, [candles, code, fetchCandles, rangeSelector]);

ohlc
ohlc는 Open, High, Low, Close = 시가, 고가, 저가, 종가를 의미합니다.
캔들스틱 차트는 하나의 캔들 데이터를 툴팁으로 표시하기 위해서 ohlc 배열이 필요합니다.
배열에는 타임스탬프가 포함되어도 포함되지 않아도 상관없지만 저는 포함시켜서 툴팁에 표시되게 만들었습니다.
candles라는 데이터는 현재 제 프로젝트에서는 업비트의 REST API를 사용하여 받아오는 중인데
여러분은 자신이 사용하는 오픈 API로 데이터를 받아와서 candles에 저장하시면 되겠습니다.

minTimestamp, maxTimestmap
차트에서 타임스탬프의 시작점과 종료지점을 설정하기 위해 필요한 속성입니다. 설정은 간단합니다. ohlc 배열에서 첫 번째 값의 타임스탬프와 가장 마지막 값의 타임스탬프를 각각 min, max로 해주면되겠죠. 이건 아래에 setOptions로 x축에 설정하는 min, max에 그대로 넣어주면 됩니다.

volume
누적 거래량을 의미합니다. 타입은 객체입니다.

  • x: x축에 설정할 옵션입니다. 저는 타임스탬프로 설정했습니다.
  • y: y축에 설정할 옵션입니다. 누적 거래량을 설정했습니다.
  • color: column 차트의 막대 컬러를 설정합니다. 저는 종가가 시가이상이면 양의 막대로 나타나게 설정했습니다.

setOptions
초기 옵션에서 추가해 줄 옵션이나 덮어쓸 옵션을 넣어줍니다.

  • rangeSelector: 1분봉, 5분봉, 일봉, 주봉, 월봉을 선택하는 범위 셀렉터입니다. 셀렉터를 옵션에 추가시켜줘야 렌더링됩니다.
  • series: 차트에 표시될 시계열 데이터를 설정하는 옵션입니다. 캔들스틱 차트, 이동평균선, 누적 거래량 막대 그래프를 배열에 넣어줍니다.

🌱 완성

구현 중 헤매는 부분은 구글링과 GPT의 도움을 받아 해결했었는데, 공식 문서만 잘 읽어도 원하는 옵션을 설정하여 차트를 구현하는데에는 무리가 없을 것 같네요. Highcharts는 사용하기 쉬워서 차트로 시각화가 필요할 때 좋은 라이브러리라고 생각합니다.

profile
안녕하세요

0개의 댓글