2025. 3. 14. 13:00ㆍ플젝) 나만의 배당주 사이트 만들기
이 전 글들이 궁금하다면?
'플젝) 나만의 배당주 사이트 만들기' 카테고리의 글 목록
개발새발대발
hsjoo126.tistory.com
이번 프로젝트의 MVP를 완성하면서 제일 많이 했던 생각은
어쨌든 되긴 하는데 ... 내 의도랑은 많이 다르네 .. ?
이렇게 로직을 더럽게 짜도 되는 것인가 ... ?
프로젝트를 하면서 중간에 생긴 오류를 해결하려다 새로운 로직을 추가하고... 추가하고 ...
그러다보니 동작은 하는데, 로직은 굉장히 더러워진..! 그런 모습이 되었다..
프로젝트가 끝났지만..? 왠지 모르게 찝찝함 ..ㅎ
여기서 마무리 해도 괜찮긴 하지만, 만약 내가 면접을 보러가서
왜 이런식으로 로직을 짰나요?
ㄴ 어.....그냥 하다보니 이렇게 됐습니다
이렇게 답할 순 없으니까 ㅠㅠㅠㅠㅠㅠ
휴... 이런 찝찝함을 간직하던 중 마침!! 피드백이 들어왔다.
Yfinance에 두번 요청하는 이유가 있나요 ? 굳이 필요한 요청인지 모르겠네요
그래서, 나는 왜 내가... 중복 요청이 들어가게끔 짰지? 하면서 생각을 해보았고,
이 과정을 통해 중복요청을 없애고 로직도 개선했다.
목차
- 기존 로직은 어떻게 되어있었나? / 이렇게 짠 이유
- 개선한 로직은? / 이렇게 짠 이유
- 의도치 않은 개선
- 결론: 개선된 부분 정리
1. 기존 로직은 어떻게 되어있었나? / 이렇게 짠 이유
내가 고쳐야할 로직은 cron.py에 있었다. 여기가 메인 로직이 짜여져 있는 곳이다.
⬇️ ⬇️ ⬇️ ⬇️ 로직 확인하기 ⬇️ ⬇️ ⬇️ ⬇️
import yfinance as yf
import FinanceDataReader as fdr
from django.core.cache import cache
import pandas as pd
from stocks.models import Ticker, DividendTicker, CollectedDividendData
import time
from time import sleep
#update_dividend_data 실행시 last_dividend가 NULL로 나오는 문제가 생겨서
# last_dividend만 따로 수집
def update_last_dividend():
# CollectedDividendData에서 모든 주식 가져오기
stocks = CollectedDividendData.objects.all()
# 주식별로 처리
for stock in stocks:
ticker_symbol = stock.ticker # CollectedDividendData의 ticker 필드 사용
try:
# Yahoo Finance에서 데이터 가져오기
ticker_data = yf.Ticker(ticker_symbol)
dividends = ticker_data.dividends
# 마지막 배당금 확인
last_dividend = dividends.iloc[-1] if not dividends.empty else None
# CollectedDividendData의 last_dividend 필드 업데이트
if last_dividend is not None:
stock.last_dividend = last_dividend
stock.save() # 변경 내용 저장
print(f"Updated last_dividend for {ticker_symbol}: {last_dividend}")
else:
print(f"No dividends found for {ticker_symbol}. Skipping.")
except Exception as e:
print(f"Error updating last_dividend for {ticker_symbol}: {e}")
print("마지막 배당금 항목의 업데이트 완료했습니다.")
# 배당관련 데이터 있는 주식을 가지고 원하는 데이터 수집
def update_dividend_data():
tickers = DividendTicker.objects.all() # DividendTicker에서 티커 가져오기
batch_size = 100 # 한 번에 처리할 티커 개수
# 티커 배치별 처리
for i in range(0, len(tickers), batch_size):
batch = tickers[i:i + batch_size] # 100개씩 배치 처리
# 배치 내 티커 처리
for ticker_obj in batch:
ticker_symbol = ticker_obj.symbol
try:
ticker_data = yf.Ticker(ticker_symbol)
# 종가 가져오기 (최근 1일 기준)
data = ticker_data.history(period="1d")
close = data['Close'].iloc[-1] if not data.empty else None # 종가가 없으면 None
# 배당금 정보
dividends = ticker_data.dividends
last_dividend = dividends.iloc[-1] if not dividends.empty else None
# 배당일 처리 (calendar에서 'Dividend Date'를 추출)
calendar = ticker_data.calendar
dividend_date = None
try:
# calendar가 dict인 경우에 'Dividend Date' 키 확인
if isinstance(calendar, dict) and 'Dividend Date' in calendar:
dividend_date = calendar['Dividend Date']
else:
print(f"No 'Dividend Date' for {ticker_symbol}, likely no dividends.")
dividend_date = "정보없음"
except Exception as e:
print(f"Error parsing Dividend Date for {ticker_symbol}: {e}, Type of calendar: {type(calendar)}")
# 배당률과 시가총액
dividend_yield = ticker_data.info.get('dividendYield', '정보없음')
market_cap = ticker_data.info.get('marketCap','정보 없음')
if isinstance(market_cap, (int, float)):
market_cap = "{:,}".format(market_cap) # 숫자일 때 쉼표 추가
else:
market_cap = market_cap # '정보 없음' 그대로 사용
# 저장할 데이터 생성
dividend_yield_category = None
if isinstance(dividend_yield, (int, float)):
if 4 <= dividend_yield <= 7:
dividend_yield_category = "4_to_7"
elif dividend_yield > 7:
dividend_yield_category = "above_7"
# CollectedDividendData에 데이터 저장
if dividend_yield_category:
CollectedDividendData.objects.update_or_create(
ticker=ticker_symbol,
defaults={
"close": close, # 종가 사용
"last_dividend": last_dividend,
"dividend_date": dividend_date,
"dividend_yield": round(dividend_yield, 2),
"market_cap": market_cap,
"dividend_yield_category": dividend_yield_category,
},
)
except Exception as e:
print(f"Error fetching data for {ticker_symbol}: {e}")
# 각 배치마다 10초 대기 후, 처리된 티커 수 출력
print(f"Processed {i + batch_size} tickers. Waiting for 10 seconds...")
time.sleep(10)
print("배당 주식의 데이터 수집이 완료되었습니다.")
#Ticker에 있는 것 중에서 배당관련 데이터가 있는 애들만 수집
def check_and_filter_dividends():
tickers = Ticker.objects.all()
dividend_tickers = []
# 100개씩 분할하여 처리(429 오류 방지)
batch_size = 100
for i in range(0, len(tickers), batch_size):
batch = tickers[i:i + batch_size]
for ticker in batch:
try:
stock = yf.Ticker(ticker.symbol)
# dividends 확인
dividends = stock.dividends
if not dividends.empty:
dividend_tickers.append(DividendTicker(
symbol=ticker.symbol,
market=ticker.market
))
except Exception as e:
print(f"Error processing {ticker.symbol}: {e}")
DividendTicker.objects.bulk_create(dividend_tickers, ignore_conflicts=True)
dividend_tickers = [] # 다음 배치를 위해 리스트 초기화
# 10초 대기
print(f"Processed {i + batch_size} tickers. Waiting for 10 seconds...")
sleep(10)
print("배당 관련 ticker만 걸러내는 작업을 완료했습니다.")
#나스닥과 뉴욕증권 거래소에 있는 ticker 수집
def update_tickers():
nasdaq_stocks = fdr.StockListing('NASDAQ')
nyse_stocks = fdr.StockListing('NYSE')
# NASDAQ 티커 저장
for _, row in nasdaq_stocks.iterrows():
symbol = row['Symbol']
Ticker.objects.get_or_create(symbol=symbol, defaults={'market': 'NASDAQ'})
# NYSE 티커 저장
for _, row in nyse_stocks.iterrows():
symbol = row['Symbol']
Ticker.objects.get_or_create(symbol=symbol, defaults={'market': 'NYSE'})
print("나스닥과 뉴욕 증권 거래소의 모든 ticker 수집을 완료했습니다.")
코드를 보면 알겠지만 지금은 쓰지 않는 import도 있고,
함수가 4개나 있어서 보기 어렵다.
기존 로직을 말로 다시 정리하면 다음과 같다.
기존 로직 구성
- FDR 에서 전체 티커리스트 조회(나스닥, 뉴욕증권거래소)
- 전체 티커리스트에서 dividends 항목이 있는 주식만 조회 후 저장
- dividends 항목이 있는 주식 조회 후 세부 데이터 수집
- 배당금만 따로 저장
문제는 2번과 3번 과정을 거치면서 중복 조회가 발생한다는 것이다.
같은 ticker들을 yf에 두 번 조회 한다.
이렇게 짠 이유
이렇게 짠 이유? 당연히 오류를 해결하기 위함이다 ㅠ
https://hsjoo126.tistory.com/92
이 글을 보면, 429에러를 해결하기 위해 고민했었다.
그래서 실시간 로딩을 포기하고 DB를 넣고, 크론탭도 도입했었다.
배당항목이 있는 주식만 걸러내서 저장하고,
이를 기반으로 크론탭을 돌리면 조회도 빨리 되고 좋지 않을까..?
하는 생각이 들었고, 생각났던 해결책을 모두 도입했었다.
지금 보면.. 429에러를 해결한 건 100개씩 처리할 수 있도록 하고, 10초 지연시키고 그 다음 100개를 처리한 방식이었다.
근데 나는 당시 도입했던 모든 해결책이 429에러를 해결했다고 생각했다!
허허....
하나씩 해보면서 어떤게 429에러를 해결할 수 있을지 봤어야했는데..
이미 멘붕상태였기 때문에 그런 건 눈에 들어오지 않았다.
2. 개선한 로직은? / 이렇게 짠 이유
실수를 알고난 뒤, 난 로직을 어떻게 개선할지 생각했다.
생각 1. 모든 티커리스트에서 정보를 다 구하면 되지 않을까?
기존 로직에서는 배당 주식을 걸러내고 데이터를 수집했다면,
그냥 안 걸러내고 전체티커리스트에서 데이터를 수집하는 것이다.
=> 중복 요청은 없으니까 괜찮은거 아냐 ?
결과 시간 오래걸림... 너무 비효율적...
중복요청은 없지만, 로컬에서 테스트를 돌려봤을 때 3시간 넘게 걸렸던 거 같다.
그리고 너무 비효율적이다...!
나는 배당률 4%이상인 애들만 데이터 수집하면 되는데, 그렇지 않은 애들까지 정보를 수집하는 거니까
이 로직은 못 쓰겠다는 판단이 들었다.
생각 2. 배당 주식 구하는 함수랑 데이터 구하는 함수랑 합치면 되지 않나?
말 그대로다! 두 함수를 합치면 중복요청도 자연스럽게 줄지 않을까? 하는 생각이 들었다.
그 과정에서 조언도 받았는데, "배당 항목이 없는 주식은 걍 건너뛰면 되는 거 아냐?" 라는 말을 듣고
참고해서 로직을 짰다.
결과 이 로직으로 채택!
⬇️ ⬇️ ⬇️ ⬇️ 로직 확인하기 ⬇️ ⬇️ ⬇️ ⬇️
import yfinance as yf
import FinanceDataReader as fdr
from stocks.models import Ticker, CollectedDividendData
import time
def update_dividend_data():
tickers = Ticker.objects.all()
batch_size = 100 # 한 번에 처리할 티커 개수
# 티커 배치별 처리
for i in range(0, len(tickers), batch_size):
batch = tickers[i:i + batch_size] # 100개씩 배치 처리
# 배치 내 티커 처리
for ticker_obj in batch:
ticker_symbol = ticker_obj.symbol
try:
ticker_data = yf.Ticker(ticker_symbol)
dividends = ticker_data.dividends
if dividends.empty:
continue
dividend_yield = ticker_data.info.get('dividendYield')
if isinstance(dividend_yield, (int, float)):
if 4 <= dividend_yield <= 7:
dividend_yield_category = "4_to_7"
elif dividend_yield > 7:
dividend_yield_category = "above_7"
else:
continue
else:
continue
# 종가 가져오기 (최근 1일 기준)
data = ticker_data.history(period="1d")
close = data['Close'].iloc[-1] if not data.empty else None # 종가가 없으면 None
# 배당금 정보
last_dividend = dividends.iloc[-1] if not dividends.empty else None
# 배당일 처리 (calendar에서 'Dividend Date'를 추출)
calendar = ticker_data.calendar
dividend_date = None
try:
# calendar가 dict인 경우에 'Dividend Date' 키 확인
if isinstance(calendar, dict) and 'Dividend Date' in calendar:
dividend_date = calendar['Dividend Date']
else:
dividend_date = "정보없음"
except Exception as e:
print(f"Error parsing Dividend Date for {ticker_symbol}: {e}, Type of calendar: {type(calendar)}")
# 시가총액
market_cap = ticker_data.info.get('marketCap','정보 없음')
if isinstance(market_cap, (int, float)):
market_cap = "{:,}".format(market_cap) # 숫자일 때 쉼표 추가
# CollectedDividendData에 데이터 저장
CollectedDividendData.objects.update_or_create(
ticker=ticker_symbol,
defaults={
"close": close,
"last_dividend": last_dividend,
"dividend_date": dividend_date,
"dividend_yield": dividend_yield,
"market_cap": market_cap,
"dividend_yield_category": dividend_yield_category,
},
)
except Exception as e:
print(f"Error fetching data for {ticker_symbol}: {e}")
# 각 배치마다 10초 대기 후, 처리된 티커 수 출력
print(f"Processed {i + batch_size} tickers. Waiting for 10 seconds...")
time.sleep(10)
print("배당 주식의 데이터 수집이 완료되었습니다.")
def update_tickers():
nasdaq_stocks = fdr.StockListing('NASDAQ')
nyse_stocks = fdr.StockListing('NYSE')
# NASDAQ 티커 저장
for _, row in nasdaq_stocks.iterrows():
symbol = row['Symbol']
Ticker.objects.get_or_create(symbol=symbol, defaults={'market': 'NASDAQ'})
# NYSE 티커 저장
for _, row in nyse_stocks.iterrows():
symbol = row['Symbol']
Ticker.objects.get_or_create(symbol=symbol, defaults={'market': 'NYSE'})
print("나스닥과 뉴욕 증권 거래소의 모든 ticker 수집을 완료했습니다.")
한글로 정리한 로직이다.
새로운 로직 구성
- Ticker에서 전체 티커리스트를 가져온다
- 100개씩 처리하겠다는 배치사이즈를 지정한다.
- 티커를 하나씩 조회해보면서 dividends가 있는지 조회한다.
- 없으면 건너뛴다.(다음티커로 넘어감 / continue )
- 배당률을 조회한다
- 없으면 건너뛴다.(다음티커로 넘어감 / continue )
- 배당률이 4~7% 라면 4_to_7로 분류
- 배당률이 7%이상이라면 above_7로 분류
- 다 해당하지 않는다면 건너뛴다(다음티커로 넘어감 / continue)
- 분류한 주식을 가지고 정보수집을 진행한다. (종가, 마지막 배당금, 마지막 배당일, 배당률, 시가총액)
- CollectedDividendData에 데이터 저장한다
이 로직에 핵심은 continue에 있다고 생각한다!
for 문을 돌면서, continue를 만나면 바로 다음 루프로 넘어가는 것!
이를 통해 배당주만 나오게끔 바로바로 걸러낼 수 있고
또 그다음에 4-7% 배당주, 7%이상 배당주 를 필터링하는 로직을 넣어
기존보다 효율적으로 구성했다!
결과는? 대만족이다 ㅋㅋㅋㅋㅋㅋ
이렇게 짠 이유
내가 원하는 주식을 걸러내는데 집중했던 거 같다.
결국엔 4%이상 주식만 필요한 거니까!
전에는 4%이상 걸러내는 작업을 로직 마지막 즈음에 했는데,
이번엔 처음으로 옮기면서 더 효율적으로 변경했다.
3. 의도치 않은 개선
사실 이번 로직을 바꾸면서 update_last_dividend() 함수도 없앨 계획을 가지고 있었다.
이 함수는 기존 로직을 실행했을 때, 왜인지는 모르겠지만 '배당금' 항목이 안 나와서 새로 만들어주었던 함수이다.
이때까진 기존 로직 수행 이후 배당금 구하는 함수를 실행해줘야했다.
기존 로직에 배당금 구하는 로직이 없었던 것도 아니고,
아무리 살펴봐도 어디서 오류가 났는지 찾을 수 없었다..(사실 오류를 해결하기엔 지쳤던 걸지도..? ㅠ)
에러메세지가 나는 것도 아니고
그렇다고 모든 항목이 다 none이 뜬 것도 아니고
어떤 건 뜨고 어떤 건 안 뜨고...
시도1. 배당금을 구하는 속성을 다른 걸로 바꿔보자
그래서 아무튼! 처음 접근은 배당금을 구하는 속성을 다른 걸로 바꿔보자였다.
기존엔 이런식으로 구했는데,
dividends = ticker_data.dividends
last_dividend = dividends.iloc[-1] if not dividends.empty else None
열심히 구글링도 하고 퍼플렉시티와 챗지피티를 괴롭혀서 다른 속성을 찾아냈다.
dy=ticker.info.get('lastDividendValue')
기존엔, dividends 속성에서 찾는 거였는데
새로 찾은 건 info 안에 있는 lastDividendValue라는 값을 바로 가져오는 거였다.
둘의 차이가 뭘까 ? (도와줘요 GPT!)
방법 | 데이터 소스 | 최신성 | 데이터형식 | 과거 내역 |
ticker_data.dividends | 과거 배당 내역 | 상대적으로 느림 | Pandas Series | ✅ (전체 배당 내역 조회 가능) |
ticker.info.get('lastDividendValue') | 최신 보고 값 | 상대적으로 빠름 | 단일 값 (float or None) | ❌ (과거 배당 내역 조회 불가) |
💡 결론
배당 내역 전체 조회 → ticker_data.dividends 사용
최근 배당금 값 빠르게 조회 → ticker.info.get('lastDividendValue') 사용
👉 따라서 **최근 배당금 값만 필요하면 ticker.info.get()**이 유용하고,
👉 **과거 배당 내역까지 포함해 분석하려면 ticker_data.dividends**가 더 적합합니다. 😎
그렇다고 한다~
하아아 lastDividendValue를 알고 있었으면 처음부터 이걸 썼을텐데 ㅠ
결과??
내가 배당금 속성을 알아보는 동안 위에서 적용한 새로운 로직을 돌려보는 중이었는데,
아니 이게 웬걸??? 새로 개선한 로직에서는 배당금 항목이 잘 나오는 것이다!
ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ
it works..... why!!!!!!!!!!!!!
왜!!! 대체 왜 되는 거야!! ㅋㅋㅋㅋㅋㅋㅋ ㅠㅠㅠㅠ
후... 아무튼 잘 되서 바꿀 필요가 없어졌다.
4. 결론: 개선된 부분 정리
이런 저런 일이 있었지만... 아무튼? 좋은 게 좋은 거다라는 마인드,,,
결론적으로 개선 된 부분들은 다음과 같다.
1. 필요없는 것들 삭제
- 배당금을 저장하기 위해 만든 함수(update_last_dividend) 삭제
- 그 함수를 실행했던 cronjob 삭제
- 배당 항목이 있는 주식만 걸러내 저장 하던 함수(check_and_filter_dividends) 삭제
- 그 함수를 저장해놓은 DB(model) 삭제
- 그 함수를 실행했던 cronjob 삭제
2. 효율 개선
- 새로운 로직 변경 후 시간 줄어들음
- 기존 로직 (check_and_filter_dividends + update_dividend_data)
- 로컬 테스트 : 1시간 51분
- EC2 테스트 : 1시간 50분
- 새로운 로직 (update_dividend_data)
- 로컬 테스트 : 1시간 34분
- EC2 테스트 : 1시간 33분
와!! 나도 테스트하면서 놀랐지만, 시간이 대폭 줄어들었다!(약 15% 개선 ㄷㄷ)
로직 개선 제대로 한 거 같아 기분이 좋다! 야호~!!
휴 - 계속 가지고 있던 찝찝함이 사라졌다!
이젠 면접 때 당당하게 말할 수 있을 거 같다!
왜 이런식으로 로직을 짰나요?
ㄴ 제가 원하는 것은 4%이상의 고배당주들만 모으는 것이었습니다
그래서 for문과 continue를 활용해 해당하지 않는 주식은 다 걸러냈고
이후 필터링된 주식 대상으로 종가, 배당일, 시가총액 등 세부 데이터들을 수집했습니다
수집한 데이터는 주기적으로 사이트에 업데이트 해주어야하기 때문에, 메인 로직을 cron.py 에 작성했습니다.
이제 주변 사람들한테 더 피드백 받아서 반영하고,
사이트가 더 안정을 찾으면 그때!
블로그에 깃헙이랑 사이트 공개할 거같다!
(아직 마음의 준비가 되지 않은걸요 ,,,
아무튼 다음 글에서 만나요~~