금융 데이터 분석 애플리케이션을 개발하면서 가장 어려운 과제 중 하나는 사용자의 자연어 쿼리에서 정확한 주식 티커 심볼을 추출하는 것입니다. 이 글에서는 정규 표현식부터 LLM(Large Language Model)을 활용한 고급 방법까지, 다양한 티커 심볼 추출 기법을 소개하고자 합니다.
금융 분석 애플리케이션에서 사용자는 다양한 방식으로 기업을 언급합니다:
이러한 다양한 입력에서 정확한 티커 심볼을 추출하는 것은 금융 API에 올바른 데이터를 요청하기 위한 필수 단계입니다.
가장 기본적인 방법은 정규 표현식을 사용하여 티커 심볼의 패턴을 찾는 것입니다.
def extract_ticker_basic(query: str):
"""기본적인 정규 표현식을 사용한 티커 추출"""
import re
# 1-5자리 대문자 패턴 찾기
match = re.search(r'\b([A-Z]{1,5})\b', query)
if match:
return match.group(1)
# 다른 패턴 시도: "티커: AAPL" 또는 "symbol: MSFT" 등
match = re.search(r'(?:[티커|ticker|symbol]?[:\s]*|[^\w\d]*\()([A-Z]{1,5})(?:\)|)', query, re.IGNORECASE)
if match:
return match.group(1).upper()
return None
이 방법은 간단하고 빠르지만, 명시적으로 티커가 언급되지 않은 경우 작동하지 않습니다.
자주 언급되는 회사들에 대해 이름-티커 매핑 사전을 구축할 수 있습니다.
def extract_ticker_with_mapping(query: str):
"""매핑 사전을 활용한 티커 추출"""
import re
# 먼저 정규 표현식으로 시도
ticker = extract_ticker_basic(query)
if ticker:
return ticker
# 매핑 사전 활용
company_to_ticker = {
"애플": "AAPL", "apple": "AAPL",
"마이크로소프트": "MSFT", "microsoft": "MSFT",
"구글": "GOOGL", "google": "GOOGL", "alphabet": "GOOGL",
"아마존": "AMZN", "amazon": "AMZN",
"테슬라": "TSLA", "tesla": "TSLA",
"페이스북": "META", "메타": "META", "facebook": "META", "meta": "META",
"넷플릭스": "NFLX", "netflix": "NFLX",
"엔비디아": "NVDA", "nvidia": "NVDA",
# ... 더 많은 회사 추가 가능
}
# 쿼리에서 회사 이름을 찾아보기
query_lower = query.lower()
for company, ticker in company_to_ticker.items():
if company.lower() in query_lower:
return ticker
return None
이 방법은 매핑된 회사들에 대해서는 효과적이지만, 사전에 포함되지 않은 회사는 처리할 수 없습니다.
최신 접근법은 LLM(Large Language Model)을 활용하여 자연어 쿼리에서 회사 이름을 인식하고 해당 티커로 변환하는 것입니다.
def extract_ticker_with_llm(query: str, llm):
"""LLM을 활용한 티커 추출"""
import re
# 먼저 기본 방법으로 시도
ticker = extract_ticker_with_mapping(query)
if ticker:
return ticker
# LLM에게 티커 추론 요청
try:
prompt = f"""
Infer the US stock market ticker symbol from the company mentioned in the following query:
"{query}"
Response format: Only output the ticker symbol (e.g., AAPL, MSFT, GOOGL).
If no company can be identified, output "UNKNOWN".
"""
response = llm.invoke(prompt).content
clean_response = response.strip().upper()
# 유효한 티커 형식인지 확인 (1-5개의 대문자)
if clean_response != "UNKNOWN" and re.match(r'^[A-Z]{1,5}$', clean_response):
return clean_response
except Exception as e:
print(f"Error querying LLM for ticker extraction: {e}")
return None
LLM 방식은 가장 유연하고 강력하지만, API 호출 비용과 지연 시간이 발생합니다.
실제 애플리케이션에서는 위의 방법들을 단계적으로 결합하는 것이 가장 효과적입니다. 다음은 LangChain과 OpenAI를 활용한 실제 구현 예시입니다.
from langchain_openai import ChatOpenAI
from typing import Optional
class USFinancialStatementTool:
"""미국 주식 재무제표 분석 도구"""
def __init__(self):
self.llm = None # LLM 인스턴스 저장용
def _extract_ticker(self, query: str) -> Optional[str]:
"""융합 접근법을 사용한 티커 추출"""
import re
# 1단계: 정규 표현식으로 명시적 티커 찾기
match = re.search(r'\b([A-Z]{1,5})\b', query)
if match:
return match.group(1)
match = re.search(r'(?:[ticker|symbol]?[:\s]*|[^\w\d]*\()([A-Z]{1,5})(?:\)|)', query, re.IGNORECASE)
if match:
return match.group(1).upper()
# 2단계: 매핑 사전 활용
company_to_ticker = {
"애플": "AAPL", "apple": "AAPL",
"마이크로소프트": "MSFT", "microsoft": "MSFT",
"구글": "GOOGL", "google": "GOOGL", "alphabet": "GOOGL",
# ... 기타 회사들
}
query_lower = query.lower()
for company, ticker in company_to_ticker.items():
if company.lower() in query_lower:
return ticker
# 3단계: LLM 활용 (있는 경우)
if self.llm is not None:
try:
prompt = f"""
Infer the US stock market ticker symbol from the company mentioned in the following query:
"{query}"
Response format: Only output the ticker symbol (e.g., AAPL, MSFT, GOOGL).
If no company can be identified, output "UNKNOWN".
"""
response = self.llm.invoke(prompt).content
clean_response = response.strip().upper()
if clean_response != "UNKNOWN" and re.match(r'^[A-Z]{1,5}$', clean_response):
return clean_response
except Exception as e:
print(f"Error querying LLM for ticker extraction: {e}")
return None
def _run(self, query: str):
"""도구 실행"""
ticker = self._extract_ticker(query)
if not ticker:
return "No valid ticker symbol found in the query. Please provide a query with a US stock ticker (e.g., AAPL, MSFT)."
# 티커로 재무 분석 진행
# ...
LLM을 기존 애플리케이션에 통합하려면 일반적으로 다음과 같은 단계가 필요합니다:
from langgraph.prebuilt import create_react_agent
from langgraph.types import Command
from langchain_core.messages import HumanMessage
from langchain_openai import ChatOpenAI
from src.graph.nodes.base import Node
from src.models.do import RawResponse
from src.tools.us_stock.tool import USFinancialStatementTool
class USFinancialAnalyzerNode(Node):
"""미국 주식 재무제표 분석 노드"""
def __init__(self):
super().__init__()
self.system_prompt = (
"You are a financial statement analysis agent for US stocks. "
"Your task is to analyze balance sheets, income statements, and financial ratios "
"to provide a comprehensive assessment of a company's financial health, growth potential, and profitability. "
"Always identify the ticker symbol in the user's query and provide accurate, data-driven analysis. "
"If a specific ticker isn't mentioned but a company name is, try to infer the ticker symbol. "
"For example, if the user mentions 'Apple', infer the ticker symbol 'AAPL'. "
"Present your findings clearly and concisely, but do not provide investment advice or recommendations. "
"Only if no company name or ticker can be identified, ask for clarification."
)
self.agent = None
self.tools = [USFinancialStatementTool()]
def _run(self, state: dict) -> Command:
if self.agent is None:
assert state["llm"] is not None, "The State model should include llm"
llm = state["llm"]
# LLM 참조를 도구의 _extract_ticker 메서드에 추가
self.tools[0].llm = llm
self.agent = create_react_agent(
llm,
self.tools,
prompt=self.system_prompt,
)
# Run the agent
result = self.agent.invoke(state)
self.logger.info(f"US Financial analysis result: \n{result['messages'][-1].content}")
# Extract the ticker symbol
ticker_info = self._extract_ticker_from_result(result['messages'][-1].content)
return Command(
update={
"messages": [
HumanMessage(
content=result["messages"][-1].content,
name="us_financial_analyzer",
)
],
# Store analysis results in structured format
"financial_analysis": {
"ticker": ticker_info["ticker"],
"market": "US",
"analysis_text": result["messages"][-1].content,
}
},
goto="supervisor",
)
LLM이 티커를 추론하도록 시스템 프롬프트를 최적화하는 것이 중요합니다. 다음 부분이 핵심입니다:
"If a specific ticker isn't mentioned but a company name is, try to infer the ticker symbol. "
"For example, if the user mentions 'Apple', infer the ticker symbol 'AAPL'."
장점:
단점:
자주 요청되는 회사 이름에 대한 티커 변환 결과를 캐싱하여 API 호출을 줄입니다.
class TickerCache:
"""티커 변환 결과 캐싱"""
def __init__(self, ttl=86400): # 기본 TTL: 1일
self.cache = {}
self.timestamps = {}
self.ttl = ttl
def get(self, company_name):
"""캐시에서 티커 가져오기"""
company_name = company_name.lower()
current_time = time.time()
if company_name in self.cache and current_time - self.timestamps.get(company_name, 0) < self.ttl:
return self.cache[company_name]
return None
def set(self, company_name, ticker):
"""캐시에 티커 저장"""
company_name = company_name.lower()
self.cache[company_name] = ticker
self.timestamps[company_name] = time.time()
가장 비용이 낮은 방법부터 시도하고, 필요한 경우에만 LLM을 호출합니다:
자주 언급되는 회사들에 대한 매핑 데이터를 구축하여 LLM 의존도를 줄입니다:
# 확장된 매핑 사전 구축
extended_company_to_ticker = {
# 한국어 회사명
"삼성전자": "005930.KS", # 한국 주식은 접미사 필요
"현대차": "005380.KS",
"네이버": "035420.KS",
"카카오": "035720.KS",
# 영어 회사명 (대소문자 구분 없음)
"nvidia": "NVDA",
"intel": "INTC",
"advanced micro devices": "AMD",
"qualcomm": "QCOM",
# 약어 및 일반적인 지칭
"앱스토어": "AAPL",
"아이폰": "AAPL",
"윈도우": "MSFT",
"오피스": "MSFT",
"안드로이드": "GOOGL",
"유튜브": "GOOGL",
# ... 수백 개의 매핑 추가 가능
}
티커 추출 시스템의 품질을 관리하기 위한 테스트 전략도 중요합니다.
import unittest
from unittest.mock import patch, MagicMock
class TestTickerExtraction(unittest.TestCase):
"""티커 추출 테스트 클래스"""
def setUp(self):
self.tool = USFinancialStatementTool()
def test_explicit_ticker_extraction(self):
"""명시적 티커 추출 테스트"""
testcases = [
("AAPL의 재무제표 분석해줘", "AAPL"),
("티커 MSFT의 재무상태는?", "MSFT"),
("TSLA에 대한 재무분석을 해주세요", "TSLA"),
("Ticker: AMZN financial analysis", "AMZN"),
("Google GOOG 재무제표 보여줘", "GOOG")
]
for query, expected in testcases:
result = self.tool._extract_ticker(query)
self.assertEqual(result, expected, f"Query: {query}")
def test_company_name_to_ticker(self):
"""회사 이름에서 티커 추출 테스트"""
testcases = [
("애플의 재무제표 분석", "AAPL"),
("마이크로소프트는 어떤가요?", "MSFT"),
("구글의 재무상태", "GOOGL"),
("테슬라에 투자해도 될까요?", "TSLA")
]
for query, expected in testcases:
result = self.tool._extract_ticker(query)
self.assertEqual(result, expected, f"Query: {query}")
@patch('langchain_openai.ChatOpenAI')
def test_llm_ticker_extraction(self, mock_llm):
"""LLM 티커 추출 테스트"""
# LLM 목 설정
mock_llm_instance = MagicMock()
mock_llm_instance.invoke.return_value.content = "NVDA"
mock_llm.return_value = mock_llm_instance
# LLM 설정
self.tool.llm = mock_llm_instance
# 테스트
result = self.tool._extract_ticker("엔비디아의 재무상태가 어떤지 알려줘")
self.assertEqual(result, "NVDA")
# LLM이 호출되었는지 확인
mock_llm_instance.invoke.assert_called_once()
실제 시스템에서의 테스트는 다음 전략을 고려합니다:
이 시스템은 다양한 방향으로 확장이 가능합니다:
자연어 쿼리에서 주식 티커를 추출하는 것은 금융 데이터 애플리케이션의 핵심 기능입니다. 정규 표현식, 매핑 사전, 그리고 LLM을 결합한 융합 접근법은 다양한 사용자 입력을 효과적으로 처리할 수 있습니다.
특히 LLM의 도입은 이 분야에 혁신을 가져왔으며, 사용자 경험을 크게 향상시킬 수 있습니다. 물론 비용과 지연 시간을 고려한 최적화가 필수적이지만, 적절히 구현된다면 매우 강력한 솔루션이 될 수 있습니다.
이 글에서 소개한 방법들은 실제 프로덕션 환경에서 테스트되었으며, 사용자들이 자연스러운 언어로 재무 데이터를 요청할 수 있게 함으로써 금융 정보 접근성을 크게 향상시켰습니다.