LS증권 신 OpenAPI 로 실시간 뉴스 키워드 텔레그램 알림 봇 만들기 — 구 xingAPI 와 뭐가 다른가
"실시간으로 쏟아지는 증권 뉴스 중에서 내가 관심 있는 키워드나 종목 관련 뉴스만 골라서 텔레그램으로 받고 싶어요" — 뉴스 기반 트레이더들이 많이 하는 요청입니다. 직접 RSS·크롤링으로 해보면 지연(5~30분) 이 커서 제시간에 반응 못 하는 경우가 대부분이에요. 실시간 증권 뉴스는 증권사 API 에서만 제대로 받을 수 있고, 한국에서 실시간 뉴스 API 가 강한 곳이 LS증권(구 이베스트) 입니다. 이 글에서는 LS 신 OpenAPI + 네이버 뉴스 API + 텔레그램을 연결한 실시간 키워드 알림 봇 제작 사례를 공유합니다. 매매는 안 하는 알림 전용 프로젝트라 트레이딩 봇과는 설계 관점이 좀 달라요.
- 플랫폼
- LS증권 신 OpenAPI (REST + WebSocket) — 구 xingAPI (COM, 32bit) 와 별개 서비스
- 런타임
- Python 3.10 64bit · PyQt5 · websockets · requests · PyInstaller 단일 exe
- 입력
- LS WebSocket NWS/NWS001 실시간 뉴스 스트림 (분당 수~수십 건)
- 처리
- 감지 키워드 매칭 → KRX 전종목(약 2,700개) 중 종목명·코드 추출 → 네이버 뉴스 API 2시간 검색 (30초×20회 재시도) → 제외키워드/동일 언론사 필터 → 텔레그램 발송
- 출력
- 텔레그램 메시지 (종목명 · 뉴스 제목 · 링크 3줄 포맷)
- 실행 환경
- Windows 10/11 64bit · 24시간 상시 실행 · 시스템 트레이 상주
왜 신 OpenAPI 를 선택했나 — 구 xingAPI 와의 차이
LS증권은 실시간 증권 뉴스가 강점인데, API 가 두 가지예요. 많이들 헷갈리는 부분이라 먼저 정리합니다:
구 xingAPI
- COM 기반, Windows 32bit 전용
- DevCenter 설치 필요 (별도 인증 툴)
- .res 파일(TR 스펙 정의) 관리 필요
- Python 연동 시
pywin32+ COM 레퍼런스 관리 복잡 - 오래된 만큼 레퍼런스·질문 자료는 풍부
신 OpenAPI (이 프로젝트에서 선택)
- 순수 HTTP REST + WebSocket
- 64bit Python 과 호환
- OAuth2 토큰 (client_credentials) · 24시간 자동 갱신
- WebSocket 은
tr_cd/tr_key조합으로 원하는 채널 구독 - 레퍼런스가 아직 적음 (이게 이 블로그 글의 가치)
이번 프로젝트는 뉴스 모니터링이라 1~3초 실시간이면 충분 → WebSocket 으로 해결. 매매가 아니라서 .res·DevCenter 설치 같은 구 xingAPI 부담을 안고 갈 이유가 없었어요.
특히 배포용 .exe 를 64bit 로 빌드할 수 있다는 게 컸습니다 — 32bit 바이너리는 설치·실행 환경 분쟁이 자주 생기거든요.
처리 파이프라인
전체 흐름은 이렇게 구성했어요:
- LS WebSocket (NWS/NWS001): 실시간 뉴스 수신 → 제목 + 본문 일부 + 언론사 + 링크
- Keyword Matcher: 사용자 등록 키워드(엑셀) 와 매칭
- Stock Resolver (KRX CSV): 제목에서 종목명·코드 추출 (긴 이름 우선 + 단어 경계)
- Naver Cross-Check: 네이버 뉴스 API 로 "같은 주제의 다른 언론사 기사" 가 있는지 2시간 윈도우 검색 (정확도 향상)
- Filter: 제외 키워드 / 동일 언론사 중복 제거
- Telegram: HTML parse mode 로 3줄 포맷 메시지 전송
"왜 네이버 교차검증까지?" 라고 물으실 수 있는데, LS 뉴스 단독만 쓰면 오보·찌라시 기사도 알림이 가는 문제가 있어요. 복수 언론사에서 같은 주제를 다루고 있는지 네이버로 한 번 걸러주면 품질이 확 올라갑니다.
📌 SK하이닉스 (000660)
SK하이닉스, 3분기 영업이익 7조 돌파… 반도체 업황 개선 기대
🔗 https://news.example.com/article/...
함정 1. PyInstaller 빌드하자마자 "No module named 'jaraco'" 로 즉사
첫 빌드한 .exe 를 실행하니 바로 이 에러가 뜨면서 종료됐어요:
Failed to execute script 'pyi_rth_pkgres' due to unhandled exception:
No module named 'jaraco'
원인을 추적해보니 PyInstaller 의 pyi_rth_pkgres 런타임 훅이 pkg_resources 를 초기화할 때
내부 의존성 jaraco.text, jaraco.functools, more_itertools, platformdirs 를 동적 import 하는데,
PyInstaller 가 이걸 자동 수집하지 못하는 문제였습니다.
배경은 신 setuptools (82+) 가 pkg_resources 를 deprecate 하면서 의존 관계가 복잡해진 것. 2024년 이후 새로 빌드 환경 꾸리면 거의 확정적으로 만나는 함정입니다.
해결: 의존성 명시 + hidden-import 패턴
# 1) setuptools 를 81 이하로 고정 (pkg_resources 되살림)
pip install "setuptools<81"
# 2) jaraco 계열 수동 설치
pip install jaraco.text jaraco.functools jaraco.context \
jaraco.collections more_itertools platformdirs
# 3) PyInstaller 호출 시 명시적 수집
pyinstaller --onefile --windowed \
--collect-submodules pkg_resources \
--hidden-import jaraco.text \
--hidden-import jaraco.functools \
--hidden-import jaraco.context \
--hidden-import more_itertools \
--hidden-import platformdirs \
main.py
pykrx·pandas·openpyxl 처럼 pkg_resources 를 쓰는 라이브러리가 의존성에 있으면 이 함정은 거의 항상 발생해요.
빌드 스크립트 템플릿에 위 6개 hidden-import 를 기본으로 포함시키는 게 안전합니다.
함정 2. GUI [시작] 버튼 누르면 20초 동안 멈춤 — asyncio + thread 하이브리드
고객이 "시작/중지 버튼 누르면 창이 수 초간 반응이 없다" 고 보고. 최악의 경우 텔레그램 서버가 느릴 때 20초 이상 freeze 됐어요.
원인은 app.start() / app.stop() 안에서 동기 호출이 GUI 스레드를 블로킹하는 것:
notifier.notify_status(...): 텔레그램 HTTP POST + 실패 시 3회 × 5초 재시도 = 최대 25초ws.stop(): 내부에서thread.join(timeout=5)— WebSocket 수신 중이면 최대 5초 대기
해결: 버튼 핸들러 백그라운드 스레드화 + pyqtSignal 콜백
# gui/main_window.py
class MainWindow(QMainWindow):
action_finished = pyqtSignal(str, bool) # (action, success)
def _handle_start(self):
# 버튼 즉시 비활성화 + 텍스트 변경 (UI 스레드에서 즉시 반영)
self.btn_start.setEnabled(False)
self.btn_start.setText("시작 중…")
# 실제 작업은 백그라운드 스레드로
threading.Thread(
target=self._run_action,
args=("start",),
daemon=True,
).start()
def _run_action(self, action: str):
try:
if action == "start":
self.app.start() # 최대 25초 걸려도 UI 영향 없음
else:
self.app.stop()
self.action_finished.emit(action, True)
except Exception as e:
self.log.exception(f"{action} 실패: {e}")
self.action_finished.emit(action, False)
def _on_action_finished(self, action: str, success: bool):
# 시그널로 다시 UI 스레드 복귀 (크로스스레드 안전)
self.btn_start.setEnabled(True)
self.btn_start.setText("시작" if action == "stop" else "중지")
핵심 규칙: PyQt5 GUI 버튼 핸들러 안에서 네트워크 I/O 또는 thread.join 을 직접 호출하지 말 것. 반드시 백그라운드 스레드 경유 + 시그널로 결과 전달.
이 패턴은 PyQt5 + 네트워크 API 쓰는 모든 프로젝트에 그대로 재사용 가능합니다.
함정 3. KRX 종목 리스트 로드 실패 — pykrx 대신 공식 CSV
처음엔 pykrx 라이브러리로 KRX 전종목을 가져왔는데 간헐적으로 빈 응답이 와서 0종목 로드되는 경우가 발생.
원인은 pykrx 가 내부적으로 KRX DB 스크래핑이라서 세션 쿠키·CSRF 토큰 관리가 완전하지 않기 때문입니다.
거기다 pandas/numpy/matplotlib 까지 끌고 와서 배포 패키지가 ~300MB로 확 커지는 부작용도 있었어요.
해결: KRX 공식 CSV 직접 다운로드
# matcher/stock_resolver.py
KRX_CORP_LIST_URL = (
"https://kind.krx.co.kr/corpgeneral/corpList.do"
"?method=download&searchType=13"
)
def load_from_krx() -> dict[str, str]:
"""KRX 공식 상장법인목록에서 회사명 → 종목코드 매핑"""
resp = requests.get(KRX_CORP_LIST_URL, timeout=10)
resp.raise_for_status()
# HTML 테이블 형식 CSV 반환됨
df = pd.read_html(io.BytesIO(resp.content))[0]
return {
str(row["회사명"]).strip(): str(row["종목코드"]).zfill(6)
for _, row in df.iterrows()
if pd.notna(row["회사명"]) and pd.notna(row["종목코드"])
}
공식 다운로드 URL 이 있으면 서드파티 래퍼 대신 직접 쓰는 게 훨씬 안정적이에요. 배포 용량도 줄어들고요.
함정 4. 종목명 부분 매칭 오탐 — "예스24" 가 "한세예스24그룹" 에서 잡히는 문제
뉴스 제목에서 종목명을 추출할 때 단순 in 연산자로 하면 문제가 생깁니다.
예: "한세예스24그룹" 기사를 처리할 때 "예스24" 도 매칭되어서 엉뚱한 종목 알림이 날아가는 거예요.
해결: 긴 이름 우선 + 단어 경계 검증:
# matcher/stock_resolver.py
def find_in_title(self, title: str) -> list[tuple[str, str]]:
"""제목에서 종목명 찾기 — 긴 이름 우선 + 단어 경계"""
matches = []
# 긴 이름부터 시도 (정렬로 우선순위 부여)
for name in sorted(self.name_to_code.keys(), key=len, reverse=True):
idx = title.find(name)
if idx == -1:
continue
# 앞 글자가 한글/영문/숫자면 단어 중간이라 거부
if idx > 0:
prev_char = title[idx - 1]
if prev_char.isalnum() or _is_hangul(prev_char):
continue
# 뒤 글자도 마찬가지
end = idx + len(name)
if end < len(title):
next_char = title[end]
if next_char.isalnum() or _is_hangul(next_char):
continue
matches.append((name, self.name_to_code[name]))
return matches
이렇게 하면 "한세예스24그룹" 에서는 "한세예스24그룹" 만 잡고 "예스24" 는 건너뜁니다 (앞 글자 "한" 이 한글이라 거부). 뉴스·공시 텍스트 처리하는 거의 모든 프로젝트에서 써먹을 수 있는 패턴이에요.
→ 전혀 관련 없는 예스24 로 오탐
→ 잘못된 알림 발송
→ 긴 이름 우선으로 정탐
→ 정확한 종목 알림
함정 5. "명세에 없던" 옵션 요청 — v1.1 필터 토글 추가
납품 직후 고객 피드백: "네이버 본문 매칭 / LS 동일 언론사 제외를 직접 on/off 하고 싶어요. 써보니 알림이 너무 많거나 적어서."
원 명세서에는 두 옵션이 고정값(활성화) 이었는데, 실사용 감각은 개인차가 크더라고요. 뉴스 많이 받고 싶은 사람은 필터 완화, 노이즈 싫은 사람은 필터 강화.
시크릿/동작/일상 옵션을 별도 탭으로 분리 — 사용자가 한 번 채워두면 자주 들춰볼 일이 없는 API 키와 매일 조정할 가능성 있는 옵션을 시각적으로 분리하는 UX 패턴.
해결: 설정 다이얼로그에 체크박스 2개 추가 + 핫 리로딩
# gui/settings_dialog.py
self.chk_match_body = QCheckBox("네이버 본문까지 매칭 (미체크 시 제목에 종목명 없는 기사 제외)")
self.chk_exclude_same_media = QCheckBox("LS 뉴스와 동일 언론사 중복 알림 제외")
# Pipeline 생성자에 플래그 전달
pipeline = Pipeline(
...,
match_body=config.naver.match_body,
exclude_same_media=config.naver.exclude_same_media,
)
교훈: 명세서의 "고정값" 파라미터 중 실사용 감각에 좌우되는 것(필터 강도, 검색 범위, 중복 제거 기준 등)은 처음부터 GUI 토글로 설계하세요. 상수로 박으면 나중에 반드시 풀어달라는 요청이 옵니다. 이건 이번 프로젝트뿐 아니라 거의 모든 자동매매·알림 프로젝트에서 반복되는 패턴이에요.
함정 6. 실시간 키워드 추가/삭제 — 재시작 없이 반영
최초 명세는 "키워드 엑셀 수정 후 재시작해야 반영" 이었는데, 24시간 돌아가는 프로그램 특성상 불편했어요.
해결: GUI 에 [키워드 재로딩] 버튼 + Python 의 원자적 속성 대입으로 안전한 런타임 교체:
def reload_keywords(self):
"""키워드 엑셀 다시 읽어서 파이프라인에 주입"""
new_keywords = load_keywords_from_excel(self.config.paths.keywords_xlsx)
# Python attribute assignment 는 원자적 — 교체 중 누락·중복 매칭 없음
self.pipeline.keywords = new_keywords
self.log.info(f"키워드 재로딩 완료: {len(new_keywords)}개")
원자성이 포인트입니다. Python 의 속성 대입은 GIL 하에서 단일 바이트코드라 교체 중간에 "반쯤 바뀐 키워드 리스트" 를 다른 스레드가 보는 일이 없어요. 락을 걸 필요도 없고요.
구 xingAPI vs 신 OpenAPI 선택 가이드
구 xingAPI 가 나은 경우
- 실제 매매 주문 + 구 버전 문서·커뮤니티 레퍼런스가 많아야 함
- DevCenter 설치·.res 파일 관리가 부담 아님
- 32bit Python 환경이 이미 있고 유지보수 가능
신 OpenAPI 가 나은 경우 (이번 케이스)
- 실시간 뉴스·시세·주문을 REST + WebSocket 으로 깔끔하게 쓰고 싶음
- 64bit Python · 패키지 배포 단순성 · 현대적 툴체인 선호
- 신규 개발 프로젝트라 구 API 레거시 자료 의존도 낮음
- 배포 .exe 를 고객 Windows 10/11 64bit 에서 무설정 실행시키고 싶음
참고: 이번 프로젝트에서 정립한 LS OpenAPI OAuth2 자동 갱신·asyncio + thread 하이브리드·KRX 공식 CSV 매칭·네이버 뉴스 교차검증 컴포넌트는 재사용 가능한 모듈로 정리되어, 이후 다른 뉴스 모니터링·공시 알림·키워드 트리거 매매 프로젝트에 그대로 투입됩니다. 플랫폼 상세는 LS증권 OpenAPI 플랫폼 페이지 를 참고하세요.
비슷한 뉴스 모니터링·알림 봇 필요하신가요?
키움·LS·KIS·업비트·바이낸스 등 359건 이상 제작한 알고랩이 맞춤 제작해드립니다.
24시간 빠른 답변 가능합니다.