AlgoLab Case Study · 뉴스 모니터링

LS증권 신 OpenAPI 로 실시간 뉴스 키워드 텔레그램 알림 봇 만들기 — 구 xingAPI 와 뭐가 다른가

LS증권 텔레그램 2026-04-25 · 약 10분 읽기 · 알고랩 AlgoLab

"실시간으로 쏟아지는 증권 뉴스 중에서 내가 관심 있는 키워드나 종목 관련 뉴스만 골라서 텔레그램으로 받고 싶어요" — 뉴스 기반 트레이더들이 많이 하는 요청입니다. 직접 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

신 OpenAPI (이 프로젝트에서 선택)

이번 프로젝트는 뉴스 모니터링이라 1~3초 실시간이면 충분 → WebSocket 으로 해결. 매매가 아니라서 .res·DevCenter 설치 같은 구 xingAPI 부담을 안고 갈 이유가 없었어요. 특히 배포용 .exe 를 64bit 로 빌드할 수 있다는 게 컸습니다 — 32bit 바이너리는 설치·실행 환경 분쟁이 자주 생기거든요.

처리 파이프라인

전체 흐름은 이렇게 구성했어요:

  1. LS WebSocket (NWS/NWS001): 실시간 뉴스 수신 → 제목 + 본문 일부 + 언론사 + 링크
  2. Keyword Matcher: 사용자 등록 키워드(엑셀) 와 매칭
  3. Stock Resolver (KRX CSV): 제목에서 종목명·코드 추출 (긴 이름 우선 + 단어 경계)
  4. Naver Cross-Check: 네이버 뉴스 API 로 "같은 주제의 다른 언론사 기사" 가 있는지 2시간 윈도우 검색 (정확도 향상)
  5. Filter: 제외 키워드 / 동일 언론사 중복 제거
  6. Telegram: HTML parse mode 로 3줄 포맷 메시지 전송

"왜 네이버 교차검증까지?" 라고 물으실 수 있는데, LS 뉴스 단독만 쓰면 오보·찌라시 기사도 알림이 가는 문제가 있어요. 복수 언론사에서 같은 주제를 다루고 있는지 네이버로 한 번 걸러주면 품질이 확 올라갑니다.

뉴스 처리 파이프라인
SOURCE
LS WebSocket (NWS/NWS001)
실시간 뉴스 스트림 · 분당 수~수십 건
STEP 1
Keyword Matcher
사용자 엑셀 키워드 리스트와 매칭
STEP 2
Stock Resolver (KRX 공식 CSV)
KOSPI/KOSDAQ/KONEX 약 2,700 종목 · 긴 이름 우선 + 단어 경계
STEP 3
Naver Cross-Check
네이버 뉴스 API 2시간 윈도우 · 30초 × 최대 20회 재시도
STEP 4
Filter
제외 키워드 / 동일 언론사 중복 제거
OUTPUT
Telegram 알림 발송
HTML parse_mode · 3줄 포맷
LS 뉴스를 받자마자 바로 알림이 아니라, 네이버 교차검증 거쳐서 오보·찌라시 필터링 → 품질 높은 알림만 전송
텔레그램 메시지 예시
A
AlgoLab 뉴스봇
@algolab_news_bot
🔔 키워드 감지: 반도체

📌 SK하이닉스 (000660)
SK하이닉스, 3분기 영업이익 7조 돌파… 반도체 업황 개선 기대
🔗 https://news.example.com/article/...
📰 연합뉴스 · ⏱ 2분 전
오전 10:23
종목명 · 뉴스 제목 · 링크 3줄 포맷 · HTML parse_mode 로 볼드·링크 지원

함정 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 스레드를 블로킹하는 것:

해결: 버튼 핸들러 백그라운드 스레드화 + 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 쓰는 모든 프로젝트에 그대로 재사용 가능합니다.

LS OpenAPI 뉴스 텔레그램 봇 실시간 모니터링 화면 - 상태 LED 3개(LS·네이버·텔레그램 모두 연결됨 녹색), 시작/중지/설정/키워드 재로딩/엑셀 열기/로그 폴더 열기 버튼, LS 토큰 발급 성공 로그, WebSocket wss://openapi.ls-sec.co.kr:9443 연결, NWS/NWS001 뉴스 구독, 실시간 LS 뉴스 수신 3건 (펄어비스·엔씨 1Q 고공행진 크래프톤 주춤, 함영훈의 멋·맛·쉼 칼럼, 챔프 김수혈 휴식기 양지용 김현우 타이틀전), 하단 통계 수신 3 키워드 0 매칭 0 알림 0
실시간 모니터링 화면 — 상태 LED 3개로 LS/네이버/텔레그램 연결 상태 한눈에 시각화. 상단 LS 토큰 발급 → WebSocket 연결 → 뉴스 구독 → 실제 뉴스 수신까지 한 흐름의 로그가 그대로 보임. 하단 통계로 "수신 / 키워드 매칭 / 매칭 / 알림" 4단계 깔때기를 실시간 추적.

함정 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" 는 건너뜁니다 (앞 글자 "한" 이 한글이라 거부). 뉴스·공시 텍스트 처리하는 거의 모든 프로젝트에서 써먹을 수 있는 패턴이에요.

매칭 로직 Before / After
❌ BEFORE (단순 in 매칭)
한세예스24그룹, 3분기 매출 증가...
감지: 예스24 (085660)
→ 전혀 관련 없는 예스24 로 오탐
→ 잘못된 알림 발송
✅ AFTER (긴 이름 + 단어 경계)
한세예스24그룹, 3분기 매출 증가...
감지: 한세예스24그룹 (105630)
→ 긴 이름 우선으로 정탐
→ 정확한 종목 알림
단어 경계 검증 없이 단순 in 매칭만 쓰면 회사명이 긴 종목의 부분 문자열 오탐이 반복적으로 발생

함정 5. "명세에 없던" 옵션 요청 — v1.1 필터 토글 추가

납품 직후 고객 피드백: "네이버 본문 매칭 / LS 동일 언론사 제외를 직접 on/off 하고 싶어요. 써보니 알림이 너무 많거나 적어서."

원 명세서에는 두 옵션이 고정값(활성화) 이었는데, 실사용 감각은 개인차가 크더라고요. 뉴스 많이 받고 싶은 사람은 필터 완화, 노이즈 싫은 사람은 필터 강화.

설정 다이얼로그 3탭 구조
LS OpenAPI 뉴스 텔레그램 봇 설정 다이얼로그 - API 키 탭. LS증권 OpenAPI AppKey/AppSecretKey, 네이버 뉴스 검색 API Client ID/Secret, 텔레그램 봇 Bot Token/Chat ID 입력 필드. 표시/숨김 토글 + LS OpenAPI/네이버 개발자센터/@BotFather 외부 링크 안내
① API 키 탭 — 시크릿 6종을 한 곳에 격리. 표시/숨김 토글로 입력 시 어깨너머 노출 방지.
LS OpenAPI 뉴스 텔레그램 봇 설정 다이얼로그 - 동작 설정 탭. 네이버 재검색 간격 30초·재검색 횟수 20회·뉴스 검색 시간 범위 2시간 스피너, 매칭·필터 옵션 두 개 (네이버 본문에서도 종목명 매칭, LS증권과 동일 언론사 기사 제외) 체크박스, 총 최대 감시 시간 약 10분 자동 계산 안내
② 동작 설정 탭 — 검색 파라미터 + 필터 옵션 2종 토글. 명세에는 고정값이었지만 사용자 피드백 반영해 v1.1 에 토글로 노출.
LS OpenAPI 뉴스 텔레그램 봇 설정 다이얼로그 - 기타 탭. 텔레그램 알림 활성화 체크박스, 창 닫기 시 트레이로 최소화 (백그라운드 실행 유지) 체크박스, 로그 레벨 드롭다운 (INFO 기본값, DEBUG 변경 시 LS WebSocket 원본 수신 내용 기록 안내)
③ 기타 탭 — 일상 운영 옵션. 트레이 최소화로 24시간 백그라운드 실행, DEBUG 로그 레벨로 WebSocket 원본 디버깅.

시크릿/동작/일상 옵션을 별도 탭으로 분리 — 사용자가 한 번 채워두면 자주 들춰볼 일이 없는 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 가 나은 경우

신 OpenAPI 가 나은 경우 (이번 케이스)

참고: 이번 프로젝트에서 정립한 LS OpenAPI OAuth2 자동 갱신·asyncio + thread 하이브리드·KRX 공식 CSV 매칭·네이버 뉴스 교차검증 컴포넌트는 재사용 가능한 모듈로 정리되어, 이후 다른 뉴스 모니터링·공시 알림·키워드 트리거 매매 프로젝트에 그대로 투입됩니다. 플랫폼 상세는 LS증권 OpenAPI 플랫폼 페이지 를 참고하세요.

비슷한 뉴스 모니터링·알림 봇 필요하신가요?

키움·LS·KIS·업비트·바이낸스 등 359건 이상 제작한 알고랩이 맞춤 제작해드립니다.
24시간 빠른 답변 가능합니다.

무료 상담 시작하기