나만의 배당주 사이트 만들기) 2-8. cron.py , view.py 작성하기, 디테일 잡기(숫자는 소수점 2번째 자리까지, 큰 숫자는 쉼표 넣어서, 페이지네이션 등)

2025. 1. 24. 13:00프로젝트

이 전 글들이 궁금하다면 ?

 

0. 사이트를 만드려는 이유

https://hsjoo126.tistory.com/80

 

1-1. 프로젝트 가능성 보기

https://hsjoo126.tistory.com/81

 

pandas 와 jupyter 이용해서 테스트해보기 

https://hsjoo126.tistory.com/82

 

1-2. 기획 단계 - 디자인, 와이어 프레임, ERD 등

https://hsjoo126.tistory.com/83

 

2. 개발 단계 - 계획짜기, 구현해보기

https://hsjoo126.tistory.com/84

 

2-1. 개발 단계 - 배당지불일, 시장별 티커리스트 구하기

https://hsjoo126.tistory.com/85

 

2-2. 개발 단계 - 코드 정리

https://hsjoo126.tistory.com/86

 

2-3. 개발 단계 - 사이트에 적용하기(장고)

https://hsjoo126.tistory.com/87

 

2-4. 개발 단계 - 사이트 로딩 속도 줄이기

https://hsjoo126.tistory.com/88

 

2-5. 개발 단계 - 주식별 동적인 페이지 만들기

https://hsjoo126.tistory.com/89

 

2-6. 개발 단계 - 티커리스트 db에 저장해서 불러오기, 해결하지 못한 트러블슈팅

https://hsjoo126.tistory.com/91

 

2-7. 429에러를 해결하기위한 노력, 로직 대폭 수정하기 엉엉... 그리고 해냄...

https://hsjoo126.tistory.com/92


 

view로직을 수정하려고 했지만, 그전에 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("Last dividend data has been successfully updated!")



# 배당관련 데이터 있는 주식을 가지고 원하는 데이터 수집
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_price = 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 = "No Dividends"
                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', 0) * 100
                market_cap = ticker_data.info.get('marketCap')

                # 저장할 데이터 생성
                dividend_yield_category = None
                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={
                            "current_price": close_price,  # 종가 사용
                            "last_dividend": last_dividend,
                            "dividend_date": dividend_date,
                            "dividend_yield": round(dividend_yield, 2),
                            "market_cap": "{:,}".format(market_cap) if market_cap else None,
                            "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("Task completed!")


#나스닥과 뉴욕증권 거래소에 있는 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 update 완료!")

CRONJOB은 언제 예약하면 좋을까 ?

뉴욕증권거래소와 나스닥 기준으로 (현지시간)

 

9:00~16:30(EST)

 

cornjob은 주식 개장시간을 피하면 제일 좋을 거 같다.

16:30(EST)이후에  크론탭을 실행하면 될 거 같은데, 

 

 

근데? 장고는 기본적으로 UTC를 사용한다.

 

 

크론탭도 똑같이 장고 시간대를 따를 거기 때문에 EST와 UTC를 다 고려해 시간을 예약했다.

CRONJOBS = [
    # 1번 함수: 매주 월요일 오전 6시 UTC (EST 기준 오전 1시)
    ('0 6 * * 1', 'yourapp.cron.update_tickers'),  
    
    # 2번 함수: 매주 월요일 오전 7시 UTC (EST 기준 오전 2시)
    ('0 7 * * 1', 'yourapp.cron.check_and_filter_dividends'),  
    
    # 3번 함수: 매일 오전 9시 UTC (EST 기준 오전 4시)
    ('0 9 * * *', 'yourapp.cron.update_dividend_data')
    
    # 4번 함수: 매일 오전 10시 UTC (EST 기준 오전 5시)
    ('0 10 * * *', 'yourapp.cron.update_last_dividend')

]
  • 1번 함수는 나스닥과 뉴욕증권거래소에 있는 주식의 ticker들을 조회하는 함수이다.
  • 2번 함수는 그 주식들을 가지고 배당 관련 데이터가 있는 주식의 ticker를 조회하는 함수이다.
  • 3번 함수는 배당 ticker들을 가지고 데이터를 뽑아내는 함수이다.
  • 4번 함수는 '마지막 배당금'을 뽑아내는 함수이다. 

 

  • 트래픽이 없는 시간대인 오전 1시를 선택했다. 
  • 2번 함수는 1번 함수 이후 실행되어야하기 때문에 1시간 뒤인 오전 2시에 실행되도록 했다. 
  • 그리고 3번 함수도 트래픽이 없는 시간대인 오전 4시, 그리고 매일 실행되도록 했다.
  • 4번 함수는 3번 이후 실행되어야하기 때문에, 오전 5시 그리고 매일 실행되도록 했다.

그리고 이를 UTC 시간대로 변경해 설정해주었다. 

추가로 redis에 저장하는 함수를 작성했다.

#redis에 업데이트 하기
def update_redis_data():
    # 4~7% 배당률 데이터를 Redis에 저장
    data_4_to_7 = CollectedDividendData.objects.filter(dividend_yield_category='4_to_7')
    stock_data_4_to_7 = []
    for item in data_4_to_7:
        stock_data_4_to_7.append({
            'ticker': item.ticker,
            'current_price': item.current_price,
            'last_dividend': item.last_dividend,
            'dividend_date': item.dividend_date,
            'dividend_yield': item.dividend_yield,
            'market_cap': item.market_cap,
        })

    cache.set("stock_data_4_to_7", stock_data_4_to_7)

    # 7% 이상 배당률 데이터를 Redis에 저장
    data_above_7 = CollectedDividendData.objects.filter(dividend_yield_category='above_7')
    stock_data_above_7 = []
    for item in data_above_7:
        stock_data_above_7.append({
            'ticker': item.ticker,
            'current_price': item.current_price,
            'last_dividend': item.last_dividend,
            'dividend_date': item.dividend_date,
            'dividend_yield': item.dividend_yield,
            'market_cap': item.market_cap,
        })

    cache.set("stock_data_above_7", stock_data_above_7)

 

3번 함수가 끝나고 redis 저장할 수도 있지만, 따로 작성했다.

 

이유는

- 유지 보수가 쉬워서

- 필요할 때 따로 호출해서 저장 가능

 

그리고 3번 함수에는 다음 코드를 추가해주었다.

    # Redis에 데이터 저장
    update_redis_data()

 

 

 

 

 

자 아무튼... cron.py는 대충 정리가 된 거 같고

이제 view를 보자!

 

view에는 간단하게 들어가면 될거같다.

1. redis 조회

2. 데이터 가져오기

3. 만약에 redis에 데이터가 없으면 ?

4. db에서 가져오기

 

def high(request):
    # Redis에서 데이터 가져오기
    cache_key = "stock_data_above_7"
    stock_data = cache.get(cache_key)

    # Redis에 데이터가 없을 경우 DB에서 데이터 조회
    if not stock_data:
        # DB에서 배당률 7% 이상인 데이터 조회 (dividend_yield_category 필드 기준)
        stock_data = []
        stocks = CollectedDividendData.objects.filter(dividend_yield_category='above_7')

        for stock in stocks:
            stock_data.append({
                'ticker': stock.ticker,
                'current_price': stock.current_price,
                'last_dividend': stock.last_dividend,
                'dividend_date': stock.dividend_date,
                'dividend_yield': stock.dividend_yield,
                'market_cap': stock.market_cap,
            })

        # DB에서 조회한 데이터를 Redis에 캐시
        cache.set(cache_key, stock_data)

    # 템플릿에 데이터 전달
    return render(request, "stocks/high.html", {"stocks_data": stock_data})

def middle(request):
    # Redis에서 데이터 가져오기
    cache_key = "stock_data_4_to_7"
    stock_data = cache.get(cache_key)

    # Redis에 데이터가 없을 경우 DB에서 데이터 조회
    if not stock_data:
        # DB에서 배당률 7% 이상인 데이터 조회 (dividend_yield_category 필드 기준)
        stock_data = []
        stocks = CollectedDividendData.objects.filter(dividend_yield_category='4_to_7')

        for stock in stocks:
            stock_data.append({
                'ticker': stock.ticker,
                'current_price': stock.current_price,
                'last_dividend': stock.last_dividend,
                'dividend_date': stock.dividend_date,
                'dividend_yield': stock.dividend_yield,
                'market_cap': stock.market_cap,
            })

        # DB에서 조회한 데이터를 Redis에 캐시
        cache.set(cache_key, stock_data)

    # 템플릿에 데이터 전달
    return render(request, "stocks/middle.html", {"stocks_data": stock_data})

이렇게 작성했다!

 

사실 메인 로직은 cron.py에 다 들어있어서 ㅋㅋㅋㅋ 이건 비교적 빠르게 작성했다

이제 N일차만에!! 드디어 테스트 시간이다..

 

기도메타 가자 ... 제발 오류없이 떠라...!!

 

 

 

야호!!! DB에 있는 내역이 그대로 떴다 엉엉 ㅠㅠㅠㅠ

 

 

휴... 아무 오류없이 뜬 것에 감사 ...

상세페이지도 아주 잘 나온다!!

 

매우매우매우 신납니다.

 


 

자 이제, 나오는 거 기반으로 더 디테일을 잡아보겠습니다! 

 

 

  • navbar에 다른 페이지 이동할 수 있게 추가
  • 페이지네이션
  • 상세페이지 
    • middle, high 페이지에 있는 것들 상세페이지에 포함시키기
  • 숫자 예쁘게 바꾸기!
    • 소수점 2번째까지만 나오게
    • 숫자마다 콤마 추가하기
  • 주가 그래프 / 배당 그래프 추가하기

 

 

이 중에서 제일 심각해보이는 ,,, 숫자 예쁘게 바꾸기 ! 먼저 해보자~

여기... 를 보기 좋게 바꾸고 싶다..!!

소수점 2자리까지만 나오면 아주 예쁘게 바뀔 거 같다!

 

 

GPT슨생님께 물어보니 ... 다양한 방법을 알려줬는데..

난 view코드를 건드리고 싶진 않아서 

이 코드를 사용했다!

|floatformat:2

 

                    <tr>
                        <td>{{ stock.date }}</td>
                        <td>{{ stock.Open|floatformat:2 }}</td>
                        <td>{{ stock.High|floatformat:2 }}</td>
                        <td>{{ stock.Low|floatformat:2 }}</td>
                        <td>{{ stock.Close|floatformat:2 }}</td>
                        <td>{{ stock.Volume }}</td>
                    </tr>

이런식으로 html코드 변경해줌!

 

 

 

그리고 시가총액도 코드를 바꿔주었는데,

시가총액은 정보가 안 나와 있는 애들도 있어서
숫자일 땐 쉼표를 넣어서 표시하고,

정보가 없는 애들은 '정보없음' 이라고 표시한 뒤 html 로 넘겨주었다.

    # 시총
    market_cap = info.get('marketCap', '정보 없음')
    if isinstance(market_cap, (int, float)):
        market_cap = "{:,}".format(market_cap)  # 숫자일 때 쉼표 추가
    else:
        market_cap = market_cap  # '정보 없음' 그대로 사용

 


 

  • navbar에 다른 페이지 이동할 수 있게 추가

그 다음이론 이걸 해보겠다!

현재는 Navbar에 home만 있는데, 고중배당주, 초고배당주를 같이 넣어서 쉽게 이동할 수 있도록 하자!

 

바꾼 코드 ...!

    <div class="nav-containor">
        <a class="nav-text" href="{% url 'main' %}">HOME</a>
        <div class="navbar">
            <a class="nav-text" href="{% url 'high' %}">HIGH</a>
            <a class="nav-text" href="{% url 'middle' %}">MIDDLE</a>
        </div>
    </div>
.nav-containor{
    font-family: 'Pretendard-Regular';
    font-size: 17px;
    margin: 0 0 30px 20px;
    padding-top: 20px;
    display: flex;
    justify-content: space-between;
}

.nav-text{
    padding-right: 20px;
}

 

 

이런식으로 했다~!!

 


  • 페이지네이션

 

원래는 이렇게 끝도 없이 펼쳐지는 ... 그런 모습이었다. 

스크롤이 무한대로 내려가서 매우 가독성이 떨어졌는데,

30개씩 나눠서 페이지네이션을 만들기로 했다.

 

 

의외로 코드는 간단하다.

#views.py
from django.core.paginator import Paginator


def high(request):

#데이터 수집 로직 등등...
	
    
    #페이지네이션
    paginator = Paginator(stock_data, 30)
    page_number = request.GET.get('page')  # URL에서 'page' 파라미터를 가져옴
    page_obj = paginator.get_page(page_number)  # 해당 페이지의 데이터

    context = {
        "stocks_data" : page_obj.object_list,
        "page_obj" : page_obj,
        "page_range": range(1, paginator.num_pages + 1),  # 모든 페이지 번호
    }


    # 템플릿에 데이터 전달
    return render(request, "stocks/high.html", context)
        <div class="pagination">
            {% for page_num in page_range %}
                {% if page_num == page_obj.number %}
                    <span class="current">{{ page_num }}</span>
                {% else %}
                    <a href="?page={{ page_num }}">{{ page_num }}</a>
                {% endif %}
            {% endfor %}
        </div>
.pagination{
    margin: 100px 0;
    font-size: 20px;
}

.pagination a {
    padding: 0 5px;
}

.current {
    padding: 0 3px;
    font-weight: bold;
    font-size: 24px;
}

 

 

이렇게 변경해준 후 결과

사진에는 다 안 담겼지만, 30개씩 나눠서, 페이지네이션을 넣었고 현재페이지가 어딘지도 알 수 있도록 했다. 


 

 

이쯤에서 할 일 리스트를 다시 점검해보면?

 

  • navbar에 다른 페이지 이동할 수 있게 추가
  • 페이지네이션
  • 상세페이지 
    • middle, high 페이지에 있는 것들 상세페이지에 포함시키기
  • 숫자 예쁘게 바꾸기!
    • 소수점 2번째까지만 나오게
    • 숫자마다 콤마 추가하기
  • 주가 그래프 / 배당 그래프 추가하기

ㅜㅜㅜㅜ 드디어 끝이 보이는 느낌이다

글이 너무 길어지니까 한 번 끊고 가자!

다음 글에선 상세페이지를 더 꾸미고, 그래프를 추가하는 작업을 해보겠다!