GitHub Actions로 주식 시세를 매일 자동으로 가져오는 법
- GitHub Actions는 CI/CD 도구가 아니라 무료 스케줄러로 쓸 수 있다.
- pykrx(국내 ETF) + yfinance(미국 주식)를 조합해서 실제 시세를 가져온다.
- 장 마감 시간에 맞춰 하루 2회 자동 실행 — 수동 확인이 사라졌다.
매일 하던 일이 있었다
투자 대시보드를 만들고 나서 한 가지 문제가 생겼다.
데이터가 수동이었다. 종목별 현재가를 직접 검색해서 portfolio.json에 넣어야 했는데, 9개 ETF에 미국 주식 몇 개까지 합치면 매일 아침 15분짜리 작업이 됐다. 대시보드를 만든 이유가 이 15분을 없애려는 거였는데 본말이 전도된 상황이었다.
GitHub Actions를 스케줄러로 쓸 수 있다는 걸 어디서 봤는데, 그때는 "CI/CD 도구 아닌가" 싶어서 넘겼다. 직접 써보고 나서야 이게 그냥 무료 서버에서 파이썬 돌려주는 서비스라는 걸 이해했다.
GitHub Actions를 스케줄러로 쓴다는 게 무슨 말인가
보통 GitHub Actions는 "코드 푸시하면 테스트 자동 실행" 같은 용도로 쓴다. 근데 트리거 중에 schedule이 있다. cron 문법으로 시간을 지정하면 그 시간에 알아서 실행된다.
무료 플랜 기준으로 월 2,000분이 주어진다. 매일 평일 2회 실행하고 1회당 2~3분이면 월 40~60분 정도다. 남는 게 훨씬 많다.
평일 2회 × 약 3분 = 6분/일
6분 × 22일(평일) = 132분/월
→ 여유분 1,868분 — 부담 없음
실제 워크플로우 파일 — `daily-update.yml`
실제로 쓰고 있는 파일이다. 설명을 위해 일부 단순화했다.
name: Daily Portfolio Update
on:
schedule:
# UTC 09:30 = KST 18:30 (장마감 후 30분)
- cron: '30 9 * * 1-5'
# UTC 11:30 = KST 20:30 (KRX 데이터 완전 반영 후)
- cron: '30 11 * * 1-5'
workflow_dispatch: # 수동 실행 버튼도 남겨둠
jobs:
update:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
run: pip install pykrx yfinance pandas requests pytz
- name: Update prices
run: python scripts/update_prices.py
- name: Commit updated data
run: |
git config user.name "portfolio-bot"
git config user.email "portfolio-bot@users.noreply.github.com"
git add data/prices.json
git diff --cached --quiet && echo "변경없음" || \
git commit -m "chore: update prices $(date +'%Y-%m-%d %H:%M KST')"
git push
몇 가지 짚고 넘어갈 것들
cron 식을 읽는 법
'30 9 * * 1-5'는 "분 시 일 월 요일" 순서다. 1-5는 월~금. 익숙해지기 전까진 crontab.guru에 붙여넣으면 바로 번역해준다.
GitHub Actions는 UTC 기준이다
한국 시간이 아니다. KST 18:30에 실행하려면 UTC 09:30을 입력해야 한다. 처음에 이걸 몰라서 엉뚱한 시간에 돌아갔다. 여름/겨울 시간 변경도 없으니 KST = UTC+9로 고정 계산하면 된다.
workflow_dispatch는 수동 버튼
GitHub 레포지토리 → Actions 탭에서 "Run workflow" 버튼이 생긴다. 테스트하거나 장 휴장일 다음날 바로 돌릴 때 쓴다.
permissions: contents: write가 없으면 push가 막힌다
기본 권한으로는 워크플로우에서 레포에 쓰기가 안 된다. 이 한 줄을 빼먹으면 commit까지 됐다가 push에서 403 에러가 난다. 삽질 30분 보장.
시세 데이터를 가져오는 파이썬 스크립트
국내 ETF는 pykrx, 미국 주식·ETF는 yfinance를 쓴다. 두 라이브러리를 조합하는 게 핵심이다.
import json
from datetime import datetime, timezone, timedelta
from pykrx import stock
import yfinance as yf
KST = timezone(timedelta(hours=9))
def get_krx_price(ticker: str) -> dict:
"""국내 ETF/주식 — pykrx 사용"""
now = datetime.now(KST)
for days_back in range(7): # 최대 7일 전까지 조회 (휴장일 대응)
date = (now - timedelta(days=days_back)).strftime("%Y%m%d")
try:
df = stock.get_market_ohlcv_by_ticker(date, market="ALL")
if ticker in df.index:
row = df.loc[ticker]
close = float(row.get("종가", 0))
if close > 0:
change = float(row.get("등락폭", 0))
return {
"price": close,
"change": change,
"changeRate": float(row.get("등락률", 0)) / 100,
"source": "pykrx"
}
except Exception:
continue
return None
def get_us_price(ticker: str) -> dict:
"""미국 주식/ETF — yfinance 사용"""
hist = yf.Ticker(ticker).history(period="5d", interval="1d")
if hist.empty:
return None
closes = hist["Close"].dropna().tolist()
price = float(closes[-1])
prev = float(closes[-2]) if len(closes) >= 2 else price
return {
"price": round(price, 2),
"change": round(price - prev, 4),
"changeRate": round((price - prev) / prev, 6),
"source": "yfinance"
}
pykrx를 쓸 때 알아야 할 것
pykrx는 KRX(한국거래소)에서 데이터를 가져온다. 공식 API가 아니라 웹 스크래핑 방식이라 가끔 느리거나 타임아웃이 난다.
그래서 range(7)로 최대 7일 전까지 루프를 돌린다. 오늘 데이터가 없으면 어제, 어제도 없으면 그제 — 이렇게 가장 최근 거래일 데이터를 찾는다. 연휴가 5일 이상 이어지는 경우가 없으니 7일이면 충분하다.
그리고 한 가지 더. pykrx의 시세는 장 마감 후 20~30분 지연된다. 처음에 이걸 몰랐는데 15:30에 바로 돌렸더니 아직 당일 데이터가 없었다. 그래서 18:30으로 늦췄다.
API 키 없이 쓸 수 있나
pykrx와 yfinance 둘 다 API 키가 필요 없다. 무료로 쓸 수 있다.
다만 yfinance는 Yahoo Finance의 비공식 API를 쓰기 때문에 가끔 응답이 달라지거나 막히는 경우가 있다. 지금까지 3개월 넘게 쓰면서 한 번 막힌 적이 있었는데 며칠 후 풀렸다. 안정성 자체는 나쁘지 않다.
만약 API 키가 필요한 서비스를 쓴다면 GitHub Secrets에 저장하면 된다.
- 레포지토리 → Settings → Secrets and variables → Actions
- New repository secret 클릭
- Name:
MY_API_KEY, Value: 실제 키 값 입력 - 워크플로우에서
${{ secrets.MY_API_KEY }}로 참조
키 값은 저장 후 다시 볼 수 없다. 어딘가에 따로 백업해두는 게 좋다.
실제로 겪은 문제들
깔끔하게 된 것처럼 썼지만 그렇지 않았다. 기억나는 것들만 정리한다.
장 휴장일에 빈 커밋이 생겼다
처음엔 변경사항이 없어도 커밋을 하는 코드를 썼다. 그러면 공휴일마다 "변경없음" 커밋이 쌓였다. git diff --cached --quiet로 변경사항이 있을 때만 커밋하도록 바꿨다.
pykrx에서 종목 코드를 못 찾는 경우
ETF 종목 코드가 6자리인데, 앞에 0이 붙는 경우 JSON에서 숫자로 저장하면 0이 사라진다. "360200"이 아니라 360200으로 저장되는 것. 문자열로 명시하거나 코드 변환할 때 zfill(6)을 써서 해결했다.
push 권한 에러
앞서 언급한 permissions: contents: write 문제다. 에러 메시지가 꽤 친절한 편이라 메시지 읽으면 바로 해결된다. 근데 처음엔 뭘 봐야 할지 몰라서 한참 헤맸다.
지금은 이렇게 쓰고 있다
평일 18:30과 20:30 두 번 돌린다. 18:30은 장 마감 직후라 KRX 데이터가 간혹 늦게 반영되는 경우가 있어서 20:30에 한 번 더 돌려서 확인한다.
아침에 대시보드를 열면 전날 종가 기준 시세가 이미 들어와 있다. 15분짜리 수동 작업이 없어졌다.
완벽하진 않다. 간혹 pykrx 타임아웃으로 실패하는 날이 있고, 그럴 때는 Actions 탭에서 수동으로 다시 돌린다. 3개월에 두 번 정도 그랬던 것 같다.
설계도
[트리거]
cron: 평일 18:30 KST (1차)
cron: 평일 20:30 KST (2차)
workflow_dispatch (수동)
↓
[GitHub Actions Runner]
ubuntu-latest 컨테이너 생성
↓
[Python 실행: update_prices.py]
국내 ETF ──▶ pykrx ──▶ KRX 거래소
미국 주식 ──▶ yfinance ──▶ Yahoo Finance
환율 ──▶ yfinance ──▶ (USDKRW, JPYKRW)
↓
data/prices.json 갱신
↓
[Git 커밋 & Push]
변경사항 있을 때만 커밋
portfolio-bot 이름으로 push
↓
[Vercel 자동 감지]
main 브랜치 push → 빌드 트리거
↓
[대시보드 갱신]
최신 시세 반영된 화면 확인 가능
⚠️ 이 글은 개인적인 투자 자동화 구현 기록입니다. 특정 종목의 매수·매도를 추천하지 않으며, 모든 투자 판단의 책임은 독자 본인에게 있습니다.