매수 타이밍 프로그램!

CHOI·2021년 12월 20일
9

미국 주식을 자주 하는 개발 공부를 하고 있는 사람입니다. 매우 부족한 코드이지만 잘 봐주시면 감사하겠습니다ㅠ 앞으로의 글은 편의를 위해 반말을 사용하고 있습니다 양해 부탁드립니다ㅠㅠ

왜 이 프로젝트를 시작하게 되었지??

스윙(매수와 매도를 반복하는 방식)으로 주식을 많이 하는 나는 내가 정한 조건에 충족이 되면 매수 타이밍을 알려주는 프로그램이 있으면 좋겠다는 생각에 이 프로젝트를 시작하게 되었다.

물론 기존에 주가 알림 기능은 HTPS나 어플에서 제공하고 있다. 하지만 이러한 알림들은 단순히 주가가 5% 상승이나 하락하거나 일정 가격에 도달했을 때 알림을 주는 간단한 설정만 가능하다.

그래서 나는 내가 주식을 매수할 때 보는 여러가지 보조 지표가 모두 조건에 충족이 될 때 알림을 주는 프로그램을 만들고 싶었다.

만들 가치가 있을까??

일단 이 프로젝트를 만들 가치가 있는지 확인하고 싶었다. 내가 생각하는 조건대로 했을 때 원하는 만큼의 수익률이 나와야 만들 가치가 있다고 생각이 들었다. 그래서 일단은 수익률을 확인하는 소스부터 만들었다.

조건 정하기

일단 어떤 종목을 사용할지 정해야 했다. 선택한 종목은 내가 제일 스윙을 많이 하는 주식인 "FNGU"이다.

이 종목을 고른 이유는 다음과 같다.

  1. 인증된 종목(애플, 마이크로소프트, 페이스북, 구글 등)으로만 구성된 ETF
  2. 스윙하기에 적합한 변동성 (3배곱 변동성)
  3. 전체적으로 우상향하는 그래프
  4. 많이 축적된 주가 데이터 (최근에 만들어진 ETF가 아니여서 보조 지표를 활용하기에 적합했다)

그리고 무엇보다도 내가 제일 많이 거래하는 종목이여서 익숙하기 때문이다.

그리고 이제 매수와 매도 조건을 정해야 하는데. 그러기 위해선 1년간의 주가를 보며 어느 시점에 주식을 사고 파는 것이 좋았는지 확인했다.


동그라미를 친 부분이 매수 또는 매도를 하기 좋은 지점이다. 이러한 지점들에서 보조 지표를 분석하여 매수와 매도의 조건을 다음과 같이 정했다(보조 지표에 대한 설명은 생략하겠다).

매수 조건

  • RSI ≤ 35
  • MFI ≤ 35
  • MACD-MACD_SIGNAL ≤ -0.5
  • CCIV ≤ - 150
  • 이평선 그래프를 넘을때

매도 조건

  • 주요 종목들의 실적발표일
  • RSI ≥ 65
  • MFI ≥ 65
  • MACD-MACD_SIGNAL ≥ 0.6
  • 이평선 그래프를 넘을때

수익률 확인 소스코드 작성하기

그러면 이제 실제로 위에서 정한 조건대로 매매를 했을 때 수익률이 얼마나 나는지 확인해보는 소스 코드를 작성해보자.

데이터 프레임 가져오기

import yfinance as yf

ticker = 'FNGU'
data = yf.download(ticker,start = '2021-01-01') 	# 시작날을 입력 받으면 그 이후 33일 정도 기록누락 발생
if start_day not in data.index:
		print('해당 날은 주가 기록이 없습니다.')
data = data.reset_index()						# 날짜 인덱스를 열로 바꾸고 숫자 인덱스 사용

일단 yfinacen 모듈을 활용하여 주가를 가져오는 소스 코드를 만들었다.

그리고 주가 시작일을 '2021-01-01'로 하였다. 가져올 주가 데이터는 실제로 매매를 시작하기 3개월 정도 앞으로 하는것이 좋다. 왜냐하면 우리는 주가의 가격 데이터만 가져오고 보조 지표는 가져오지 않기 때문이다.

이말은 즉, 우리가 직접 주가 데이터를 활용하여 보조 지표를 계산해야 하는데 그렇게 되면 가져온 시작일로부터 어느정도의 주가 정보 데이터가 쌓여야 'RIS, MFI, MACD'등등의 보조 지표를 계산할 수 있기 때문이다.

그리고 yfinance 로 가져오는 데이터 프레임을 확인해보면 다음과 같은데

보다시피 인덱스가 날짜로 되어있다. 나는 앞으로 매매를 할 때 어느 날이 더 앞에 있고 더 뒤에 있는지확인하기 위해서 날짜를 열로 바꾸고 새롭게 인덱스를 만들기 위해 data = data.reset_index() 와 같이 하였다.

마지막으로 나중에도 데이터 프레임을 가져오는 기능을 여러번 사용할 가능성이 있어서 위 기능을 다음과 같이 함수로 정의하고 함수를 호출하는 형식으로 하였다.

def return_dataframe(ticker, start_day):
	""" 사용할 데이터 프레임을 리턴하는 함수"""
		data = yf.download(ticker,start = '2021-01-01') 	# 시작날을 입력 받으면 그 이후 33일 정도 기록누락 발생
		if start_day not in data.index:
				print('해당 날은 주가 기록이 없습니다.')
				return
		data = data.reset_index()						# 날짜 인덱스를 열로 바꾸고 숫자 인덱스 사용

fngu_data = return_dataframe('FNGU', '2021-04-01')

보조 지표 계산하기

그러면 이제 가져온 데이터 프레임을 활용하여 주가 정보를 확인해보자

보조 지표는 ta 라이브러리를 가져오면 쉽게 계산할 수 있다. 내가 사용할 보조 지표는 여러개가 있었는데 일단 간단하게 RSI, MFI, MACD 만 사용하였다. 아무튼 다음과 같이 하면 이러한 보조 지표를 계산할 수 있다.

import ta

rsi = ta.momentum.rsi(data['Close'])
mfi = ta.volume.money_flow_index(data['High'],data['Low'],data['Close'],data['Volume'])
macd = ta.trend.macd(data['Close'])
macd_sig = ta.trend.macd_signal(data['Close'])

주가의 기준은 종가(Close price)로 하였다.

데이터 프레임 만들기

그러면 이제 매수 조건과 매도 조건에 충족되는 데이터 프레임만 추출하여 변수에 저장해보자

buy_condition = (rsi <= 35) & (mfi <= 35) & (macd - macd_sig < -0.5)		# 매수하는 조건
sell_condition = (rsi >= 65) & (mfi >= 65) & (macd - macd_sig >= 0.6)		# 매도하는 조건
buy_data = data[buy_condition]			# 매수할 날에 대한 데이터 프레임
sell_data = data[sell_condition]		# 매도할 날에 대한 데이터 프레임

이게 시작일부터 매매를 시작하면 되는데 여러번 반복적으로 매매를 하는 것이기 때문에 반복문을 돌아야한다고 생각하여 반복문을 활용할 생각을 했다. 일단 두 데이터프레임을 확인해 봤다.
아래 사진중 첫번째는 매수를 해야하는 날이고 두번째는 매도를 해야하는 날이다.


여기서 선택을 해야했다. 반복문을 두개 사용해서 각자의 데이터 프레임에서 돌아야할지 아니면 두 데이터프레임을 합쳐서 하나의 반복문으로 돌아야할지.

나는 후자를 선택했다. 그 이유는 다음과 같다.

  1. 전자 선택시 반복문 안에 반복문을 만들어야 해서 너무 많은 메모리를 사용할 것 같았다.
  2. 인덱스로 어느 날이 더 앞에 있는지 확인이 가능하기 때문에 하나의 데이터프레임으로 합쳐도 문제가 없을 것 같았다.

따라서 일단 데이터 프레임을 합쳤다. 그리고 컴퓨터가 반복문을 돌면서 매수인지 매도인지를 확인할 수 있게 하기 위해 매수/매도 를 알려주는 열을 추가하고 마지막으로 인덱스 순서대로 정렬하였다.

buy_data['state'] = 'buy'				# 상태 열 추가
sell_data['state'] = 'sell'				# 상태 열 추가
data = pd.concat([buy_data, sell_data]).sort_index()


이렇게 하면 다음과 같이 나온다.

주식을 매매하기

그러면 이제 반복문이 데이터프레임을 돌면서 매수인 경우 매수를 하고 매도인 경우 매도를 하면 된다. 그런데 매수를 할 때 이미 매수를 했으면 그냥 넘어가고 매도를 할 때 이미 매도를 했으면 그냥 넘어가도록 해야 했다. 그러기 위해서 flag 를 사용하였다. 따라서 다음과 같이 작성했다.

flag = 0								# 매수 여부 flag
for row in data.itertuples():			# 한 줄씩 가져오기
		price = row[5]						# 종가
		state = row[8]						# 매수 / 매도
		if flag == 0 and state == 'buy':
				# 전량 매수 함수
				flag = 1
				pass
		elif flag == 1 and state == 'sell':
				# 전량 매도 함수
				flag = 0
				pass

이제 매수 함수와 매도 함수를 만들면 된다. 그러기 위해선 함수를 실행할 때 마다 자산이 바뀌어야 한다. 그래서 클래스를 만들고 인스턴스를 생성하도록 하여 각자 자산을 넣고 메서드를 통해서 매수와 매도가 이루어지는 방식으로 구성해야겠다고 생각했다.

class stock_fn:
		def __init__(self, cash):
				self.first = cash	# 초기 자산
				self.cash = cash	# 시작 자산
				self.stock = 0		# 주식 자산
				pass
	
		def buy_fn(self, price, count):
				""" 주식을 매수하는 함수 """
				if (self.cash < price * count):
					print("돈이 충분하지 않습니다.")
					return
				self.cash -= price * count
				self.stock += count
				pass
	
		def sell_fn(self, price, count):
				""" 주식을 매도하는 함수 """
				if (self.stock < count):
					print('그 정도 주식을 가지고 있지 않습니다.')
					return
				self.stock -= count
				self.cash += price * count
				pass
		def print_profit(self):
				print("{0:->30}".format(""))
				print(f"순이익 : {self.cash - self.first}")
				print(f"수익률 : {(self.cash / self.first) * 100 - 100}")

그리고 나중에 매도시 수익률을 보여주는 print_profit 메서드도 추가하였다.

최종 완성 코드

최종적으로 완성된 코드는 다음과 같다.

import yfinance as yf
import pandas as pd
import ta

class stock_fn:
		def __init__(self, cash):
				self.first = cash	# 초기 자산
				self.cash = cash	# 시작 자산
				self.stock = 0		# 주식 자산
				pass
	
		def buy_fn(self, price, count):
				""" 주식을 매수하는 함수 """
				if (self.cash < price * count):
					print("돈이 충분하지 않습니다.")
					return
				self.cash -= price * count
				self.stock += count
				pass
		
		def sell_fn(self, price, count):
				""" 주식을 매도하는 함수 """
				if (self.stock < count):
					print('그 정도 주식을 가지고 있지 않습니다.')
					return
				self.stock -= count
				self.cash += price * count
				pass
	
		def print_profit(self):
				print("{0:->30}".format(""))
				print(f"순이익 : {self.cash - self.first}")
				print(f"수익률 : {(self.cash / self.first) * 100 - 100}")

def return_dataframe(ticker, start_day):
	""" 사용할 데이터 프레임을 리턴하는 함수"""
		data = yf.download(ticker,start = '2021-01-01') 	# 시작날을 입력 받으면 그 이후 33일 정도 기록누락 발생
		if start_day not in data.index:
				print('해당 날은 주가 기록이 없습니다.')
				return
		data = data.reset_index()						# 날짜 인덱스를 열로 바꾸고 숫자 인덱스 사용
		return data
	

def if_used(data, person):
	""" 만약 내가 정한 조건에 맞춰 매수, 매도시 수익률 확인 함수"""

		rsi = ta.momentum.rsi(data['Close'])
		mfi = ta.volume.money_flow_index(data['High'],data['Low'],data['Close'],data['Volume'])
		macd = ta.trend.macd(data['Close'])
		macd_sig = ta.trend.macd_signal(data['Close'])
		buy_condition = (rsi <= 35) & (mfi <= 35) & (macd - macd_sig < -0.5)		# 매수하는 조건
		sell_condition = (rsi >= 65) & (mfi >= 65) & (macd - macd_sig >= 0.6)		# 매도하는 조건
		buy_data = data[buy_condition].copy()			# 매수할 날에 대한 데이터 프레임, 수정시 기존에 파일을 수정할지 아니면 복사본만 수정할지 정해줘야 해서 copy() 시용하여 복사본을 만들어 수정한다고 명시
		sell_data = data[sell_condition].copy()			# 매도할 날에 대한 데이터 프레임
		buy_data['state'] = 'buy'				# 상태 열 추가
		sell_data['state'] = 'sell'				# 상태 열 추가
		data = pd.concat([buy_data, sell_data]).sort_index()
		flag = 0								# 매수 여부 flag
		for row in data.itertuples():			# 한 줄씩 가져오기
				price = row[5]						# 종가
				state = row[8]						# 매수 / 매도
				if flag == 0 and state == 'buy':
						person.buy_fn(price, person.cash // price)	# 전량 매수
						flag = 1
						pass
				elif flag == 1 and state == 'sell':
						person.sell_fn(price, person.stock)	# 전량 매도
						person.print_profit()
						flag = 0
						pass

james = stock_fn(20000)			# james 라는 사람이 20000달러를 가지고 시작할 때
mike = stock_fn(10000)			# mike 라는 사람이 10000달러를 가지고 시작할 때
fngu_data = return_dataframe('FNGU', '2021-04-01')
if_used(fngu_data, james)
#if_used(fngu_data, mike)

출력 결과

[*********************100%***********************]  1 of 1 completed
------------------------------
순이익 : 2875.8400268554688
수익률 : 28.75840026855471
------------------------------
순이익 : 7910.799396514893
수익률 : 79.10799396514892
------------------------------
순이익 : 12690.698879241943
수익률 : 126.90698879241941
------------------------------
순이익 : 21249.249727249146
수익률 : 212.49249727249145

james 라는 사람이 20000달러로 위와 같은 방법으로 매매시 최종 수익률이 212% 라고 나오는 것을 볼 수 있다. 이를 통해서 충분히 이런 방식으로 매매를 하기 적합하다고 판단하였다.

완성된 코드의 깃헙주소는 아래와 같다.
https://github.com/jhon3242/buy_now

회고

좋았던 점

  • 처음으로 프로젝트 시작 전에 왜 이런 프로젝트를 시작하게 되었는지 글로 정리해 보았는데 확실한 목적을 다시 한번 스스로 정리할 수 있는 시간이 되어 딴길로 새지 않고 프로젝트를 잘 마무리 할 수 있었다.
  • 데이터 처리 강의를 듣고 직접 원하는 프로그램을 만들어 보면서 이전에 배웠던 강의 내용들을 전체적으로 복습할 기회가 되었다.

아쉬웠던 점

  • 나름 다른 사람도 쉽게 사용할 수 있도록 설계를 한 것 같은데 막상 코드를 뜯어보면서 이해하지 않는 이상 해당 코드를 사용하기에는 어려움이 있는 것 같다. 좀 더 사용자 친화적인 방식으로 코드를 설계했다면 어땠을까?
  • 해당 기능을 웹으로 제공한다면 더 많은 사람이 시도할 수 있지 않았을까? 라는 아쉬움이 있다.
  • 설정한 보조지표를 사용자가 설정할 수 있도록 확장할 수 있게 만들 수 있지 않았을까? 라는 아쉬움이 있다.
profile
벨로그보단 티스토리를 사용합니다! https://flight-developer-stroy.tistory.com/

2개의 댓글

comment-user-thumbnail
2023년 9월 4일

흥미로운 블로그를 운영하고계시네요 구경하고 갑니다.
자주 방문하겠습니다. 혹시 퀀트투자에 관심있으시면 제 블로그도 한번 구경 와주세요. https://quantai.tistory.com/3

1개의 답글

관련 채용 정보