2025. 2. 12. 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
2-9. 개발 단계 - 주가그래프그리기(matplotlib), Non-GUI 백엔드? Agg가 뭐야!, 상세페이지 수정, 페이지네이션 등
https://hsjoo126.tistory.com/94
오늘은 배포를 위한 준비 단계를 할 것이다.
할 일
- Pip list 정리하기
- Docker 관련 설정
- nginx 관련
다음은 내가 생각해본... 배포를 위한 준비 순서이다!
- .env
- Dockerfile
- docker-compose.yml
- web
- redis
- db
- nginX
- nginx.conf
- 배포
- EC2
- 도메인 연결
- ssl 인증(https)
1. Pip list 정리하기
프로젝트를 진행하면서 pip 리스트가 굉장히 많이 쌓였다.
이 중에서 쓸모 없는 Pip 지우고 필요한 pip 만 requirements.txt에 넣을 예정이다.
제일 단순 무식한 방법은
새폴더에 깃클론을 받아다가 필요한 것들만 하나하나 설치하는 거다 ㅋㅋㅋㅋㅋㅋㅋ
그리고 그렇게 만든 requirements.txt
asgiref==3.8.1
async-timeout==5.0.1
beautifulsoup4==4.13.1
certifi==2025.1.31
charset-normalizer==3.4.1
contourpy==1.3.1
cycler==0.12.1
dj-database-url==2.3.0
Django==5.1.5
django-crontab==0.7.1
django-environ==0.12.0
django-redis==5.4.0
finance-datareader==0.9.94
fonttools==4.55.8
frozendict==2.4.6
html5lib==1.1
idna==3.10
kiwisolver==1.4.8
lxml==5.3.0
matplotlib==3.10.0
multitasking==0.0.11
narwhals==1.25.0
numpy==2.2.2
packaging==24.2
pandas==2.2.3
peewee==3.17.8
pillow==11.1.0
platformdirs==4.3.6
plotly==6.0.0
psycopg2-binary==2.9.10
pyparsing==3.2.1
python-dateutil==2.9.0.post0
pytz==2025.1
redis==5.2.1
requests==2.32.3
requests-file==2.1.0
six==1.17.0
soupsieve==2.6
sqlparse==0.5.3
tqdm==4.67.1
typing_extensions==4.12.2
tzdata==2025.1
urllib3==2.3.0
webencodings==0.5.1
yfinance==0.2.52
그 다음은
2. Docker 관련 설정
# Django Dockerfile
FROM python:3.10.11
# 프로젝트 작업 디렉터리
WORKDIR /app
# 프로젝트 요구사항 파일 복사 및 설치
COPY requirements.txt /app/
RUN apt-get update && apt-get install -y python3-pip cron && \
pip install --no-cache-dir -r requirements.txt
# 프로젝트 파일 복사
COPY . /app/
# 포트
EXPOSE 8000
# 장고 서버 실행
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
#docker-compose.yml
services:
web:
build: .
command: >
sh -c "python manage.py migrate &&
python manage.py crontab add &&
service cron start &&
python manage.py runserver 0.0.0.0:8000"
volumes:
- .:/app
ports:
- "8000:8000"
depends_on:
- redis
- db
db:
image: postgres:latest
container_name: postgres-db
env_file:
- ./dividend_stock/.env
ports:
- "5432:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
redis:
image: redis:latest
ports:
- "6379:6379"
volumes:
postgres-data:
.env도 이렇게 바꿔주었다.
DJANGO_SECRET_KEY=
DEBUG=True
ALLOWED_HOSTS=localhost,127.0.0.1
DATABASE_URL=
POSTGRES_USER=
POSTGRES_PASSWORD=
POSTGRES_DB=
REDIS_URL=redis://redis:6379/1
DJANGO_SETTINGS_MODULE=dividend_stock.settings
보안에 문제있을 수 있으니 문제가 될만한 건 빼고 올렸다!
그리고 .env에 맞춰서 settings.py 도 다음과 같이 바꿔주었다.
"""
Django settings for dividend_stock project.
Generated by 'django-admin startproject' using Django 5.1.4.
For more information on this file, see
https://docs.djangoproject.com/en/5.1/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.1/ref/settings/
"""
from pathlib import Path
import environ
# 환경변수 초기화
env = environ.Env()
environ.Env.read_env()
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = env('DJANGO_SECRET_KEY')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = env('ALLOWED_HOSTS').split(',')
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'stocks',
'django_crontab'
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'dividend_stock.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'dividend_stock.wsgi.application'
# Database
# https://docs.djangoproject.com/en/5.1/ref/settings/#databases
DATABASES = {
'default': env.db(), # DATABASE_URL 환경 변수를 사용하여 자동 설정
}
# Password validation
# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/5.1/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.1/howto/static-files/
STATIC_URL = 'static/'
# Default primary key field type
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
#django-crontab설정
CRONJOBS = [
('0 6 * * 1', 'stocks.cron.update_tickers', ">> /var/log/dividend_stock.log"),
('0 7 * * 1', 'stocks.cron.check_and_filter_dividends', ">> /var/log/dividend_stock.log"),
('0 9 * * *', 'stocks.cron.update_dividend_data', ">> /var/log/dividend_stock.log"),
('0 10 * * *', 'stocks.cron.update_last_dividend', ">> /var/log/dividend_stock.log"),
('0 * * * *', 'stocks.cron.update_redis_data', ">> /var/log/dividend_stock.log")
]
REDIS_URL = env("REDIS_URL")
#redis 설정
CACHES = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': REDIS_URL
}
}
2-1. Docker test
그리고 DB를 바꿔주었으니 업데이트도 할 겸 Postgesql에 새로운 주식 데이터를 넣어주었다.
먼저 도커 빌드랑 업 해서 오류 없는지 확인해주고!
docker-compose build
docker-compose up
새 터미널을 열어서 web 컨테이너에 접속하자.
docker-compose exec web bash
그 뒤로는 전 글에서 알려줬던 거랑 똑같다.
쉘에 들어간 다음, 함수 import 해주고
실행하면 된다
python manage.py shell
from 앱이름.cron import update_tickers
update_tickers()
이런 식으로 한 줄씩 입력하면 잘 실행된다!
cron.py 안에 있는 함수를 일일히 실행해서 postgresql안에 데이터를 새로 넣어주면 된다.
사실 함수 실행하면서 시행착오가 없었던 건 아닌데, 시행착오가 궁금한 사람들은? ⬇️⬇️⬇️⬇️
cron.py에는 총 5개의 함수가 있다.
5. redis에 업데이트 하는 함수
일단 1번 함수는 진짜 너무 오래걸린다 ㅋㅋㅋㅋㅋㅋ
약 6천~7천개의 주식을 수집하는데 1시간 반 정도 걸린 거 같고..
2,3번도 다 오래걸린다.
데이터들을 수집하면서 3번함수를 돌릴 때 오류가 많이 떴었는데,
3번 함수 코드는 다음과 같다.
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', '정보없음') * 100
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 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)
# Redis에 데이터 저장
update_redis_data()
print("배당 주식의 데이터 수집과 redis 저장이 완료되었습니다.")
다음은 뜬 오류메시지들과 왜 이런 메시지가 떴는지 주석으로 정리해보았다.
# 숫자가 아닌 문자열에 비교연산자를 써서 발생한 오류
Error fetching data for ADBE: '<=' not supported between instances of 'int' and 'str'
# 배당일이 없는 경우
No 'Dividend Date' for ADBE, likely no dividends.
# 종가 구할 때 뜨는 오류, yfinance에 없는 주식인 경우가 대부분이었음
$HTLF: possibly delisted; no price data found (period=1d)
# info를 호출할 때 내부적으로 exchangeTimezoneName을 참조함,
# 'exchangeTimezoneName' 필드가 없을 경우 오류 메시지가 출력될 수 있음
$HFWA: possibly delisted; no timezone found
# 가격정보를 가져올 때 chart키를 사용해서 가져오는데, chart가 없을 경우
Could not get exchangeTimezoneName for ticker 'HFWA' reason: 'chart'
# ...ㅎ 데이터를 빠른시간안에 너무 많이 요청해서 생긴 에러 ..
Error fetching data for LINC: Too Many Requests. Rate limited. Try after a while.
401 Client Error: Unauthorized for url: https://query2.finance.yahoo.com/v10/finance/quoteSummary/SCI?modules=calendarEvents&corsDomain=finance.yahoo.com&formatted=false&symbol=SCI&crumb=Edge%3A+Too+Many+Requests
전에도 이 오류들이 떴었던 거 같은데,
오류를 정리했었는지 기억이 안나서 .. 그냥 이참에 다시 정리했다
첫 번째 오류는 배당률을 구하고, 배당률 기준으로 초고배당주 or 고중배당주로 나누는 과정에서 생긴 오류였고
마지막은 .... 원인을 알아도 해결할 수 없는 ... 오류이고
나머지는 yfinance에 데이터가 없어서 생긴 오류였다.
(실제 야후 파이낸스에 들어가 오류가 뜬 주식을 검색해보면 대부분의 데이터가 텅텅 비어있다.)
![](https://blog.kakaocdn.net/dn/RmwYA/btsMfb700hU/5HpkhZAY6r7kjJujwyh7H1/img.png)
사실상 내가 해결할 수 있는 오류는 첫번째 오류이기 때문에
가볍게 트러블 슈팅을 짚고 넘어가자
다음은 문제가 되었던 코드이다.
#배당률 구하는 함수
dividend_yield = ticker_data.info.get('dividendYield', '정보없음') * 100
# 배당률 4~7% 기준, 7%이상 기준으로 주식 나누기
dividend_yield_category = None
if 4 <= dividend_yield <= 7:
dividend_yield_category = "4_to_7"
elif dividend_yield > 7:
dividend_yield_category = "above_7"
배당률 구하는 함수는
- 배당률이 없을 경우 정보없음으로 하고 있을 경우 그 숫자대로 *100을 하는 게 끝이다
배당률 카테고리 설정하는 함수는
- 숫자와 비교해서 4_to_7 혹은 above_7 카테고리로 분류하는 함수인데
이 코드를 돌렸더니 다음과 같은 오류가 떴다.
Error fetching data for ADBE: '<=' not supported between instances of 'int' and 'str'
이 오류가 뜬 이유 ? 앞서 말했다시피 배당률이 없는 애들은 '정보없음'으로 설정해놨는데
카테고리 분류하는 과정에서 숫자랑 비교하니까 에러가 난 것이다...! (ㅋㅋ..멍청이슈..ㅠ)
해결방법은 그냥 간단하게, 숫자만 들어올 수 있도록 바꿔주었다.
# 배당률과 시가총액
dividend_yield = ticker_data.info.get('dividendYield', '정보없음')
if isinstance(dividend_yield, (int, float)): # 숫자인 경우에만 처리
dividend_yield = dividend_yield * 100
# 배당률 4~7% 기준, 7%이상 기준으로 주식 나누기
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"
안 궁금한 사람들은 ! 데이터가 postgresql에 잘 들어갔는지 한 번 확인해보자
docker 안에 있는 postgresql 접속하기
1. docker에 postgresql 컨테이너 접속하기
docker exec -it postgres-db bash
2. 유저 이름으로 접속
psql -U 설정해놓은 유저이름
3. 테이블 조회하기
#전체 테이블 조회
\dt
#개별 테이블 조회
SELECT * FROM 테이블이름
전체 주식들 중에 배당관련 데이터가 있는 주식만 수집하는 함수를 돌린 후,
데이터를 조회해봤다.
SELECT * FROM stocks_dividendticker;
사진처럼 잘 나오는 것을 확인할 수 있다.
터미널에서 db를 조회하다가 나가고 싶으면 다음 명령어를 쓰면 된다.
\q
matplotilb 폰트 변경하기
원래 폰트는 AppleGothic을 썼었다.
맥북에 있는 폰트여서 쓰기가 쉬웠기 때문이다.
근데 웬걸? ㅋㅋㅋㅋ 도커 설정 후 실행하니까 글자가 네모네모로 변해버렸다.
도커에 폰트 설정을 하고 지나가야한다는 걸 까먹고 있었다 .... ㅋㅋㅋㅋㅋㅋㅋ
그래서 나눔고딕으로 폰트를 변경해주었다.
#Dockerfile
# 프로젝트 요구사항 파일 복사 및 설치
COPY requirements.txt /app/
RUN apt-get update && apt-get install -y python3-pip cron fonts-nanum && \
pip install --no-cache-dir -r requirements.txt
#views.py
#폰트 설정
matplotlib.rc('font', family='NanumGothic')
도커가 처음에 이거저거 설치할 때 폰트도 같이 설치할 수 있도록 코드를 추가하고
detail view에서 matplotlib 폰트를 나눔고딕으로 설정해주었다.
결과는? 사진처럼 네모네모가 사라지고 글씨가 정상적으로 나오는 것을 볼 수 있다.
어우 ... 원래는 nginx까지 하려고 했으나 길이 길어지는 거 같아
다음글에서 해보자!