315105cebb
- 투자 의견 조회
ㄴ 전반적인 주식 판단 기준(ex. 채무, 리스크 등등)을 기준으로 판단하여 도출
- 투자 추천
ex)
{
"tickers": [
"NVDA", "GOOGL", "AAPL"
],
"risk_type": "aggressive"
}
242 lines
6.9 KiB
Python
242 lines
6.9 KiB
Python
from backend.schemas.stock_schemas import TickerInfo
|
|
import yahooquery as yq
|
|
from fastapi import HTTPException
|
|
from backend.graph.stock_graph import build_stock_graph
|
|
from backend.repository.models import StockAnalysis, InvestmentOpinion
|
|
from fastapi.encoders import jsonable_encoder
|
|
from typing import Optional
|
|
from backend.services.score_service import evaluation, get_rating
|
|
from backend.ai.llm import hugging_llm
|
|
|
|
COMPANY_MAP = {
|
|
"NVDA": {
|
|
"company_name": "NVIDIA",
|
|
"aliases": ["엔비디아", "nvidia", "nvda"],
|
|
},
|
|
"AAPL": {
|
|
"company_name": "Apple",
|
|
"aliases": ["애플", "apple", "aapl"],
|
|
},
|
|
"MSFT": {
|
|
"company_name": "Microsoft",
|
|
"aliases": ["마이크로소프트", "microsoft", "msft"],
|
|
},
|
|
"TSLA": {
|
|
"company_name": "Tesla",
|
|
"aliases": ["테슬라", "tesla", "tsla"],
|
|
},
|
|
"GOOGL": {
|
|
"company_name": "Alphabet (Google)",
|
|
"aliases": ["구글", "google", "알파벳", "alphabet", "googl"],
|
|
},
|
|
"AMZN": {
|
|
"company_name": "Amazon",
|
|
"aliases": ["아마존", "amazon", "amzn"],
|
|
},
|
|
"META": {
|
|
"company_name": "Meta Platforms",
|
|
"aliases": ["메타", "meta", "페이스북", "facebook", "fb"],
|
|
},
|
|
"AMD": {
|
|
"company_name": "Advanced Micro Devices",
|
|
"aliases": ["amd"],
|
|
},
|
|
"INTC": {
|
|
"company_name": "Intel",
|
|
"aliases": ["인텔", "intel", "intc"],
|
|
},
|
|
"TSM": {
|
|
"company_name": "TSMC",
|
|
"aliases": ["tsmc", "tsm", "대만반도체"],
|
|
},
|
|
"QCOM": {
|
|
"company_name": "Qualcomm",
|
|
"aliases": ["퀄컴", "qualcomm", "qcom"],
|
|
},
|
|
"AVGO": {
|
|
"company_name": "Broadcom",
|
|
"aliases": ["브로드컴", "broadcom", "avgo"],
|
|
},
|
|
"MU": {
|
|
"company_name": "Micron Technology",
|
|
"aliases": ["마이크론", "micron", "mu"],
|
|
},
|
|
"V": {
|
|
"company_name": "Visa",
|
|
"aliases": ["비자", "visa", "v"],
|
|
},
|
|
"MA": {
|
|
"company_name": "Mastercard",
|
|
"aliases": ["마스터카드", "mastercard", "ma"],
|
|
},
|
|
"JPM": {
|
|
"company_name": "JPMorgan Chase",
|
|
"aliases": ["jp모건", "jpmorgan", "jpm"],
|
|
},
|
|
"JNJ": {
|
|
"company_name": "Johnson & Johnson",
|
|
"aliases": ["존슨앤존슨", "johnson", "jnj"],
|
|
},
|
|
"MSFT": {
|
|
"company_name": "Microsoft",
|
|
"aliases": ["마이크로소프트", "microsoft", "msft"],
|
|
},
|
|
}
|
|
|
|
class StockService:
|
|
"""주식 분석"""
|
|
async def _extract(self, query: str):
|
|
"""
|
|
사용자 쿼리에서 티커와 회사명 추출
|
|
1차 : Map 이용
|
|
2차 : yahooquery 이용
|
|
|
|
query : 엔비디아 분석해줘
|
|
"""
|
|
keyword = self._extract_company_keyword(query)
|
|
|
|
# 1 차 Map
|
|
result = self._extract_ticker_from_query(keyword)
|
|
|
|
if result:
|
|
return result
|
|
|
|
# 2 차 yahooquery
|
|
result = await self._search_yahoo_symbol(keyword)
|
|
|
|
if result:
|
|
return result
|
|
|
|
raise HTTPException(status_code=30000, detail="종목을 찾을 수 없습니다.")
|
|
|
|
def _extract_ticker_from_query(self, query:str):
|
|
"""
|
|
COMPANY_MAP 안에서 일치하는 회사 찾기
|
|
"""
|
|
|
|
query = query.lower()
|
|
|
|
for ticker, info in COMPANY_MAP.items():
|
|
for alias in info["aliases"]:
|
|
if alias.lower() in query:
|
|
return TickerInfo(ticker = ticker, company_name = info["company_name"])
|
|
|
|
return None
|
|
|
|
def _extract_company_keyword(self, query:str):
|
|
"""
|
|
query : 엔비디아 분석해줘
|
|
=> 필요없는 문장 제거한 후 키워드만 추출
|
|
"""
|
|
|
|
stop_words = [
|
|
"분석해줘", "분석해", "분석", "해줘", "해", "주가", "전망", "재무", "알려줘", "어때", "어떻게", ""
|
|
]
|
|
|
|
keyword = query
|
|
|
|
for word in stop_words:
|
|
keyword = keyword.replace(word, "")
|
|
|
|
return keyword.strip()
|
|
|
|
async def _search_yahoo_symbol(self, keyword: str):
|
|
"""
|
|
yahooquery 이용
|
|
"""
|
|
try:
|
|
result = yq.search(keyword, first_quote=True)
|
|
|
|
if not result or (isinstance(result, dict) and "explanation" in result):
|
|
return None
|
|
|
|
return TickerInfo(ticker=result["symbol"], company_name=result["longname"])
|
|
|
|
except Exception:
|
|
return None
|
|
|
|
async def analyze(self, query: str, db):
|
|
# 티커, 회사명 추출
|
|
ticker_info = await self._extract(query)
|
|
|
|
# 그래프 실행
|
|
stock_graph = build_stock_graph()
|
|
result = await stock_graph.ainvoke(
|
|
{
|
|
"query": query,
|
|
"ticker": ticker_info.ticker,
|
|
"company_name": ticker_info.company_name
|
|
}
|
|
)
|
|
|
|
# 데이터베이스 저장
|
|
# 테이블과 관련있는 모델 객체 생성
|
|
entity = StockAnalysis(
|
|
ticker = result['ticker'],
|
|
company_name = result['company_name'],
|
|
analysis_json = jsonable_encoder(result),
|
|
report = result['report']
|
|
)
|
|
db.add(entity)
|
|
db.commit()
|
|
db.refresh(entity)
|
|
|
|
return {"analysis_id":entity.analysis_id, **result}
|
|
|
|
async def opinion_service(self, analysis_id, db):
|
|
# analysis_id 디비 조회
|
|
analysis = db.get(StockAnalysis, analysis_id)
|
|
|
|
score_result = await evaluation(analysis)
|
|
|
|
#
|
|
total_score = score_result['total_score']
|
|
rating = get_rating(total_score)
|
|
|
|
# AI 투자 의견서
|
|
opinion = await self.generate_opinion(analysis.report, total_score, rating)
|
|
|
|
entity = InvestmentOpinion(
|
|
analysis_id = analysis_id,
|
|
opinion = opinion,
|
|
rating = rating,
|
|
score = total_score,
|
|
)
|
|
db.add(entity)
|
|
db.commit()
|
|
db.refresh(entity)
|
|
|
|
return {"opinion_id":entity.opinion_id, "analysis_id":analysis_id, "opinion":opinion, "rating":rating, **score_result,}
|
|
|
|
async def generate_opinion(self, report, total_score, rating):
|
|
prompt = f"""
|
|
당신은 월가의 수석 애널리스트입니다.
|
|
|
|
종합점수 :
|
|
{total_score} / 100
|
|
투자등급 :
|
|
{rating}
|
|
|
|
다음 기업 분석 보고서를 기반으로 투자 의견서를 작성하세요.
|
|
{report}
|
|
|
|
반드시 아래 형식으로 작성하세요.
|
|
1. 투자등급
|
|
2. 투자근거
|
|
3. 핵심리스크
|
|
4. 단기전망
|
|
5. 장기전망
|
|
6. 최종의견
|
|
"""
|
|
result = await hugging_llm.ainvoke(prompt)
|
|
return result.content
|
|
|
|
# 싱글톤(객체 하나만 생성) 서비스 인스턴스
|
|
_service:Optional[StockService] = None
|
|
|
|
def get_stock_service():
|
|
global _service
|
|
if _service is None:
|
|
_service = StockService()
|
|
return _service
|