안녕하세요, 재무성장 연구소💰입니다. 매일 여러 증권사 앱을 돌아다니며 포트폴리오를 확인하는 번거로움에서 벗어나, 한 곳에서 모든 투자 현황을 자동으로 관리하는 스마트한 방법을 소개합니다.
파이썬과 노션 API를 활용한 실시간 주식 포트폴리오 자동화 시스템을 구축하면, 수동 입력 없이도 최신 시장 데이터가 자동으로 동기화되어 투자 현황을 한눈에 파악할 수 있습니다. 이 시스템은 노션을 사용자 인터페이스로, 파이썬을 자동화 엔진으로 활용하여 개인 맞춤형 금융 대시보드를 완성합니다.
투자 효율성을 높이는 다양한 도구들을 소개합니다
1. 노션 워크스페이스 설계와 API 통합 설정
💡 핵심 포인트: 노션을 단순한 메모 도구가 아닌 '금융 데이터 허브'로 활용하는 것이 이 시스템의 핵심입니다. 올바른 데이터베이스 구조 설계가 전체 자동화의 성공을 좌우합니다.
현대 투자자들은 국내외 주식, ETF, 암호화폐 등 다양한 자산을 여러 플랫폼에 분산 보유하고 있습니다. 이러한 정보를 하나로 통합하려면 수작업으로 엑셀에 입력하는 방법밖에 없었지만, 노션의 데이터베이스 기능과 파이썬 자동화를 결합하면 완전히 다른 차원의 관리 시스템을 구축할 수 있습니다.
⚠️ 보안 주의사항: 노션 API 키는 여러분의 워크스페이스에 대한 마스터 키입니다. 절대로 코드에 직접 입력하거나 공개된 곳에 노출하지 마세요. 이후 설명할 .env 파일 방식을 반드시 사용하시기 바랍니다.
효율적인 투자 도구 추천
2. 파이썬 개발 환경 구성과 필수 라이브러리
자동화 시스템의 엔진 역할을 할 파이썬 환경을 제대로 구성하는 것은 프로젝트의 안정성과 확장성을 결정하는 중요한 단계입니다. 가상 환경을 사용하여 의존성을 격리하고, 필요한 라이브러리만 깔끔하게 관리하는 것이 핵심입니다.
가상환경 생성과 활성화
# 프로젝트 폴더 생성 및 이동 mkdir notion-portfolio-automation cd notion-portfolio-automation # 가상환경 생성 python -m venv venv # 가상환경 활성화 (Windows) venv\Scripts\activate # 가상환경 활성화 (macOS/Linux) source venv/bin/activate
필수 라이브러리 설치
이 프로젝트에 필요한 핵심 라이브러리들을 requirements.txt 파일로 관리합니다:
# requirements.txt 파일 생성 후 일괄 설치 pip install -r requirements.txt
❌ 잘못된 방법
시스템 전역에 라이브러리 설치 API 키를 코드에 직접 입력 의존성 버전 관리 무시
VS
✅ 올바른 방법
가상환경으로 프로젝트 격리 .env 파일로 보안 정보 분리 requirements.txt로 버전 고정
환경변수 설정 (.env 파일)
보안이 중요한 API 키들은 별도의 .env 파일에 저장합니다:
# .env 파일 내용 NOTION_API_KEY=secret_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx NOTION_DATABASE_ID=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ALPHA_VANTAGE_API_KEY=xxxxxxxxxxxxxxxx # .gitignore 파일에 추가 (매우 중요!) .env venv/ __pycache__/ *.pyc
가상환경 생성 및 활성화 완료
필수 라이브러리 설치 확인
.env 파일 생성 및 API 키 입력
.gitignore 설정으로 보안 파일 제외
프로젝트 폴더 구조 정리
3. 금융 데이터 API 연동과 보안 설정
실시간 주가 정보를 가져오기 위해서는 신뢰할 수 있는 금융 데이터 API를 선택해야 합니다. 각 API의 특성과 제한사항을 이해하고, 프로젝트 규모에 맞는 최적의 선택을 하는 것이 중요합니다.
주요 금융 데이터 API 비교
API 서비스
무료 한도
데이터 지연시간
장단점
Alpha Vantage
일 25회
15-20분 지연
안정적이지만 제한적
yfinance
무제한
거의 실시간
무료지만 비공식적
Finnhub.io
분당 60회
실시간
관대한 무료 한도
IEX Cloud
제한적 샌드박스
실시간
전문가급, 유료 위주
yfinance를 활용한 주가 데이터 수집
초보자에게 가장 접근하기 쉽고 안정적인 yfinance 라이브러리를 사용한 구현 예시입니다:
import yfinance as yf from datetime import datetime def get_stock_price(ticker): """ 주어진 티커의 현재 주가를 조회하는 함수 """ try: stock = yf.Ticker(ticker) history = stock.history(period="1d") if not history.empty: current_price = history['Close'].iloc[-1] return round(float(current_price), 2) else: print(f"❌ {ticker} 데이터를 찾을 수 없습니다.") return None except Exception as e: print(f"❌ {ticker} 조회 중 오류: {str(e)}") return None # 사용 예시 apple_price = get_stock_price("AAPL") print(f"🍎 Apple 현재가: ${apple_price}")
🎯 투자자의 관점에서 보는 자동화의 진정한 가치
대부분의 투자자들은 자동화를 단순한 '편의 기능'으로 생각하지만, 실제로는 감정적 거래를 방지하고 일관된 투자 원칙을 유지하게 해주는 강력한 도구입니다. 매일 정해진 시간에 자동으로 업데이트되는 포트폴리오를 보며, 시장의 일시적 변동에 흔들리지 않는 장기적 관점을 유지할 수 있게 됩니다. 이것이 바로 자동화가 부(富)를 부르는 숨겨진 원리입니다.
Alpha Vantage API 활용 (대안)
더 안정적이고 공식적인 데이터가 필요한 경우 Alpha Vantage를 사용할 수 있습니다:
import requests import os from dotenv import load_dotenv load_dotenv() def get_stock_price_alpha_vantage(ticker): """ Alpha Vantage API를 사용한 주가 조회 """ api_key = os.getenv('ALPHA_VANTAGE_API_KEY') url = f"https://www.alphavantage.co/query" params = { 'function': 'GLOBAL_QUOTE', 'symbol': ticker, 'apikey': api_key } try: response = requests.get(url, params=params) response.raise_for_status() data = response.json() if 'Global Quote' in data: price = float(data['Global Quote']['05. price']) return round(price, 2) else: print(f"❌ {ticker} 데이터 형식 오류") return None except requests.exceptions.RequestException as e: print(f"❌ API 요청 실패: {str(e)}") return None
💡 Pro Tip: 개발 초기에는 yfinance로 빠르게 프로토타입을 만들고, 안정성이 중요한 운영 환경에서는 Alpha Vantage나 다른 공식 API로 전환하는 것을 추천합니다.
스마트한 투자 관리를 위한 도구들
4. 자동화 스크립트 핵심 로직 구현
이제 모든 구성 요소를 연결하여 실제로 노션 데이터베이스를 자동으로 업데이트하는 핵심 스크립트를 구현해보겠습니다. 이 스크립트는 노션에서 포트폴리오 정보를 읽어오고, 최신 주가를 조회한 후, 다시 노션에 업데이트하는 전체 프로세스를 담당합니다.
메인 자동화 스크립트 (main.py)
import os import logging from datetime import datetime from dotenv import load_dotenv from notion_client import Client import yfinance as yf # 환경변수 로드 load_dotenv() # 로깅 설정 logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler('portfolio_update.log', encoding='utf-8'), logging.StreamHandler() ] ) class PortfolioAutomation: def __init__(self): self.notion = Client(auth=os.getenv('NOTION_API_KEY')) self.database_id = os.getenv('NOTION_DATABASE_ID') def get_portfolio_items(self): """노션 데이터베이스에서 포트폴리오 항목들을 가져옵니다""" try: response = self.notion.databases.query(database_id=self.database_id) return response.get('results', []) except Exception as e: logging.error(f"포트폴리오 조회 실패: {str(e)}") return [] def get_stock_price(self, ticker): """yfinance를 사용하여 주가를 조회합니다""" try: stock = yf.Ticker(ticker) history = stock.history(period="1d") if not history.empty: current_price = history['Close'].iloc[-1] return round(float(current_price), 2) else: logging.warning(f"{ticker} 데이터가 없습니다") return None except Exception as e: logging.error(f"{ticker} 주가 조회 실패: {str(e)}") return None def update_notion_page(self, page_id, current_price): """노션 페이지의 현재가와 업데이트 시간을 갱신합니다""" try: properties = { "현재가": {"number": current_price}, "마지막업데이트": { "date": {"start": datetime.now().isoformat()} } } self.notion.pages.update(page_id=page_id, properties=properties) return True except Exception as e: logging.error(f"페이지 업데이트 실패: {str(e)}") return False def run_automation(self): """전체 자동화 프로세스를 실행합니다""" logging.info("=== 포트폴리오 자동 업데이트 시작 ===") portfolio_items = self.get_portfolio_items() if not portfolio_items: logging.warning("포트폴리오 항목이 없습니다") return success_count = 0 total_count = len(portfolio_items) for item in portfolio_items: try: # 노션 페이지에서 필요한 정보 추출 page_id = item['id'] properties = item['properties'] # 자산명과 티커 정보 가져오기 title_property = properties.get('자산명', {}) if title_property.get('title'): asset_name = title_property['title'][0]['text']['content'] else: continue ticker_property = properties.get('티커', {}) if ticker_property.get('rich_text'): ticker = ticker_property['rich_text'][0]['text']['content'] else: logging.warning(f"{asset_name}: 티커 정보가 없습니다") continue # 현재 주가 조회 current_price = self.get_stock_price(ticker) if current_price is not None: # 노션 페이지 업데이트 if self.update_notion_page(page_id, current_price): logging.info(f"✅ {asset_name} ({ticker}): ${current_price}") success_count += 1 else: logging.error(f"❌ {asset_name} 업데이트 실패") else: logging.warning(f"⚠️ {asset_name} ({ticker}): 주가 조회 실패") except Exception as e: logging.error(f"처리 중 오류: {str(e)}") continue logging.info(f"=== 업데이트 완료: {success_count}/{total_count} ===") if __name__ == "__main__": automation = PortfolioAutomation() automation.run_automation()
스크립트 실행 및 테스트
1
환경변수 확인: .env 파일에 올바른 API 키와 데이터베이스 ID가 설정되어 있는지 확인
2
노션 데이터 준비: 포트폴리오 데이터베이스에 최소 1-2개의 테스트 종목 입력
3
스크립트 실행:python main.py 명령어로 자동화 테스트
4
결과 확인: 노션 데이터베이스와 로그 파일에서 업데이트 결과 검증
⚠️ 주의사항: 처음 실행할 때는 소수의 종목으로 테스트한 후, 정상 동작을 확인하고 나서 전체 포트폴리오에 적용하세요. API 호출 한도를 초과하지 않도록 주의하시기 바랍니다.
5. 클라우드 배포와 스케줄링 설정
개인 컴퓨터에 의존하지 않고 클라우드에서 자동으로 실행되는 진정한 자동화 시스템을 구축하기 위해 GitHub Actions를 활용합니다. 이는 마이크로소프트가 제공하는 무료 클라우드 컴퓨팅 자원을 활용하여 24시간 안정적인 서비스를 만드는 방법입니다.
GitHub Repository 설정
# 1. GitHub에 새 저장소 생성 # 2. 로컬 프로젝트를 GitHub에 업로드 git init git add . git commit -m "Initial commit: Portfolio automation system" git branch -M main git remote add origin https://github.com/yourusername/notion-portfolio-automation.git git push -u origin main
GitHub Secrets 설정
민감한 정보를 안전하게 관리하기 위해 GitHub Secrets를 설정합니다:
GitHub 저장소 → Settings → Secrets and variables → Actions 메뉴 접속
New repository secret 클릭하여 환경변수 추가
NOTION_API_KEY: 노션 통합 시크릿 키
NOTION_DATABASE_ID: 포트폴리오 데이터베이스 ID
ALPHA_VANTAGE_API_KEY: 금융 API 키 (선택사항)
GitHub Actions 워크플로우 파일
프로젝트 루트에 .github/workflows/portfolio-update.yml 파일을 생성합니다:
name: Portfolio Auto Update on: schedule: # 평일 오후 6시 (UTC 기준 9시)에 실행 - cron: '0 9 * * 1-5' workflow_dispatch: # 수동 실행 허용 jobs: update-portfolio: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v4 with: python-version: '3.11' - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements.txt - name: Run portfolio automation env: NOTION_API_KEY: ${{ secrets.NOTION_API_KEY }} NOTION_DATABASE_ID: ${{ secrets.NOTION_DATABASE_ID }} ALPHA_VANTAGE_API_KEY: ${{ secrets.ALPHA_VANTAGE_API_KEY }} run: | python main.py - name: Upload logs uses: actions/upload-artifact@v3 if: always() with: name: portfolio-logs path: portfolio_update.log
스케줄링 옵션
실행 주기
Cron 표현식
설명
매일 오후 6시
0 9 * * *
장 마감 후 업데이트
평일만 오후 6시
0 9 * * 1-5
주말 제외 (권장)
매주 일요일 오전 9시
0 0 * * 0
주간 리포트용
매월 1일 오전 9시
0 0 1 * *
월간 리밸런싱용
🚀 이제 완전 자동화 시스템이 완성되었습니다!
설정된 시간에 GitHub 클라우드에서 자동으로 스크립트가 실행되어 여러분의 노션 포트폴리오가 최신 상태로 업데이트됩니다.
사람의 개입 없이 자동으로 실행되는 시스템에서는 예상치 못한 오류에 대한 대응 능력이 매우 중요합니다. 네트워크 장애, API 서버 다운, 데이터 형식 변경 등 다양한 예외 상황에서도 시스템이 안정적으로 동작하도록 견고한 오류 처리 로직을 구현해야 합니다.
고급 오류 처리 및 재시도 로직
import time import logging from functools import wraps def retry_on_failure(max_retries=3, delay=5): """함수 실행 실패 시 자동 재시도하는 데코레이터""" def decorator(func): @wraps(func) def wrapper(*args, **kwargs): for attempt in range(max_retries): try: return func(*args, **kwargs) except Exception as e: if attempt == max_retries - 1: logging.error(f"{func.__name__} 최종 실패: {str(e)}") raise else: logging.warning(f"{func.__name__} 재시도 {attempt + 1}/{max_retries}: {str(e)}") time.sleep(delay) return None return wrapper return decorator class EnhancedPortfolioAutomation: def __init__(self): self.notion = Client(auth=os.getenv('NOTION_API_KEY')) self.database_id = os.getenv('NOTION_DATABASE_ID') self.failed_updates = [] @retry_on_failure(max_retries=3, delay=10) def get_stock_price_with_retry(self, ticker): """재시도 로직이 포함된 주가 조회""" stock = yf.Ticker(ticker) history = stock.history(period="1d") if history.empty: raise ValueError(f"No data found for {ticker}") current_price = history['Close'].iloc[-1] return round(float(current_price), 2) def validate_notion_response(self, response): """노션 API 응답 유효성 검사""" if not response: raise ValueError("Empty response from Notion API") if 'results' not in response: raise ValueError("Invalid response format from Notion API") return True def send_error_notification(self, error_message): """오류 발생 시 알림 전송 (이메일, 슬랙 등)""" # 여기에 이메일이나 슬랙 알림 로직을 추가할 수 있습니다 logging.critical(f"🚨 CRITICAL ERROR: {error_message}") def run_safe_automation(self): """안전한 자동화 실행""" try: logging.info("=== 포트폴리오 자동 업데이트 시작 ===") # 노션 연결 상태 확인 response = self.notion.databases.query( database_id=self.database_id, page_size=1 ) self.validate_notion_response(response) # 전체 포트폴리오 조회 portfolio_items = self.get_portfolio_items() if not portfolio_items: logging.warning("포트폴리오 항목이 없습니다") return success_count = 0 total_count = len(portfolio_items) for item in portfolio_items: try: success = self.process_single_item(item) if success: success_count += 1 except Exception as e: logging.error(f"항목 처리 실패: {str(e)}") self.failed_updates.append({ 'item_id': item.get('id', 'unknown'), 'error': str(e), 'timestamp': datetime.now().isoformat() }) continue # 결과 리포트 self.generate_update_report(success_count, total_count) except Exception as e: error_msg = f"전체 자동화 프로세스 실패: {str(e)}" self.send_error_notification(error_msg) logging.critical(error_msg) def process_single_item(self, item): """개별 포트폴리오 항목 처리""" page_id = item['id'] properties = item['properties'] # 안전한 데이터 추출 asset_name = self.safe_extract_title(properties.get('자산명', {})) ticker = self.safe_extract_text(properties.get('티커', {})) if not asset_name or not ticker: logging.warning(f"필수 정보 누락: {asset_name or 'Unknown'}") return False # 주가 조회 (재시도 포함) current_price = self.get_stock_price_with_retry(ticker) if current_price is not None: # 노션 업데이트 if self.update_notion_page(page_id, current_price): logging.info(f"✅ {asset_name} ({ticker}): ${current_price}") return True return False def safe_extract_title(self, title_property): """안전한 제목 추출""" try: if title_property.get('title') and len(title_property['title']) > 0: return title_property['title'][0]['text']['content'] except (KeyError, IndexError, TypeError): pass return None def safe_extract_text(self, text_property): """안전한 텍스트 추출""" try: if text_property.get('rich_text') and len(text_property['rich_text']) > 0: return text_property['rich_text'][0]['text']['content'] except (KeyError, IndexError, TypeError): pass return None def generate_update_report(self, success_count, total_count): """업데이트 결과 리포트 생성""" success_rate = (success_count / total_count * 100) if total_count > 0 else 0 logging.info(f"=== 업데이트 완료 ===") logging.info(f"성공: {success_count}/{total_count} ({success_rate:.1f}%)") if self.failed_updates: logging.warning(f"실패한 항목: {len(self.failed_updates)}개") for failure in self.failed_updates: logging.warning(f" - {failure['item_id']}: {failure['error']}") # 성공률이 낮으면 알림 if success_rate < 80: self.send_error_notification(f"업데이트 성공률 낮음: {success_rate:.1f}%")
로그 분석 및 모니터링
💡 모니터링 Best Practice:
GitHub Actions의 'Actions' 탭에서 실행 결과 확인
로그 파일을 정기적으로 다운로드하여 패턴 분석
연속 실패 시 이메일/슬랙 알림 설정
API 호출 한도 모니터링으로 사전 대응
재시도 로직으로 일시적 오류 자동 복구
데이터 유효성 검사로 잘못된 입력 방지
상세한 로깅으로 문제 상황 추적 가능
실패한 항목 별도 기록으로 사후 처리
성공률 모니터링으로 시스템 건강도 확인
🙋♂️ 자주 묻는 질문
노션 API 키가 유출되면 어떻게 되나요?
API 키가 유출되면 해커가 여러분의 노션 워크스페이스에 무제한 접근할 수 있습니다. 즉시 노션 통합 페이지에서 해당 통합을 삭제하고 새로운 통합을 생성하세요. 앞으로는 .env 파일과 .gitignore 설정을 반드시 확인하시기 바랍니다.
주가 데이터가 업데이트되지 않는 이유는?
가장 흔한 원인은 ①잘못된 티커 심볼, ②API 호출 한도 초과, ③네트워크 연결 문제입니다. GitHub Actions의 실행 로그를 확인하여 구체적인 오류 메시지를 파악하고, 필요시 티커 심볼을 올바른 형식(예: AAPL, GOOGL)으로 수정하세요.
해외 주식과 국내 주식을 동시에 관리할 수 있나요?
네, 가능합니다. yfinance는 전 세계 대부분의 주요 거래소를 지원합니다. 국내 주식의 경우 티커 뒤에 '.KS'(코스피) 또는 '.KQ'(코스닥)를 붙여주세요. 예: 삼성전자는 '005930.KS', 카카오는 '035720.KS'로 입력하면 됩니다.
GitHub Actions 무료 사용량을 초과하면 어떻게 되나요?
GitHub의 무료 계정은 월 2,000분의 실행 시간을 제공합니다. 이 시스템은 한 번 실행에 약 1-2분 소요되므, 매일 실행해도 월 60분 정도만 사용합니다. 혹시 한도를 초과하더라도 과금되지 않고 단순히 실행이 중단될 뿐입니다.
더 많은 기능을 추가하고 싶다면 어떻게 해야 하나요?
이 시스템은 확장 가능하도록 설계되었습니다. ①암호화폐 지원(ccxt 라이브러리), ②환율 자동 변환(exchangerate-api), ③이메일 알림(smtplib), ④차트 생성(matplotlib) 등의 기능을 추가할 수 있습니다. 각 기능은 독립적인 모듈로 개발하여 기존 시스템에 영향을 주지 않고 확장 가능합니다.
🎯 핵심 요약: 완전 자동화된 포트폴리오 관리 시스템 완성
노션 API와 파이썬을 활용한 실시간 주식 포트폴리오 자동화를 통해 수동 작업 없이도 투자 현황을 실시간으로 파악할 수 있게 되었습니다. 클라우드 기반 스케줄링으로 24시간 안정적으로 작동하며, 견고한 오류 처리 시스템으로 장기간 무인 운영이 가능합니다.
이제 여러분은 데이터 입력의 번거로움에서 벗어나 진정한 투자 전략 수립과 실행에만 집중할 수 있습니다. 이것이 바로 기술이 투자자에게 주는 가장 큰 선물입니다.
본 글의 정보는 작성일 기준이며 API 정책이나 서비스 변경으로 인해 내용이 달라질 수 있습니다. 투자 결정은 항상 본인의 판단과 책임 하에 이루어져야 하며, 자동화 시스템 사용 시에도 주기적인 점검과 관리가 필요합니다. 코딩이나 API 사용 중 발생하는 오류나 손실에 대해서는 책임지지 않습니다.