[Python] 주식 데이터 분석 만들어보기

반응형

안녕하세요, 오늘은 파이썬으로 주식 데이터를 분석하고 시각화하는 방법에 대해 이야기해볼게요. 간략하게 이 정도도 가능하다 정도를 생각하고 같이 만들어보겠습니다.

주식 데이터 획득하기: yfinance와 pandas-datareader

주식 데이터 분석의 첫 단계는 당연히 데이터 확보다. 신뢰할 수 있는 데이터 없이는 아무리 복잡한 알고리즘도 쓸모가 없다. 파이썬에서는 주로 두 가지 라이브러리를 사용해 주식 데이터를 가져온다: yfinancepandas-datareader다.

yfinance는 야후 파이낸스 API의 비공식 래퍼로, 사용하기 쉽고 무료다. 대부분의 기본적인 분석에는 이걸로 충분하다. 다만 야후의 정책 변경에 영향을 받을 수 있으니 상용 서비스라면 유료 API를 고려해라.

import yfinance as yf

# 애플 주식 데이터 가져오기 (최근 5년)
aapl = yf.Ticker("AAPL")
df = aapl.history(period="5y")

# 데이터 확인
print(df.head())

pandas-datareader는 야후 파이낸스 외에도 FRED, World Bank 같은 다양한 데이터 소스에 접근할 수 있다. 여러 출처의 데이터를 통합해야 한다면 이 라이브러리가 적합하다.

import pandas_datareader as pdr
from datetime import datetime

# 시작일과 종료일 설정
start_date = datetime(2018, 1, 1)
end_date = datetime.now()

# 구글 주식 데이터 가져오기
goog = pdr.get_data_yahoo('GOOG', start_date, end_date)

# 데이터 확인
print(goog.head())

기본적인 주가 데이터 분석 방법

데이터를 가져왔다면 이제 기본적인 분석을 시작해보자. 주가 데이터 분석에서 가장 기본이 되는 지표들은 일일 수익률, 누적 수익률, 변동성, 이동평균 등이다. pandas를 사용하면 이런 계산들을 쉽게 할 수 있다.

분석 지표 파이썬 코드 활용 방법
일일 수익률 df['Returns'] = df['Close'].pct_change() 일별 가격 변동 확인, 변동성 계산의 기초
누적 수익률 df['Cumulative'] = (1 + df['Returns']).cumprod() 장기 투자 성과 측정, 다른 자산과 비교
변동성(표준편차) volatility = df['Returns'].std() * (252 ** 0.5) 리스크 측정, 옵션 가격 책정
이동평균(MA) df['MA50'] = df['Close'].rolling(50).mean() 추세 파악, 지지/저항선 확인
베타(β) beta = cov(stock, market) / var(market) 체계적 리스크 측정, 포트폴리오 구성

기본적인 수익률과 변동성 계산은 다음과 같이 할 수 있다:

import numpy as np

# 일일 수익률 계산
df['Daily_Return'] = df['Close'].pct_change()

# 누적 수익률 계산
df['Cumulative_Return'] = (1 + df['Daily_Return']).cumprod() - 1

# 연간 변동성 계산 (252는 일반적인 연간 거래일수)
annual_volatility = df['Daily_Return'].std() * np.sqrt(252)
print(f"연간 변동성: {annual_volatility:.4f} (또는 {annual_volatility*100:.2f}%)")

# 이동평균 계산
df['MA20'] = df['Close'].rolling(window=20).mean()  # 20일 이동평균
df['MA50'] = df['Close'].rolling(window=50).mean()  # 50일 이동평균
df['MA200'] = df['Close'].rolling(window=200).mean()  # 200일 이동평균

기술적 지표 계산 및 해석

이동평균 외에도 주식 트레이더들이 자주 사용하는 기술적 지표들이 있다. 이러한 지표들은 주가의 추세, 모멘텀, 변동성 등을 파악하는 데 도움이 된다. 파이썬에서는 ta-lib 또는 pandas-ta 라이브러리를 사용하여 이러한 지표들을 쉽게 계산할 수 있다.

ta-lib는 설치가 조금 까다롭지만 성능이 좋고, pandas-ta는 순수 파이썬으로 작성되어 설치가 쉽다. 아래는 몇 가지 주요 기술적 지표를 계산하는 방법이다.

  • RSI(상대강도지수): 과매수/과매도 상태를 판단하는 지표. 0-100 사이의 값을 가지며, 일반적으로 70 이상이면 과매수, 30 이하면 과매도로 해석한다.
  • MACD(이동평균수렴발산): 단기 이동평균과 장기 이동평균의 차이를 보여주는 지표. 추세의 방향과 모멘텀을 파악하는 데 유용하다.
  • 볼린저 밴드: 이동평균을 중심으로 표준편차를 기반으로 한 상한선과 하한선을 그린 지표. 변동성과 가능한 가격 범위를 시각화하는 데 도움이 된다.
  • ATR(평균진폭): 주가의 변동성을 측정하는 지표. 주로 리스크 관리와 손절매 수준을 설정하는 데 사용된다.
  • 스토캐스틱 오실레이터: 현재 가격이 특정 기간의 가격 범위 내에서 어디에 위치하는지 보여주는 모멘텀 지표. 과매수/과매도 상태를 판단하는 데 사용된다.
# pandas-ta 설치: pip install pandas-ta
import pandas_ta as ta

# RSI 계산 (14일)
df['RSI'] = ta.rsi(df['Close'], length=14)

# MACD 계산
macd = ta.macd(df['Close'], fast=12, slow=26, signal=9)
df = df.join(macd)

# 볼린저 밴드 계산
bollinger = ta.bbands(df['Close'], length=20, std=2)
df = df.join(bollinger)

# ATR 계산
df['ATR'] = ta.atr(df['High'], df['Low'], df['Close'], length=14)

# 스토캐스틱 오실레이터 계산
stoch = ta.stoch(df['High'], df['Low'], df['Close'], k=14, d=3, smooth_k=3)
df = df.join(stoch)
📝 TIP: 기술적 지표의 한계 이해하기

기술적 지표는 유용한 도구지만 만능이 아니다. 과거 데이터에 기반하기 때문에 예측력에 한계가 있고, 시장이 급변할 때는 신뢰성이 떨어진다. 특히 개별 지표보다는 여러 지표를 종합적으로 분석하는 것이 중요하다. 또한 기본적 분석(기업의 재무상태, 성장성 등)과 함께 활용할 때 더 나은 결과를 얻을 수 있다.

효과적인 주식 데이터 시각화 테크닉

숫자만 보면 패턴을 파악하기 어렵다. 데이터 시각화는 주가 패턴과 트렌드를 직관적으로 이해하는 데 필수적이다. 파이썬에서는 matplotlib, seaborn, plotly 같은 라이브러리를 사용해 다양한 차트를 그릴 수 있다.

주식 데이터 시각화에서 가장 기본이 되는 것은 캔들스틱 차트다. 일일 시가, 고가, 저가, 종가를 한 눈에 볼 수 있어 가격 흐름을 파악하기 좋다. plotly를 사용하면 인터랙티브한 캔들스틱 차트를 쉽게 만들 수 있다.

import plotly.graph_objects as go

# 캔들스틱 차트 생성
fig = go.Figure(data=[go.Candlestick(
    x=df.index,
    open=df['Open'],
    high=df['High'],
    low=df['Low'],
    close=df['Close'],
    name='캔들스틱'
)])

# 이동평균선 추가
fig.add_trace(go.Scatter(
    x=df.index,
    y=df['MA20'],
    line=dict(color='blue', width=1),
    name='20일 이동평균'
))

fig.add_trace(go.Scatter(
    x=df.index,
    y=df['MA50'],
    line=dict(color='orange', width=1),
    name='50일 이동평균'
))

# 레이아웃 설정
fig.update_layout(
    title='애플 주가 차트',
    yaxis_title='가격 (USD)',
    xaxis_title='날짜',
    xaxis_rangeslider_visible=False
)

# 차트 표시
fig.show()

캔들스틱 차트 외에도 다양한 차트를 활용하면 데이터에서 더 많은 인사이트를 얻을 수 있다. 예를 들어, 거래량 차트, 수익률 분포, 다른 종목과의 상관관계 등을 시각화할 수 있다.

matplotlib을 사용해 수익률 분포를 히스토그램으로 시각화하는 방법은 다음과 같다:

import matplotlib.pyplot as plt
import seaborn as sns

# Seaborn 스타일 설정
sns.set(style='whitegrid')

# 수익률 분포 히스토그램
plt.figure(figsize=(10, 6))
sns.histplot(df['Daily_Return'].dropna(), kde=True, bins=50)
plt.title('일일 수익률 분포')
plt.xlabel('수익률')
plt.ylabel('빈도')
plt.axvline(x=0, color='r', linestyle='--')  # 0 수익률 위치에 수직선 추가
plt.show()

# 누적 수익률 그래프
plt.figure(figsize=(10, 6))
plt.plot(df.index, df['Cumulative_Return'])
plt.title('누적 수익률')
plt.xlabel('날짜')
plt.ylabel('누적 수익률')
plt.grid(True)
plt.show()

기술적 지표들도 시각화하면 추세와 패턴을 더 쉽게 파악할 수 있다. 아래는 주가 차트와 함께 RSI, MACD를 시각화하는 코드다.

plt.figure(figsize=(12, 8))

# 주가 차트
plt.subplot(3, 1, 1)
plt.plot(df.index, df['Close'])
plt.plot(df.index, df['MA20'], 'r--', alpha=0.6)
plt.plot(df.index, df['MA50'], 'g--', alpha=0.6)
plt.title('주가와 이동평균')
plt.ylabel('가격')
plt.grid(True)

# RSI 차트
plt.subplot(3, 1, 2)
plt.plot(df.index, df['RSI'])
plt.axhline(y=70, color='r', linestyle='--', alpha=0.5)  # 과매수 기준선
plt.axhline(y=30, color='g', linestyle='--', alpha=0.5)  # 과매도 기준선
plt.title('RSI (14)')
plt.ylabel('RSI')
plt.grid(True)

# MACD 차트
plt.subplot(3, 1, 3)
plt.plot(df.index, df['MACD_12_26_9'], 'b', label='MACD')
plt.plot(df.index, df['MACDs_12_26_9'], 'r', label='Signal')
plt.bar(df.index, df['MACDh_12_26_9'], color='gray', alpha=0.3, label='Histogram')
plt.title('MACD')
plt.ylabel('MACD')
plt.legend()
plt.grid(True)

plt.tight_layout()
plt.show()

간단한 트레이딩 전략 백테스팅

지표를 계산하고 시각화하는 것은 시작일 뿐이다. 실제로 이러한 지표를 바탕으로 트레이딩 전략을 개발하고 과거 데이터로 테스트해봐야 한다. 이를 백테스팅(backtesting)이라고 한다. 파이썬에서는 backtesting.py 또는 pyalgotrade 같은 라이브러리를 사용할 수 있지만, 간단한 전략은 직접 구현하는 것도 가능하다.

아래는 이동평균 교차 전략(Golden Cross/Death Cross)을 구현하고 백테스팅하는 간단한 예제다.

백테스팅 주요 지표 설명 계산 방법
총 수익률 전체 백테스팅 기간의 누적 수익률 최종 포트폴리오 가치 / 초기 투자금 - 1
연간 수익률 연간화된 수익률 (1 + 총 수익률)^(1/연수) - 1
최대 낙폭(MDD) 최고점에서 최저점까지의 최대 하락 비율 min(현재가치/최고가치 - 1)
샤프 비율 리스크 대비 초과 수익률 (전략 수익률 - 무위험 수익률) / 전략 변동성
승률 이익을 본 거래의 비율 이익 거래 수 / 전체 거래 수
# 이동평균 교차 전략 구현
def moving_average_crossover_strategy(df, short_window=20, long_window=50):
    # 전략 정보를 저장할 새로운 DataFrame 생성
    signals = pd.DataFrame(index=df.index)
    signals['price'] = df['Close']
    signals['short_mavg'] = df['Close'].rolling(window=short_window, min_periods=1).mean()
    signals['long_mavg'] = df['Close'].rolling(window=long_window, min_periods=1).mean()
    
    # 시그널 생성: 단기 이평선이 장기 이평선을 상향 돌파하면 1(매수), 하향 돌파하면 -1(매도), 그 외에는 0
    signals['signal'] = 0.0
    signals['signal'] = np.where(signals['short_mavg'] > signals['long_mavg'], 1.0, 0.0)
    signals['position'] = signals['signal'].diff()
    
    return signals

# 백테스팅 함수 구현
def backtest_strategy(signals, initial_capital=100000.0):
    # 시그널을 기반으로 포트폴리오 성과 계산
    positions = pd.DataFrame(index=signals.index).fillna(0.0)
    positions['price'] = signals['price']
    positions['holdings'] = signals['signal'] * positions['price']
    
    # 현금 및 전체 자산가치 계산
    portfolio = positions.copy()
    pos_diff = positions['holdings'].diff()
    portfolio['cash'] = initial_capital - (pos_diff * positions['price']).cumsum()
    portfolio['total'] = portfolio['cash'] + positions['holdings']
    portfolio['returns'] = portfolio['total'].pct_change()
    
    return portfolio

# 전략 실행 및 백테스팅
signals = moving_average_crossover_strategy(df)
portfolio = backtest_strategy(signals)

# 백테스팅 결과 계산
total_return = (portfolio['total'][-1] / portfolio['total'][0]) - 1
annual_return = (1 + total_return) ** (252 / len(portfolio)) - 1
sharpe_ratio = np.sqrt(252) * (portfolio['returns'].mean() / portfolio['returns'].std())
max_drawdown = (portfolio['total'] / portfolio['total'].cummax() - 1.0).min()

# 결과 출력
print(f"총 수익률: {total_return:.4f} ({total_return*100:.2f}%)")
print(f"연간 수익률: {annual_return:.4f} ({annual_return*100:.2f}%)")
print(f"샤프 비율: {sharpe_ratio:.4f}")
print(f"최대 낙폭: {max_drawdown:.4f} ({max_drawdown*100:.2f}%)")

이 전략의 성과를 시각화하는 코드는 다음과 같다:

plt.figure(figsize=(12, 8))

# 주가 및 이동평균선
plt.subplot(2, 1, 1)
plt.plot(signals.index, signals['price'])
plt.plot(signals.index, signals['short_mavg'], 'r--', label=f'{short_window}일 이동평균')
plt.plot(signals.index, signals['long_mavg'], 'g--', label=f'{long_window}일 이동평균')

# 매수/매도 신호 표시
plt.plot(signals.loc[signals['position'] == 1.0].index, 
         signals.loc[signals['position'] == 1.0]['short_mavg'], 
         '^', markersize=10, color='g', label='매수 신호')
         
plt.plot(signals.loc[signals['position'] == -1.0].index, 
         signals.loc[signals['position'] == -1.0]['short_mavg'], 
         'v', markersize=10, color='r', label='매도 신호')
         
plt.title('이동평균 교차 전략 - 신호')
plt.ylabel('가격')
plt.legend()
plt.grid(True)

# 포트폴리오 가치
plt.subplot(2, 1, 2)
plt.plot(portfolio.index, portfolio['total'], label='포트폴리오 가치')
plt.plot(portfolio.index, portfolio['cash'], 'r--', label='현금 보유량')
plt.title('이동평균 교차 전략 - 포트폴리오 가치')
plt.ylabel('가치')
plt.legend()
plt.grid(True)

plt.tight_layout()
plt.show()

머신러닝을 활용한 주가 예측

기존의 기술적 분석을 넘어, 머신러닝을 활용하면 더 복잡한 패턴을 발견하고 예측 모델을 만들 수 있다. 물론 주가 예측은 불확실성이 높아 100% 정확한 예측은 불가능하지만, 머신러닝 모델은 유의미한 통찰을 제공할 수 있다.

주가 예측에 자주 사용되는 머신러닝 알고리즘으로는 선형 회귀, 랜덤 포레스트, 서포트 벡터 머신(SVM), 그리고 시계열 데이터에 특화된 ARIMA, LSTM 등이 있다. 여기서는 scikit-learn의 랜덤 포레스트와 keras의 LSTM을 사용하는 예제를 살펴보자.

  1. 특성 엔지니어링(Feature Engineering): 모델의 입력으로 사용할 특성(feature)들을 만든다. 주가, 거래량, 기술적 지표 등을 활용한다.
  2. 데이터 정규화: 데이터를 0-1 또는 -1-1 범위로 정규화하여 모델의 학습 효율을 높인다.
  3. 데이터 분할: 훈련 데이터와 테스트 데이터로 나눈다. 시계열 데이터의 경우, 단순 무작위 분할이 아닌 시간 순서를 고려한 분할이 필요하다.
  4. 모델 학습: 선택한 알고리즘으로 모델을 훈련시킨다.
  5. 모델 평가: 테스트 데이터로 모델의 성능을 평가한다. RMSE, MAE, R² 등의 지표를 사용한다.

랜덤 포레스트를 이용한 간단한 주가 예측 모델:

from sklearn.ensemble import RandomForestRegressor
from sklearn.preprocessing import MinMaxScaler
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
import numpy as np

# 특성 엔지니어링: 기본 특성 및 기술적 지표 추가
def create_features(df):
    # 날짜 관련 특성
    df['DayOfWeek'] = df.index.dayofweek
    df['Month'] = df.index.month
    df['Year'] = df.index.year
    
    # 가격 관련 특성
    df['PriceRatio'] = df['Close'] / df['Open']
    df['HL_Ratio'] = df['High'] / df['Low']
    
    # 이동평균 및 거래량 특성
    df['MA5'] = df['Close'].rolling(window=5).mean()
    df['MA10'] = df['Close'].rolling(window=10).mean()
    df['MA20'] = df['Close'].rolling(window=20).mean()
    df['VolumeDelta'] = df['Volume'].diff()
    
    # 전날 대비 변화율
    df['CloseChange'] = df['Close'].pct_change()
    
    # 결측치 제거
    df = df.dropna()
    
    return df

# 특성 생성
df_ml = create_features(df.copy())

# 목표 변수(다음 날 종가)
df_ml['NextClose'] = df_ml['Close'].shift(-1)
df_ml = df_ml.dropna()

# 입력 특성과 목표 변수 분리
X = df_ml.drop(['NextClose', 'Open', 'High', 'Low', 'Close', 'Volume'], axis=1)
y = df_ml['NextClose']

# 데이터 정규화
scaler = MinMaxScaler()
X_scaled = scaler.fit_transform(X)

# 훈련/테스트 세트 분할 (시간 순서 고려)
train_size = int(len(X_scaled) * 0.8)
X_train, X_test = X_scaled[:train_size], X_scaled[train_size:]
y_train, y_test = y[:train_size], y[train_size:]

# 랜덤 포레스트 모델 학습
model = RandomForestRegressor(n_estimators=100, random_state=42)
model.fit(X_train, y_train)

# 예측
y_pred = model.predict(X_test)

# 모델 평가
mse = mean_squared_error(y_test, y_pred)
rmse = np.sqrt(mse)
mae = mean_absolute_error(y_test, y_pred)
r2 = r2_score(y_test, y_pred)

print(f"MSE: {mse:.4f}")
print(f"RMSE: {rmse:.4f}")
print(f"MAE: {mae:.4f}")
print(f"R²: {r2:.4f}")

# 특성 중요도 시각화
feature_importance = model.feature_importances_
features = X.columns

plt.figure(figsize=(10, 6))
plt.barh(features, feature_importance)
plt.xlabel('중요도')
plt.title('특성 중요도')
plt.tight_layout()
plt.show()
📝 TIP: 머신러닝 모델의 함정 피하기

주가 예측에 머신러닝을 적용할 때 주의할 점이 있다. 첫째, 과거 성과가 미래 성과를 보장하지 않는다. 과적합(overfitting)을 방지하기 위해 교차 검증과 정규화를 적용하고, 모델의 복잡성을 제한해야 한다. 둘째, 룩어헤드 바이어스(lookahead bias)를 조심해야 한다. 미래 정보가 학습 과정에 유입되지 않도록 시간 순서를 철저히 지켜야 한다. 마지막으로, 실제 거래에 적용할 때는 거래 비용, 슬리피지 등 현실적인 요소들을 반영해야 한다.

📝 TIP: 리스크 관리 원칙

아무리 좋은 분석과 모델을 갖추고 있더라도, 리스크 관리 없이는 결국 실패한다. 한 종목에 전체 자금의 5% 이상을 투자하지 말 것, 손실을 본 투자에 추가 자금을 투입하지 말 것(물타기 금지), 일관된 손절매 룰을 정하고 감정에 휘둘리지 말 것, 백테스팅 결과가 아무리 좋아도 실전에서는 보수적으로 접근할 것. 이런 기본 원칙들을 무시하고 무작정 알고리즘을 믿다가는 큰 손실을 볼 수 있다.

종합 주식 분석 도구 - 실전 코드

이제 앞서 설명한 모든 개념을 종합해서 실용적인 주식 분석 도구를 만들어보자. 이 코드는 종목 티커를 입력하면 데이터를 가져와 분석하고, 기술적 지표를 계산하며, 시각화까지 수행한다. 또한 간단한 이동평균 교차 전략을 백테스팅하여 결과도 보여준다.

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import yfinance as yf
import pandas_ta as ta
from datetime import datetime, timedelta
from sklearn.preprocessing import MinMaxScaler
from matplotlib.gridspec import GridSpec

class StockAnalyzer:
    def __init__(self, ticker, start_date=None, end_date=None, period='5y'):
        """
        주식 분석 도구 초기화
        
        Parameters:
        -----------
        ticker : str
            분석할 종목의 티커 심볼 (예: 'AAPL', '005930.KS' 등)
        start_date : str, optional
            분석 시작일 (YYYY-MM-DD 형식), 기본값은 None
        end_date : str, optional
            분석 종료일 (YYYY-MM-DD 형식), 기본값은 오늘
        period : str, optional
            분석 기간 (start_date가 None일 경우 사용), 기본값은 '5y'
        """
        self.ticker = ticker
        self.start_date = start_date
        self.end_date = end_date if end_date else datetime.now().strftime('%Y-%m-%d')
        self.period = period
        self.stock_data = None
        self.signals = None
        self.portfolio = None
        self.performance = {}
        
        # 데이터 가져오기
        self._get_data()
    
    def _get_data(self):
        """주식 데이터 가져오기"""
        try:
            if self.start_date:
                self.stock_data = yf.download(self.ticker, start=self.start_date, end=self.end_date)
            else:
                stock = yf.Ticker(self.ticker)
                self.stock_data = stock.history(period=self.period)
            
            # 데이터가 비어있는지 확인
            if self.stock_data.empty:
                raise ValueError(f"'{self.ticker}'에 대한 데이터를 찾을 수 없습니다.")
                
            # 기본 지표 계산
            self._calculate_basic_indicators()
            print(f"'{self.ticker}' 데이터 로드 완료. 행 개수: {len(self.stock_data)}")
        except Exception as e:
            print(f"데이터 가져오기 오류: {e}")
            raise
    
    def _calculate_basic_indicators(self):
        """기본 지표 계산"""
        df = self.stock_data.copy()
        
        # 일일 및 누적 수익률
        df['Daily_Return'] = df['Close'].pct_change()
        df['Cumulative_Return'] = (1 + df['Daily_Return']).cumprod() - 1
        
        # 이동평균
        df['MA5'] = df['Close'].rolling(window=5).mean()
        df['MA20'] = df['Close'].rolling(window=20).mean()
        df['MA50'] = df['Close'].rolling(window=50).mean()
        df['MA200'] = df['Close'].rolling(window=200).mean()
        
        # 볼린저 밴드
        bollinger = ta.bbands(df['Close'], length=20, std=2)
        df = df.join(bollinger)
        
        # RSI
        df['RSI'] = ta.rsi(df['Close'], length=14)
        
        # MACD
        macd = ta.macd(df['Close'], fast=12, slow=26, signal=9)
        df = df.join(macd)
        
        # ATR
        df['ATR'] = ta.atr(df['High'], df['Low'], df['Close'], length=14)
        
        self.stock_data = df
    
    def calculate_performance_metrics(self):
        """주요 성과 지표 계산"""
        df = self.stock_data
        
        # 연간 수익률
        total_days = (df.index[-1] - df.index[0]).days
        total_years = total_days / 365.25
        total_return = df['Cumulative_Return'].iloc[-1]
        annualized_return = (1 + total_return) ** (1 / total_years) - 1
        
        # 변동성
        daily_volatility = df['Daily_Return'].std()
        annualized_volatility = daily_volatility * np.sqrt(252)
        
        # 샤프 비율 (무위험 수익률 0% 가정)
        sharpe_ratio = (annualized_return / annualized_volatility) if annualized_volatility > 0 else 0
        
        # 최대 낙폭
        df['Cum_Max'] = df['Close'].cummax()
        df['Drawdown'] = (df['Close'] / df['Cum_Max']) - 1
        max_drawdown = df['Drawdown'].min()
        
        # 베타 (시장 데이터가 필요하므로 여기서는 계산 생략)
        
        # 성능 지표 저장
        self.performance = {
            'Total Return': total_return,
            'Annualized Return': annualized_return,
            'Annualized Volatility': annualized_volatility,
            'Sharpe Ratio': sharpe_ratio,
            'Max Drawdown': max_drawdown,
            'Start Date': df.index[0].strftime('%Y-%m-%d'),
            'End Date': df.index[-1].strftime('%Y-%m-%d'),
            'Duration (Years)': total_years
        }
        
        return self.performance
    
    def moving_average_crossover_strategy(self, short_window=20, long_window=50):
        """이동평균 교차 전략 구현"""
        df = self.stock_data.copy()
        
        # 시그널 DataFrame 생성
        signals = pd.DataFrame(index=df.index)
        signals['price'] = df['Close']
        signals['short_mavg'] = df[f'MA{short_window}']
        signals['long_mavg'] = df[f'MA{long_window}']
        
        # 매수 시그널: 단기선이 장기선 위로
        signals['signal'] = 0.0
        signals['signal'] = np.where(signals['short_mavg'] > signals['long_mavg'], 1.0, 0.0)
        signals['position'] = signals['signal'].diff()
        
        self.signals = signals
        
        # 백테스팅
        return self._backtest_strategy()
    
    def _backtest_strategy(self, initial_capital=100000.0, position_size=1.0):
        """전략 백테스팅"""
        if self.signals is None:
            raise ValueError("먼저 전략을 실행해야 합니다.")
        
        signals = self.signals
        
        # 포지션 계산
        positions = pd.DataFrame(index=signals.index).fillna(0.0)
        positions['price'] = signals['price']
        positions['signal'] = signals['signal']
        
        # 각 시점에서 보유할 주식 수량 (간단하게 position_size로 조정)
        positions['shares'] = positions['signal'] * position_size
        positions['cash'] = initial_capital - (positions['shares'].diff() * positions['price']).cumsum()
        positions['holdings'] = positions['shares'] * positions['price']
        positions['total'] = positions['cash'] + positions['holdings']
        positions['returns'] = positions['total'].pct_change()
        
        self.portfolio = positions
        
        # 백테스팅 성과 계산
        strategy_return = (positions['total'][-1] / positions['total'][0]) - 1
        buy_hold_return = (signals['price'][-1] / signals['price'][0]) - 1
        
        # 거래 횟수 계산
        trades = signals[signals['position'] != 0].shape[0]
        
        # 최대 낙폭
        positions['peak'] = positions['total'].cummax()
        positions['drawdown'] = (positions['total'] / positions['peak']) - 1
        max_drawdown = positions['drawdown'].min()
        
        # 연간화된 수익률 및 샤프 비율
        days = (positions.index[-1] - positions.index[0]).days
        years = days / 365.25
        annualized_return = (1 + strategy_return) ** (1 / years) - 1
        annualized_volatility = positions['returns'].std() * np.sqrt(252)
        sharpe_ratio = (annualized_return / annualized_volatility) if annualized_volatility > 0 else 0
        
        # 결과 저장
        strategy_performance = {
            'Strategy Return': strategy_return,
            'Buy & Hold Return': buy_hold_return,
            'Outperformance': strategy_return - buy_hold_return,
            'Annualized Return': annualized_return,
            'Annualized Volatility': annualized_volatility,
            'Sharpe Ratio': sharpe_ratio,
            'Max Drawdown': max_drawdown,
            'Trades': trades
        }
        
        return strategy_performance
    
    def visualize_data(self, start_date=None, end_date=None):
        """주가 데이터 시각화"""
        df = self.stock_data.copy()
        
        # 날짜 필터링
        if start_date:
            df = df[df.index >= start_date]
        if end_date:
            df = df[df.index <= end_date]
        
        # 그래프 설정
        plt.figure(figsize=(14, 10))
        gs = GridSpec(3, 1, height_ratios=[2, 1, 1])
        
        # 주가 및 이동평균선 차트
        ax1 = plt.subplot(gs[0])
        ax1.plot(df.index, df['Close'], label='종가')
        ax1.plot(df.index, df['MA20'], 'g--', alpha=0.6, label='20일 이동평균')
        ax1.plot(df.index, df['MA50'], 'r--', alpha=0.6, label='50일 이동평균')
        ax1.plot(df.index, df['MA200'], 'b--', alpha=0.6, label='200일 이동평균')
        
        # 볼린저 밴드 추가
        if 'BBU_20_2.0' in df.columns:
            ax1.plot(df.index, df['BBU_20_2.0'], 'k--', alpha=0.3, label='볼린저 상단')
            ax1.plot(df.index, df['BBL_20_2.0'], 'k--', alpha=0.3, label='볼린저 하단')
            ax1.fill_between(df.index, df['BBU_20_2.0'], df['BBL_20_2.0'], color='gray', alpha=0.1)
        
        ax1.set_title(f'{self.ticker} 주가 차트', fontsize=16)
        ax1.set_ylabel('가격', fontsize=12)
        ax1.legend(loc='best')
        ax1.grid(True, alpha=0.3)
        
        # 거래량 차트
        ax2 = plt.subplot(gs[1], sharex=ax1)
        ax2.bar(df.index, df['Volume'], color='gray', alpha=0.5)
        ax2.set_ylabel('거래량', fontsize=12)
        ax2.grid(True, alpha=0.3)
        
        # RSI 차트
        ax3 = plt.subplot(gs[2], sharex=ax1)
        ax3.plot(df.index, df['RSI'], color='purple')
        ax3.axhline(y=70, color='r', linestyle='--', alpha=0.3)
        ax3.axhline(y=30, color='g', linestyle='--', alpha=0.3)
        ax3.set_ylabel('RSI (14)', fontsize=12)
        ax3.set_ylim(0, 100)
        ax3.grid(True, alpha=0.3)
        
        # X축 레이블 포맷 조정
        plt.gcf().autofmt_xdate()
        plt.tight_layout()
        
        return plt.gcf()
    
    def visualize_strategy(self):
        """전략 백테스팅 결과 시각화"""
        if self.signals is None or self.portfolio is None:
            raise ValueError("먼저 전략을 실행하고 백테스팅해야 합니다.")
        
        signals = self.signals
        portfolio = self.portfolio
        
        plt.figure(figsize=(14, 10))
        gs = GridSpec(2, 1, height_ratios=[2, 1])
        
        # 주가, 이동평균, 매수/매도 신호
        ax1 = plt.subplot(gs[0])
        ax1.plot(signals.index, signals['price'], label='종가')
        ax1.plot(signals.index, signals['short_mavg'], 'g--', alpha=0.6, label='단기 이동평균')
        ax1.plot(signals.index, signals['long_mavg'], 'r--', alpha=0.6, label='장기 이동평균')
        
        # 매수/매도 신호
        buy_signals = signals[signals['position'] == 1.0]
        sell_signals = signals[signals['position'] == -1.0]
        
        ax1.plot(buy_signals.index, buy_signals['price'], '^', markersize=10, color='g', alpha=0.7, label='매수 신호')
        ax1.plot(sell_signals.index, sell_signals['price'], 'v', markersize=10, color='r', alpha=0.7, label='매도 신호')
        
        ax1.set_title(f'{self.ticker} 이동평균 교차 전략', fontsize=16)
        ax1.set_ylabel('가격', fontsize=12)
        ax1.legend(loc='best')
        ax1.grid(True, alpha=0.3)
        
        # 포트폴리오 가치
        ax2 = plt.subplot(gs[1], sharex=ax1)
        ax2.plot(portfolio.index, portfolio['total'], label='포트폴리오 가치', color='blue')
        
        # 주가 변화와 비교 (정규화)
        price_normalized = signals['price'] / signals['price'][0] * portfolio['total'][0]
        ax2.plot(signals.index, price_normalized, 'k--', alpha=0.6, label='Buy & Hold')
        
        ax2.set_ylabel('가치', fontsize=12)
        ax2.legend(loc='best')
        ax2.grid(True, alpha=0.3)
        
        # X축 레이블 포맷 조정
        plt.gcf().autofmt_xdate()
        plt.tight_layout()
        
        return plt.gcf()
    
    def print_performance_summary(self):
        """성과 요약 출력"""
        if not self.performance:
            self.calculate_performance_metrics()
        
        print(f"\n{'-'*50}")
        print(f"{self.ticker} 성과 요약")
        print(f"{'-'*50}")
        print(f"분석 기간: {self.performance['Start Date']} ~ {self.performance['End Date']} ({self.performance['Duration (Years)']:.2f} 년)")
        print(f"총 수익률: {self.performance['Total Return']*100:.2f}%")
        print(f"연간 수익률: {self.performance['Annualized Return']*100:.2f}%")
        print(f"연간 변동성: {self.performance['Annualized Volatility']*100:.2f}%")
        print(f"샤프 비율: {self.performance['Sharpe Ratio']:.2f}")
        print(f"최대 낙폭: {self.performance['Max Drawdown']*100:.2f}%")
        print(f"{'-'*50}")


# 사용 예시
if __name__ == "__main__":
    # 분석할 종목 설정
    ticker = "AAPL"  # 애플 주식 (다른 종목으로 바꿀 수 있음)
    
    # 분석 도구 초기화
    analyzer = StockAnalyzer(ticker, period='2y')
    
    # 주요 성과 지표 계산
    performance = analyzer.calculate_performance_metrics()
    analyzer.print_performance_summary()
    
    # 주가 데이터 시각화
    analyzer.visualize_data()
    plt.show()
    
    # 이동평균 교차 전략 실행 및 백테스팅
    strategy_performance = analyzer.moving_average_crossover_strategy(short_window=20, long_window=50)
    
    # 전략 성과 출력
    print(f"\n{'-'*50}")
    print(f"{ticker} 이동평균 교차 전략 성과")
    print(f"{'-'*50}")
    print(f"전략 수익률: {strategy_performance['Strategy Return']*100:.2f}%")
    print(f"Buy & Hold 수익률: {strategy_performance['Buy & Hold Return']*100:.2f}%")
    print(f"초과 수익률: {strategy_performance['Outperformance']*100:.2f}%")
    print(f"연간 수익률: {strategy_performance['Annualized Return']*100:.2f}%")
    print(f"샤프 비율: {strategy_performance['Sharpe Ratio']:.2f}")
    print(f"최대 낙폭: {strategy_performance['Max Drawdown']*100:.2f}%")
    print(f"거래 횟수: {strategy_performance['Trades']}")
    print(f"{'-'*50}")
    
    # 전략 시각화
    analyzer.visualize_strategy()
    plt.show()

위 코드는 주식 분석을 위한 클래스를 구현한 것이다. 주요 기능은 다음과 같다:

  • 데이터 획득: yfinance를 사용해 실시간 주식 데이터를 가져온다.
  • 기술적 지표 계산: 이동평균, RSI, MACD, 볼린저 밴드 등 다양한 지표를 계산한다.
  • 성과 측정: 총 수익률, 연간 수익률, 변동성, 샤프 비율, 최대 낙폭 등을 계산한다.
  • 전략 구현: 이동평균 교차 전략을 구현하고 백테스팅한다.
  • 데이터 시각화: 주가 차트, 기술적 지표, 전략 백테스팅 결과를 시각화한다.

이 코드는 실제 투자나 학습 목적으로 사용할 수 있으며, 필요에 따라 다른 전략이나 지표를 추가할 수 있다. 단, 실제 투자에 활용할 때는 반드시 충분한 검증과 리스크 관리 전략이 필요하다는 점을 명심해야 한다.

마치며

파이썬을 활용한 주식 데이터 분석의 세계를 살펴봤다. 데이터 획득부터 시각화, 기술적 지표 계산, 전략 백테스팅, 머신러닝 적용까지, 파이썬은 이 모든 과정을 상당히 효율적으로 처리할 수 있는 도구다. 특히 yfinance, pandas-ta, matplotlib, scikit-learn 같은 라이브러리들은 금융 데이터 분석을 위한 강력한 생태계를 제공한다.

그러나 모든 분석과 모델에는 한계가 있다. 아무리 정교한 알고리즘이라도 시장의 불확실성을 완벽히 예측할 수는 없다. 큰 경제 이벤트, 예상치 못한 뉴스, 시장 심리 변화 등은 기술적 분석만으로는 파악하기 어려운 요소들이다. 따라서 기술적 분석과 함께 기본적 분석(기업의 재무상태, 성장성, 산업 동향 등)도 함께 고려하는 것이 중요하다.

 

2025.03.19 - [Developer/Python] - [Python] Seaborn으로 멋진 데이터 시각화 만들기

 

[Python] Seaborn으로 멋진 데이터 시각화 만들기

엑셀로 그래프 그리다 지친 당신, Python의 Seaborn으로 단 몇 줄의 코드만으로 전문가급 데이터 시각화를 만들 수 있다면 어떨까요?안녕하세요, 데이터 시각화에 관심 있는 여러분. 오늘은 Seaborn이

dmoogi.tistory.com

2025.03.14 - [Developer/Python] - [Python] 함수 정의와 호출: 재사용 가능한 코드 작성법

 

[Python] 함수 정의와 호출: 재사용 가능한 코드 작성법

똑같은 코드 몇 번이나 복사-붙여넣기 하고 계신가요? 함수를 사용하면 그런 비효율은 이제 그만.안녕하세요. 오늘은 코드 중복을 없애고 유지보수를 쉽게 만드는 파이썬 함수에 대해 이야기해

dmoogi.tistory.com

2025.03.14 - [Developer/Python] - [Python] input() 함수로 사용자와 상호작용하기

 

[Python] input() 함수로 사용자와 상호작용하기

안녕하세요, 오늘은 파이썬에서 가장 기본적인 함수 중에 하나인 input() 함수에 대해 파헤쳐 볼게요. 간단해 보이지만 제대로 알고 쓰지 않으면 프로그램 실행 중 예상치 못한 버그가 발생할 수

dmoogi.tistory.com

 

Designed by JB FACTORY