AI 자동매매 시스템 만들기 #2 - 채널 모니터링과 5단계 매칭 시스템

2026. 2. 12. 00:24·일상다반사/개발

지난 글에서는 프로젝트 구조와 설계에 대해 이야기했는데요, 오늘은 가장 재미있었던 부분인 텔레그램 채널 모니터링 기능 구현에 대해 이야기해보려고 합니다.

 

왜 텔레그램 채널을 모니터링하나?

출처: Nano Banana

 

주식 투자를 하다 보면 여러 텔레그램 채널을 구독하게 됩니다. 증권사 리포트 채널, 뉴스 속보 채널, 개별 종목 분석 채널 등등... 문제는 이 채널들에서 쏟아지는 정보를 실시간으로 다 확인하기 어렵다는 거죠.

"내가 관심 있는 종목에 대한 소식만 골라서 알림을 받을 수 있다면?"

이런 생각에서 시작했습니다. AI가 채널 메시지를 읽고, 내가 감시 중인 종목과 관련이 있는지 판단해서 알려주면 좋겠다는 거죠.

 

TDLib - 텔레그램의 공식 라이브러리

텔레그램 봇 API를 쓸까 하다가, 더 강력한 방법을 찾았습니다. 바로 TDLib입니다.

TDLib(Telegram Database Library)는 텔레그램이 공식적으로 제공하는 C++ 라이브러리인데요, 봇 API와는 달리 일반 사용자처럼 동작할 수 있습니다. 즉, 내가 직접 가입한 채널들의 메시지를 실시간으로 받아볼 수 있죠.

다만 한 가지 문제가 있었습니다. Java/Kotlin에서 쓸 수 있는 JNI 바인딩이 필요한데, Windows에서 빌드하는 게 생각보다 까다로웠어요.

TDLib Windows 빌드 삽질기

많은 수행 착오를 겪었습니다.
1. Visual Studio 2022 필요
2. vcpkg로 의존성 설치 (openssl, zlib 등)
3. CMake로 빌드
4. Java JNI 바인딩까지 포함해서 컴파일

10번 이상 시도 끝에 또 실패인가 하던 순간..

 

너무 이쁜 초록 글씨 발생!

 

채널 모니터링 서비스 구조

이제 본격적으로 코드를 볼까요? 채널 모니터링 서비스의 핵심 구조입니다:

@Service
@ConditionalOnProperty(name = ["tdlib.api-id"], matchIfMissing = false)
class ChannelMonitorService(
    private val tdLibProperties: TdLibProperties,
    private val channelMessageRepository: ChannelMessageRepository,
    private val watchStockRepository: WatchStockRepository,
    @param:Qualifier("general")
    private val chatClient: ChatClient,
    private val braveSearchService: BraveSearchService? = null,
    private val tavilyService: TavilyService? = null,
) {
    private var client: Client? = null
    private val authorized = AtomicBoolean(false)
    private val channelChatIds = ConcurrentHashMap<Long, String>()

    @PostConstruct
    fun init() {
        // TDLib 네이티브 라이브러리 로딩
        loadNativeLibraries()

        // 클라이언트 생성
        client = Client.create(
            { update -> handleUpdate(update) },
            { e -> logger.error(e) { "TDLib 에러" } },
            { e -> logger.error(e) { "TDLib 기본 에러" } },
        )
    }
}



ConditionalOnProperty를 써서 TDLib 설정이 없으면 이 기능 자체를 비활성화하도록 했습니다. 개발 환경에서는 끄고 싶을 때가 있거든요.

5단계 매칭 시스템

채널 메시지를 받으면, 내가 감시 중인 종목과 관련이 있는지 5단계로 판단합니다:

1차: 직접 매칭

가장 간단한 방법입니다. 메시지에 종목명이나 종목코드가 직접 나오는지 확인합니다.

private fun findDirectMatches(text: String, watchStocks: List<WatchStock>): List<WatchStock> =
    watchStocks.filter { stock ->
        text.contains(stock.stockName) || text.contains(stock.stockCode)
    }


"삼성전자 실적 발표" → 삼성전자 매칭!

2차: 섹터 키워드 매칭

종목을 섹터별로 분류하고, 각 섹터마다 키워드를 등록해뒀습니다. 메시지에 해당 키워드가 있으면 그 섹터의 모든 종목을 매칭합니다.

private fun findSectorMatches(
    text: String,
    watchStocks: List<WatchStock>,
    alreadyMatched: Set<WatchStock>,
): List<WatchStock> {
    val allKeywords = sectorKeywordRepository.findAll()
    val textLower = text.lowercase()

    val matchedSectorIds = allKeywords
        .filter { kw -> textLower.contains(kw.keyword.lowercase()) }
        .map { it.sector.id }
        .toSet()

    return watchStocks.filter { stock ->
        stock !in alreadyMatched && stock.sector?.id in matchedSectorIds
    }
}


"반도체 수출 증가" → 반도체 섹터의 모든 종목 매칭!

3차: AI 연관성 판단

1차, 2차에서 걸러지지 않은 메시지는 AI에게 물어봅니다.

 

private fun findLlmMatches(text: String, watchStocks: List<WatchStock>): List<WatchStock> {
    val stockList = watchStocks.joinToString("\n") { "- ${it.stockName}(${it.stockCode})" }
    val prompt = """
        아래 뉴스/메시지가 어떤 종목에 영향을 줄 수 있는지 판단하세요.
        관련 없으면 빈 배열을 반환하세요.

        ## 감시 종목 목록
        $stockList

        ## 메시지
        ${text.take(1000)}
    """.trimIndent()

    val result = chatClient.prompt()
        .user(prompt)
        .call()
        .entity(ChannelMatchResult::class.java)
        ?: return emptyList()

    return watchStocks.filter { it.stockCode in result.relatedStockCodes }
}


"전기차 배터리 화재 논란" → AI가 LG에너지솔루션, SK온 등을 연관 지음

4차: 웹 검색 보강 (Brave Search)

메시지만으로는 맥락이 부족할 때가 있습니다. 이럴 때는 웹 검색을 해서 추가 정보를 얻습니다.

private fun findBraveMatches(text: String, watchStocks: List<WatchStock>): List<WatchStock> {
    // AI가 검색 쿼리 생성
    val refinement = refineBraveQuery(text)
    if (refinement == null || !refinement.worthSearching) {
        return emptyList()
    }

    // Brave API로 웹 검색
    val results = braveSearchService?.searchWeb(
        refinement.searchQuery,
        count = 3,
        freshness = "pd"  // past day
    ) ?: return emptyList()

    // 검색 결과에서 종목명 찾기
    val allText = results.joinToString(" ") {
        "${it.title ?: ""} ${it.description ?: ""}"
    }

    return watchStocks.filter { stock ->
        allText.contains(stock.stockName) || allText.contains(stock.stockCode)
    }
}

 

중요한 점: 모든 메시지에 대해 웹 검색을 하면 비용이 너무 많이 나갑니다. 그래서 AI에게 먼저 "이 메시지가 검색할 가치가 있나?"를 물어봅니다.

private fun refineBraveQuery(text: String): BraveQueryRefinementResult? {
    val prompt = """
        아래 텔레그램 채널 메시지를 분석하여 주식 종목 관련 웹 검색 쿼리를 만들어주세요.

        ## 규칙
        1. 특정 기업/종목/사건에 대한 메시지면 worthSearching=true
        2. 아래 유형은 worthSearching=false:
           - 코스피/코스닥 지수 마감/등락 속보
           - 일반 시황 요약
           - 단순 수치 나열

        ## 메시지
        ${text.take(500)}
    """.trimIndent()

    return chatClient.prompt()
        .user(prompt)
        .call()
        .entity(BraveQueryRefinementResult::class.java)
}


"코스피 2,600선 마감" → worthSearching=false (검색 안 함)
"○○기업 CEO 사퇴" → worthSearching=true (검색함)

5차: URL 본문 추출 (Tavily)

메시지에 URL이 포함되어 있으면, 그 링크의 실제 내용을 읽어봅니다.

private fun findTavilyUrlMatches(text: String, watchStocks: List<WatchStock>): List<WatchStock> {
    val urls = URL_PATTERN.findAll(text).map { it.value }.toList().take(2)
    if (urls.isEmpty()) return emptyList()

    val extracts = tavilyService?.extract(urls) ?: return emptyList()
    val allContent = extracts.joinToString(" ") { it.rawContent ?: "" }

    return watchStocks.filter { stock ->
        allContent.contains(stock.stockName) || allContent.contains(stock.stockCode)
    }
}

 

텔레그램 채널에 "속보 https://..." 이렇게만 올라오는 경우가 많거든요. 이럴 때 링크 안의 내용까지 확인합니다.

이미지도 분석한다

텔레그램 채널에는 차트 이미지, 뉴스 캡처 등이 많이 올라옵니다. 텍스트만 보면 놓치는 정보가 많아요.

그래서 Vision AI도 추가했습니다:

 

private fun analyzePhotoMessage(
    photoContent: TdApi.MessagePhoto,
    caption: String?,
): ImageAnalysisResult? {
    // 가장 큰 사이즈의 사진 선택
    val photoSize = photoContent.photo?.sizes
        ?.maxByOrNull { it.width * it.height }
        ?: return null

    // TDLib로 이미지 다운로드
    val localPath = downloadFileWithRetry(photoSize.photo.id)
        ?: return null

    val imageBytes = File(localPath).readBytes()

    // Vision AI로 분석
    return imageAnalysisService.analyzeImage(imageBytes, mimeType, caption)
}


차트 이미지가 올라오면 AI가 "이 차트는 ○○종목의 일봉 차트이며, 상승 추세를 보이고 있습니다"라고 분석해줍니다.

성능 최적화 - 캐싱

매 메시지마다 DB에서 감시 종목 목록을 조회하면 느립니다. 그래서 간단한 캐싱을 추가했습니다:

 

companion object {
    private const val WATCH_STOCKS_CACHE_TTL_MS = 3 * 60 * 1000L  // 3분
}

@Volatile
private var cachedWatchStocks: List<WatchStock> = emptyList()
@Volatile
private var watchStocksCacheTime: Long = 0L

private fun getActiveWatchStocks(): List<WatchStock> {
    val now = System.currentTimeMillis()
    if (now - watchStocksCacheTime > WATCH_STOCKS_CACHE_TTL_MS) {
        synchronized(this) {
            val nowInLock = System.currentTimeMillis()
            if (nowInLock - watchStocksCacheTime > WATCH_STOCKS_CACHE_TTL_MS) {
                cachedWatchStocks = watchStockRepository.findByActiveTrue()
                watchStocksCacheTime = System.currentTimeMillis()
            }
        }
    }
    return cachedWatchStocks
}


3분마다 한 번씩만 DB 조회하고, 그 사이에는 메모리의 캐시를 씁니다. 동시성 처리도 잊지 않았죠. (synchronized + double-check locking)

인증 처리 - REST API로

TDLib를 처음 쓸 때는 전화번호와 인증 코드 입력이 필요합니다. 터미널에서 입력받을 수도 있지만, REST API로 만들어서 더 편하게 했습니다:

fun submitAuthCode(code: String) {
    client?.send(TdApi.CheckAuthenticationCode().apply {
        this.code = code
    }) { result ->
        if (result is TdApi.Error) {
            logger.error { "인증 코드 실패: ${result.message}" }
        }
    }
}

 

서버 시작하고 로그에 이렇게 나옵니다:

========================================
텔레그램 인증 코드를 입력해주세요.
POST /api/tdlib/auth-code?code=XXXXX
========================================


그럼 Postman이나 curl로 코드 입력하면 됩니다. 편하죠?

실제 커밋 히스토리

이 기능을 만들면서 남긴 커밋들을 보면:

 

b2d6469 - Add Telegram channel monitoring via TDLib
  └─ 기본 채널 모니터링 기능
     TDLib 클라이언트 초기화
     메시지 수신 및 DB 저장

2bd4e8a - Add HTML support for Telegram messages
  └─ 텔레그램 메시지의 HTML 포맷 지원
     주식 거래 로직 검증 추가
     사용자 피드백 개선


처음에는 단순하게 만들고, 점점 기능을 추가해갔습니다.

그래서 어떻게 동작하나?

실제로 동작 했었던 모습을 말씀드리자면

1. 텔레그램 채널에 메시지 도착: "○○기업, 신규 수주 1,000억원 확보"
2. 1차 매칭 실패 (종목명 직접 언급 없음)
3. 2차 매칭 실패 (섹터 키워드 없음)
4. 3차 AI 판단: "○○기업은 감시 종목 중 ABC주식과 관련 있음"
5. DB에 저장:
   - message.content = "○○기업, 신규 수주..."
   - match.stockCode = "123456"
   - match.matchType = "LLM"
6. AI가 이 정보를 바탕으로 매매 판단

와 같은 모습이 되겠습니다.

배운 점들

1. 네이티브 라이브러리 연동은 생각보다 까다롭다
TDLib 빌드하느라 반나절을 썼습니다. 의존성 버전, 빌드 옵션... 한 번 성공하면 문서화해두는 게 중요해요.

2. 멀티스레드 환경에서의 캐싱 주의
TDLib는 콜백 방식이라 여러 스레드에서 동시에 메시지가 들어올 수 있습니다. synchronized와 volatile을 적절히 써야 합니다.

3. AI 호출 비용 최적화 필요
처음에는 모든 메시지에 대해 AI/웹검색을 했다가 비용이 급증했습니다. "검색할 가치가 있나?" 를 먼저 물어보는 게 중요합니다.

4. 5단계 매칭은 충분하다
처음에는 "더 복잡한 매칭 알고리즘이 필요하지 않을까?" 고민했는데, 5단계면 거의 모든 경우를 커버합니다.

다음 글 예고

오늘은 텔레그램 채널 모니터링 기능에 대해 이야기했습니다. 다음 글에서는:
- 키움증권 REST API 연동 과정
- 실시간 시세 수신 구현
- WebSocket 장 상태 감지
- 실제 매매 주문 실행 로직

이런 내용들을 다뤄볼 예정입니다. 실제로 돈이 오가는 부분이라 더 신중하게 만들었거든요!

반응형
저작자표시 비영리 (새창열림)
'일상다반사/개발' 카테고리의 다른 글
  • AI 자동매매 시스템 만들기 #1 - 프로젝트의 시작과 설계
  • Jetbrains 가격 인상 공지와 저의 선택
  • Java 17.0.1 버그를 경험한 후기
  • 갤럭시 S22+ 삼성단독 컬러 사전구매가 너무 하고 싶었던 개발자의 개발
Kua
Kua
정보 공유, 개인 정리 공간 입니다.
  • Kua
    Kua's Miscellaneous
    Kua
  • 전체
    오늘
    어제
    • 분류 전체보기 (188) N
      • 대문 (2)
      • Tips (1)
        • Chrome (2)
        • Windows (4)
        • IDE (3)
        • 기타 (16)
      • CodingTest (44)
      • Language (20)
        • PHP (5)
        • C# (7)
        • Java (1)
        • Kotlin (7)
      • Framework & Runtime (16)
        • SpringBoot (12)
        • Node.js (2)
        • Vue.js (1)
        • Gradle (1)
      • DevOps (13)
        • Linux (1)
        • Docker (4)
        • Kubernetes (2)
        • Apache Kafka (1)
        • AWS (1)
      • 일상다반사 (55) N
        • 도서 (1)
        • 개발 (10) N
        • 후기 - IT (7)
        • 후기 - 일상 (13)
        • 차가리 (4)
        • 방송통신대학교 (4)
        • 음식 (2)
      • Games (12)
        • Minecraft (7)
        • VR (2)
        • 그외 (3)
  • 인기 글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
Kua
AI 자동매매 시스템 만들기 #2 - 채널 모니터링과 5단계 매칭 시스템
상단으로

티스토리툴바