코딩 개발일지/파이썬 데이터분석(스파르타 코딩클럽)

<스파르타 코딩클럽 파이썬 데이터 분석 4주차>

찐쥬 2023. 3. 16. 11:13

<파이썬 데이터 분석 4주차 개발일지>

 

 

4주차에선 백테스팅을 직접 구현해보고 그래프를 그려 본다고 한다.

 

 

 

 

백테스팅이란 ?

이전 주가의 추이에 전략을 대입해보는 것

 

날씨가 흐리면 사고, 좋으면 팔아볼까? 하는 모든 전략이 벡테스팅 대상이라고함

이런 전략을 몇 년치 과거 주가에 대입해보고 수익률이 좋았으면, 앞으로도 좋을 확률이 높다 라는!

 

 

 

전략 1. 이동평균선

* 이동평균선 : 5일 20일 50일 등 며칠 간의 가격을 평균하여 움직이는 선

 

이동평균선을 이용한 골든크로스, 데드크로스 전략!

골든크로스 : 주가가 높아질 가능성이 있다는 신호

데드크로스 : 주가가 낮아질 가능성이 있다는 신호

 

두 가지 단기 이평선(20일)이 장기 이평선(60일)을 돌파하는 경우인데,

그 방향이 다르다고 보면 됨

대게 골든크로스땐 사야한다! 데드크로스땐 팔아야한다!(정답은 없다)

 

 

 

Colab 새노트를 열어서 라이브러리 설치부터 먼저

# 라이브러리 3개 설치
!pip install yfinance pandas-datareader finance-datareader

 

라이브러리 가져오기

# 라이브러리 가져오기
from pandas_datareader import data as pdr

import yfinance as yf
yf.pdr_override()

import numpy as np
import pandas as pd

import FinanceDataReader as fdr

df = fdr.DataReader('005930','2018') # 삼성전자의 종목코드
                             # 2018년부터 현재까지 보고싶다
df.head() # 상위 5개만

Open : 시초가 / High : 최고가 / Low : 최저가 / Close : 종가 / Volume : 거래량 / Change : 전일대비등락

 

 

아래와 같이 특정 컬럼, 조건 값만 추출할 수 있음

df = fdr.DataReader('005930','2018')

df[['Close']] # 삼성전자의 종가만

df = fdr.DataReader('005930','2018')

df[df['Change'] > 0.05] # 삼성전자 주가가 전일대비 5% 이상 오른 것

 

 

 

그래프 그려보기

.plot 만 붙여주면 끝!

 

df = fdr.DataReader('066570','2018') # LG전자의 2018년~현재까지

df.plot(y=['Close']) # lg전자의 종가를 그래프로 그림

 

df = fdr.DataReader('066570','2018')

df.plot(y=['Close'],figsize=[15,8]) # 조금 더 큰 사이즈 그래프 그리기

 

df = fdr.DataReader('066570','2018')

df.plot(y=['Close'],figsize=[15,8],grid=True) # 격자 선 추가

 

df = fdr.DataReader('066570','2018')

df.plot(y=['Open','Close'],figsize=[15,8],grid=True) # 시가, 종가 같이 그리기

 

df = fdr.DataReader('066570','2018')

df.plot() # 아무것도 안넣으면, 전체 데이터를 그래프화

 

df_1 = fdr.DataReader('005930','2018') # 삼성전자
df_2 = fdr.DataReader('066570','2018') # lg전가

df = pd.DataFrame() # 새로운 데이터프레임 만들기
df['Samsung'] = df_1['Close'] # Samsung 컬럼을 만들어서 df_1 의 종가데이터를 넣어주고
df['LG'] = df_2['Close'] #LG 컬럼을 만들어서 df_2 의 종가데이터를 넣어주고
df.plot() #그래프 그리기

하지만 주식수도 금액도 다른 종목을 종가로 확인하면 정확하지 않기 때문에, 변동률을 보는게 정확함

 

df_1 = fdr.DataReader('005930','2018')
df_2 = fdr.DataReader('066570','2018')

df = pd.DataFrame()
df['Samsung'] = df_1['Change'] # 삼성의 전일대비 등락률
df['LG'] = df_2['Change'] # 엘전의 전일대비 등락률

df.tail(100).plot(figsize=[15,8]) # 최근 100일(tail(1oo))로 그래프 그리기, 사이즈크게

 

 

 

백테스팅 해보기

 

이동평균선을 가지고 백테스팅을 해봐야 하기 때문에,

이동평균값을 만들어야함

종가가 필요!

df = fdr.DataReader('005930','2018') # 삼성전자

df = df[['Close']] # df의 종가를 df 자체에 넣어주기

df

 

df = fdr.DataReader('005930','2018')

df = df[['Close']]

df.rolling(3).mean() # 3개씩 묶어서 평균을 보겠다

여기서 잠깐!

3개씩 본다고해서 2일 3일 4일 / 5일 6일 7일 이 아니라!

2일 3일 4일 / 3일 4일 5일 / 4일 5일 6일 이 되는것임

 

df = fdr.DataReader('005930','2018')

df = df[['Close']]

df['ma'] = df.rolling(3).mean() # ma라는 컬럼을 새로 만들어 3일 평균값을 넣어줌

df

2018-01-04  ma 에 있는 값은 2일 3일 4일의 평균값임

 

2 3 4일의 평균값과 다음날 종가를 비교해야 하는 것

(다음날 종가가, 전일3일의 평균보다 높으면 상승추세)

 

두 값을 비교해야하니, ma컬럼의 값을 전체적으로 한 칸 아래로 내려줘야함!

df = fdr.DataReader('005930','2018')

df = df[['Close']]

df['ma'] = df.rolling(3).mean().shift(1) # shift(1)을 쓰면 한 칸 아래로 내려감

df

한 칸 아래로 내려간 것 확인!

이제 두 값을 비교해서 Close - ma플러스면 사는 것, 마이너스면 파는 것!

 

조건넣기? np.where

df = fdr.DataReader('005930','2018')

df = df[['Close']]

df['ma'] = df.rolling(3).mean().shift(1)

df['action'] = np.where(df['Close'] > df['ma'], 'buy', 'sell')
 # action컬럼 생성      # Close가 ma보다 크면 '산다', 아니면 '판다' 
df

 

이제 action 값을 기반으로 사고 팔 때는 알 수 있음

 

buy buy buy 일 때 계속 사는게 아니라 갖고 있으면 되고

sell sell sell 일 때 계속 파는게 아니라 안사면 됨

 

그럼 사고 팔 때는?

buy에서 sell로 바뀔 때 팔면되고

sell에서 buy로 바뀔 때 사면됨!

df = fdr.DataReader('005930','2018')

df = df[['Close']]

df['ma'] = df.rolling(3).mean().shift(1)

df['action'] = np.where(df['Close'] > df['ma'], 'buy', 'sell')
df['action_temp'] = df['action'].shift(1) # action_temp라는 컬럼을 생성하고, action을 한칸씩 내림

df

한 칸씩 아래로 내려감

action과 action_temp값을 비교하면됨

 

 

조건을 달아서 쓰자면

df = fdr.DataReader('005930','2018')

df = df[['Close']]

df['ma'] = df.rolling(3).mean().shift(1)

df['action'] = np.where(df['Close'] > df['ma'], 'buy', 'sell')


cond = (df['action'] =='buy') & (df['action'].shift(1) == 'sell') 
        # aciton이 buy이고,       action한 칸 내린 값(action_temp)이 sell인 조건
df['real_buy'] = np.where(cond,'buy','')
        # real_buy라는 컬럼을 생성해서, 위의 조건이 맞으면 buy, 아니면 공백으로 두는 값을 넣어줌 
df

 real_buy가 buy 인게 사는 시점

 

 

df = fdr.DataReader('005930','2018')

df = df[['Close']]

df['ma'] = df.rolling(3).mean().shift(1)

df['action'] = np.where(df['Close'] > df['ma'], 'buy', 'sell')


cond1 = (df['action'] =='buy') & (df['action'].shift(1) == 'sell') # 사는 시점
cond2 = (df['action'] =='sell') & (df['action'].shift(1) == 'buy') # 파는 시점
         
df[cond1]

이렇게 컨디션만 지정해서 넣으면, 이 시기가 사는 시점!

1/5 이전에 1/4은 sell 이었을 거고,

1/15 이전에 1/14도 sell 이었을 것,

sell -> buy로 바뀐 시점

 

마찬가지로 con2를 넣으면 파는 시점

1/9 이전 1/8은 buy 였을것이고,

1/19 이전 1/18은 buy 였을 것이니

buy -> sell로 바뀐 시점

 

 

사는 시점, 파는 시점을 구해두고

마지막 날은? 무조건 팔아야 내가 수익률 책정을 하니

마지막날 셀은 무조건 파는 것으로 값을 넣어둔다(iloc 사용)

df = fdr.DataReader('005930','2018')

df = df[['Close']]

df['ma'] = df.rolling(3).mean().shift(1)

df['action'] = np.where(df['Close'] > df['ma'], 'buy', 'sell')

df.iloc[-1, -1] = 'sell' # 마지막날은 무조건 팔아야 수익률 책정을하니 마지막셀은 sell로

cond1 = (df['action'] =='buy') & (df['action'].shift(1) == 'sell') 
cond2 = (df['action'] =='sell') & (df['action'].shift(1) == 'buy')

df_buy = df[cond1] # 사는 타이밍
df_sell = df[cond2] # 파는 타이밍

 

 

사는 타이밍과, 파는 타이밍을 같이 붙여서 언제 사고 팔아야하는지 알아보기!

df = fdr.DataReader('005930','2018')

df = df[['Close']]

df['ma'] = df.rolling(3).mean().shift(1)

df['action'] = np.where(df['Close'] > df['ma'], 'buy', 'sell')

df.iloc[-1, -1] = 'sell'

cond1 = (df['action'] =='buy') & (df['action'].shift(1) == 'sell')
cond2 = (df['action'] =='sell') & (df['action'].shift(1) == 'buy')

df_buy = df[cond1]
df_sell = df[cond2]

df_result = pd.concat([df_buy,df_sell],axis=1) # buy와 sell을 같이 합치는데,
                                               # axis=1 을 쓰면 옆에(가로)붙음
df_result.head(30)

axis = 1을 해서 buy와 sell을 나란히 붙여놓고

1/5 52120원에 사서, 1/9 50400원에 팔고

1/15 48540원에 사서, 1/19 49320원에 팔고

이런식으로 데이터를 분석하면 되는 것!

 

 

근데 또 한칸씩 아래 있고, NaN이 많아서 보기 힘드니 한 줄로 합쳐주는 작업을 해줘야함

.reset_index() 

df = fdr.DataReader('005930','2018')

df = df[['Close']]

df['ma'] = df.rolling(3).mean().shift(1)

df['action'] = np.where(df['Close'] > df['ma'], 'buy', 'sell')

df.iloc[-1, -1] = 'sell'

cond1 = (df['action'] =='buy') & (df['action'].shift(1) == 'sell')
cond2 = (df['action'] =='sell') & (df['action'].shift(1) == 'buy')

df_buy = df[cond1].reset_index()  #reset_index()만 추가로 붙여주기
df_sell = df[cond2].reset_index()  #reset_index()만 추가로 붙여주기

df_result = pd.concat([df_buy,df_sell],axis=1)

df_result.head(30)

위에 데이터프레임을 보면, 날짜가 인덱스로 되어있으니

인덱스를 reset해서 일반 숫자 인덱스 0 1 2 3~ 으로 만들어주면

날짜는 컬럼으로 빠져서 날짜까지 옆으로 붙여주게 되는 것!

 

 

 

위의 표만 보면 어디가 buy이고, 어디가 sell인지 구분하기 어려우니 컬럼명 변경

df_buy = df[cond1].reset_index()
df_buy.columns = ['날짜', '종가(buy)','이평값', '액션'] # buy 컬럼명 순서대로

df_sell = df[cond2].reset_index()
df_sell.columns = ['날짜', '종가(sell)','이평값', '액션'] # sell 컬럼명 순서대로

 

 

수익률을 구하기 위해선, 종가가 필요함!

매도가 / 매수가 = 를 해주면 수익률이 구해지니 수익률 컬럼 추가하기

df_result['수익률'] = df_result['종가(sell)'] / df_result['종가(buy)']
  #컬럼 추가해서  =  값넣기(수익률구하기)
df_result

 

 

저렇게 사고 팔았을 때의 전체 총 수익률이 어떤지 알아보기 위해선,

수익률 전체를 곱해주면됨!

df_result[['수익률']].cumprod() # 수익률만 따로 꺼낸다음
                                # .cumprod() 를 써주면 누적곱을 나타내줌

누적 곱!

위의 표와 비교해보면 되는데, 1번 인덱스는 0번 1번의 수익률을 곱한값,

2번 인덱스는 0번 1번 2번의 수익률을 곱한값으로 이해하면 됨

 

 

그렇다면 제일 끝의 값을 위의 수익률을 전부 다 곱한 값이 되는 것

df_result[['수익률']].cumprod().iloc[-1,-1] -1 # 제일 마지막 누적 수익률 구하는 것
                                               # 1을 빼줘야 수익률

0.3024082832109143

삼성전자를 2018년부터 현재까지 3일 이평선을 가지고 했을 때의 수익률이 되는 것

 

 

위의 코드에 종목과 이동평균선 기준일자를 넣으면 수익률을 나오도록

함수로 만들어 주면 됨

def get_return(code, n): # code : 종목 n : 이동평균선 기준일
  df = fdr.DataReader(code ,'2018') # code 들어감

  df = df[['Close']].copy()

  df['ma'] = df.rolling(n).mean().shift(1) # 이동평균선 기준일 들어감

  df['action'] = np.where(df['Close'] > df['ma'], 'buy', 'sell')

  df.iloc[-1, -1] = 'sell'

  cond1 = (df['action'] =='buy') & (df['action'].shift(1) == 'sell')
  cond2 = (df['action'] =='sell') & (df['action'].shift(1) == 'buy')

  df_buy = df[cond1].reset_index()
  df_buy.columns = ['날짜', '종가(buy)','이평값', '액션']

  df_sell = df[cond2].reset_index()
  df_sell.columns = ['날짜', '종가(sell)','이평값', '액션']

  df_result = pd.concat([df_buy,df_sell],axis=1)

  df_result['수익률'] = df_result['종가(sell)'] / df_result['종가(buy)']

  return df_result[['수익률']].cumprod().iloc[-1,-1] -1 # 마지막은 return

이렇게 한번 짜놓은 코드로 이평선을 이용한 수익률을 예상해볼 수 있다!

 

 

 

 

이제 주목적이었던 단기 이평선과 장기 이평선의 골든크로스, 데드크로스 때를 확인해야함! 

 

위의 코드를 끌고와서 단기(3일) 장기(30일) 두 가지 이평선을 만들고

단기 종가가 장기 종가보다 클 때(골든크로스) 매수하는 코드를 만들면됨

df = fdr.DataReader('005930' ,'2018') # 2018년부터 현재까지, 삼성전지

df = df[['Close']].copy() # 종가

df['ma1'] = df.rolling(3).mean().shift(1) # 3일 단기 이평선
df['ma2'] = df.rolling(30).mean().shift(1) # 30일 장기 이평선

df['action'] = np.where(df['ma1'] > df['ma2'], 'buy', 'sell') 
                        # 단기 이평선이 장기 이평선보다 크면, 사고, 아니면 팔기
df

결과는 에러!

 

 

 

이유는 ma1의 값만 우선 확인해보면

df 자체의 3일 평균을 내기 때문에,

처음 시작의 2 3 4일은 평균을 낼 수 없어서 NaN이 뜨고 그로인해 발생한 에러!

 

df['Close'] 의 값으로 rolling 해주면 해결

df = fdr.DataReader('005930' ,'2018')

df = df[['Close']].copy()

df['ma1'] = df['Close'].rolling(3).mean().shift(1) # 3일 단기 이평선
df['ma2'] = df['Close'].rolling(30).mean().shift(1) # 30일 장기 이평선

df['action'] = np.where(df['ma1'] > df['ma2'], 'buy', 'sell')

df

 

이어서 위의 코드 계속 복붙

df = fdr.DataReader('005930' ,'2018')

df = df[['Close']].copy()

df['ma1'] = df['Close'].rolling(3).mean().shift(1) # 3일 단기 이평선
df['ma2'] = df['Close'].rolling(30).mean().shift(1) # 30일 장기 이평선

df['action'] = np.where(df['ma1'] > df['ma2'], 'buy', 'sell')

cond1 = (df['action'] =='buy') & (df['action'].shift(1) == 'sell') # 사는 조건
cond2 = (df['action'] =='sell') & (df['action'].shift(1) == 'buy') # 파는 조건

df_buy = df[cond1].reset_index()
df_buy.columns = ['날짜', '종가(buy)','이평값1','이평값2', '액션'] # 이평선1, 이평선2로 수정

df_sell = df[cond2].reset_index()
df_sell.columns = ['날짜', '종가(sell)','이평값1','이평값2','액션'] # 이평선1, 이평선2로 수정

df_result = pd.concat([df_buy,df_sell],axis=1) # 두 컬럼 가로로 붙여 합치기

df_result['수익률'] = df_result['종가(sell)'] / df_result['종가(buy)']

df_result[['수익률']].cumprod().iloc[-1,-1] -1

0.03710270766835899

 

 

 

위의 코드를 로 만들어보기!

df = fdr.DataReader('005930' ,'2018')

df = df[['Close']].copy()

df['ma1'] = df['Close'].rolling(3).mean().shift(1) 
df['ma2'] = df['Close'].rolling(30).mean().shift(1) 

df['action'] = np.where(df['ma1'] > df['ma2'], 'buy', 'sell')

cond1 = (df['action'] =='buy') & (df['action'].shift(1) == 'sell')
cond2 = (df['action'] =='sell') & (df['action'].shift(1) == 'buy')

df_buy = df[cond1].reset_index()
df_buy.columns = ['날짜', '종가(buy)','이평값1','이평값2', '액션']

df_sell = df[cond2].reset_index()
df_sell.columns = ['날짜', '종가(sell)','이평값1','이평값2','액션']

df_result = pd.concat([df_buy,df_sell],axis=1)

df_result['수익률'] = df_result['종가(sell)'] / df_result['종가(buy)']

df_final = (df_result[['수익률']].cumprod().tail(1) -1)*100 # 최종 수익률 마지막 끝값만 불러옴
df_final['단기'] = 3 # 3일
df_final['장기'] = 30 # 30일

df_final

표로 깔끔하게 내가 필요한 값만 추출!

 

 

 

이제 위의 코드를 함수로 만들어버리기!

def get_return_sl(code, short, long): # code, 단기, 장기
  df = fdr.DataReader(code ,'2018') # code 추가

  df = df[['Close']].copy()

  df['ma1'] = df['Close'].rolling(short).mean().shift(1) # short 추가
  df['ma2'] = df['Close'].rolling(long).mean().shift(1) # long 추가

  df['action'] = np.where(df['ma1'] > df['ma2'], 'buy', 'sell')

  cond1 = (df['action'] =='buy') & (df['action'].shift(1) == 'sell')
  cond2 = (df['action'] =='sell') & (df['action'].shift(1) == 'buy')

  df_buy = df[cond1].reset_index()
  df_buy.columns = ['날짜', '종가(buy)','이평값1','이평값2', '액션']

  df_sell = df[cond2].reset_index()
  df_sell.columns = ['날짜', '종가(sell)','이평값1','이평값2','액션']

  df_result = pd.concat([df_buy,df_sell],axis=1)

  df_result['수익률'] = df_result['종가(sell)'] / df_result['종가(buy)']

  df_final = (df_result[['수익률']].cumprod().tail(1) -1) *100
  df_final['단기'] = short # short 추가
  df_final['장기'] = long # long 추가

  return df_final # return값 추가

 

최적의 단기 이평선과 최적의 장기 이평선을 알아보기

이중 반복문 사용!

for short in range(3,11): # 3~10일
  for long in range(30,61): # 30~60일
    print(short, long)

이렇게, 단기 안에 장기를 돌려 이중 반복문이 되도록!

dfs = [] # df를 담을 빈리스트
for short in range(3,11): # 단기
  for long in range(30,61): # 장기
    df = get_return_sl('005930',short,long) df라는 변수에 함수 값을 넣어주고
    dfs.append(df) # 나온 df를 dfs에 추가해주기

df_result = pd.concat(dfs) # dfs 합치기
df_result.sort_values(by='수익률', ascending=False) # 수익률을 내림차순해서 수익률이 제일 높은순으로

단기 10일, 장기 59일일 때 약 47%로 수익률이 제일 높다!

 

 

 

 

이렇게 4주차는 마무리 되었고,

마지막에 배운 단기 이평선, 장기 이평선을 사용해서 수익률을 확인해보니

실제로 내 주식 매수 매도 할 때 이용해볼 수 있을 것 같다!

(전부 물려서 암것도 못하지만,,,,,,)

 

실제로 내가 활용할 수 있는 코드라 넘나 유용한 주차였지만,

 

함수를 만들고, 반복문을 사용하는게 아직 혼자서는 버벅거려 어렵다고 느낀다ㅠ_ㅠ

오랜만에 들으니 금새 또 까묵,.....

 

남은 마지막 주차도 화이탱