나만의 배당주 사이트 만들기) 2-9. 개발 단계 - 주가그래프그리기(matplotlib), Non-GUI 백엔드? Agg가 뭐야!, 상세페이지 수정, 페이지네이션 등

2025. 2. 3. 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

 

2-8. cron.py , view.py 작성하기, 디테일 잡기

https://hsjoo126.tistory.com/93


 

저번 글에서 페이지네이션 설명이 부족했던 거 같아서,

추가로 적은 후 다른 작업을 하려고 한다!

밑에는 view.py와 html이다. 

paginator = Paginator(stock_data, 30) #stock_data를 30개씩 자르겠다는 의미
    page_number = request.GET.get('page')  # 유저가 선택한 'page'의 키값을 가져옴(템플릿 참고: ?page=)
    page_obj = paginator.get_page(page_number)  # 페이지 숫자에 맞는 데이터를 가져옴 (2페이지면 2페이지의 데이터)

    context = {
        "stocks_data" : page_obj.object_list, #page_obj.object_list: 현재 페이지 데이터만 포함.
        "page_obj" : page_obj, #page_obj: 현재 페이지 데이터와 페이지네이션 정보를 모두 포함.
        "page_range": range(1, paginator.num_pages + 1),  #모든 페이지 번호, 1부터 마지막 페이지까지
    }
        <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 %}

 

1. page_range를 page_num이 돌면서 순회한다. 

2. 만약에 page_num과 page_obj.number가 같다면(page_obj.number는 현재 페이지를 의미한다)

3. span태그를 써서 page_num 을 표시한다.

-> 이는 현재 페이지를 의미하며 현재 페이지는 링크로(a태그로) 표시할 필요가 없기 때문에 span태그를 사용했다.

 

4. 현재페이지가 아닌 애들은 else문을 타게 된다.

5. 현재 페이지가 아닌 애들은 사용자가 누르고 이동할 수 있게끔 a태그로 표시한다.

6. ?page={{page_num}} : 사용자가 요청한 페이지를 서버에 전달한다.

 

예시)

-> 현재 페이지가 '2'이라면, 나머지 애들은 a태그로 걸려있다. 

-> 만약 사용자가 '7'을 선택한다면 ?page={{ 7 }} 로 바뀌며 서버에게 7페이지를 요청한다.

-> 서버는 7페이지를 반환한다.

 


 

 

  • 상세페이지 
    • middle, high 페이지에 있는 것들 상세페이지에 포함시키기

현재 상세 페이지는 안 들어가있는 정보가 있다.

 

- 마지막 배당일(배당락일과 다름)

- 배당수익률

- 현재 주가

 

 

디자인은 이런식으로 하면 좋을 거 같다.

디자인한거!!

 

 

저대로 넣기 위해... 코드는 다음과 같이 작성했다.

그리고 찾아보니까 배당락일은 yfinance 에서 적용하지 않는다고 해서 그거 빼고 적용했다!

def detail(request, ticker):
    stock = yf.Ticker(ticker)
    info = stock.info

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

    # 현재 주가
    current_price = info.get('currentPrice', '정보 없음')
    # 배당일 처리 (calendar에서 'Dividend Date'를 추출)
    calendar = stock.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 {stock}, likely no dividends.")
            dividend_date = "No Dividends"
    except Exception as e:
        print(f"Error parsing Dividend Date for {stock}: {e}, Type of calendar: {type(calendar)}")
    # 배당금 정보
    dividends = stock.dividends
    last_dividend = dividends.iloc[-1] if not dividends.empty else None
    #배당수익률
    dividend_yield = info.get('dividendYield', '정보 없음')
    if isinstance(dividend_yield, (int, float)):
        dividend_yield = f"{dividend_yield * 100:.2f}%"  # 백분율로 변환


    # 주식 간단 정보
    summary = info.get('longBusinessSummary', '정보 없음')

    # 배당 내역
    divi = stock.dividends
    # 배당 내역이 비어있는지 확인
    if divi.empty:
        dividend_data = "배당 내역이 없습니다."  # 배당 내역이 없으면 메시지 반환
    else:
        # 최근 3년 기준 날짜 계산
        three_years_ago = timezone.now() - timedelta(days=3*365)

        # 최근 3년 데이터 필터링 후 인덱스를 리셋하고 'Date' 컬럼을 날짜로 변환
        dividend_data = divi[divi.index >= three_years_ago].reset_index()
        dividend_data['date'] = dividend_data['Date'].dt.date  # 날짜만 추출

        # 배당금과 날짜만 선택하여 리스트로 변환
        dividend_list = dividend_data[[
            'date', 'Dividends']].to_dict(orient='records')

    # 주가 내역 (최근 3개월)
    stock_history = stock.history(
        interval='1d', period='1y', auto_adjust=False)
    # 'Date' 열을 Datetime 형식으로 변환하고, 날짜만 출력
    stock_history['date'] = stock_history.index.date
    # 필요한 열만 선택
    stock_history_selected = stock_history[[
        'date', 'Open', 'High', 'Low', 'Close', 'Volume']]
    # 데이터를 템플릿에 전달
    stock_history_list = stock_history_selected.to_dict(orient='records')

    context = {
        'stock': stock,
        'market_cap': market_cap,
        'current_price' : current_price,
        'dividend_date' :dividend_date,
        'last_dividend' :last_dividend,
        'dividend_yield' : dividend_yield,
        'summary': summary,
        'dividend_list': dividend_list,
        'stock_history_list': stock_history_list,
    }
    return render(request, "stocks/detail.html", context)
        <table class="current_containor">
            <tr>
                <td>현재 주가</td>
                <td>{{ current_price }}</td>
            </tr>
            <tr>
                <td>배당 지불일</td>
                <td>{{ dividend_date }}</td>
            </tr>
            <tr>
                <td>마지막 배당금</td>
                <td>{{ last_dividend }}</td>
            </tr>
            <tr>
                <td>연간 배당률</td>
                <td>{{ dividend_yield }}</td>
            </tr>
        </table>

 

.current_containor{
    width: 40%;
    font-size: 20px;
    font-family: 'Recipekorea';
}

 

배당락일 못 넣은 게 매우...매우 아쉽긴하지만 ,, 

더 알아보기엔 시간이 좀 부족...ㅎ


  • 주가 그래프 / 배당 그래프 추가하기

와우.. 생각보다 어려워서 내일로 미루겠음!
내일 파일 새로 하나 만들어서 그래프 시도해보는 걸로~

 

--

 

그리고 다음날이 되었다. (사실 연휴여서 엄청 쉬다 옴 ㅋㅋㅋㅋㅋㅋ)

결론부터 보여주자면! 주가 그래프는 다음과 같은 모습으로 완성 되었다~

 

Matpolib을 이용했고, 종가 기준으로 그래프를 구성했다.

만든 그래프를 PNG로 저장해 홈페이지에 띄웠다. 

 

다음은 그래프 코드가 들어간 views.py 다.

주석을 달아놨기에 대충 알아볼 수는 있을 거다!

#views.py
import matplotlib
import matplotlib.pyplot as plt
from io import BytesIO
import base64


def detail(request, ticker):

#등등 전에 있던 코드들 .....


    #주가그래프
    matplotlib.use('Agg') #non-GUI 백엔드 Agg 사용
    matplotlib.rc('font', family='AppleGothic')     #폰트 설정
    close_prices = stock_history[['date', 'Close']]  # 날짜와 종가만 선택
    #그래프 그리기
    plt.figure(figsize=(8, 4))#그래프 사이즈 조절
    plt.plot(close_prices['date'], close_prices['Close'], label='주가', color='#7cfae8',  linewidth=2) #종가 기준으로 그래프 그리고 스타일 적용
    plt.title(f'1년간 {ticker} 주식 그래프 (종가 기준)',color='white') #그래프 제목 설정
    plt.xticks(close_prices['date'][::30], rotation=45,color='white')  #30일 기준으로 날짜 표시, 잘 보일 수 있게 45도 틀어서 색상 흰색으로
    plt.ylabel('주가 (USD)',color='white') #Y축 설명
    plt.yticks(color="white") #Y축 글자색 흰색으로
    plt.legend(facecolor='#4b4b4b', edgecolor='white', loc='best', labelcolor='white') #범례 스타일 적용 (상단에 조그만 박스)
    plt.grid(color='white',linestyle='--', linewidth=1) #그리드 설정
    # 테두리 색상 설정
    for spine in plt.gca().spines.values():
        spine.set_edgecolor('white')
    plt.tight_layout()
    

    #그래프를 PNG로 저장
    buffer = BytesIO()
    plt.savefig(buffer, format='png', transparent=True)  # PNG 형식으로 저장
    buffer.seek(0)
    image_png = buffer.getvalue()
    buffer.close()

    #PNG를 base64로 인코딩
    graph = base64.b64encode(image_png).decode('utf-8')



    context = {
        'stock': stock,
        'market_cap': market_cap,
        'current_price' : current_price,
        'dividend_date' :dividend_date,
        'last_dividend' :last_dividend,
        'dividend_yield' : dividend_yield,
        'summary': summary,
        'dividend_list': dividend_list,
        'stock_history_list': stock_history_list,
        'graph': graph #넘겨주기
    }
    return render(request, "stocks/detail.html", context)

 

그래프를 이미지로 저장해서 html로 넘기는 형식이었기 때문에, 그래프를 예쁘게 꾸미는(?) 과정은 다 view에서 처리해주었다.

전체적으로 코드가 어렵진 않기 때문에(낯설 수는 있음)

대부분의 설명은 건너뛰어도 괜찮을 거 같고, 

 

 

 

이 코드에 대한 설명을 하고 넘어가면 좋을 거 같다

matplotlib.use('Agg') #non-GUI 백엔드 Agg 사용

 

위 코드는 Matplotlib의 백엔드를 "Agg"로 설정하는 코드이다.

 

먼저 백엔드는 뭘까?

Matplotlib은 그래프를 그리는 방식을 여러 개 제공하는데,

이를 백엔드(backend) 라고 부른다.

 

백엔드는 크게 두 가지로 나뉜다.

 

1️⃣ GUI 백엔드 (화면에 바로 표시)

  • TkAgg, Qt5Agg, GTK3Agg 같은 게 있고
  • 이 백엔드는 Jupyter Notebook이나 로컬에서 바로 그래프를 보여줄 때 사용하면 유용하다.
  • plt.show()를 사용하면 화면에 그래프를 표시할 수 있다.

예를들어, 애플의 1년 주가를 그래프로 만든다면

다음과 같이 코드를 작성할 수 있다.

import yfinance as yf
import matplotlib.pyplot as plt

# 주가 데이터 가져오기 (예: 애플 - AAPL)
ticker = "AAPL"
data = yf.download(ticker, start="2023-01-01", end="2024-01-01")

# 종가(Close) 데이터로 그래프 그리기
plt.figure(figsize=(10, 5))
plt.plot(data['Close'], label='Close Price')
plt.title(f'{ticker} Stock Price')
plt.xlabel('Date')
plt.ylabel('Price (USD)')
plt.xticks(data.index[::30], rotation=45)
plt.legend()
plt.grid()
plt.show()

 

이 코드를 실행하면 다음과 같이 그래프 사진이 자동으로 띄워진다.

이 사진은 어디에 저장되는 건 아니고 plt.show(), 말그대로 보여주는 것이다.

 

plt.show() 이 명령어가 없다면 그래프는 띄워지지 않는다. 

 

 

 

2️⃣ Non-GUI 백엔드 (파일로 저장)

  • Agg, Cairo, PS, PDF, SVG 등이 있다.
  • 화면을 표시하지 않고 바로 파일로 저장할 때 사용한다.
  • Agg는 PNG 같은 래스터 이미지 파일을 생성할 때 최적화되어 있다.

내 프로젝트에선 그래프를 홈페이지에 띄워야했기 때문에,

그래프를 사진으로 저장 후 넘기는 방법을 고려했고

Non-GUI 백엔드 중에서도  PNG 이미지에 적합한 Agg를 선택했다.

 

그래서 내 코드를 확인해보면 plt.show()코드는 존재하지 않는다. 바로 사진으로 넘기기 때문이다.

 

다시 한 번 정리해보자.

matplotlib.use('Agg') #non-GUI 백엔드 Agg 사용

⬇️ 이 코드를 사용한 이유 ⬇️ 

1. matplotlib 는 그래프를 그릴 때 '백엔드'를 사용한다.

2. 백엔드는 두 가지가 있는데,

    - 그래프를 화면에 바로 표시하는 GUI 백엔드가 있고

    - 파일로 저장하는 Non-GUI 백엔드가 있다.

3. Non-GUI 백엔드 중에서도 Agg는 PNG 같은 래스터 이미지 파일을 생성할 때 최적화되어 있다.

4. 그래서 백엔드를 Agg로 설정해주었다.

 

🔥 결론 : matplotlib.use('Agg')의 핵심 역할

서버에서 GUI 없이 그래프 생성 가능
PNG 같은 이미지 파일로 저장할 때 최적화
plt.show() 없이도 오류 없이 실행됨
배포 환경(EC2, Docker 등)에서도 안정적으로 작동

 

이정도면 다들 이해 되셨죵? 

안 됐으면 GPT ㄱ


 

와아 -

주가 그래프까지 완성했다!!

휴..... 디자인도 대충 손 봤고(사실 마음에 안 듦..)

이제 전체적으로 코드 정리해서 깃허브에 올리고

배포를 위한 준비준비 단계로 넘어갈 거 같다!

 

그럼 다음 글에서 보자구용 

안뇽 -