Skip to content

chart

1 post with the tag “chart”

bokeh로 봉차트(Candlestick Chart) 그리기

파이썬에서 봉차트를 제공하는 라이브러리가 많지 않습니다. 필자는 matplotlib에서 떨어져 나온 mpl_finance와 bokeh 정도로 알고 있습니다. 이미 mpl_finance로 봉차트를 그리는 방법은 이전 포스트 Matplotlib으로 봉차트(Candlestick Chart) 그리기 에서 다루었습니다.

이번 포스트에서 bokeh로 봉차트를 그리는 방법에 대해서 다루겠습니다. bokeh는 Anaconda에서 개발 중인 차트 라이브러리 입니다. matplotlib과 matplotlib 기반인 seaborn과는 다르게 독자 노선을 가지는 라이브러리 입니다. bokeh는 웹에 최적화 되어있는 라이브러리 입니다. matplotlib을 웹브라우저에 띄우려면 차트를 png 등의 그림으로 변환하여 보여줘야 해서 응답형(reponsible) 차트를 웹에 띄우기가 어렵습니다. 그래서 보통 웹에 차트를 띄울 때는 데이터만 서버에서 받고 Chart.jsD3.js 같은 자바스크립트 라이브러리를 사용하여 데이터를 가시화 합니다. bokeh는 파이썬에서 차트를 가시화하고 그 결과를 쉽게 웹에 띄울 수 있는 여러 방법을 제공합니다. 이 포스트에서는 다루지 않겠습니다.

bokeh에서 봉차트를 제공하고 있습니다. bokeh 봉차트 문서에서 상세한 정보를 확인해 보세요.

먼저 Anaconda3가 설치되어 있는 상태라 가정하고 글을 이어나가겠습니다. Jupyter Lab(Notebook)에서 bokeh가 잘 설치되어 있는지 확인합니다.

from bokeh.io import output_notebook, show
from bokeh.plotting import figure, gridplot
output_notebook()

BokehJS 0.12.16 successfully loaded.

이렇게 로딩이 성공되었다는 메시지가 뜨는지 확인합니다. 만약 다음과 같은 메시지가 뜬다면 Jupyter Lab의 Extension을 설치해야 합니다.

Loading BokehJS ...
JavaScript output is disabled in JupyterLab

Anaconda Prompt에서 다음과 같이 jupyterlab_bokeh 확장을 설치합니다.

Terminal window
jupyter labextension install jupyterlab_bokeh

설치가 되지 않는다면 아마도 node.js가 설치되어 있지 않아서일 것입니다. 그렇다면 conda install nodejs를 통해 먼저 node.js를 설치하고 다시 jupyterlab_bokeh 확장을 설치합니다.

bokeh가 잘 설치되어 있다면 이제 봉차트를 그릴 준비가 되었습니다. 차트 데이터는 다음과 같이 ['date', 'open', 'high', 'low', 'close', 'volume']의 리스트로 있습니다.

import pandas as pd
data = [['20190219', 125000.0, 127500.0, 123500.0, 126000.0, 57757], ['20190220', 125000.0, 127000.0, 124500.0, 126000.0, 68453], ['20190221', 125500.0, 126000.0, 124000.0, 125000.0, 43961], ['20190222', 125000.0, 125000.0, 123500.0, 125000.0, 31065], ['20190225', 125500.0, 126000.0, 124000.0, 125500.0, 45852], ['20190226', 125000.0, 127000.0, 124500.0, 126500.0, 37404], ['20190227', 126500.0, 127000.0, 124500.0, 126000.0, 36131], ['20190228', 126500.0, 127000.0, 124500.0, 125000.0, 69474], ['20190304', 124500.0, 125500.0, 122500.0, 123500.0, 65517], ['20190305', 123000.0, 125000.0, 122000.0, 124500.0, 35186], ['20190306', 124000.0, 124500.0, 122500.0, 123500.0, 35449], ['20190307', 123500.0, 124000.0, 122000.0, 123500.0, 34768], ['20190308', 122500.0, 122500.0, 120500.0, 121500.0, 35118], ['20190311', 121500.0, 122500.0, 120000.0, 122500.0, 39576], ['20190312', 123000.0, 124000.0, 122000.0, 123500.0, 24117], ['20190313', 123000.0, 123500.0, 121500.0, 123500.0, 37649], ['20190314', 123000.0, 124000.0, 122000.0, 123500.0, 95132], ['20190315', 123000.0, 128000.0, 123000.0, 126500.0, 107246], ['20190318', 127000.0, 131000.0, 126500.0, 131000.0, 74644], ['20190319', 130000.0, 134000.0, 129500.0, 133000.0, 68348], ['20190320', 132000.0, 133000.0, 129500.0, 131000.0, 42697], ['20190321', 130000.0, 132000.0, 127500.0, 128500.0, 54018], ['20190322', 127500.0, 129500.0, 126500.0, 127000.0, 32380], ['20190325', 125500.0, 126500.0, 124000.0, 124500.0, 37185], ['20190326', 124500.0, 125500.0, 123500.0, 124000.0, 45161], ['20190327', 124000.0, 125000.0, 123500.0, 124000.0, 34336], ['20190328', 124000.0, 124500.0, 120500.0, 121500.0, 43518], ['20190329', 122500.0, 125000.0, 122000.0, 124500.0, 39035], ['20190401', 124000.0, 126500.0, 124000.0, 126500.0, 22463], ['20190402', 125500.0, 126500.0, 123500.0, 126000.0, 31754], ['20190403', 124500.0, 128000.0, 124500.0, 128000.0, 36250], ['20190404', 128500.0, 128500.0, 126000.0, 128000.0, 34854], ['20190405', 127500.0, 129000.0, 126000.0, 127500.0, 33513], ['20190408', 127500.0, 128000.0, 126000.0, 128000.0, 39005], ['20190409', 128000.0, 129000.0, 127500.0, 128500.0, 33266], ['20190410', 128000.0, 129000.0, 126500.0, 128000.0, 64476], ['20190411', 128500.0, 129000.0, 125000.0, 125000.0, 84802], ['20190412', 126000.0, 127500.0, 125500.0, 127000.0, 39663], ['20190415', 126500.0, 128500.0, 126000.0, 127000.0, 61140], ['20190416', 127000.0, 129000.0, 126500.0, 128500.0, 40123], ['20190417', 128500.0, 129000.0, 127000.0, 128000.0, 30846], ['20190418', 128000.0, 128500.0, 124000.0, 124500.0, 55346], ['20190419', 124500.0, 125000.0, 122000.0, 123000.0, 53439], ['20190422', 123000.0, 123500.0, 121000.0, 122500.0, 30421], ['20190423', 122500.0, 123500.0, 120500.0, 121500.0, 54997], ['20190424', 122500.0, 122500.0, 119500.0, 120000.0, 63486], ['20190425', 121000.0, 121000.0, 118500.0, 119000.0, 36046], ['20190426', 118500.0, 119500.0, 117000.0, 119000.0, 43749], ['20190429', 119000.0, 119500.0, 117000.0, 119500.0, 33516], ['20190430', 122000.0, 123000.0, 119000.0, 119500.0, 94118], ['20190502', 118500.0, 122000.0, 118000.0, 121000.0, 56723], ['20190503', 121000.0, 122000.0, 119500.0, 120000.0, 35240], ['20190507', 118500.0, 119500.0, 117000.0, 117500.0, 44453], ['20190508', 116500.0, 117000.0, 115000.0, 116500.0, 52805], ['20190509', 117000.0, 117000.0, 113000.0, 113000.0, 116012], ['20190510', 113000.0, 114500.0, 110000.0, 111500.0, 86072], ['20190513', 110500.0, 110500.0, 107500.0, 108500.0, 70847], ['20190514', 107500.0, 108000.0, 105000.0, 106500.0, 92820], ['20190515', 107000.0, 107500.0, 105000.0, 107000.0, 68937], ['20190516', 107000.0, 107500.0, 104000.0, 105000.0, 64047]]
df = pd.DataFrame(data, columns=['date', 'open', 'high', 'low', 'close', 'volume'])
dateopenhighlowclosevolume
20190219125000.0127500.0123500.0126000.057757
20190220125000.0127000.0124500.0126000.068453
20190221125500.0126000.0124000.0125000.043961
20190222125000.0125000.0123500.0125000.031065
20190225125500.0126000.0124000.0125500.045852
20190226125000.0127000.0124500.0126500.037404
20190227126500.0127000.0124500.0126000.036131
20190228126500.0127000.0124500.0125000.069474
20190304124500.0125500.0122500.0123500.065517
20190305123000.0125000.0122000.0124500.035186
20190306124000.0124500.0122500.0123500.035449
20190307123500.0124000.0122000.0123500.034768
20190308122500.0122500.0120500.0121500.035118
20190311121500.0122500.0120000.0122500.039576
20190312123000.0124000.0122000.0123500.024117
20190313123000.0123500.0121500.0123500.037649
20190314123000.0124000.0122000.0123500.095132
20190315123000.0128000.0123000.0126500.0107246
20190318127000.0131000.0126500.0131000.074644
20190319130000.0134000.0129500.0133000.068348
20190320132000.0133000.0129500.0131000.042697
20190321130000.0132000.0127500.0128500.054018
20190322127500.0129500.0126500.0127000.032380
20190325125500.0126500.0124000.0124500.037185
20190326124500.0125500.0123500.0124000.045161
20190327124000.0125000.0123500.0124000.034336
20190328124000.0124500.0120500.0121500.043518
20190329122500.0125000.0122000.0124500.039035
20190401124000.0126500.0124000.0126500.022463
20190402125500.0126500.0123500.0126000.031754
20190403124500.0128000.0124500.0128000.036250
20190404128500.0128500.0126000.0128000.034854
20190405127500.0129000.0126000.0127500.033513
20190408127500.0128000.0126000.0128000.039005
20190409128000.0129000.0127500.0128500.033266
20190410128000.0129000.0126500.0128000.064476
20190411128500.0129000.0125000.0125000.084802
20190412126000.0127500.0125500.0127000.039663
20190415126500.0128500.0126000.0127000.061140
20190416127000.0129000.0126500.0128500.040123
20190417128500.0129000.0127000.0128000.030846
20190418128000.0128500.0124000.0124500.055346
20190419124500.0125000.0122000.0123000.053439
20190422123000.0123500.0121000.0122500.030421
20190423122500.0123500.0120500.0121500.054997
20190424122500.0122500.0119500.0120000.063486
20190425121000.0121000.0118500.0119000.036046
20190426118500.0119500.0117000.0119000.043749
20190429119000.0119500.0117000.0119500.033516
20190430122000.0123000.0119000.0119500.094118
20190502118500.0122000.0118000.0121000.056723
20190503121000.0122000.0119500.0120000.035240
20190507118500.0119500.0117000.0117500.044453
20190508116500.0117000.0115000.0116500.052805
20190509117000.0117000.0113000.0113000.0116012
20190510113000.0114500.0110000.0111500.086072
20190513110500.0110500.0107500.0108500.070847
20190514107500.0108000.0105000.0106500.092820
20190515107000.0107500.0105000.0107000.068937
20190516107000.0107500.0104000.0105000.064047

bokeh를 이용해 봉차트를 그려봅니다. 먼저 데이터에서 양봉과 음봉에 대한 mask를 저장합니다.

inc = df.close >= df.open
dec = df.open > df.close

inc는 양봉, dec는 음봉에 해당합니다. 이제 봉들을 그려줍니다. 하나의 봉은 bokeh에서 segmentvbar로 구성되어 있습니다. segment는 high와 low까지 선으로 그은 봉의 꼬리를 그리는데 사용됩니다. vbar로는 봉의 몸통을 그립니다.

p_candlechart = figure(plot_width=1050, plot_height=200, x_range=(-1, len(df)), tools="crosshair")
p_candlechart.segment(df.index[inc], df.high[inc], df.index[inc], df.low[inc], color="red")
p_candlechart.segment(df.index[dec], df.high[dec], df.index[dec], df.low[dec], color="blue")
p_candlechart.vbar(df.index[inc], 0.5, df.open[inc], df.close[inc], fill_color="red", line_color="red")
p_candlechart.vbar(df.index[dec], 0.5, df.open[dec], df.close[dec], fill_color="blue", line_color="blue")

봉차트 밑에 거래량 막대차트(Bar Chart)까지 그려 줍니다. 이 때도 vbar를 사용하면 됩니다.

p_volumechart = figure(plot_width=1050, plot_height=100, x_range=p_candlechart.x_range, tools="crosshair")
p_volumechart.vbar(df.index, 0.5, df.volume, fill_color="black", line_color="black")

그리고 마지막으로 위 차트들을 gridplot으로 배치하여 가시화합니다.

p = gridplot([[p_candlechart], [p_volumechart]], toolbar_location=None)
show(p)

이 코드들을 모아서 다음과 같은 봉차트를 그릴 수 있습니다.

from bokeh.io import show, output_file
from bokeh.plotting import figure
from bokeh.layouts import gridplot
inc = df.close >= df.open
dec = df.open > df.close
p_candlechart = figure(plot_width=1050, plot_height=200, x_range=(-1, len(df)), tools="crosshair")
p_candlechart.segment(df.index[inc], df.high[inc], df.index[inc], df.low[inc], color="red")
p_candlechart.segment(df.index[dec], df.high[dec], df.index[dec], df.low[dec], color="blue")
p_candlechart.vbar(df.index[inc], 0.5, df.open[inc], df.close[inc], fill_color="red", line_color="red")
p_candlechart.vbar(df.index[dec], 0.5, df.open[dec], df.close[dec], fill_color="blue", line_color="blue")
p_volumechart = figure(plot_width=1050, plot_height=100, x_range=p_candlechart.x_range, tools="crosshair")
p_volumechart.vbar(df.index, 0.5, df.volume, fill_color="black", line_color="black")
p = gridplot([[p_candlechart], [p_volumechart]], toolbar_location=None)
show(p)

chart1

그러나 여기서 각 axis에 표시된 레이블들이 유용한 정보를 주지 못하고 있습니다. 좀 더 보기 좋게 레이블들을 포메팅 해보겠습니다.

p_candlechart.yaxis[0].formatter = NumeralTickFormatter(format='0,0')
p_candlechart.xaxis.visible = False

p_candlechart의 y 레이블을 다음과 같이 천단위로 콤마를 붙인 숫자로 표현합니다.

여기서 x 레이블은 거래량 차트에만 표시하기 위해서 숨기도록 합니다.

p_volumechart의 x 레이블을 날짜로 표현하고 y 레이블을 숫자로 표현합니다.

major_label = {
i: date.strftime('%Y%m%d') for i, date in enumerate(pd.to_datetime(df["date"]))
}
major_label.update({len(df): ''})
p_volumechart.xaxis.major_label_overrides = major_label
p_volumechart.yaxis[0].formatter = NumeralTickFormatter(format='0,0')

x 레이블의 마지막 값은 잘려서 나와서 숨겼습니다. 이제 이 코드들을 추가해서 다시 봉차트를 그려봅니다.

from bokeh.io import show, output_file
from bokeh.plotting import figure
from bokeh.layouts import gridplot
from bokeh.models.formatters import NumeralTickFormatter
inc = df.close >= df.open
dec = df.open > df.close
p_candlechart = figure(plot_width=1050, plot_height=200, x_range=(-1, len(df)), tools="crosshair")
p_candlechart.segment(df.index[inc], df.high[inc], df.index[inc], df.low[inc], color="red")
p_candlechart.segment(df.index[dec], df.high[dec], df.index[dec], df.low[dec], color="blue")
p_candlechart.vbar(df.index[inc], 0.5, df.open[inc], df.close[inc], fill_color="red", line_color="red")
p_candlechart.vbar(df.index[dec], 0.5, df.open[dec], df.close[dec], fill_color="blue", line_color="blue")
p_candlechart.yaxis[0].formatter = NumeralTickFormatter(format='0,0')
p_candlechart.xaxis.visible = False
p_volumechart = figure(plot_width=1050, plot_height=100, x_range=p_candlechart.x_range, tools="crosshair")
p_volumechart.vbar(df.index, 0.5, df.volume, fill_color="black", line_color="black")
major_label = {
i: date.strftime('%Y%m%d') for i, date in enumerate(pd.to_datetime(df["date"]))
}
major_label.update({len(df): ''})
p_volumechart.xaxis.major_label_overrides = major_label
p_volumechart.yaxis[0].formatter = NumeralTickFormatter(format='0,0')
p = gridplot([[p_candlechart], [p_volumechart]], toolbar_location=None)
show(p)

chart2

이렇게 x축은 날짜가, y축은 숫자가 보기좋게 나오게 됩니다. 이 차트를 html, json 등으로 웹에 넣어줄 수 있습니다. 자세한 사항은 bokeh 문서 Embedding Plots and Apps를 확인해 주세요.