* 뭔가 원하는대로 나온 것 같지는 않지만, 일단 진행하면서 글을 써서 아까워서 발행해봅니다. 마지막에 모르는 것이 있어서 발행을 머뭇거렸습니다. 다른 함수 기능에 대해 공부할 수 있어서 좋았습니다.
● 생각해보니까 그냥 백테스팅 프로그램 예시를 들어달라고 하면 됐다 !
아니 엄청 깊은 공부하기 전에 일단 그냥 백테스팅 프로그램 예시 하나 들어보고 Run한 다음에 그 코드가 어떻게 돌아갔는지 역으로 공부해보는 과정을 거치면 효과적이지 않을까? 라고 생각을해서 한 번 물어봤다. (근데, 이게 여러번 물어봤는데,물어볼 때마다 약간 조금씩 다른 방식으로 제시해주었다.)
나 : can you suggest simple back-testing program based on python code?
GPT : Sure. Here's an example of a backtesting program that buys shares of a stock when the 200-day moving average is greater than the 50-day moving average and sells shares when the 200-day moving average is less than the 50-day moving average. This version uses "Adj Close" instead of "Close" when pulling data from Quandl.
import quandl
import pandas as pd
import numpy as np
# Enter your Quandl API key here
quandl.ApiConfig.api_key = "Your API Keys"
# Define the stock ticker and the start and end dates for the backtest
ticker = "AAPL"
start_date = "2010-01-01"
end_date = "2020-01-01"
# Pull the stock data from Quandl
data = quandl.get("WIKI/" + ticker, start_date=start_date, end_date=end_date)
# Calculate the 50-day and 200-day moving averages
data["50d"] = data["Adj. Close"].rolling(window=50, min_periods=0).mean()
data["200d"] = data["Adj. Close"].rolling(window=200, min_periods=0).mean()
# Create a column to hold the trading signals
data["Signal"] = np.where(data["50d"] > data["200d"], 1, -1)
# Create a column to hold the stock's returns
data["Returns"] = data["Adj. Close"].pct_change()
# Set the initial capital
initial_capital = 10000.0
# Create a column to hold the current capital
data["Capital"] = initial_capital
# Buy or sell shares based on the trading signals
for i in range(1, len(data)):
if data["Signal"].iloc[i] == 1:
data["Capital"].iloc[i] = data["Capital"].iloc[i-1] * (1 + data["Returns"].iloc[i])
elif data["Signal"].iloc[i] == -1:
data["Capital"].iloc[i] = data["Capital"].iloc[i-1] / (1 + data["Returns"].iloc[i])
# Print the final capital
print("Final Capital: ", data["Capital"].iloc[-1])
This is a basic example of a backtesting program, it's not taking into account transaction costs, taxes and other important factors which should be considered before making real-world trades.
자 뭔가 막 써줬다. 이거를 가지고 이런저런 코드가 왜 여기에 어떻게 써졌고 어떤 방식으로 돌아가는지 탐구를 해보는 시간을 갖자. 이런식으로 공부하면 좀 빨리 배우지 않을까?
코드의 핵심은 200일 이동평균선이 50일 이동평균선보다 커지는 순간 주식을 매수하고, 200일 이동평균선이 50일 이동평균선보다 더 내려오면 매도한다의 전략이다. 예컨대
이렇게 AAPL주식이 있으면 하늘색 선이 200일 이동평균 선이고, 초록색선이 50일 이동평균선이다. 이 사진에서 찍어놓은 2022년 6월 2일의 경우 200일 이동평균선이 50일 이동평균선 위에 위치해 있으므로 주식을 매수하는 것이다.
쉽게 말해 단기적인 흐름이 아래로 꺼졌고, 장기적인 평균으로는 가격이 높으니 순간적인 저평가가 되었다고 판단하는 것. 보통 단기 흐름의 이동평균선이 장기흐름의 이동평균선을 깨고 아래로 내려가버려서 안 좋은 쪽으로 추세를 깰 때 데드크로스라고 한다.
근데, 거꾸로 이렇게 초록새 이동평균선이 50일 이동평균선이 200일 이동평균선을 깨고 올라가버리면 (200일 이평선이 50일 이평선보다 낮아지면) 골든크로스라고 하는데, 이는 하락추세를 반전하고 상승추세로 확 돌아선 것을 의미한다. 이는 단기적으로 고평가 되었을 수 있으니 주식을 매도하여 이득을 취한다. 이런 기술적분석의 간단한 백테스팅 전략이라고 할 수 있다.
다만, 실제 상황에서는 50일 이평선이 200일 이평선을 뚫고 내려갔을 때, 도저히 어디까지 내려갈지 알 수 없어서 계속적인 추가매수가 없으면 적절한 매수 가격을 유지할 수 없고 큰 손실로 이어질 수있고, 50일 이평선이 200일 이평선을 뚫고 올라갔을 때는 언제까지 올라갈지 알 수 없기 때문에 몇 백프로 먹을 거 고작 몇 퍼센트 먹고 끝날 수 있다. 그러니 실전에 활용하려면 다양한 근거와 더 많은 판단기준으로 매매를 진행해야하니 참고하길 바란다.
● 라인 바이 라인으로 뜯어보기
한줄씩 뜯어보자.
import quandl
import pandas as pd
import numpy as np
일단, quandl과 pandas와 numpy를 불러온다. pandas는 pd라고 명명하고 numpy는 np라고 명명한다.
# Enter your Quandl API key here
quandl.ApiConfig.api_key = "Your API Key"
# Define the stock ticker and the start and end dates for the backtest
ticker = "AAPL"
start_date = "2010-01-01"
end_date = "2020-01-01"
# Pull the stock data from Quandl
data = quandl.get("WIKI/" + ticker, start_date=start_date, end_date=end_date)
API key를 얻는 방법은 저번 글에서 소개 한대로 진행하면 된다.
https://jhhistory.tistory.com/18
아직까지는 쉽다. ticker라는 방에 "AAPL"을 저장하고, start_date라는 방에 2010년 1월 1일, end_date라는 방에 2020년 1월 1일 이렇게 저장한다. 그리고 나서 quandl 모듈에서 get이라는 함수를 써서 data를 가져오는 것이다.
보통 "WIKI/AAPL"이런식으로 가져와야하는데, ticker에다가 수정하면 이게 반영될 수 있도록 함수를 짰다. 그러니까, 이 코드에서는 "WIKI/특정종목" , 언제부터, 언제까지 의 data를 가져와서 data라는 방에다가 리스트 함수로 저장한 것이다.
● Rolling 함수
자 이제부터 처음 보는 것이다.
# Calculate the 50-day and 200-day moving averages
data["50d"] = data["Adj. Close"].rolling(window=50, min_periods=0).mean()
data["200d"] = data["Adj. Close"].rolling(window=200, min_periods=0).mean()
# Create a column to hold the trading signals
data["Signal"] = np.where(data["50d"] > data["200d"], 1, -1)
# Create a column to hold the stock's returns
data["Returns"] = data["Adj. Close"].pct_change()
이제 이 가져온 데이터로 50일 이동평균과 200일 이동평균을 계산해야한다. 보면, 여기 Rolling이라는 함수를 사용한 것을 볼 수 있다. Rolling은 pandas에 숨어있는 기능인데, 아래에 자세한 설명이 기술되어있다.
https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.Series.rolling.html
여기서 쪼개서 설명하면, window는 몇 개씩 연산할지 입력하는 것이고, mean은 평균을 내는 것이다. 이게 근데 50일 이동평균선과 200일 이동평균선에 대해 계산하고 있는데, 만약 data가 1개라도 존재하면 평균을 내고 싶을 땐, min_period기능을 이용하면 된다. 기본적으로 min_period를 사용하지 않으면 window의 값과 같은 것으로 사용이 된다고 한다.
즉, 아래의 코드는 data에 저장되어있었던 Adj. Close라는 열에서 값들을 50개씩 평균내는데, 일단 data있으면 50개가 미달이되어도 무조건 평균을 내는 것이고, 밑에는 값들을 200개씩 평균내는데, 일단 data가 있으면 200개가 미달이 되어도 무조건 평균을 내는 기능이라고 보면되겠다.
data["50d"] = data["Adj. Close"].rolling(window=50, min_periods=0).mean()
data["200d"] = data["Adj. Close"].rolling(window=200, min_periods=0).mean()
그리고 나서 data에서 새로운 리스트를 추가할 건데, 그걸 "50d"라고 추가할 것이고, 그것을 "200d"라고 추가하는 것이다. 오케!1 그럼 window의 값을 60 120 이런식으로 바꾸면 60일 이동평균선, 120일 이동평균선으로 바꿀 수 있는 것이다. 다음 코드를 살펴보자.
# Create a column to hold the trading signals
data["Signal"] = np.where(data["50d"] > data["200d"], 1, -1)
# Create a column to hold the stock's returns
data["Returns"] = data["Adj. Close"].pct_change()
자 또 새로운게 나왔다.
● np.where 함수
np.where함수는 넘파이를 사용하면 많이 접하게될 수 있는 함수라고 한다. 개념은 where내의 조건이 맞으면 맞는 조건의 리스트자료에서의 인덱스를 반환한다고 한다. 예를들어
import numpy as np
a = np.arange(10, 20)
b= np.where(a>15)
print(b)
결과는 이렇게 나온다.
(array([6, 7, 8, 9], dtype=int64),)
근데, 만약에 np.where (조건 , ? , ! )으로 ? ! 가 추가 콤마로 들어가면, 조건에 만족하는 값은 ?로 조건에 만족하지 않는 값은 !로 출력한다.
import numpy as np
a = np.arange(10, 20)
b= np.where(a>15,'happy','sad')
print(b)
이러면 결과가
['sad' 'sad' 'sad' 'sad' 'sad' 'sad' 'happy' 'happy' 'happy' 'happy']
이런식으로 출력된다.
즉 저 위에서 코드를 다시 뜯어보면
data["Signal"] = np.where(data["50d"] > data["200d"], 1, -1)
아까 data["50d"]로 저장한 리스트의 값이 data["200d"]에 저장한 리스트의 값보다 크면 1, 작으면 -1로 표기하자는 것이고 그 +1과 -1로 모아진 리스트를 다시 data라는 방에 ["signal"]이라는 열을 추가하여 저장하라는 뜻이다.
● pct_change 함수
여기 자세히 설명되어 있다. 개념은한 객체 내에서 행과 행의 차이를 현재값과의 백분율로 출력하는 함수이다. 즉, (다음행 - 현재행)÷현재행 을 의미한다.
data["Returns"] = data["Adj. Close"].pct_change()
일단, 이 포스팅을 적는 순간 이 함수를 왜 사용했는지는 모르겠으나, Adj. close의 행간 백분율 값을 Returns 열로 추가하는 작업을 진행했다. 밑에꺼 공부해보면 왜 썼는지 알게 되겠지.. (알아야한다.)
그 다음은 일단 한 시름 놓았다. initial_captial에다가 10000.0을 저장하고 (아마 달러겠지) 그리고 그 값을 전부 data의 Captial이라는 열에 추가한다.
# Set the initial capital
initial_capital = 10000.0
# Create a column to hold the current capital
data["Capital"] = initial_capital
● iloc 함수
드디어 아마 핵심인 것 같다. for와 if가 혼재되어 있다. 하나하나 씩 천천히 뜯어보자.
# Buy or sell shares based on the trading signals
for i in range(1, len(data)):
if data["Signal"].iloc[i] == 1:
data["Capital"].iloc[i] = data["Capital"].iloc[i-1] * (1 + data["Returns"].iloc[i])
elif data["Signal"].iloc[i] == -1:
data["Capital"].iloc[i] = data["Capital"].iloc[i-1] / (1 + data["Returns"].iloc[i])
일단, for 반복문을 돌릴건데, 몇개나 돌릴거냐면 range (1, len(data))만큼 돌릴 것이다. len은 매트랩의 length()와 같아 보이는데, 그 리스트의 크기,개수를 반환하는 함수다. 그래서 len(data)하면 data가 가지고 있는 크기만큼 (data 행 끝까지) for반복문을 돌리겠다는 얘기이다.
자 이제 if문을 보자. 새로운게 나왔다. iloc은 '몇 번 째 너 나와 !' 이런 함수이다. 보면, i번째 signal data가 만약 1과 같으면, if문을 수행한다. signal data가 1과 같은 것은 무엇이었냐 ?? 바로 '50d'가 '200d'보다 높은 상황이었다. -1은 반대상황이겠지?
자 이제, 내가 이해 못하는 영역이 나왔다. 이 로직이 이해가 잘 안되서 발행을 멈췄다. 누가 좀 알려줬으면 좋겠다.
if문 속은 이렇다. i번째 captial 열들에다가 저장을해라. 뭘 저장을 하냐면, i-1번째 captial행 * (1 + i번째 Return행)을 곱한 것을 저장해라! 다. 아 일단 이래서 Return열을 따로 저장했구나라는 것을 알 수 있었다. 수익률 계산 시 1+n%로 계산하려고 따로 빼놓은 것 같다. elif속에서는 signal 이 -1과 같으면, 즉 50일 이동평균선이 200일 이동평균선보다 낮으면 i-1번째 capital행의 값을 / (1+ i번째 리턴행)으로 나눠라! 라는 것이다.
내 생각엔 Capital이 자본금일텐데, (1+r%)하고 /(1+r%)하는 행위가 어떤 과정을 나타내는지 잘 모르겠다. 내 생각엔 cash와 보유 stock수로 바꿔서 stock의 숫자가 조정되고 cash가 계속 바뀌면서 결과적으로 내 asset = cash + stock수*평가액 이런식으로 나타나야 될 것 같은데, 백테스팅 프로그램이라 모든 자산을 전부 매수했다고 가정하고 돌리는 것 같다.
gpt는 이 코드가 buy or sell이라고 했는데, 이게 왜 buy고 저게 왜 sell인지 잘 이해가 안된다. 몇 번 더 물어봤는데, 그냥 그렇다고 한다. 하여간 그렇다. 설명으로는 Adjusting Close를 사용했기 떄문에 이렇게 계산한다는데, 왜 이렇게 돌아가는지는 정확하게는 모르겠다. 아시는 분 있으시면 답변을 부탁드립니다 ㅠㅠ
● Print및 csv로 저장하기 !
그래서 이걸 print하고 그 마지막 capital값을 읽어오는 행위이다. iloc[-1] 값을 콜하면 마지막 행만 불러오는 값이라고 한다.
# Print the final capital
print("Final Capital: ", data["Capital"].iloc[-1])
이 때, 나는 이 자료의 구성을 모두 보고 싶어서 csv로 저장했다. 저장하고 나서 위의 if문을 그대로 뭔가 검증해봤는데, 연산은 제대로 들어간 것 같은데 왜 저렇게 수행했는지는 결과를 잘 봐도 모르겠다. csv 값을 이렇게 저렇게 만진 결과는 아래 유튜브 동영상 마지막에 참고로 달아놓았다.
https://youtu.be/IKthLdXf99Y
'미래걱정 > 주식' 카테고리의 다른 글
슬기로운 퀀트투자 - 2장 일단 해보자 편 (1) | 2023.02.08 |
---|---|
파이썬으로 배우는 포트폴리오 2장 (투자와 자산배분) (2) | 2023.02.06 |
파이썬으로 배우는 포트폴리오 (책 리뷰 및 실습 기록) (2) | 2023.01.29 |
Python의 Quandl을 통해 주가 그래프 그려보기 (with Chat GPT) (0) | 2023.01.28 |
S&P500 그래프와 장단기금리차의 그래프를 같이 그려보자 (with Chat gpt, ax1.twinx) - 2 (0) | 2023.01.27 |
댓글