본문 바로가기

StockBot

첫 주식 매매 테스트용 코드

import yfinance as yf
import pandas as pd
import datetime
import os
import matplotlib.pyplot as plt
import numpy as np

# 한글 폰트 설정 (Windows)
plt.rcParams['font.family'] = 'Malgun Gothic'
plt.rcParams['axes.unicode_minus'] = False


def get_macro_data_and_save():
    print(f"--- 2025년 12월 26일 경제지표 수집 및 CSV 저장 시작 ---")

    # 1. 수집 대상 설정 (티커와 이름 매핑)
    tickers = {
        "KRW_USD": "KRW=X",  # 원/달러 환율
        "US_10Y_Yield": "^TNX",  # 미국 10년물 국채 금리
        "SOX_Index": "^SOX",  # 필라델피아 반도체 지수
        "VIX_Index": "^VIX",  # 공포지수
        "WTI_Oil": "CL=F",  # 원유 선물
        "KOSPI_200": "^KS200"  # 코스피 200 (추가)
    }

    # 2. 데이터 수집 및 병합
    combined_df = pd.DataFrame()

    for name, ticker in tickers.items():
        try:
            print(f"수집 중: {name} ({ticker})...")
            # 최근 60일 데이터를 수집하여 이동평균선 계산 등 활용 가능하게 함
            raw_data = yf.download(ticker, period="60d", interval="1d", progress=False)

            if raw_data.empty:
                print(f"{name}: 데이터가 비어있습니다.")
                continue

            # 멀티인덱스 처리
            if isinstance(raw_data.columns, pd.MultiIndex):
                close_series = raw_data['Close'].iloc[:, 0].rename(name)
            else:
                close_series = raw_data['Close'].rename(name)

            if combined_df.empty:
                combined_df = close_series.to_frame()
            else:
                combined_df = combined_df.join(close_series, how='outer')
        except Exception as e:
            print(f"{name} 수집 실패: {e}")

    # 3. 데이터 전처리 (결측치 처리)
    # 주말/공휴일 등으로 인한 결측치를 직전 데이터로 채움 (Forward Fill)
    combined_df = combined_df.ffill()

    # 4. CSV 파일 저장
    file_name = f"a. macro_indicators_{datetime.datetime.now().strftime('%Y%m%d')}.csv"
    combined_df.to_csv(file_name, encoding='utf-8-sig')  # 한글 깨짐 방지 인코딩

    print(f"\n--- 저장 완료 ---")
    print(f"파일명: {os.path.abspath(file_name)}")

    # 5. 데이터프레임 확인 (최근 5일치)
    print("\n[최근 5일 수집 데이터 확인]")
    print(combined_df.tail())

    return combined_df


# 함수 실행
df = get_macro_data_and_save()

# --- 수집된 DF를 활용한 간단한 전략 점수 계산 예시 ---
if len(df) >= 2:
    latest = df.iloc[-1]
    prev = df.iloc[-2]

    score = 0
    if latest['KRW_USD'] < df['KRW_USD'].rolling(20).mean().iloc[-1]: score += 1  # 원/달러 환율 하락 (원화 강세)
    if latest['US_10Y_Yield'] < prev['US_10Y_Yield']: score += 1  # 금리 하락
    if latest['SOX_Index'] > prev['SOX_Index']: score += 1  # 반도체 지수 상승
    if latest['VIX_Index'] < 20: score += 1  # 변동성 안정
    if latest['WTI_Oil'] > prev['WTI_Oil']: score += 1  # 원유 가격 상승 (경기 회복 신호)
    if latest['KOSPI_200'] > df['KOSPI_200'].rolling(20).mean().iloc[-1]: score += 1  # 코스피200 20일 이평선 돌파

    print(f"\n금일 매크로 스코어: {score}/6")

    if score >= 4:
        print(f"결과: STRONG BUY (코스피 레버리지/대형주 매수)")
    elif score >= 3:
        print(f"결과: HOLD (보유 및 관망)")
    else:
        print(f"결과: SELL (현금 확보 및 인버스 검토)")
else:
    print(f"\n데이터가 부족합니다. (수집된 데이터: {len(df)}행)")


# ========== 추가 분석 ==========

if len(df) >= 20:
    print("\n" + "="*60)
    print("1. 상관관계 분석 (코스피200과의 상관계수)")
    print("="*60)
    correlation = df.corr()['KOSPI_200'].sort_values(ascending=False)
    print(correlation)
    print("\n해석: 1에 가까울수록 양의 상관관계, -1에 가까울수록 음의 상관관계")

    print("\n" + "="*60)
    print("2. 시각화: 환율 vs 코스피200 반비례 관계")
    print("="*60)

    # 정규화 (0~100 스케일로 변환하여 비교)
    df_normalized = df.copy()
    for col in df_normalized.columns:
        df_normalized[col] = (df_normalized[col] - df_normalized[col].min()) / (df_normalized[col].max() - df_normalized[col].min()) * 100

    plt.figure(figsize=(14, 6))
    plt.plot(df_normalized.index, df_normalized['KRW_USD'], label='원/달러 환율', linewidth=2)
    plt.plot(df_normalized.index, df_normalized['KOSPI_200'], label='코스피200', linewidth=2)
    plt.title('환율 vs 코스피200 (정규화)', fontsize=14, fontweight='bold')
    plt.xlabel('날짜')
    plt.ylabel('정규화 값 (0~100)')
    plt.legend()
    plt.grid(True, alpha=0.3)
    plt.tight_layout()
    plt.savefig('1. correlation_KRW_KOSPI.png', dpi=150)
    print("→ 그래프 저장: correlation_KRW_KOSPI.png")

    print("\n" + "="*60)
    print("3. 백테스팅: 매크로 스코어 전략 수익률")
    print("="*60)

    # 매일 스코어 계산
    df['MA20_KRW'] = df['KRW_USD'].rolling(20).mean()
    df['MA20_KOSPI'] = df['KOSPI_200'].rolling(20).mean()

    signals = []
    for i in range(1, len(df)):
        if pd.isna(df['MA20_KRW'].iloc[i]) or pd.isna(df['MA20_KOSPI'].iloc[i]):
            signals.append(0)  # 데이터 부족
            continue

        latest = df.iloc[i]
        prev = df.iloc[i-1]

        score = 0
        if latest['KRW_USD'] < latest['MA20_KRW']: score += 1
        if latest['US_10Y_Yield'] < prev['US_10Y_Yield']: score += 1
        if latest['SOX_Index'] > prev['SOX_Index']: score += 1
        if latest['VIX_Index'] < 20: score += 1
        if latest['WTI_Oil'] > prev['WTI_Oil']: score += 1
        if latest['KOSPI_200'] > latest['MA20_KOSPI']: score += 1

        # 매수/매도 신호 생성
        if score >= 4:
            signals.append(1)  # 매수
        elif score <= 2:
            signals.append(-1)  # 매도
        else:
            signals.append(0)  # 관망

    # 첫날은 신호 없음
    signals.insert(0, 0)
    df['Signal'] = signals

    # 포지션 계산 (1: 보유, 0: 미보유)
    df['Position'] = df['Signal'].replace(-1, 0).ffill().fillna(0)

    # 일일 수익률
    df['KOSPI_Return'] = df['KOSPI_200'].pct_change()

    # 전략 수익률 (포지션이 1일 때만 수익)
    df['Strategy_Return'] = df['Position'].shift(1) * df['KOSPI_Return']

    # 누적 수익률
    df['Cumulative_Market'] = (1 + df['KOSPI_Return']).cumprod()
    df['Cumulative_Strategy'] = (1 + df['Strategy_Return']).cumprod()

    total_market_return = (df['Cumulative_Market'].iloc[-1] - 1) * 100
    total_strategy_return = (df['Cumulative_Strategy'].iloc[-1] - 1) * 100

    print(f"백테스팅 기간: {df.index[0].date()} ~ {df.index[-1].date()}")
    print(f"Buy & Hold 수익률: {total_market_return:.2f}%")
    print(f"매크로 스코어 전략 수익률: {total_strategy_return:.2f}%")
    print(f"초과 수익률: {total_strategy_return - total_market_return:.2f}%")

    # 백테스팅 시각화
    plt.figure(figsize=(14, 6))
    plt.plot(df.index, df['Cumulative_Market'], label='Buy & Hold', linewidth=2)
    plt.plot(df.index, df['Cumulative_Strategy'], label='매크로 스코어 전략', linewidth=2)
    plt.title('백테스팅: 매크로 스코어 전략 vs Buy & Hold', fontsize=14, fontweight='bold')
    plt.xlabel('날짜')
    plt.ylabel('누적 수익률 (1 = 100%)')
    plt.legend()
    plt.grid(True, alpha=0.3)
    plt.tight_layout()
    plt.savefig('2. backtest_strategy.png', dpi=150)
    print("→ 그래프 저장: backtest_strategy.png")

    print("\n" + "="*60)
    print("4. 개선된 전략 제안: 가중치 기반 스코어")
    print("="*60)

    # 상관계수 기반 가중치 부여
    weights = {
        'KRW_USD': abs(correlation['KRW_USD']) * (-1),  # 음의 상관관계이므로 반전
        'US_10Y_Yield': abs(correlation['US_10Y_Yield']),
        'SOX_Index': abs(correlation['SOX_Index']),
        'VIX_Index': abs(correlation['VIX_Index']) * (-1),
        'WTI_Oil': abs(correlation['WTI_Oil']),
    }

    print("\n가중치 (상관계수 기반):")
    for key, val in weights.items():
        print(f"  {key}: {val:.3f}")

    # 가중치 기반 스코어 계산
    weighted_signals = []
    for i in range(1, len(df)):
        if pd.isna(df['MA20_KRW'].iloc[i]) or pd.isna(df['MA20_KOSPI'].iloc[i]):
            weighted_signals.append(0)
            continue

        latest = df.iloc[i]
        prev = df.iloc[i-1]

        weighted_score = 0
        if latest['KRW_USD'] < latest['MA20_KRW']: weighted_score += abs(weights['KRW_USD'])
        if latest['US_10Y_Yield'] < prev['US_10Y_Yield']: weighted_score += abs(weights['US_10Y_Yield'])
        if latest['SOX_Index'] > prev['SOX_Index']: weighted_score += abs(weights['SOX_Index'])
        if latest['VIX_Index'] < 20: weighted_score += abs(weights['VIX_Index'])
        if latest['WTI_Oil'] > prev['WTI_Oil']: weighted_score += abs(weights['WTI_Oil'])

        # 가중치 합으로 정규화
        max_weighted_score = sum(abs(v) for v in weights.values())
        normalized_score = weighted_score / max_weighted_score

        print(f"  {df.index[i].date()}: {normalized_score:.3f}")

        if normalized_score >= 0.6:
            weighted_signals.append(1)
        elif normalized_score <= 0.4:
            weighted_signals.append(-1)
        else:
            weighted_signals.append(0)

    weighted_signals.insert(0, 0)
    df['Weighted_Signal'] = weighted_signals
    df['Weighted_Position'] = df['Weighted_Signal'].replace(-1, 0).ffill().fillna(0)
    df['Weighted_Strategy_Return'] = df['Weighted_Position'].shift(1) * df['KOSPI_Return']
    df['Cumulative_Weighted'] = (1 + df['Weighted_Strategy_Return']).cumprod()

    # 4. CSV 파일 저장
    file_name = f"b. weighted_signals_{datetime.datetime.now().strftime('%Y%m%d')}.csv"
    df.to_csv(file_name, encoding='utf-8-sig')  # 한글 깨짐 방지 인코딩

    total_weighted_return = (df['Cumulative_Weighted'].iloc[-1] - 1) * 100

    print(f"\n백테스팅 기간: {df.index[0].date()} ~ {df.index[-1].date()}")
    print(f"Buy & Hold 수익률: {total_market_return:.2f}%")
    print(f"가중치 전략 수익률: {total_weighted_return:.2f}%")
    print(f"초과 수익률: {total_weighted_return - total_market_return:.2f}%")
    print(f"개선 효과: {total_weighted_return - total_strategy_return:.2f}%p")

    # 최종 비교 시각화
    plt.figure(figsize=(14, 6))
    plt.plot(df.index, df['Cumulative_Market'], label='Buy & Hold', linewidth=2, alpha=0.7)
    plt.plot(df.index, df['Cumulative_Strategy'], label='매크로 스코어 전략', linewidth=2, alpha=0.7)
    plt.plot(df.index, df['Cumulative_Weighted'], label='가중치 기반 전략 (개선)', linewidth=2.5)
    plt.title('전략 비교: Buy & Hold vs 매크로 스코어 vs 가중치 전략', fontsize=14, fontweight='bold')
    plt.xlabel('날짜')
    plt.ylabel('누적 수익률 (1 = 100%)')
    plt.legend()
    plt.grid(True, alpha=0.3)
    plt.tight_layout()
    plt.savefig('3. strategy_comparison.png', dpi=150)
    print("→ 그래프 저장: strategy_comparison.png")

    print("\n" + "="*60)
    print("분석 완료!")
    print("="*60)

 

긍정적 측면
상관계수 기반 가중치를 적용해 단순 스코어보다 1.56%p 더 나은 성과
중요한 지표(코스피와 상관관계 높은)에 더 큰 가중치를 주어 신호 품질 개선


여전히 문제
Buy & Hold 대비 -18.89%p 여전히 큰 격차
가중치를 적용해도 보수적 매매 전략의 한계는 극복 못함
상승장에서는 "시장에 계속 머물러 있는 것"이 최선


결론
가중치 적용으로 전략은 개선되었으나, 상승장에서는 여전히 Buy & Hold에 비해 크게 부족합니다.
이 전략이 진가를 발휘하려면:
📉 하락장/변동성 장세에서 테스트 필요
📅 더 긴 백테스팅 기간(1~3년)으로 다양한 시장 국면 포함
🎯 목표: 상승장 일부 포기하더라도 하락장 손실 방어로 장기 수익률 개선