Scriptone Scriptone

[Python을 활용한 주식 분석] 모던 라이브러리로 이동평균선과 골든크로스 검출하기

blog-image

개요

Polars, Plotly, marimo, kand 등 Python의 모던 라이브러리를 사용하여 이동평균선과 골든크로스를 검출합니다.

동기

저는 급여의 일부를 RSU(Restricted Stock Units, 양도제한부 주식) 형태로 받고 있습니다. RSU는 해당 종목이 호조일 때는 수입원으로서 매력적이지만, 모든 달걀을 한 바구니에 담고 운반하는 것과 같아서 그 회사의 실적이 나빠지면 손실이 커집니다. 따라서 달걀을 여러 바구니에 분산하는 것이 중요합니다. 현금만 보유하거나 하나의 종목만 RSU로 보유하는 것도 바람직하지 않으며, 리스크에 대비해 적절히 분산하는 것이 중요합니다. 인덱스 펀드(투자신탁)나 금 상품 등에 빠르게 분산시키는 것이 편하지만, 적극적으로 리스크를 감수하거나 배당금과 주주우대를 즐기려면 개별 종목도 재미있다고 생각합니다(애초에 RSU도 개별 종목과 같은 것입니다). 그래서 제가 보유한 RSU에 대한 이해를 깊게 하는 것도 염두에 두면서 개별 종목 분석을 해나가겠습니다.

Python에는 Pandas, Ta-Lib-Python, Matplotlib, yfinance, Jupyter 등 성숙하고 풍부한 라이브러리가 있어 각각의 연동 용이성 면에서도 매우 편리합니다. 하지만 2020년대에 들어서 모던 라이브러리들이 다수 등장하여 기존 라이브러리와는 다른 장점들이 있습니다. 기존 라이브러리는 성숙하여 정보도 풍부하므로, 새로운 라이브러리를 동시에 배우면서 분석을 즐기고 싶습니다. 참고로, 시스템 트레이딩에도 활용할 수 있는 방법이지만, 저는 사람이 이해하기 쉽게 관리하고 파악할 수 있으며 펀더멘털 문제가 없는지도 확인하고 싶기 때문에 매매 후보와 타이밍을 알기 위해 기술적 분석을 시도합니다.

사용하는 라이브러리

라이브러리는 다음과 같습니다.

라이브러리관련 라이브러리설명
polarspandas데이터프레임을 고속으로 다룰 수 있는 라이브러리
plotlymatplotlib사용자가 인터랙티브하게 그래프를 조작할 수 있는 시각화 라이브러리
marimojupyterpy 형식으로 동작하는 노트북으로 인터랙티브하게 코드를 실행할 수 있음
kandta-lib-pythonRust 기반의 기술적 분석 계산 라이브러리로, PYO3나 Wasm을 통해 Python이나 웹에서도 활용 가능
yfinance-plyfinanceyfinance-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에는 1y1m 등 기간을 입력합니다. 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.36

5일 이동평균선은 해당 일을 포함한 과거 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으로 실천하는 주식 투자 분석』(카타후치 히카루 (저), 야마다 요시히로 (감수))

comment

댓글을 불러오는 중...