Scriptone Scriptone

[Python 주식 분석] Polars로 일목균형표 계산 및 표시하기

blog-image

개요

이전 글에서는 볼린저 밴드를 통한 가격 변동 범위 파악 방법에 대해 설명했습니다. 이번에는 일본에서 개발된 기술적 지표인 일목균형표에 대해 Polars를 사용한 계산 방법과 Plotly를 통한 시각화 방법을 설명합니다.

일목균형표는 전환선, 기준선, 선행스팬1·2, 후행스팬의 5가지 요소로 구성되며, 여러 시간 축을 동시에 분석할 수 있는 강력한 지표입니다.

동기

일목균형표(一目均衡表)는 1936년 일본의 호소다 고이치(細田悟一)에 의해 개발된 기술적 지표입니다. 해외에서도 “Ichimoku Cloud(일목의 구름)“로 알려져 있으며, 일본 이외의 트레이더들도 사용하고 있습니다. 일목균형표의 주요 특징은 다음과 같습니다.

  • 여러 시간 축을 동시에 분석: 단기(9일), 중기(26일), 장기(52일) 추세를 한 번에 파악 가능
  • 구름을 통한 지지·저항 시각화: 선행스팬1과 2 사이의 “구름”이 미래의 지지선이나 저항선 역할 수행
  • 추세의 방향성과 강도 파악: 가격과 구름의 위치 관계, 전환선과 기준선의 교차 등으로 추세의 강도와 전환점 판단 가능
  • 시각적으로 이해하기 쉬움: 차트상에서 구름이 명확히 표시되어 추세의 방향성을 한눈에 파악 가능

이전과 마찬가지로 Python의 모던한 라이브러리인 Polars, Plotly, marimo, yfinance-pl을 사용하여 분석을 진행합니다.

사용 라이브러리

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

라이브러리관련 라이브러리설명
polarspandas데이터프레임을 고속으로 처리할 수 있는 라이브러리
numpy-수치 계산 라이브러리로, 배열 조작과 차분 계산(np.diff())을 사용하여 구름의 음전·양전 교차점 검출
plotlymatplotlib사용자가 인터랙티브하게 그래프를 조작할 수 있는 시각화 라이브러리
marimojupyterpy 형식으로 작동하는 노트북으로 인터랙티브하게 코드 실행 가능
yfinance-plyfinanceyfinance-rs를 래핑하여 만든 Python용 라이브러리로 Polars에서 Yahoo Finance 정보 처리 가능

노트와 코드

marimo 노트의 HTML 버전은 여기에 있습니다. GitHub의 Python 형식 코드로도 링크합니다

해설

일목균형표란

일목균형표는 다음 5가지 요소로 구성되는 기술적 지표입니다.

  1. 전환선(Conversion Line / 転換線): 과거 9일간의 최고가와 최저가의 평균
  2. 기준선(Base Line / 基準線): 과거 26일간의 최고가와 최저가의 평균
  3. 선행스팬1(Leading Span A / 先行スパン1): (전환선 + 기준선) / 2 를 26일 미래로 이동
  4. 선행스팬2(Leading Span B / 先行スパン2): 과거 52일간의 최고가와 최저가의 평균을 26일 미래로 이동
  5. 후행스팬(Lagging Span / 遅行スパン): 당일 종가를 26일 과거로 이동

선행스팬1과 선행스팬2 사이의 영역이 “구름(Kumo / Cloud)“이라 불리며, 이 구름이 지지선이나 저항선 역할을 합니다.

미래의 구름: 선행스팬은 26일 미래로 이동되어 있기 때문에 데이터의 마지막 26일분은 미래 날짜에 해당합니다. 이 미래 부분을 표시함으로써 장래의 지지·저항을 미리 파악할 수 있습니다. 이것이 일목균형표의 가장 큰 특징 중 하나입니다.

일목균형표 계산 방법

Polars를 사용하여 일목균형표의 각 요소를 계산합니다. rolling_max()rolling_min()을 사용하여 기간 내 최고가·최저가를 가져오고, shift()로 시간축을 이동합니다.

import polars as pl
from typing import TypedDict


class IchimokuValues(TypedDict):
    conversion_line: pl.Series
    base_line: pl.Series
    leading_span1: pl.Series
    leading_span2: pl.Series
    lagging_span: pl.Series


def get_ichimoku_values(df: pl.DataFrame) -> IchimokuValues:
    # Decimal 타입은 rolling 연산을 지원하지 않으므로 먼저 Float64로 변환
    high = df["high.amount"].cast(pl.Float64)
    low = df["low.amount"].cast(pl.Float64)
    close = df["close.amount"].cast(pl.Float64)

    # 전환선: 과거 9일간 (Max + Min) / 2
    conversion_line = (high.rolling_max(9) + low.rolling_min(9)) / 2

    # 기준선: 과거 26일간 (Max + Min) / 2
    base_line = (high.rolling_max(26) + low.rolling_min(26)) / 2

    # 선행스팬1: (전환선 + 기준선) / 2 를 26일 미래로 이동
    # Polars의 shift는 기본적으로 빈 부분을 null로 채웁니다
    leading_span1 = ((conversion_line + base_line) / 2).shift(26)

    # 선행스팬2: 과거 52일간 (Max + Min) / 2 를 26일 미래로 이동
    leading_span2 = ((high.rolling_max(52) + low.rolling_min(52)) / 2).shift(26)

    # 후행스팬: 오늘 종가를 26일 과거로 이동
    lagging_span = close.shift(-26)

    return {
        "conversion_line": conversion_line,
        "base_line": base_line,
        "leading_span1": leading_span1,
        "leading_span2": leading_span2,
        "lagging_span": lagging_span,
    }

몇 가지 주의사항이 있습니다.

  • Decimal 타입 변환: yfinance-pl로 가져온 데이터는 Decimal 타입이지만, Polars의 rolling_max()rolling_min()은 Decimal 타입을 지원하지 않으므로 cast(pl.Float64)로 변환합니다
  • shift() 방향: shift()는 양수값으로 미래 방향, 음수값으로 과거 방향으로 이동합니다. 선행스팬은 shift(26)로 26일 미래로, 후행스팬은 shift(-26)로 26일 과거로 이동합니다
  • 기간의 의미: 9일, 26일, 52일이라는 기간은 일목균형표의 표준 설정값입니다. 이는 일본 시장의 영업일을 기반으로 설정되었습니다

일목균형표의 기본적인 보는 법

일목균형표는 시각적으로 이해하기 쉬운 지표이지만, 몇 가지 기본적인 보는 법이 있습니다.

구름(선행스팬1과 2 사이)

구름은 미래의 지지선이나 저항선 역할을 합니다.

  • 가격이 구름 위에 있음: 상승 추세. 구름이 지지선 역할을 할 가능성이 높음
  • 가격이 구름 안에 있음: 박스권 장세. 방향성이 정해지지 않음
  • 가격이 구름 아래에 있음: 하락 추세. 구름이 저항선 역할을 할 가능성이 높음

또한 구름의 두께도 추세의 강도를 나타냅니다. 구름이 두꺼울수록 지지·저항으로서의 강도가 증가합니다.

구름의 음전과 양전

구름의 색은 선행스팬1과 선행스팬2의 위치 관계에 따라 변합니다.

  • 양전(강세 구름): 선행스팬1이 선행스팬2를 상회하는 상태. 상승 추세 시사
  • 음전(약세 구름): 선행스팬1이 선행스팬2를 하회하는 상태. 하락 추세 시사

음전에서 양전으로 변하는 타이밍, 또는 그 반대 타이밍은 추세 전환의 중요한 신호가 됩니다.

전환선과 기준선의 교차

전환선(단기선)이 기준선(중기선)을 상향 돌파하면 매수 신호, 하향 돌파하면 매도 신호로 간주됩니다. 이동평균선의 골든크로스·데드크로스와 같은 개념입니다.

후행스팬

후행스팬은 현재 가격과 26일 전 가격을 비교하기 위한 지표입니다. 후행스팬이 과거 가격을 상회하면 강세, 하회하면 약세로 판단됩니다.

미래의 구름 활용

미래의 구름은 선행스팬이 26일 앞으로 이동되어 있기 때문에 표시되는 미래의 지지·저항입니다.

  • 두꺼운 미래의 구름: 강력한 지지/저항이 예상됨. 돌파가 어려울 가능성
  • 얇은 미래의 구름: 지지/저항이 약함. 가격이 돌파하기 쉬울 가능성
  • 미래의 구름의 꼬임: 음전⇔양전이 전환되는 지점. 추세 전환 가능성 시사

미래의 구름은 어디까지나 현재 데이터를 기반으로 한 예측이며, 실제 가격 동향에 따라 변화합니다. 정기적으로 차트를 업데이트하여 확인하는 것이 중요합니다.

Plotly로 시각화

Plotly를 사용하여 캔들스틱과 일목균형표의 5가지 요소를 표시합니다. 특히 선행스팬1과 2 사이를 채워 “구름”을 시각화하고, 음전·양전에 따라 색을 변경합니다. 또한 미래의 구름도 표시하여 미래의 지지·저항을 시각화합니다.

미래 날짜를 생성하는 함수

일목균형표의 선행스팬은 26일 미래로 이동되어 있기 때문에 미래 26일분의 구름을 표시하려면 미래 날짜가 필요합니다. Python의 datetime으로 영업일 기준 미래 날짜를 생성합니다.

from datetime import datetime, timedelta

def generate_future_business_days(last_date: str, n_days: int = 26) -> list[str]:
    """
    마지막 날짜부터 미래의 영업일(평일)을 생성

    Args:
        last_date: 마지막 데이터의 날짜(문자열 형식: "YYYY-MM-DD")
        n_days: 생성할 영업일수(기본값 26일)

    Returns:
        미래 영업일 리스트(문자열 형식: "YYYY-MM-DD")
    """
    # 마지막 날짜를 날짜 객체로 변환
    current = datetime.strptime(last_date, "%Y-%m-%d")
    future_dates = []

    # 필요한 영업일수가 채워질 때까지 하루씩 진행
    while len(future_dates) < n_days:
        current += timedelta(days=1)
        # 영업일(월~금)만 추가(weekday(): 월=0, 금=4)
        if current.weekday() < 5:
            future_dates.append(current.strftime("%Y-%m-%d"))

    return future_dates

이 함수의 포인트:

  • Python의 datetime: datetime.strptime()으로 문자열을 날짜로 변환하고, timedelta(days=1)로 하루씩 진행
  • 영업일 필터링: weekday()로 요일을 가져와(월=0, 화=1, …, 금=4, 토=5, 일=6) 5 미만(월~금)만 추가
  • 필요한 일수까지 반복: 토일을 건너뛰면서 26 영업일분이 채워질 때까지 루프

구름의 음전·양전을 색으로 구분하는 함수

먼저 구름을 세그먼트별로 분할하여 색으로 구분하는 함수를 정의합니다.

import numpy as np
import plotly.graph_objects as go

def create_cloud_segments(dates, span1, span2):
    """
    일목균형표의 구름을 음전·양전에 따라 색으로 구분한 세그먼트로 분할
    """
    # Polars Series를 NumPy 배열로 변환
    if hasattr(span1, "to_numpy"):
        span1 = span1.to_numpy()
    if hasattr(span2, "to_numpy"):
        span2 = span2.to_numpy()

    # 양전 판정(선행스팬1 > 선행스팬2)
    is_bullish = np.nan_to_num(span1 > span2, nan=False)

    # 교차점 검출(음전⇔양전이 전환되는 부분)
    crossovers = np.where(np.diff(is_bullish.astype(int)) != 0)[0] + 1

    # 세그먼트 경계 생성
    boundaries = np.concatenate([[0], crossovers, [len(dates)]])

    traces = []

    # 각 세그먼트별로 처리
    for i in range(len(boundaries) - 1):
        start_idx = boundaries[i]
        end_idx = boundaries[i + 1]

        segment_dates = dates[start_idx:end_idx]
        segment_span1 = span1[start_idx:end_idx]
        segment_span2 = span2[start_idx:end_idx]

        # NaN만 있는 세그먼트는 건너뛰기
        if np.all(np.isnan(segment_span1)) or np.all(np.isnan(segment_span2)):
            continue

        # 세그먼트의 양전·음전을 판정하여 색 설정
        segment_is_bullish = is_bullish[start_idx]
        fillcolor = (
            "rgba(135, 206, 250, 0.3)"  # 양전: 라이트블루
            if segment_is_bullish
            else "rgba(255, 165, 0, 0.3)"  # 음전: 오렌지
        )

        # 선행스팬1 라인(채우기 없음)
        traces.append(
            go.Scatter(
                x=segment_dates,
                y=segment_span1,
                mode="lines",
                line=dict(width=0.5, color="green"),
                showlegend=False,
                hoverinfo="skip",
            )
        )

        # 선행스팬2 라인(선행스팬1과의 사이를 채우기)
        traces.append(
            go.Scatter(
                x=segment_dates,
                y=segment_span2,
                mode="lines",
                line=dict(width=0.5, color="orange"),
                fill="tonexty",  # 이전 트레이스(선행스팬1)와의 사이를 채우기
                fillcolor=fillcolor,
                showlegend=False,
                hoverinfo="skip",
            )
        )

    return traces

이 함수의 포인트는 다음과 같습니다.

  • np.diff()로 교차점 검출: 음전·양전이 전환되는 지점을 자동 검출
  • 세그먼트 분할: 교차점으로 구름을 분할하고 각 세그먼트별로 색 설정
  • 색 구분: 양전 시에는 라이트블루, 음전 시에는 오렌지로 시각적 구분
  • fill="tonexty": 선행스팬1과 2 사이를 채워 구름 표현

미래의 구름을 포함한 구름 세그먼트 생성

미래의 구름을 표시하기 위해 create_cloud_segments() 함수를 확장합니다.

def create_cloud_segments_with_future(dates, span1, span2, num_future_days=26):
    """
    일목균형표의 구름(미래의 구름 포함)을 음전·양전에 따라 색으로 구분한 세그먼트로 분할
    """
    # 미래 날짜 생성
    last_date = dates[-1]
    future_dates = generate_future_business_days(last_date, num_future_days)

    # 날짜 결합(과거 + 미래)
    all_dates = dates + future_dates

    # Polars Series를 NumPy 배열로 변환
    if hasattr(span1, "to_numpy"):
        span1_values = span1.to_numpy()
    else:
        span1_values = span1
    if hasattr(span2, "to_numpy"):
        span2_values = span2.to_numpy()
    else:
        span2_values = span2

    # 선행스팬 배열 확장(앞 26개를 NaN으로 채우기)
    span1_extended = np.concatenate([[np.nan] * num_future_days, span1_values])
    span2_extended = np.concatenate([[np.nan] * num_future_days, span2_values])

    # 과거 부분과 미래 부분 분리
    past_dates = dates
    future_span1 = span1_extended[-num_future_days:]
    future_span2 = span2_extended[-num_future_days:]
    past_span1 = span1_extended[:-num_future_days]
    past_span2 = span2_extended[:-num_future_days]

    # 과거 부분의 양전 판정
    is_bullish = np.nan_to_num(past_span1 > past_span2, nan=False)

    # 교차점 검출
    crossovers = np.where(np.diff(is_bullish.astype(int)) != 0)[0] + 1

    # 세그먼트 경계 생성
    boundaries = np.concatenate([[0], crossovers, [len(past_dates)]])

    traces = []
    last_segment_color = None

    # 과거 부분의 각 세그먼트 처리
    for i in range(len(boundaries) - 1):
        start_idx = boundaries[i]
        end_idx = boundaries[i + 1]

        segment_dates = past_dates[start_idx:end_idx]
        segment_span1 = past_span1[start_idx:end_idx]
        segment_span2 = past_span2[start_idx:end_idx]

        if np.all(np.isnan(segment_span1)) or np.all(np.isnan(segment_span2)):
            continue

        # 세그먼트의 양전·음전을 판정하여 색 설정
        segment_is_bullish = is_bullish[start_idx]
        fillcolor = (
            "rgba(135, 206, 250, 0.3)"  # 양전: 라이트블루
            if segment_is_bullish
            else "rgba(255, 165, 0, 0.3)"  # 음전: 오렌지
        )
        last_segment_color = fillcolor

        # 선행스팬1 라인
        traces.append(
            go.Scatter(
                x=segment_dates,
                y=segment_span1,
                mode="lines",
                line=dict(width=0.5, color="green"),
                showlegend=False,
                hoverinfo="skip",
            )
        )

        # 선행스팬2 라인
        traces.append(
            go.Scatter(
                x=segment_dates,
                y=segment_span2,
                mode="lines",
                line=dict(width=0.5, color="orange"),
                fill="tonexty",
                fillcolor=fillcolor,
                showlegend=False,
                hoverinfo="skip",
            )
        )

    # 미래의 구름 추가(마지막 세그먼트 색 계속)
    if last_segment_color and not np.all(np.isnan(future_span1)):
        traces.append(
            go.Scatter(
                x=future_dates,
                y=future_span1,
                mode="lines",
                line=dict(width=0.5, color="green", dash="dot"),  # 점선으로 미래 표시
                showlegend=False,
                hoverinfo="skip",
            )
        )

        traces.append(
            go.Scatter(
                x=future_dates,
                y=future_span2,
                mode="lines",
                line=dict(width=0.5, color="orange", dash="dot"),  # 점선으로 미래 표시
                fill="tonexty",
                fillcolor=last_segment_color,  # 마지막 세그먼트 색 계속
                showlegend=False,
                hoverinfo="skip",
            )
        )

    return traces

이 함수의 포인트:

  • 미래 날짜 생성: generate_future_business_days()로 26 영업일분의 미래 날짜 생성
  • 배열 확장: np.concatenate([[np.nan] * 26, span1_values])로 앞 26개를 np.nan으로 채우고 그 후에 실제 선행스팬 값을 연결
  • 과거와 미래 분리: 확장된 배열의 과거 부분([:-26])으로 음전·양전을 판정하고, 미래 부분([-26:])은 마지막 세그먼트 색을 계속
  • 점선으로 미래 표현: dash="dot"로 미래의 구름을 점선 표시하여 과거 데이터와 시각적으로 구분

차트 생성

다음으로 데이터를 가져와 일목균형표 차트를 생성합니다.

import polars as pl
import yfinance_pl as yf

# 데이터 가져오기
ticker = yf.Ticker("8381.T")
hist = ticker.history(period="6mo")

# 일목균형표 계산
ichimoku = get_ichimoku_values(hist)

# DataFrame에 추가
df_plot = hist.with_columns(
    conversion_line=ichimoku["conversion_line"],
    base_line=ichimoku["base_line"],
    leading_span1=ichimoku["leading_span1"],
    leading_span2=ichimoku["leading_span2"],
    lagging_span=ichimoku["lagging_span"],
).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()

# 차트 생성
data = []

# 캔들스틱
data.append(
    go.Candlestick(
        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="주가",
    )
)

# 전환선
data.append(
    go.Scatter(
        x=dates,
        y=df_plot["conversion_line"],
        mode="lines",
        name="전환선",
        line=dict(color="blue", width=1),
    )
)

# 기준선
data.append(
    go.Scatter(
        x=dates,
        y=df_plot["base_line"],
        mode="lines",
        name="기준선",
        line=dict(color="red", width=1),
    )
)

# 구름(음전·양전 색 구분 + 미래의 구름)
cloud_traces = create_cloud_segments_with_future(
    dates,
    df_plot["leading_span1"],
    df_plot["leading_span2"],
    num_future_days=26,
)
data.extend(cloud_traces)

# 후행스팬
data.append(
    go.Scatter(
        x=dates,
        y=df_plot["lagging_span"],
        mode="lines",
        name="후행스팬",
        line=dict(color="purple", width=1, dash="dot"),
    )
)

# 레이아웃 설정
layout = go.Layout(
    title="주가와 일목균형표",
    xaxis_title="날짜",
    yaxis_title="가격",
    xaxis_rangeslider_visible=False,
)

fig = go.Figure(data=data, layout=layout)
fig.show()

이 코드에서는 create_cloud_segments_with_future() 함수로 구름을 음전·양전에 따라 색으로 구분합니다. 양전 시에는 라이트블루, 음전 시에는 오렌지로 표시되어 추세 변화를 시각적으로 이해하기 쉽습니다.

이 코드의 포인트는 다음과 같습니다.

  • 구름의 음전·양전 자동 검출: create_cloud_segments_with_future() 함수가 선행스팬1과 2의 대소 관계를 판정하여 교차점 검출
  • 세그먼트별 색 구분: 양전 시에는 라이트블루, 음전 시에는 오렌지로 구름을 채워 추세 변화를 시각적으로 표현
  • 효율적인 그리기: NumPy의 np.diff()를 사용하여 교차점을 검출하고 필요 최소한의 트레이스 수로 구름 그리기
  • 색의 투명도: rgba() 형식으로 투명도 30%를 설정하여 캔들스틱과 다른 라인을 보기 쉽게 배려
  • 미래의 구름 표시: Python의 datetime으로 26 영업일분의 미래 날짜를 생성하고, np.concatenate()로 선행스팬 배열을 확장하여 미래의 구름 표시
  • 배열 확장: 앞 26개를 np.nan으로 채움으로써 미래 날짜에 선행스팬의 실제 값을 매핑
  • 점선으로 미래 구분: 미래의 구름은 dash="dot"로 점선 표시하여 과거 데이터와 명확히 구분
  • 색의 계속: 과거 부분의 마지막 세그먼트(음전/양전) 색을 미래의 구름에도 적용하여 추세의 연속성 시각화

정리

이번에는 일목균형표의 5가지 요소(전환선, 기준선, 선행스팬1·2, 후행스팬)의 계산 방법과 Plotly를 통한 시각화 방법을 설명했습니다. Polars의 rolling_max(), rolling_min(), shift()를 사용함으로써 일목균형표의 복잡한 계산을 간결하게 구현할 수 있습니다. 특히 선행스팬과 후행스팬의 시간축 이동을 shift()로 쉽게 구현할 수 있는 점이 편리합니다. 이전의 볼린저 밴드, 이동평균선과의 관련성은 낮지만, 거래량 등 간단한 서브차트와 조합하여 일목균형표만으로 대략적인 추세를 파악할 수 있습니다. 추세 전환 직후의 시작과 끝은 펀더멘털 분석이 도움이 되지만, 일목만으로는 파악하기 어렵습니다. 하지만 일목균형표만으로 완결되는 이해하기 쉬움과 깊이가 매력적이므로 일목균형표를 배울 가치가 있다고 생각합니다.

참고 서적: 『Pythonで実践する株式投資分析』(片渕彼富 (著), 山田祥寛 (監修))

comment

댓글을 불러오는 중...