주식 데이터를 분석할 때 숫자 테이블만 보면 시가(Open), 고가(High), 저가(Low), 종가(Close)의 관계나 거래량 변화를 한눈에 파악하기 어렵다.
이럴 때 Plotly의 graph_objects를 사용하면 캔들스틱 차트, 이동평균선, 볼린저 밴드, 거래량을 하나의 인터랙티브 차트로 구성할 수 있다.
이번 글에서는 단순히 차트를 그리는 데서 끝내지 않고 다음 흐름으로 정리한다.
OHLCV 데이터 준비
↓
Candlestick 생성
↓
이동평균선 추가
↓
변동성 밴드 추가
↓
거래량 subplot 구성
↓
레이아웃 조정
↓
HTML로 저장
plotly.graph_objects를 사용할까?Plotly에는 간단한 그래프를 빠르게 만드는 plotly.express도 있지만, 금융 차트처럼 여러 종류의 그래프를 하나의 Figure에 쌓아야 할 때는 graph_objects가 더 유연하다.
import plotly.graph_objects as go
graph_objects에서는 대체로 다음 구조로 차트를 만든다.
Figure 생성
↓
Trace 추가
↓
Layout 수정
↓
Figure 출력
핵심 메서드는 다음과 같다.
| 기능 | 역할 |
|---|---|
go.Figure() | 전체 차트 객체 생성 |
add_trace() | 그래프 요소 하나 추가 |
add_traces() | 여러 그래프 요소 추가 |
update_layout() | 제목, 축, 범례, 크기 등 설정 |
show() | 인터랙티브 그래프 출력 |
여기서 Trace는 하나의 선, 막대, 캔들스틱 같은 개별 시각화 요소라고 생각하면 된다.
예제로 사용할 데이터는 일반적인 OHLCV 구조라고 가정한다.
Date,Open,High,Low,Close,Volume
각 컬럼의 의미는 다음과 같다.
| 컬럼 | 의미 |
|---|---|
Open | 시가 |
High | 고가 |
Low | 저가 |
Close | 종가 |
Volume | 거래량 |
필요한 라이브러리를 불러온다.
import pandas as pd
import numpy as np
import plotly
import plotly.graph_objects as go
CSV 파일을 읽을 때 날짜 컬럼을 인덱스로 사용하면 이후 x축에 바로 활용하기 편하다.
df = pd.read_csv(
'./data/stock_price.csv',
index_col=0
)
display(df.head())
데이터를 불러온 뒤에는 먼저 인덱스와 컬럼을 확인하는 것이 좋다.
print(df.index)
print(df.columns)
실습에서 확인해야 할 핵심은 다음과 같다.
Open, High, Low, Close, Volume 컬럼이 존재하는가?날짜가 문자열이라면 명시적으로 변환해두는 것도 좋다.
df.index = pd.to_datetime(df.index)
df = df.sort_index()
Plotly의 graph_objects는 Figure 객체에 Trace를 추가하는 방식으로 동작한다.
가장 단순한 Figure부터 생성해보자.
fig = go.Figure()
객체의 타입도 확인할 수 있다.
type(fig)
Figure는 크게 data와 layout으로 구성된다.
Figure
├─ data → 실제 그래프 요소
└─ layout → 제목, 축, 범례, 크기 등
초기 Figure는 데이터가 비어 있으므로 아직 화면에 표현할 내용이 없다.
주식 데이터를 시각화할 때 가장 먼저 사용할 수 있는 것이 Candlestick 차트다.
하나의 캔들은 일정 기간 동안의 네 가지 가격을 표현한다.
시가 Open
고가 High
저가 Low
종가 Close
Plotly에서는 go.Candlestick()을 사용한다.
fig = go.Figure(
data=[
go.Candlestick(
x=df.index,
open=df['Open'],
high=df['High'],
low=df['Low'],
close=df['Close'],
increasing_line_color='red',
decreasing_line_color='blue'
)
]
)
fig.show()
여기서 increasing_line_color와 decreasing_line_color는 상승·하락 캔들의 색을 지정한다.
색상 자체는 분석 규칙이 아니라 표현 방식이므로 서비스나 사용자 환경에 맞게 바꿀 수 있다.
그래프를 그린 뒤 제목, 축 이름, 높이 같은 표현 요소를 조정하려면 update_layout()을 사용한다.
fig.update_layout(
title='Stock Price (OHLC)',
yaxis_title='Price'
)
fig.update_layout(
height=600
)
fig.show()
Plotly의 레이아웃은 내부적으로 여러 속성을 포함하는 구조다.
간단한 옵션은 위처럼 바로 지정할 수 있고, 복잡한 설정은 딕셔너리 형태로 중첩해서 작성할 수도 있다.
예를 들어 제목 스타일을 세부 조정하려면 다음과 같이 구성할 수 있다.
fig.update_layout(
title={
'text': 'Stock Price Dashboard',
'x': 0.5
}
)
즉 update_layout()은 단순 꾸미기 함수가 아니라 Figure 전체의 표현 방식을 관리하는 핵심 API다.
캔들스틱만으로도 가격 움직임은 볼 수 있지만, 단기 변동이 많으면 전체 흐름을 파악하기 어렵다.
이때 자주 사용하는 것이 이동평균(Moving Average, MA)이다.
이동평균은 최근 일정 기간의 평균을 계속 계산해서 가격 변화의 흐름을 부드럽게 보여준다.
df['MA3'] = df['Close'].rolling(3).mean()
df['MA10'] = df['Close'].rolling(10).mean()
df['MA30'] = df['Close'].rolling(30).mean()
예를 들어 3기간 이동평균은 다음 의미를 가진다.
현재 시점의 MA3
= 최근 3개의 Close 평균
이동평균을 계산하면 앞부분에 NaN이 생긴다.
display(df[['Close', 'MA3', 'MA10', 'MA30']].head(12))
이것은 오류가 아니다.
예를 들어 rolling(30)은 30개의 관측값이 모여야 첫 평균을 계산할 수 있기 때문이다.
MA3 → 처음 2개 구간 계산 불가
MA10 → 처음 9개 구간 계산 불가
MA30 → 처음 29개 구간 계산 불가
시계열 데이터를 다룰 때는 이런 window 기반 연산의 초기 결측값을 자연스러운 결과로 이해해야 한다.
Plotly에서는 선 그래프도 go.Scatter()로 만든다.
ma3 = go.Scatter(
x=df.index,
y=df['MA3'],
mode='lines',
name='MA(3)'
)
ma10 = go.Scatter(
x=df.index,
y=df['MA10'],
mode='lines',
name='MA(10)'
)
ma30 = go.Scatter(
x=df.index,
y=df['MA30'],
mode='lines',
name='MA(30)'
)
기존 캔들스틱 Figure에 추가하면 된다.
fig.add_trace(ma3)
fig.add_trace(ma10)
fig.add_trace(ma30)
fig.show()
이렇게 하면 하나의 Figure 안에서
Candlestick
+
MA3
+
MA10
+
MA30
을 동시에 비교할 수 있다.
동적 차트이므로 범례를 클릭해 특정 이동평균선만 숨기거나 다시 표시할 수도 있다.
가격의 추세뿐 아니라 변동성까지 함께 보고 싶다면 볼린저 밴드와 같은 지표를 추가할 수 있다.
기본 아이디어는 이동평균을 중심으로 일정 배수의 표준편차를 위아래에 더하는 것이다.
이번 예에서는 30기간 이동평균을 기준선으로 사용한다.
df['STD30'] = df['Close'].rolling(30).std()
상단과 하단 밴드는 다음과 같이 계산한다.
상단 밴드 = MA30 + 2 × STD30
하단 밴드 = MA30 - 2 × STD30
코드로 표현하면 다음과 같다.
df['UpperBand'] = df['MA30'] + (df['STD30'] * 2)
df['LowerBand'] = df['MA30'] - (df['STD30'] * 2)
upper_bound = go.Scatter(
x=df.index,
y=df['UpperBand'],
mode='lines',
line=dict(
dash='dash',
width=1
),
name='Upper Band'
)
lower_bound = go.Scatter(
x=df.index,
y=df['LowerBand'],
mode='lines',
line=dict(
dash='dash',
width=1
),
fill='tonexty',
opacity=0.3,
name='Lower Band'
)
Figure에 추가한다.
fig.add_traces([
upper_bound,
lower_bound
])
fig.show()
fill='tonexty'를 사용하면 직전에 추가된 Trace와 현재 Trace 사이 영역을 채울 수 있다.
따라서 상단과 하단 밴드 사이가 하나의 영역처럼 표현된다.
볼린저 밴드는 가격이 반드시 특정 확률분포를 따른다고 보장하는 지표라기보다, 이동평균과 변동성을 함께 보는 기술적 지표로 이해하는 편이 안전하다.
가격과 거래량은 단위가 다르기 때문에 같은 y축에 억지로 그리는 것보다 subplot으로 분리하는 것이 읽기 쉽다.
거래량을 Bar Trace로 만든다.
trading_volume = go.Bar(
x=df.index,
y=df['Volume'],
name='Volume'
)
그리고 subplot 기능을 불러온다.
from plotly.subplots import make_subplots
2행 1열 구조의 Figure를 만든다.
fig = make_subplots(
rows=2,
cols=1,
shared_xaxes=True,
vertical_spacing=0.02,
subplot_titles=(
'Stock Price',
'Trading Volume'
)
)
여기서 shared_xaxes=True가 중요하다.
가격과 거래량 모두 날짜를 기준으로 보기 때문에 x축을 공유하면 같은 시점을 비교하기 쉽다.
캔들스틱은 첫 번째 행에 추가한다.
fig.add_trace(
go.Candlestick(
x=df.index,
open=df['Open'],
high=df['High'],
low=df['Low'],
close=df['Close'],
increasing_line_color='red',
decreasing_line_color='blue',
name='OHLC'
),
row=1,
col=1
)
이동평균선도 같은 위치에 추가한다.
fig.add_trace(ma3, row=1, col=1)
fig.add_trace(ma10, row=1, col=1)
fig.add_trace(ma30, row=1, col=1)
볼린저 밴드도 같은 영역에 넣는다.
fig.add_trace(upper_bound, row=1, col=1)
fig.add_trace(lower_bound, row=1, col=1)
결과적으로 첫 번째 그래프는 다음 요소로 구성된다.
가격 영역
├─ Candlestick
├─ MA3
├─ MA10
├─ MA30
├─ Upper Band
└─ Lower Band
거래량은 두 번째 행에 추가한다.
fig.add_trace(
trading_volume,
row=2,
col=1
)
이제 하나의 Figure에서 가격과 거래량을 같이 볼 수 있다.
fig.show()
두 개의 subplot을 만든 뒤에는 축 이름과 Range Slider 등을 조정한다.
fig.update_layout(
height=700,
yaxis1_title='Stock Price',
yaxis2_title='Trading Volume',
xaxis2_title='Date',
xaxis1_rangeslider_visible=False,
xaxis2_rangeslider_visible=True
)
fig.show()
rangeslider는 Plotly의 인터랙티브 기능 중 하나다.
사용자가 원하는 기간만 선택해서 확대해 볼 수 있기 때문에 긴 시계열 데이터에서 특히 유용하다.
앞의 내용을 하나의 흐름으로 정리하면 다음과 같다.
import pandas as pd
import plotly.graph_objects as go
from plotly.subplots import make_subplots
# 데이터 로딩
df = pd.read_csv(
'./data/stock_price.csv',
index_col=0
)
df.index = pd.to_datetime(df.index)
df = df.sort_index()
# 이동평균
df['MA3'] = df['Close'].rolling(3).mean()
df['MA10'] = df['Close'].rolling(10).mean()
df['MA30'] = df['Close'].rolling(30).mean()
# 변동성 계산
df['STD30'] = df['Close'].rolling(30).std()
df['UpperBand'] = df['MA30'] + (df['STD30'] * 2)
df['LowerBand'] = df['MA30'] - (df['STD30'] * 2)
# Figure 생성
fig = make_subplots(
rows=2,
cols=1,
shared_xaxes=True,
vertical_spacing=0.02,
subplot_titles=(
'Stock Price',
'Trading Volume'
)
)
# Candlestick
fig.add_trace(
go.Candlestick(
x=df.index,
open=df['Open'],
high=df['High'],
low=df['Low'],
close=df['Close'],
increasing_line_color='red',
decreasing_line_color='blue',
name='OHLC'
),
row=1,
col=1
)
# 이동평균선
fig.add_trace(
go.Scatter(
x=df.index,
y=df['MA3'],
mode='lines',
name='MA(3)'
),
row=1,
col=1
)
fig.add_trace(
go.Scatter(
x=df.index,
y=df['MA10'],
mode='lines',
name='MA(10)'
),
row=1,
col=1
)
fig.add_trace(
go.Scatter(
x=df.index,
y=df['MA30'],
mode='lines',
name='MA(30)'
),
row=1,
col=1
)
# Bollinger Band
fig.add_trace(
go.Scatter(
x=df.index,
y=df['UpperBand'],
mode='lines',
line=dict(
dash='dash',
width=1
),
name='Upper Band'
),
row=1,
col=1
)
fig.add_trace(
go.Scatter(
x=df.index,
y=df['LowerBand'],
mode='lines',
line=dict(
dash='dash',
width=1
),
fill='tonexty',
opacity=0.3,
name='Lower Band'
),
row=1,
col=1
)
# 거래량
fig.add_trace(
go.Bar(
x=df.index,
y=df['Volume'],
name='Volume'
),
row=2,
col=1
)
# Layout
fig.update_layout(
height=700,
yaxis1_title='Stock Price',
yaxis2_title='Trading Volume',
xaxis2_title='Date',
xaxis1_rangeslider_visible=False,
xaxis2_rangeslider_visible=True
)
fig.show()
이 코드는 다음 요소를 하나의 인터랙티브 Figure로 구성한다.
OHLC Candlestick
+
Moving Average
+
Bollinger Band
+
Trading Volume
Plotly의 가장 큰 장점 중 하나는 동적 기능을 유지한 채 HTML 파일로 저장할 수 있다는 점이다.
fig.write_html(
'./interactive_stock_chart.html'
)
HTML 파일로 저장하면 브라우저에서 열어도 다음 기능을 그대로 사용할 수 있다.
즉 Python 실행 환경이 없어도 결과물을 브라우저에서 확인할 수 있다.
Plotly 화면 상단의 카메라 버튼을 사용하면 이미지로 저장할 수 있다.
하지만 PNG는 정적인 이미지이므로 인터랙션은 사라진다.
| 형식 | 특징 |
|---|---|
| PNG | 보고서·문서 삽입에 적합 |
| HTML | Plotly의 인터랙션 유지 |
결과를 어디에서 사용할지에 따라 저장 형식을 선택하면 된다.
이 실습에서 중요한 것은 Candlestick API를 외우는 것이 아니다.
graph_objects의 구조를 이해하는 것이 더 중요하다.
Figure
↓
Trace 추가
↓
Trace 추가
↓
subplot 배치
↓
layout 설정
예를 들어 주식 차트가 아니라 웹 서비스 모니터링 화면을 만든다고 생각해도 구조는 동일하다.
Figure
├─ API 응답시간 Line
├─ 에러율 Line
├─ 요청량 Bar
└─ 서버 상태 Marker
즉 Plotly의 Trace 중심 구조를 이해하면 금융 데이터뿐 아니라 로그 분석, 서비스 모니터링, 운영 대시보드, 시계열 분석 결과에도 그대로 적용할 수 있다.
add_trace()와 add_traces() 헷갈리지 않기이름이 비슷해서 자주 헷갈릴 수 있다.
fig.add_trace(trace)
fig.add_traces([
trace1,
trace2
])
subplot에서 row, col을 지정하면서 하나씩 배치할 때는 add_trace()를 사용하는 편이 직관적이다.
fig.add_trace(
trace,
row=1,
col=1
)
코드를 실행한 뒤 단순히 그래프가 출력되는지만 확인하지 말고 다음을 같이 살펴보는 것이 좋다.
NaN이 생기는 이유를 이해했는가?graph_objects는 그래프 요소를 세밀하게 제어할 수 있는 저수준 API다.go.Figure()는 전체 그래프를 담는 객체다.Open, High, Low, Close 데이터를 이용해 만든다.rolling()을 이용하면 이동평균과 이동 표준편차를 계산할 수 있다.NaN은 계산 가능한 데이터가 부족해서 생긴다.make_subplots()를 사용하면 가격과 거래량처럼 단위가 다른 그래프를 하나의 Figure에 구성할 수 있다.update_layout()은 제목, 축, 크기, Range Slider 등 전체 레이아웃을 관리한다.write_html()을 사용하면 Plotly의 동적 기능을 유지한 채 결과를 브라우저용 파일로 저장할 수 있다.Plotly의 graph_objects를 처음 보면 Figure, Trace, Layout, subplot 등 여러 개념이 한꺼번에 등장해 복잡해 보인다.
하지만 구조를 단순화하면 결국 다음 흐름이다.
데이터 준비
→ Figure 생성
→ Trace 추가
→ Layout 설정
→ 출력 또는 저장
Candlestick, 이동평균선, 볼린저 밴드, 거래량은 서로 다른 그래프처럼 보이지만 Plotly에서는 모두 Figure 안에 배치되는 Trace라는 공통 구조를 가진다.
이 구조를 익혀두면 이후 주식 차트뿐 아니라 실시간 서비스 지표, 로그 모니터링, 데이터 분석 대시보드처럼 더 복잡한 인터랙티브 시각화로도 확장하기 쉬워진다.