[Python을 활용한 주식 분석] 모던 라이브러리로 이동평균선과 골든크로스 검출하기
Table of Contents
개요
Polars, Plotly, marimo, kand 등 Python의 모던 라이브러리를 사용하여 이동평균선과 골든크로스를 검출합니다.
동기
저는 급여의 일부를 RSU(Restricted Stock Units, 양도제한부 주식) 형태로 받고 있습니다. RSU는 해당 종목이 호조일 때는 수입원으로서 매력적이지만, 모든 달걀을 한 바구니에 담고 운반하는 것과 같아서 그 회사의 실적이 나빠지면 손실이 커집니다. 따라서 달걀을 여러 바구니에 분산하는 것이 중요합니다. 현금만 보유하거나 하나의 종목만 RSU로 보유하는 것도 바람직하지 않으며, 리스크에 대비해 적절히 분산하는 것이 중요합니다. 인덱스 펀드(투자신탁)나 금 상품 등에 빠르게 분산시키는 것이 편하지만, 적극적으로 리스크를 감수하거나 배당금과 주주우대를 즐기려면 개별 종목도 재미있다고 생각합니다(애초에 RSU도 개별 종목과 같은 것입니다). 그래서 제가 보유한 RSU에 대한 이해를 깊게 하는 것도 염두에 두면서 개별 종목 분석을 해나가겠습니다.
Python에는 Pandas, Ta-Lib-Python, Matplotlib, yfinance, Jupyter 등 성숙하고 풍부한 라이브러리가 있어 각각의 연동 용이성 면에서도 매우 편리합니다. 하지만 2020년대에 들어서 모던 라이브러리들이 다수 등장하여 기존 라이브러리와는 다른 장점들이 있습니다. 기존 라이브러리는 성숙하여 정보도 풍부하므로, 새로운 라이브러리를 동시에 배우면서 분석을 즐기고 싶습니다. 참고로, 시스템 트레이딩에도 활용할 수 있는 방법이지만, 저는 사람이 이해하기 쉽게 관리하고 파악할 수 있으며 펀더멘털 문제가 없는지도 확인하고 싶기 때문에 매매 후보와 타이밍을 알기 위해 기술적 분석을 시도합니다.
사용하는 라이브러리
라이브러리는 다음과 같습니다.
| 라이브러리 | 관련 라이브러리 | 설명 |
|---|---|---|
| polars | pandas | 데이터프레임을 고속으로 다룰 수 있는 라이브러리 |
| plotly | matplotlib | 사용자가 인터랙티브하게 그래프를 조작할 수 있는 시각화 라이브러리 |
| marimo | jupyter | py 형식으로 동작하는 노트북으로 인터랙티브하게 코드를 실행할 수 있음 |
| kand | ta-lib-python | Rust 기반의 기술적 분석 계산 라이브러리로, PYO3나 Wasm을 통해 Python이나 웹에서도 활용 가능 |
| yfinance-pl | yfinance | yfinance-rs를 래핑하여 만든 Python용 라이브러리로 Polars로 Yahoo Finance 정보를 다룰 수 있음 |
노트와 코드
marimo 노트의 HTML 버전은 여기에 있습니다. GitHub의 Python 형식 코드에도 링크합니다.
설명
주가 정보 수집
주가 정보는 yfinance-pl을 사용하여 수집합니다. 증권 코드에 .T를 붙이면 yfinance-pl에서 Polars의 DataFrame 형식으로 가져올 수 있습니다. 토요타라면 7203.T, 소프트뱅크라면 9984.T 등입니다. 저는 배당금과 주주우대를 노리고 지방 은행이지만 경영이 안정적이라고 생각되는 산인 고도 은행(고긴) 8381.T을 보유하고 있어서 8381.T를 예로 들겠습니다.
import yfinance_pl as yf
ticker = yf.Ticker("8381.T")
hist = ticker.history(period="1y")period에는 1y나 1m 등 기간을 입력합니다. yfinance_pl에서는 Literal을 지정하고 있어서 Visual Studio Code나 PyCharm 등을 사용하면 코드 자동완성으로 입력할 수 있습니다. hist를 TSV 형식으로 표시하면 아래와 같습니다(최신 5건입니다).
open.amount open.currency high.amount high.currency low.amount low.currency close.amount close.currency close_unadj.amount close_unadj.currency volume date
Decimal('1374.0000000000') JPY Decimal('1404.0000000000') JPY Decimal('1372.0000000000') JPY Decimal('1400.0000000000') JPY Decimal('1400.0000000000') JPY 400000 2025-11-21 00:00:00
Decimal('1405.0000000000') JPY Decimal('1413.0000000000') JPY Decimal('1384.0000000000') JPY Decimal('1393.0000000000') JPY Decimal('1393.0000000000') JPY 317800 2025-11-25 00:00:00
Decimal('1409.0000000000') JPY Decimal('1412.0000000000') JPY Decimal('1399.0000000000') JPY Decimal('1407.0000000000') JPY Decimal('1407.0000000000') JPY 387300 2025-11-26 00:00:00
Decimal('1420.0000000000') JPY Decimal('1454.0000000000') JPY Decimal('1420.0000000000') JPY Decimal('1442.0000000000') JPY Decimal('1442.0000000000') JPY 547300 2025-11-27 00:00:00
Decimal('1450.0000000000') JPY Decimal('1469.0000000000') JPY Decimal('1449.0000000000') JPY Decimal('1453.0000000000') JPY Decimal('1453.0000000000') JPY 428800 2025-11-28 00:00:00이동평균선 계산
이동평균선은 당일을 포함한 N일분의 데이터를 날짜별로 이동하면서 계산합니다. 단기간의 평균이라면 5일 이동평균선, 1개월의 트렌드라면 25일 이동평균선으로 계산합니다. 매일 주가가 움직이지만 평균을 내면 일별 변동을 억제하면서 트렌드를 파악할 수 있습니다. 이 계산에는 Ta-Lib-Python이 아닌 Rust 기반의 kand를 사용하지만, 차이는 거의 없고 간단하게 계산할 수 있습니다.
import kand as ka
import polars as pl
import yfinance_pl as yf
ticker = yf.Ticker("8381.T")
hist = ticker.history(period="1y")
close = hist["close.amount"].to_numpy().astype("float64")
hist_with_ma = hist.with_columns(
ma5=pl.Series(ka.sma(close, period=5)),
ma25=pl.Series(ka.sma(close, period=25)),
)sma가 Simple Moving Average(단순이동평균선)의 약자이며, period의 수치를 변경하면 90일, 200일 등 더 장기 투자를 위한 트렌드를 산출할 수 있습니다.
hist_with_ma의 처음 30행을 TSV 형식으로 표시하면 아래와 같습니다.
open.amount open.currency high.amount high.currency low.amount low.currency close.amount close.currency close_unadj.amount close_unadj.currency volume date ma5 ma25
Decimal('1147.0000000000') JPY Decimal('1155.0000000000') JPY Decimal('1142.0000000000') JPY Decimal('1151.0000000000') JPY Decimal('1195.0000000000') JPY 212000 2024-11-28 00:00:00 NaN NaN
Decimal('1151.0000000000') JPY Decimal('1169.0000000000') JPY Decimal('1145.0000000000') JPY Decimal('1163.0000000000') JPY Decimal('1207.0000000000') JPY 222500 2024-11-29 00:00:00 NaN NaN
Decimal('1173.0000000000') JPY Decimal('1213.0000000000') JPY Decimal('1169.0000000000') JPY Decimal('1211.0000000000') JPY Decimal('1257.0000000000') JPY 493800 2024-12-02 00:00:00 NaN NaN
Decimal('1214.0000000000') JPY Decimal('1235.0000000000') JPY Decimal('1208.0000000000') JPY Decimal('1226.0000000000') JPY Decimal('1272.0000000000') JPY 440100 2024-12-03 00:00:00 NaN NaN
Decimal('1220.0000000000') JPY Decimal('1226.0000000000') JPY Decimal('1190.0000000000') JPY Decimal('1190.0000000000') JPY Decimal('1235.0000000000') JPY 279400 2024-12-04 00:00:00 1188.2 NaN
Decimal('1200.0000000000') JPY Decimal('1213.0000000000') JPY Decimal('1198.0000000000') JPY Decimal('1209.0000000000') JPY Decimal('1255.0000000000') JPY 241000 2024-12-05 00:00:00 1199.8 NaN
Decimal('1210.0000000000') JPY Decimal('1216.0000000000') JPY Decimal('1198.0000000000') JPY Decimal('1202.0000000000') JPY Decimal('1248.0000000000') JPY 130400 2024-12-06 00:00:00 1207.6 NaN
Decimal('1207.0000000000') JPY Decimal('1225.0000000000') JPY Decimal('1196.0000000000') JPY Decimal('1216.0000000000') JPY Decimal('1262.0000000000') JPY 309500 2024-12-09 00:00:00 1208.6 NaN
Decimal('1232.0000000000') JPY Decimal('1232.0000000000') JPY Decimal('1209.0000000000') JPY Decimal('1215.0000000000') JPY Decimal('1261.0000000000') JPY 300100 2024-12-10 00:00:00 1206.4 NaN
Decimal('1216.0000000000') JPY Decimal('1226.0000000000') JPY Decimal('1204.0000000000') JPY Decimal('1226.0000000000') JPY Decimal('1272.0000000000') JPY 256900 2024-12-11 00:00:00 1213.6 NaN
Decimal('1238.0000000000') JPY Decimal('1242.0000000000') JPY Decimal('1230.0000000000') JPY Decimal('1233.0000000000') JPY Decimal('1280.0000000000') JPY 307600 2024-12-12 00:00:00 1218.4 NaN
Decimal('1223.0000000000') JPY Decimal('1232.0000000000') JPY Decimal('1210.0000000000') JPY Decimal('1216.0000000000') JPY Decimal('1262.0000000000') JPY 348500 2024-12-13 00:00:00 1221.2 NaN
Decimal('1218.0000000000') JPY Decimal('1221.0000000000') JPY Decimal('1194.0000000000') JPY Decimal('1201.0000000000') JPY Decimal('1247.0000000000') JPY 252900 2024-12-16 00:00:00 1218.2 NaN
Decimal('1205.0000000000') JPY Decimal('1215.0000000000') JPY Decimal('1193.0000000000') JPY Decimal('1195.0000000000') JPY Decimal('1240.0000000000') JPY 215500 2024-12-17 00:00:00 1214.2 NaN
Decimal('1193.0000000000') JPY Decimal('1195.0000000000') JPY Decimal('1182.0000000000') JPY Decimal('1193.0000000000') JPY Decimal('1238.0000000000') JPY 189600 2024-12-18 00:00:00 1207.6 NaN
Decimal('1175.0000000000') JPY Decimal('1195.0000000000') JPY Decimal('1169.0000000000') JPY Decimal('1190.0000000000') JPY Decimal('1235.0000000000') JPY 363000 2024-12-19 00:00:00 1199 NaN
Decimal('1197.0000000000') JPY Decimal('1197.0000000000') JPY Decimal('1168.0000000000') JPY Decimal('1168.0000000000') JPY Decimal('1212.0000000000') JPY 511500 2024-12-20 00:00:00 1189.4 NaN
Decimal('1171.0000000000') JPY Decimal('1204.0000000000') JPY Decimal('1171.0000000000') JPY Decimal('1204.0000000000') JPY Decimal('1250.0000000000') JPY 403500 2024-12-23 00:00:00 1190 NaN
Decimal('1216.0000000000') JPY Decimal('1226.0000000000') JPY Decimal('1209.0000000000') JPY Decimal('1222.0000000000') JPY Decimal('1268.0000000000') JPY 320300 2024-12-24 00:00:00 1195.4 NaN
Decimal('1217.0000000000') JPY Decimal('1217.0000000000') JPY Decimal('1194.0000000000') JPY Decimal('1205.0000000000') JPY Decimal('1251.0000000000') JPY 207300 2024-12-25 00:00:00 1197.8 NaN
Decimal('1204.0000000000') JPY Decimal('1208.0000000000') JPY Decimal('1193.0000000000') JPY Decimal('1208.0000000000') JPY Decimal('1254.0000000000') JPY 230600 2024-12-26 00:00:00 1201.4 NaN
Decimal('1209.0000000000') JPY Decimal('1220.0000000000') JPY Decimal('1205.0000000000') JPY Decimal('1220.0000000000') JPY Decimal('1266.0000000000') JPY 252400 2024-12-27 00:00:00 1211.8 NaN
Decimal('1224.0000000000') JPY Decimal('1230.0000000000') JPY Decimal('1214.0000000000') JPY Decimal('1219.0000000000') JPY Decimal('1265.0000000000') JPY 159400 2024-12-30 00:00:00 1214.8 NaN
Decimal('1228.0000000000') JPY Decimal('1228.0000000000') JPY Decimal('1211.0000000000') JPY Decimal('1225.0000000000') JPY Decimal('1271.0000000000') JPY 443500 2025-01-06 00:00:00 1215.4 NaN
Decimal('1231.0000000000') JPY Decimal('1232.0000000000') JPY Decimal('1217.0000000000') JPY Decimal('1222.0000000000') JPY Decimal('1268.0000000000') JPY 403500 2025-01-07 00:00:00 1218.8 1205.2
Decimal('1216.0000000000') JPY Decimal('1244.0000000000') JPY Decimal('1215.0000000000') JPY Decimal('1237.0000000000') JPY Decimal('1284.0000000000') JPY 469100 2025-01-08 00:00:00 1224.6 1208.64
Decimal('1232.0000000000') JPY Decimal('1232.0000000000') JPY Decimal('1207.0000000000') JPY Decimal('1207.0000000000') JPY Decimal('1253.0000000000') JPY 421100 2025-01-09 00:00:00 1222 1210.4
Decimal('1204.0000000000') JPY Decimal('1207.0000000000') JPY Decimal('1190.0000000000') JPY Decimal('1196.0000000000') JPY Decimal('1241.0000000000') JPY 328400 2025-01-10 00:00:00 1217.4 1209.8
Decimal('1190.0000000000') JPY Decimal('1194.0000000000') JPY Decimal('1164.0000000000') JPY Decimal('1173.0000000000') JPY Decimal('1217.0000000000') JPY 394900 2025-01-14 00:00:00 1207 1207.68
Decimal('1182.0000000000') JPY Decimal('1207.0000000000') JPY Decimal('1178.0000000000') JPY Decimal('1207.0000000000') JPY Decimal('1253.0000000000') JPY 362200 2025-01-15 00:00:00 1204 1208.365일 이동평균선은 해당 일을 포함한 과거 5일분의 데이터가 없으면 계산할 수 없으므로 처음 4행은 NaN입니다. 25일 이동평균선도 마찬가지로 과거 데이터가 필요하므로 24행이 NaN입니다.
차트 시각화
Plotly의 Candlestick(캔들스틱)을 사용하면 쉽게 차트에 표시할 수 있습니다. 캔들스틱은 사각형(몸통)과 선(꼬리)으로 표현되며, 상단 꼬리, 하단 꼬리, 시가, 종가를 표시하고, 시가와 종가의 상하 관계에 따라 빨간색과 초록색 등으로 표현되어 증감과 그날의 가격 변동폭을 알 수 있습니다. 이 4가지 정보와 색상, 가로축의 날짜 데이터를 전달하면 쉽게 표시할 수 있습니다.
# 생략
df_plot = hist_with_ma.with_columns(
[
pl.col("open.amount").cast(pl.Float64),
pl.col("high.amount").cast(pl.Float64),
pl.col("low.amount").cast(pl.Float64),
pl.col("close.amount").cast(pl.Float64),
]
)
dates = df_plot["date"].to_list()
ma_data = [
go.Candlestick(
yaxis="y1",
x=dates,
open=df_plot["open.amount"],
high=df_plot["high.amount"],
low=df_plot["low.amount"],
close=df_plot["close.amount"],
increasing_line_color="red",
decreasing_line_color="green",
name=f"{company_name} 주가",
),
# 중략
]
ma_fig = go.Figure(data=ma_data, layout=go.Layout(ma_layout))
# 중략
ma_fig # 노트에서 도표가 표시됨골든크로스와 데드크로스
장기와 단기의 이동평균선이 교차하면 트렌드 전환(또는 매매 신호)을 나타내는 하나의 지표가 됩니다. 장기 이동평균선을 단기 이동평균선이 상향 돌파했을 때를 “골든크로스”, 장기 이동평균선을 단기 이동평균선이 하향 돌파했을 때를 “데드크로스”라고 부릅니다.
이들은 단기 이동평균선과 장기 이동평균선의 차이를 계산하고, 전일과 당일에 그 차이값의 부호가 변화하는지로 검출할 수 있습니다.
(1) 전일에는 장기 이동평균보다 단기 이동평균의 가격이 낮았고, 당일에는 단기 이동평균이 장기 이동평균을 상회하면 골든크로스 (2) 전일에는 장기 이동평균보다 단기 이동평균의 가격이 높았고, 당일에는 단기 이동평균선이 장기 이동평균선을 하회하면 데드크로스
# 전일과 당일의 SMA5와 SMA25의 차분을 계산
diff = SMA5 - SMA25
prev_diff = diff.shift(1)
# 골든크로스: 전일은 SMA5 < SMA25, 당일은 SMA5 > SMA25
golden_cross = (prev_diff < 0) & (diff > 0)
# 데드크로스: 전일은 SMA5 > SMA25, 당일은 SMA5 < SMA25
dead_cross = (prev_diff > 0) & (diff < 0)golden_cross가 True일 때 골든크로스, dead_cross가 True일 때 데드크로스를 표시할 수 있습니다.
Plotly에도 골든크로스와 데드크로스를 표시할 수 있습니다.
# SMA5와 SMA25의 차분을 계산
df_cross = (
hist_with_ma.with_columns(
diff=(pl.col("ma5") - pl.col("ma25")),
)
.with_columns(
prev_diff=pl.col("diff").shift(1),
)
.with_columns(
# 골든크로스: 전일은 음수(SMA5 < SMA25), 당일은 양수(SMA5 > SMA25)
golden_cross=(pl.col("prev_diff") < 0) & (pl.col("diff") > 0),
# 데드크로스: 전일은 양수(SMA5 > SMA25), 당일은 음수(SMA5 < SMA25)
dead_cross=(pl.col("prev_diff") > 0) & (pl.col("diff") < 0),
)
)
# 골든크로스 날짜 추출
golden_crosses = df_cross.filter(pl.col("golden_cross")).select(
pl.col("date"),
pl.col("close.amount").alias("price"),
pl.lit("골든크로스").alias("signal"),
)
# 데드크로스 날짜 추출
dead_crosses = df_cross.filter(pl.col("dead_cross")).select(
pl.col("date"),
pl.col("close.amount").alias("price"),
pl.lit("데드크로스").alias("signal"),
)
# 골든크로스와 데드크로스를 결합하여 정렬
signals = pl.concat([golden_crosses, dead_crosses]).sort("date")
# 골든크로스와 데드크로스 데이터를 분리
_golden = signals.filter(pl.col("signal") == "골든크로스")
_dead = signals.filter(pl.col("signal") == "데드크로스")
signal_data = [
go.Candlestick(
yaxis="y1",
x=_dates,
open=_df_plot["open.amount"],
high=_df_plot["high.amount"],
low=_df_plot["low.amount"],
close=_df_plot["close.amount"],
increasing_line_color="red",
decreasing_line_color="green",
name=f"{_company_name} 주가",
),
# 중략
# 골든크로스 마커
go.Scatter(
yaxis="y1",
x=_golden["date"].to_list(),
y=_golden["price"].cast(pl.Float64).to_list(),
mode="markers",
name="골든크로스",
marker={
"symbol": "triangle-up",
"size": 15,
"color": "lime",
"line": {"color": "darkgreen", "width": 2},
},
),
# 데드크로스 마커
go.Scatter(
yaxis="y1",
x=_dead["date"].to_list(),
y=_dead["price"].cast(pl.Float64).to_list(),
mode="markers",
name="데드크로스",
marker={
"symbol": "triangle-down",
"size": 15,
"color": "red",
"line": {"color": "darkred", "width": 2},
},
),
]정리
여기까지 주가 수집 및 캔들스틱 차트 표시, 이동평균 계산, 골든크로스와 데드크로스 검출까지 소개했습니다. Pandas를 사용하는 것이 편리한 상황이 계속되었지만, 코드에서 보시는 바와 같이 Polars만으로도 주가 수집부터 시각화까지 Pandas를 거치지 않고 할 수 있습니다. 골든크로스와 데드크로스만으로는 부족하지만, 기본적이고 이해하기 쉬운 지표를 먼저 만들 수 있게 되어 분석의 첫걸음을 내딛을 수 있었다고 생각합니다. 앞으로도 다른 지표나 더 고급 분석을 모던 라이브러리를 통해 진행하겠습니다.
참고 서적: 『Python으로 실천하는 주식 투자 분석』(카타후치 히카루 (저), 야마다 요시히로 (감수))
![[Python을 활용한 주식 분석] 주봉, 월봉으로 가격 변동 범위를 파악하는 볼린저 밴드](https://b.rmc-8.com/img/2025/12/01/0218c7e535f81400a91a9ad80357a034.png)
![[Python 주식 분석] Polars로 일목균형표 계산 및 표시하기](https://b.rmc-8.com/img/2025/12/02/00541cf97f979f588ad2cad0ac6d6d7f.png)



댓글을 불러오는 중...