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 ㄱ
와아 -
주가 그래프까지 완성했다!!
휴..... 디자인도 대충 손 봤고(사실 마음에 안 듦..)
이제 전체적으로 코드 정리해서 깃허브에 올리고
배포를 위한 준비준비 단계로 넘어갈 거 같다!
그럼 다음 글에서 보자구용
안뇽 -